Spaces:
Running
Running
rdassignies
commited on
Upload 7 files
Browse files- .gitattributes +1 -0
- app.py +153 -0
- bodacc.json +3 -0
- graph.py +153 -0
- models.py +129 -0
- nodes.py +166 -0
- prompts.py +73 -0
- requirements.txt +58 -0
.gitattributes
CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
bodacc.json filter=lfs diff=lfs merge=lfs -text
|
app.py
ADDED
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
# -*- coding: utf-8 -*-
|
3 |
+
"""
|
4 |
+
Created on Sun Sep 22 15:43:16 2024
|
5 |
+
|
6 |
+
@author: Raphaël d'Assignies (rdassignies@protonmail.ch)
|
7 |
+
"""
|
8 |
+
import json
|
9 |
+
from typing import Literal, Optional, List, Union, Any
|
10 |
+
from langchain_openai import ChatOpenAI
|
11 |
+
import pandas as pd
|
12 |
+
from langchain_core.prompts import ChatPromptTemplate
|
13 |
+
from langgraph.graph import END, StateGraph, START
|
14 |
+
from langchain_core.output_parsers import StrOutputParser
|
15 |
+
from pydantic import BaseModel, Field
|
16 |
+
from models import NatureJugement
|
17 |
+
from nodes import (GradeResults, GraphState, generate_query_node,
|
18 |
+
generate_results_node, query_feedback_node,
|
19 |
+
evaluate_query_node, evaluate_results_node)
|
20 |
+
import streamlit as st
|
21 |
+
|
22 |
+
|
23 |
+
|
24 |
+
|
25 |
+
# Instanciate pipeline
|
26 |
+
pipeline = StateGraph(GraphState)
|
27 |
+
|
28 |
+
pipeline.add_node('generate_query', generate_query_node)
|
29 |
+
pipeline.add_node('generate_results', generate_results_node)
|
30 |
+
pipeline.add_node('query_feedback', query_feedback_node)
|
31 |
+
|
32 |
+
# Only query
|
33 |
+
#pipeline.add_edge(START,'generate_query')
|
34 |
+
#pipeline.add_edge('generate_query', generate_query_node)
|
35 |
+
#pipeline.add_edge('generate_query', END)
|
36 |
+
|
37 |
+
# Full scenario
|
38 |
+
pipeline.add_edge(START,'generate_query')
|
39 |
+
pipeline.add_conditional_edges(
|
40 |
+
'generate_query',
|
41 |
+
evaluate_query_node,
|
42 |
+
{'error_query' : 'generate_query',
|
43 |
+
'ok' : 'generate_results'
|
44 |
+
})
|
45 |
+
|
46 |
+
pipeline.add_conditional_edges(
|
47 |
+
'generate_results',
|
48 |
+
evaluate_results_node,
|
49 |
+
{
|
50 |
+
"yes": END,
|
51 |
+
"no": 'query_feedback',
|
52 |
+
"max_generation_reached": END
|
53 |
+
|
54 |
+
}
|
55 |
+
)
|
56 |
+
|
57 |
+
|
58 |
+
# Création du graph
|
59 |
+
graph = pipeline.compile()
|
60 |
+
|
61 |
+
# Load le dataframe
|
62 |
+
df = pd.read_json('bodacc.json', orient='table')
|
63 |
+
|
64 |
+
# Initialise le dictionnaire
|
65 |
+
inputs = {
|
66 |
+
'df_head': df.head().to_csv(),
|
67 |
+
'df': df
|
68 |
+
}
|
69 |
+
|
70 |
+
# Créé un dictionnaire des sorties vide
|
71 |
+
outputs = {}
|
72 |
+
|
73 |
+
|
74 |
+
# Titre de l'application
|
75 |
+
st.title("Chat with BODACC !")
|
76 |
+
|
77 |
+
# Message d'avertissement
|
78 |
+
warning_message = (f"Cet outil, purement pédagogique, est basé sur des données réelles allant de {df['dateparution'].min()} "
|
79 |
+
f"à {df['dateparution'].max()}, et permet d'interroger le BODACC en langage naturel. Compte tenu de la variabilité des modèles, nous ne pouvons pas garantir la fiabilité des réponses.")
|
80 |
+
|
81 |
+
st.warning(warning_message)
|
82 |
+
# Interface utilisateur pour entrer la requête
|
83 |
+
user_query = st.text_input("Entrez votre requête:", "Trouve moi les restaurants à reprendre en Bretagne dans les 30 derniers jours")
|
84 |
+
|
85 |
+
|
86 |
+
# Afficher les résultats avec Streamlit
|
87 |
+
inputs["instructions"] = user_query
|
88 |
+
|
89 |
+
|
90 |
+
# Afficher un bouton pour démarrer la recherche
|
91 |
+
if st.button("Lancer la recherche"):
|
92 |
+
config = {"configurable": {"thread_id": "2"}}
|
93 |
+
|
94 |
+
# Étape 1 : Afficher le message "Je réfléchis..."
|
95 |
+
st.write("Je réfléchis...")
|
96 |
+
|
97 |
+
# Stream des résultats au fur et à mesure
|
98 |
+
with st.spinner('Recherche en cours...'):
|
99 |
+
for output in graph.stream(inputs, stream_mode='values', debug=False):
|
100 |
+
# Ajouter les résultats au dictionnaire outputs
|
101 |
+
for k, v in output.items():
|
102 |
+
if k not in outputs:
|
103 |
+
outputs[k] = []
|
104 |
+
outputs[k].append(v)
|
105 |
+
|
106 |
+
# Ne pas afficher les messages pour les clés non pertinentes (comme error_query)
|
107 |
+
if 'query' in output and len(output['query'])>0:
|
108 |
+
st.write(f"query : {output['query']}")
|
109 |
+
#st.write(outputs.get('query_feedbacks', 'pas de feedback'))
|
110 |
+
#st.write(outputs.get('results_feedbacks', 'pas de resultfeedback'))
|
111 |
+
if "results" in output and len(output["results"]) > 0:
|
112 |
+
records = json.loads(output['results'])
|
113 |
+
st.write(f"Résultats intermédiaires trouvés : {len(records)} résultats jusqu'à présent.")
|
114 |
+
|
115 |
+
# Après la fin du traitement
|
116 |
+
if "results" in outputs and len(outputs["results"]) > 0:
|
117 |
+
# Agréger tous les résultats accumulés
|
118 |
+
all_results = []
|
119 |
+
for res in outputs["results"]:
|
120 |
+
json_data = json.loads(res) # Convertir chaque ensemble de résultats en JSON
|
121 |
+
all_results.extend(json_data) # Accumuler tous les résultats
|
122 |
+
|
123 |
+
results_df = pd.DataFrame(all_results) # Créer un DataFrame avec tous les résultats accumulés
|
124 |
+
# Afficher un aperçu des résultats (jusqu'à 5 premiers)
|
125 |
+
num_results = len(results_df)
|
126 |
+
st.write(f"J'ai trouvé {num_results} résultats.")
|
127 |
+
if num_results > 0:
|
128 |
+
preview_count = min(5, num_results) # Gérer le cas où il y a moins de 5 résultats
|
129 |
+
st.write(f"Voici un aperçu des {preview_count} premiers résultats :")
|
130 |
+
st.write(results_df.head(preview_count))
|
131 |
+
|
132 |
+
trunc = outputs.get('truncated', 'pas de traunc')
|
133 |
+
|
134 |
+
if trunc[0] == True:
|
135 |
+
st.warning("Les résultats de votre recherche ont été tronqués car celle-ci était trop large ! ")
|
136 |
+
|
137 |
+
# Convertir tous les résultats en CSV
|
138 |
+
csv = results_df.to_csv(index=False)
|
139 |
+
|
140 |
+
# Ajouter un bouton pour télécharger tous les résultats
|
141 |
+
st.download_button(
|
142 |
+
label="Télécharger le résultat complet au format CSV",
|
143 |
+
data=csv,
|
144 |
+
file_name="results.csv",
|
145 |
+
mime="text/csv"
|
146 |
+
)
|
147 |
+
|
148 |
+
else:
|
149 |
+
# Si aucun résultat n'est trouvé
|
150 |
+
st.write("Aucun résultat trouvé.")
|
151 |
+
|
152 |
+
|
153 |
+
|
bodacc.json
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:a5172c9d6946aef8bbf0d9c17be1159ffbb64e773bf800b65335797e35ffff1a
|
3 |
+
size 25389210
|
graph.py
ADDED
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
# -*- coding: utf-8 -*-
|
3 |
+
"""
|
4 |
+
Created on Sun Sep 22 15:43:16 2024
|
5 |
+
|
6 |
+
@author: Raphaël d'Assignies (rdassignies@protonmail.ch)
|
7 |
+
"""
|
8 |
+
import json
|
9 |
+
from typing import Literal, Optional, List, Union, Any
|
10 |
+
from langchain_openai import ChatOpenAI
|
11 |
+
import pandas as pd
|
12 |
+
from langchain_core.prompts import ChatPromptTemplate
|
13 |
+
from langgraph.graph import END, StateGraph, START
|
14 |
+
from langchain_core.output_parsers import StrOutputParser
|
15 |
+
from pydantic import BaseModel, Field
|
16 |
+
from models import NatureJugement
|
17 |
+
from nodes import (GradeResults, GraphState, generate_query_node,
|
18 |
+
generate_results_node, query_feedback_node,
|
19 |
+
evaluate_query_node, evaluate_results_node)
|
20 |
+
import streamlit as st
|
21 |
+
|
22 |
+
|
23 |
+
|
24 |
+
|
25 |
+
# Instanciate pipeline
|
26 |
+
pipeline = StateGraph(GraphState)
|
27 |
+
|
28 |
+
pipeline.add_node('generate_query', generate_query_node)
|
29 |
+
pipeline.add_node('generate_results', generate_results_node)
|
30 |
+
pipeline.add_node('query_feedback', query_feedback_node)
|
31 |
+
|
32 |
+
# Only query
|
33 |
+
#pipeline.add_edge(START,'generate_query')
|
34 |
+
#pipeline.add_edge('generate_query', generate_query_node)
|
35 |
+
#pipeline.add_edge('generate_query', END)
|
36 |
+
|
37 |
+
# Full scenario
|
38 |
+
pipeline.add_edge(START,'generate_query')
|
39 |
+
pipeline.add_conditional_edges(
|
40 |
+
'generate_query',
|
41 |
+
evaluate_query_node,
|
42 |
+
{'error_query' : 'generate_query',
|
43 |
+
'ok' : 'generate_results'
|
44 |
+
})
|
45 |
+
|
46 |
+
pipeline.add_conditional_edges(
|
47 |
+
'generate_results',
|
48 |
+
evaluate_results_node,
|
49 |
+
{
|
50 |
+
"yes": END,
|
51 |
+
"no": 'query_feedback',
|
52 |
+
"max_generation_reached": END
|
53 |
+
|
54 |
+
}
|
55 |
+
)
|
56 |
+
|
57 |
+
|
58 |
+
# Création du graph
|
59 |
+
graph = pipeline.compile()
|
60 |
+
|
61 |
+
# Load le dataframe
|
62 |
+
df = pd.read_json('bodacc.json', orient='table')
|
63 |
+
|
64 |
+
# Initialise le dictionnaire
|
65 |
+
inputs = {
|
66 |
+
'df_head': df.head().to_csv(),
|
67 |
+
'df': df
|
68 |
+
}
|
69 |
+
|
70 |
+
# Créé un dictionnaire des sorties vide
|
71 |
+
outputs = {}
|
72 |
+
|
73 |
+
|
74 |
+
# Titre de l'application
|
75 |
+
st.title("Chat with BODACC !")
|
76 |
+
|
77 |
+
# Message d'avertissement
|
78 |
+
warning_message = (f"Cet outil, purement pédagogique, est basé sur des données réelles allant de {df['dateparution'].min()} "
|
79 |
+
f"à {df['dateparution'].max()}, et permet d'interroger le BODACC en langage naturel. Compte tenu de la variabilité des modèles, nous ne pouvons pas garantir la fiabilité des réponses.")
|
80 |
+
|
81 |
+
st.warning(warning_message)
|
82 |
+
# Interface utilisateur pour entrer la requête
|
83 |
+
user_query = st.text_input("Entrez votre requête:", "Trouve moi les restaurants à reprendre en Bretagne dans les 30 derniers jours")
|
84 |
+
|
85 |
+
|
86 |
+
# Afficher les résultats avec Streamlit
|
87 |
+
inputs["instructions"] = user_query
|
88 |
+
|
89 |
+
|
90 |
+
# Afficher un bouton pour démarrer la recherche
|
91 |
+
if st.button("Lancer la recherche"):
|
92 |
+
config = {"configurable": {"thread_id": "2"}}
|
93 |
+
|
94 |
+
# Étape 1 : Afficher le message "Je réfléchis..."
|
95 |
+
st.write("Je réfléchis...")
|
96 |
+
|
97 |
+
# Stream des résultats au fur et à mesure
|
98 |
+
with st.spinner('Recherche en cours...'):
|
99 |
+
for output in graph.stream(inputs, stream_mode='values', debug=False):
|
100 |
+
# Ajouter les résultats au dictionnaire outputs
|
101 |
+
for k, v in output.items():
|
102 |
+
if k not in outputs:
|
103 |
+
outputs[k] = []
|
104 |
+
outputs[k].append(v)
|
105 |
+
|
106 |
+
# Ne pas afficher les messages pour les clés non pertinentes (comme error_query)
|
107 |
+
if 'query' in output and len(output['query'])>0:
|
108 |
+
st.write(f"query : {output['query']}")
|
109 |
+
#st.write(outputs.get('query_feedbacks', 'pas de feedback'))
|
110 |
+
#st.write(outputs.get('results_feedbacks', 'pas de resultfeedback'))
|
111 |
+
if "results" in output and len(output["results"]) > 0:
|
112 |
+
records = json.loads(output['results'])
|
113 |
+
st.write(f"Résultats intermédiaires trouvés : {len(records)} résultats jusqu'à présent.")
|
114 |
+
|
115 |
+
# Après la fin du traitement
|
116 |
+
if "results" in outputs and len(outputs["results"]) > 0:
|
117 |
+
# Agréger tous les résultats accumulés
|
118 |
+
all_results = []
|
119 |
+
for res in outputs["results"]:
|
120 |
+
json_data = json.loads(res) # Convertir chaque ensemble de résultats en JSON
|
121 |
+
all_results.extend(json_data) # Accumuler tous les résultats
|
122 |
+
|
123 |
+
results_df = pd.DataFrame(all_results) # Créer un DataFrame avec tous les résultats accumulés
|
124 |
+
# Afficher un aperçu des résultats (jusqu'à 5 premiers)
|
125 |
+
num_results = len(results_df)
|
126 |
+
st.write(f"J'ai trouvé {num_results} résultats.")
|
127 |
+
if num_results > 0:
|
128 |
+
preview_count = min(5, num_results) # Gérer le cas où il y a moins de 5 résultats
|
129 |
+
st.write(f"Voici un aperçu des {preview_count} premiers résultats :")
|
130 |
+
st.write(results_df.head(preview_count))
|
131 |
+
|
132 |
+
trunc = outputs.get('truncated', 'pas de traunc')
|
133 |
+
|
134 |
+
if trunc[0] == True:
|
135 |
+
st.warning("Les résultats de votre recherche ont été tronqués car celle-ci était trop large ! ")
|
136 |
+
|
137 |
+
# Convertir tous les résultats en CSV
|
138 |
+
csv = results_df.to_csv(index=False)
|
139 |
+
|
140 |
+
# Ajouter un bouton pour télécharger tous les résultats
|
141 |
+
st.download_button(
|
142 |
+
label="Télécharger le résultat complet au format CSV",
|
143 |
+
data=csv,
|
144 |
+
file_name="results.csv",
|
145 |
+
mime="text/csv"
|
146 |
+
)
|
147 |
+
|
148 |
+
else:
|
149 |
+
# Si aucun résultat n'est trouvé
|
150 |
+
st.write("Aucun résultat trouvé.")
|
151 |
+
|
152 |
+
|
153 |
+
|
models.py
ADDED
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
# -*- coding: utf-8 -*-
|
3 |
+
"""
|
4 |
+
Created on Thu Dec 28 10:32:52 2023
|
5 |
+
|
6 |
+
@author: raphael
|
7 |
+
"""
|
8 |
+
import json
|
9 |
+
from typing import List, Optional, Dict
|
10 |
+
from pydantic import BaseModel, Field, UUID4
|
11 |
+
from enum import Enum, unique
|
12 |
+
|
13 |
+
@unique
|
14 |
+
class FamilleJugement(str, Enum):
|
15 |
+
ARRET_APPEL = "Arrêt de la Cour d'Appel"
|
16 |
+
AVIS_DEPOT = 'Avis de dépôt'
|
17 |
+
EXTRAIT_JUGEMENT = 'Extrait de jugement'
|
18 |
+
JUGEMENT_OUVERTURE_ouverture = "Jugement d'ouverture"
|
19 |
+
JUGEMENT_CLOTURE = 'Jugement de clôture'
|
20 |
+
JUGEMENT = 'Jugement prononçant'
|
21 |
+
|
22 |
+
@unique
|
23 |
+
class NatureJugement(str, Enum):
|
24 |
+
ARRET_COUR_APPEL_INFIRMANT = "Arrêt de la cour d'appel infirmant une décision soumise à publicité"
|
25 |
+
AUTRE_ARRET_COUR_APPEL = "Autre arrêt de la Cour d'Appel"
|
26 |
+
AUTRE_AVIS_DE_DEPOT = "Autre avis de dépôt"
|
27 |
+
AUTRE_JUGEMENT_OUVERTURE = "Autre jugement d'ouverture"
|
28 |
+
AUTRE_JUGEMENT_CLOTURE = "Autre jugement de clôture"
|
29 |
+
AUTRE_JUGEMENT_ORDONNANCE = "Autre jugement et ordonnance"
|
30 |
+
AUTRE_JUGEMENT_PRONONCANT = "Autre jugement prononçant"
|
31 |
+
DEPOT_ETAT_COLLOCATION = "Dépôt de l'état de collocation"
|
32 |
+
DEPOT_ETAT_CREANCES = "Dépôt de l'état des créances"
|
33 |
+
DEPOT_ETAT_CREANCES_1985 = "Dépôt de l'état des créances Loi de 1985"
|
34 |
+
DEPOT_PROJET_REPARTITION = "Dépôt du projet de répartition"
|
35 |
+
JUGEMENT_PLAN_SAUVEGARDE = "Jugement arrêtant le plan de sauvegarde"
|
36 |
+
JUGEMENT_PLAN_CESSION = "Jugement arrêtant un plan de cession"
|
37 |
+
EXTENSION_LIQUIDATION_JUDICIAIRE = "Jugement d'extension de liquidation judiciaire"
|
38 |
+
INTERDICTION_GERER = "Jugement d'interdiction de gérer"
|
39 |
+
OUVERTURE_PROCEDURE_RESTRUCTURATION = "Jugement d'ouverture d'une procédure de redressement judiciaire"
|
40 |
+
OUVERTURE_PROCEDURE_SAUVEGARDE = "Jugement d'ouverture d'une procédure de sauvegarde"
|
41 |
+
OUVERTURE_LIQUIDATION_JUDICIAIRE = "Jugement d'ouverture de liquidation judiciaire"
|
42 |
+
CLOTURE_PROCEDURE_SAUVEGARDE = "Jugement de clôture de la procédure de sauvegarde"
|
43 |
+
CLOTURE_EXTINCTION_PASSIF = "Jugement de clôture pour extinction du passif"
|
44 |
+
CLOTURE_INSUFFISANCE_ACTIF = "Jugement de clôture pour insuffisance d'actif"
|
45 |
+
CONVERSION_LIQUIDATION_JUDICIAIRE = "Jugement de conversion en liquidation judiciaire"
|
46 |
+
CONVERSION_LIQUIDATION_SAUVEGARDE = "Jugement de conversion en liquidation judiciaire de la procédure de sauvegarde"
|
47 |
+
CONVERSION_RESTRUCTURATION_SAUVEGARDE = "Jugement de conversion en redressement judiciaire de la procédure de sauvegarde"
|
48 |
+
JUGEMENT_FAILLITE_PERSONNELLE = "Jugement de faillite personnelle"
|
49 |
+
JUGEMENT_PLAN_RESTRUCTURATION = "Jugement de plan de redressement"
|
50 |
+
REPRISE_PROCEDURE_LIQUIDATION = "Jugement de reprise de la procédure de liquidation judiciaire"
|
51 |
+
FIN_PROCEDURE_RESTRUCTURATION = "Jugement mettant fin à la procédure de redressement judiciaire"
|
52 |
+
FIN_PROCEDURE_SAUVEGARDE = "Jugement mettant fin à la procédure de sauvegarde"
|
53 |
+
MODIFICATION_DATE_CESSATION_PAIEMENTS = "Jugement modifiant la date de cessation des paiements"
|
54 |
+
MODIFICATION_PLAN_RESTRUCTURATION = "Jugement modifiant le plan de redressement"
|
55 |
+
MODIFICATION_PLAN_SAUVEGARDE = "Jugement modifiant le plan de sauvegarde"
|
56 |
+
MODIFICATION_PLAN_TRAITEMENT_SORTIE_CRISE = "Jugement modifiant le plan de traitement de sortie de crise"
|
57 |
+
RESOLUTION_PLAN_RESTRUCTURATION_LIQUIDATION = "Jugement prononçant la résolution du plan de redressement et la liquidation judiciaire"
|
58 |
+
RESOLUTION_PLAN_SAUVEGARDE_LIQUIDATION = "Jugement prononçant la résolution du plan de sauvegarde et la liquidation judiciaire"
|
59 |
+
RESOLUTION_PLAN_SAUVEGARDE_RESTRUCTURATION = "Jugement prononçant la résolution du plan de sauvegarde et le redressement judiciaire"
|
60 |
+
RESOLUTION_PLAN_SORTIE_CRISE_LIQUIDATION = "Jugement prononçant la résolution du plan de traitement de sortie de crise et la liquidation judiciaire"
|
61 |
+
LISTE_CREANCES_POST_OUVERTURE_LIQUIDATION = "Liste des créances nées après le jugement d'ouverture d'une procédure de liquidation judiciaire"
|
62 |
+
LISTE_CREANCES_POST_OUVERTURE_RESTRUCTURATION = "Liste des créances nées après le jugement d'ouverture d'une procédure de redressement judiciaire"
|
63 |
+
|
64 |
+
# Méthode pour obtenir l'énumération à partir de la chaîne
|
65 |
+
@staticmethod
|
66 |
+
def from_string(s):
|
67 |
+
for member in NatureJugement:
|
68 |
+
if member.value == s:
|
69 |
+
return member
|
70 |
+
raise ValueError(f"{s} n'est pas une valeur valide de TypeJugement")
|
71 |
+
|
72 |
+
class Personne(BaseModel):
|
73 |
+
typePersonne: str
|
74 |
+
numeroImmatriculation: Optional[Dict] = Field(default=None)
|
75 |
+
denomination: Optional[str] = Field(default=None)
|
76 |
+
activite: Optional[str] = Field(default=None)
|
77 |
+
formeJuridique: Optional[str] = Field(default=None)
|
78 |
+
adresseSiegeSocial: Optional[Dict] = Field(default=None)
|
79 |
+
|
80 |
+
class Jugement(BaseModel):
|
81 |
+
type:Optional[str] = None
|
82 |
+
famille:Optional[str] = None
|
83 |
+
nature:Optional[str] = None
|
84 |
+
#nature: NatureJugement = None
|
85 |
+
date: Optional[str] = None
|
86 |
+
complementJugement: Optional[str] = None
|
87 |
+
|
88 |
+
class Annonce(BaseModel):
|
89 |
+
#uuid: UUID4
|
90 |
+
publicationavis: Optional[str]
|
91 |
+
publicationavis_facette: Optional[str]
|
92 |
+
parution: Optional[int]
|
93 |
+
dateparution: Optional[str]
|
94 |
+
numeroannonce: int
|
95 |
+
typeavis: Optional[str]
|
96 |
+
typeavis_lib: Optional[str]
|
97 |
+
familleavis: Optional[str]
|
98 |
+
familleavis_lib: Optional[str]
|
99 |
+
numerodepartement: Optional[str]
|
100 |
+
departement_nom_officiel: Optional[str]
|
101 |
+
region_code: int
|
102 |
+
region_nom_officiel: Optional[str]
|
103 |
+
tribunal: Optional[str]
|
104 |
+
commercant: Optional[str]
|
105 |
+
ville: Optional[str]
|
106 |
+
registre: Optional[List[str]] # Rendre le champ optionnel
|
107 |
+
cp: Optional[str]
|
108 |
+
pdf_parution_subfolder: int
|
109 |
+
ispdf_unitaire: str
|
110 |
+
listepersonnes: Optional[List[Personne]] # Liste d'instances de Personne
|
111 |
+
listeetablissements: Optional[None]
|
112 |
+
jugement: Optional[Jugement] # JSON string or None
|
113 |
+
acte: Optional[None]
|
114 |
+
modificationsgenerales: Optional[None]
|
115 |
+
radiationaurcs: Optional[None]
|
116 |
+
depot: Optional[None]
|
117 |
+
listeprecedentexploitant: Optional[None]
|
118 |
+
listeprecedentproprietaire: Optional[None]
|
119 |
+
divers: Optional[None]
|
120 |
+
parutionavisprecedent: Optional[None]
|
121 |
+
|
122 |
+
|
123 |
+
|
124 |
+
|
125 |
+
|
126 |
+
|
127 |
+
|
128 |
+
|
129 |
+
|
nodes.py
ADDED
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
# -*- coding: utf-8 -*-
|
3 |
+
"""
|
4 |
+
Created on Sun Oct 13 10:30:56 2024
|
5 |
+
|
6 |
+
@author: legalchain
|
7 |
+
"""
|
8 |
+
from typing import Literal, Optional, List, Union, Any
|
9 |
+
from langchain_openai import ChatOpenAI
|
10 |
+
import pandas as pd
|
11 |
+
from langchain_core.prompts import ChatPromptTemplate
|
12 |
+
from langgraph.graph import END, StateGraph, START
|
13 |
+
from langchain_core.output_parsers import StrOutputParser
|
14 |
+
from pydantic import BaseModel, Field
|
15 |
+
from models import NatureJugement
|
16 |
+
from prompts import df_prompt, feed_back_prompt, reflection_prompt
|
17 |
+
from dotenv import load_dotenv
|
18 |
+
load_dotenv()
|
19 |
+
|
20 |
+
llm = ChatOpenAI(model="gpt-4o-mini")
|
21 |
+
MAX_GENERATIONS = 2
|
22 |
+
MAX_ROWS: int = 10
|
23 |
+
|
24 |
+
class Query(BaseModel):
|
25 |
+
query:str = Field(..., title="Requête pour filtrer les résultats du dataframe entourée avec des gullemets de type \" ")
|
26 |
+
|
27 |
+
def clean_query(self):
|
28 |
+
# Correction des échappements dans la chaîne de la requête
|
29 |
+
corrected_query = self.query.replace("\\'", "\\'")
|
30 |
+
# Extraire la condition à l'intérieur des crochets
|
31 |
+
import re
|
32 |
+
condition = re.search(r"df\[(.*)\]", corrected_query).group(1)
|
33 |
+
return condition
|
34 |
+
|
35 |
+
class GradeResults(BaseModel):
|
36 |
+
binary_score: Literal["yes", "no"] = Field(
|
37 |
+
description="Les résultats sont satisfaisants -> 'yes' ou il y une erreur ou pas de résultats ou les résultats sont améliorables -> 'no'"
|
38 |
+
)
|
39 |
+
|
40 |
+
class GraphState(BaseModel):
|
41 |
+
|
42 |
+
df : Any
|
43 |
+
df_head:str
|
44 |
+
instructions: Optional[str] = None
|
45 |
+
nature_jugement: List = ', '.join([e.value for e in NatureJugement])
|
46 |
+
region:str = ''
|
47 |
+
dep:str = ''
|
48 |
+
query: Optional[str] = None
|
49 |
+
results :Union[str, List[str]] = []
|
50 |
+
query_feedbacks: Optional[str] = None
|
51 |
+
results_feedbacks: bool = None
|
52 |
+
generation_num: int = 0
|
53 |
+
retrieval_num: int = 0
|
54 |
+
search_mode: Literal["vectorstore", "websearch", "QA_LM"] = "QA_LM"
|
55 |
+
error_query: Optional[Any] = ""
|
56 |
+
error_results: Optional[Any] = ""
|
57 |
+
truncated: bool = False
|
58 |
+
|
59 |
+
# Méthode pour récupérer le DataFrame
|
60 |
+
def get_df(self) -> pd.DataFrame:
|
61 |
+
return pd.read_json(self.df)
|
62 |
+
|
63 |
+
# Surcharger l'initialisation pour créer les champs 'region' et 'dep'
|
64 |
+
def __init__(self, **data):
|
65 |
+
super().__init__(**data)
|
66 |
+
|
67 |
+
# Extraire le DataFrame
|
68 |
+
#df = self.get_df()
|
69 |
+
|
70 |
+
# Générer les chaînes pour les régions et départements
|
71 |
+
distinct_regions = self.df['region_nom_officiel'].dropna().unique().tolist()
|
72 |
+
distinct_departements = self.df['departement_nom_officiel'].dropna().unique().tolist()
|
73 |
+
|
74 |
+
# Convertir en chaînes séparées par des virgules
|
75 |
+
self.region = ', '.join(distinct_regions)
|
76 |
+
self.dep = ', '.join(distinct_departements)
|
77 |
+
|
78 |
+
def generate_query_node(state: GraphState):
|
79 |
+
|
80 |
+
prompt = ChatPromptTemplate.from_messages(messages = df_prompt)
|
81 |
+
generate_df_query = prompt | llm.with_structured_output(
|
82 |
+
Query,
|
83 |
+
include_raw=True, # permet de checker les erreurs en sortie
|
84 |
+
)
|
85 |
+
# Ajouter le retour erreur de parse_error
|
86 |
+
try :
|
87 |
+
query_generate = generate_df_query.invoke({
|
88 |
+
'df_head' : state.df_head,
|
89 |
+
'instructions' : state.instructions,
|
90 |
+
'feedback' : state.query_feedbacks,
|
91 |
+
'error' : state.error_query,
|
92 |
+
'nature_jugement' : state.nature_jugement,
|
93 |
+
'dep' : state.dep,
|
94 |
+
'region': state.region
|
95 |
+
})
|
96 |
+
query_final = query_generate['parsed'].clean_query()
|
97 |
+
return {
|
98 |
+
"query": query_final,
|
99 |
+
"error_query" : "" # si il ya une erreur cela remet le compteur à zéro
|
100 |
+
}
|
101 |
+
except Exception as e:
|
102 |
+
return {'error_query' : e}
|
103 |
+
|
104 |
+
def evaluate_query_node(state:GraphState):
|
105 |
+
if state.error_query != "":
|
106 |
+
return "Il y a une erreur dans la requête. Je me suis sûrement trompé. Veuillez réessayer."
|
107 |
+
else:
|
108 |
+
return "ok"
|
109 |
+
|
110 |
+
|
111 |
+
def generate_results_node(state:GraphState):
|
112 |
+
try :
|
113 |
+
query = state.query
|
114 |
+
print("query ", query)
|
115 |
+
print('je suis dans generate', type(state.df))
|
116 |
+
query = eval(query, {"df": state.df})
|
117 |
+
new_df = state.df[query]
|
118 |
+
print("new_df", new_df.empty)
|
119 |
+
if new_df.empty:
|
120 |
+
return {
|
121 |
+
"generation_num": state.generation_num + 1}
|
122 |
+
elif len(new_df)> MAX_ROWS:
|
123 |
+
return {'results' : new_df.head(MAX_ROWS).to_json(orient='records'),
|
124 |
+
"generation_num": state.generation_num + 1,
|
125 |
+
"truncated": True
|
126 |
+
}
|
127 |
+
else:
|
128 |
+
return {'results' : new_df.to_json(orient='records'),
|
129 |
+
"generation_num": state.generation_num + 1,
|
130 |
+
|
131 |
+
}
|
132 |
+
except Exception as e :
|
133 |
+
return {'error_results' : e,
|
134 |
+
"generation_num": state.generation_num + 1}
|
135 |
+
|
136 |
+
|
137 |
+
def evaluate_results_node(state:GraphState):
|
138 |
+
prompt_eval = ChatPromptTemplate.from_messages(messages=reflection_prompt)
|
139 |
+
generate_eval = prompt_eval | llm.with_structured_output(
|
140 |
+
GradeResults,
|
141 |
+
include_raw=False, # permet de checker les erreurs en sortie
|
142 |
+
)
|
143 |
+
|
144 |
+
evaluation = generate_eval.invoke({'df_head' : state.df_head,
|
145 |
+
'results' :state.results,
|
146 |
+
'instructions' : state.instructions})
|
147 |
+
|
148 |
+
if state.generation_num > MAX_GENERATIONS:
|
149 |
+
return "max_generation_reached"
|
150 |
+
|
151 |
+
return evaluation.binary_score
|
152 |
+
|
153 |
+
def query_feedback_node(state: GraphState):
|
154 |
+
prompt_feed_back = ChatPromptTemplate.from_messages(messages=feed_back_prompt)
|
155 |
+
query_feedback_chain = prompt_feed_back| llm |StrOutputParser()
|
156 |
+
|
157 |
+
feedback = query_feedback_chain.invoke({
|
158 |
+
"df_head" : state.df_head,
|
159 |
+
"instructions": state.instructions,
|
160 |
+
"results": state.results,
|
161 |
+
"query": state.query
|
162 |
+
})
|
163 |
+
|
164 |
+
feedback = f"Evaluation de la recherche : {feedback}"
|
165 |
+
print(feedback)
|
166 |
+
return {"query_feedbacks": feedback}
|
prompts.py
ADDED
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
# -*- coding: utf-8 -*-
|
3 |
+
"""
|
4 |
+
Created on Sun Oct 13 10:32:26 2024
|
5 |
+
|
6 |
+
@author: legalchain
|
7 |
+
"""
|
8 |
+
|
9 |
+
df_prompt = [
|
10 |
+
(
|
11 |
+
"system",
|
12 |
+
"Tu travailles avec dees pandas dataframes en Python. Le nom du dataframe est `df`."
|
13 |
+
" Voici les instructions à connaître pour effectuer une recherche performante :"
|
14 |
+
"1. La colonne 'jugement_nature' renseigne sur le type de jugement. C'est dans cette colonne que tu peux trouver les sociétés ou les personnes en liquidation judiciaire ou en redressement judiciaire"
|
15 |
+
" La liste des valeurs que peut prendre ce champ est la suivante : {nature_jugement}"
|
16 |
+
"2. La colonne 'activite' indique le type d'activité comme restauration, hotelerie, avocat, etc. Ce champ est libre. Il faudra souvent élargir la recherche initiale pour trouver la bonne activité."
|
17 |
+
"3. Les colonnes 'departement_nom_officiel, 'region_nom_officiel' servent à localiser l'entreprise concernée"
|
18 |
+
" Voici la liste des valeurs possibles pour les régions : \n {region} \n"
|
19 |
+
" Voici la liste des valeurs possibles pour les départements : \n {dep} \n"
|
20 |
+
"4. La colonne 'jugement_complementJugement' sert à obtenir des détails sur le jugement comme le nom du mandataires par exemple."
|
21 |
+
"5. Si on te demande une durée depuis la date de jugement utilise la colonne 'temps_ecoule' qui contient la durée en jours entre aujourd'hui et la date de jugement"
|
22 |
+
"La recherche est souvent formulée en des termes communs et a une correspondance juridique dans le dataframe"
|
23 |
+
"Par exemple le mot faillite ne s'applique qu'aux faillites personnelles, on parlera de liquidation judiciaire "
|
24 |
+
"Voici le résultat de la fonction `print(df.head())`:{df_head}"
|
25 |
+
,
|
26 |
+
),
|
27 |
+
("human",
|
28 |
+
"{instructions}"
|
29 |
+
"### Ta tâche : \n"
|
30 |
+
"Ecris la requêtes pour extraire les informations pertinentes du dataframe : \n\n "
|
31 |
+
"Si on te demande une durée depuis la date de jugement utilise la colonne 'temps_ecoule' qui contient la durée en jours entre aujourd'hui et la date de jugement"
|
32 |
+
"Voici l'évaluation d'une précédente recherche : {feedback}"
|
33 |
+
"Corrige éventuellement la requête précédente qui a généré cette erreur {error}"
|
34 |
+
|
35 |
+
),
|
36 |
+
]
|
37 |
+
|
38 |
+
reflection_prompt = [
|
39 |
+
(
|
40 |
+
"system",
|
41 |
+
"Tu travailles avec dees pandas dataframes en Python."
|
42 |
+
" Le nom du dataframe est df "
|
43 |
+
"Voici le résultat de la fonction `print(df.head())`:{df_head}"
|
44 |
+
" Tu es chargé d'évaluer les résultats d'une requête/recherche sur le dataframe"
|
45 |
+
|
46 |
+
|
47 |
+
),
|
48 |
+
("human",
|
49 |
+
"Voici l'instruction donnée pour la recherche dans le dataframe {instructions} \n"
|
50 |
+
"Voici un extrait (df.head() )des résultats de la recherche : {results} \n"
|
51 |
+
"### Ta tâche : \n"
|
52 |
+
" Tu dois évaluer les résultats de la recherche. \n"
|
53 |
+
),
|
54 |
+
]
|
55 |
+
|
56 |
+
feed_back_prompt = [
|
57 |
+
(
|
58 |
+
"system",
|
59 |
+
"Tu travailles avec dees pandas dataframes en Python."
|
60 |
+
" Le nom du dataframe est df "
|
61 |
+
"Voici le résultat de la fonction `print(df.head())`:{df_head}"
|
62 |
+
" Tu es chargé d'évaluer les résultats d'une requête/recherche sur le dataframe"
|
63 |
+
|
64 |
+
),
|
65 |
+
("human",
|
66 |
+
"Voici l'instructions de recherche: {instructions}"
|
67 |
+
"Voici la recherche dans le dataframe : {query}"
|
68 |
+
"Voici les resultats insatisfaisants : {results} "
|
69 |
+
"### Ta tâche : \n"
|
70 |
+
" Tu dois évaluer la recherche et donner des instructions pour la modifier en fonction "
|
71 |
+
" de ton diagnostique. "
|
72 |
+
),
|
73 |
+
]
|
requirements.txt
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
altair==5.4.1
|
2 |
+
annotated-types==0.7.0
|
3 |
+
anyio==4.6.0
|
4 |
+
attrs==24.2.0
|
5 |
+
blinker==1.8.2
|
6 |
+
cachetools==5.5.0
|
7 |
+
certifi==2024.8.30
|
8 |
+
charset-normalizer==3.4.0
|
9 |
+
click==8.1.7
|
10 |
+
gitdb==4.0.11
|
11 |
+
GitPython==3.1.43
|
12 |
+
h11==0.14.0
|
13 |
+
httpcore==1.0.6
|
14 |
+
httpx==0.27.2
|
15 |
+
idna==3.10
|
16 |
+
Jinja2==3.1.4
|
17 |
+
jsonpatch==1.33
|
18 |
+
jsonpointer==3.0.0
|
19 |
+
jsonschema==4.23.0
|
20 |
+
jsonschema-specifications==2024.10.1
|
21 |
+
langchain-core==0.3.10
|
22 |
+
langgraph==0.2.35
|
23 |
+
langgraph-checkpoint==2.0.1
|
24 |
+
langsmith==0.1.134
|
25 |
+
markdown-it-py==3.0.0
|
26 |
+
MarkupSafe==3.0.1
|
27 |
+
mdurl==0.1.2
|
28 |
+
msgpack==1.1.0
|
29 |
+
narwhals==1.9.3
|
30 |
+
numpy==2.1.2
|
31 |
+
orjson==3.10.7
|
32 |
+
packaging==24.1
|
33 |
+
pandas==2.2.3
|
34 |
+
pillow==10.4.0
|
35 |
+
protobuf==5.28.2
|
36 |
+
pyarrow==17.0.0
|
37 |
+
pydantic==2.9.2
|
38 |
+
pydantic_core==2.23.4
|
39 |
+
pydeck==0.9.1
|
40 |
+
Pygments==2.18.0
|
41 |
+
python-dateutil==2.9.0.post0
|
42 |
+
pytz==2024.2
|
43 |
+
PyYAML==6.0.2
|
44 |
+
referencing==0.35.1
|
45 |
+
requests==2.32.3
|
46 |
+
requests-toolbelt==1.0.0
|
47 |
+
rich==13.9.2
|
48 |
+
rpds-py==0.20.0
|
49 |
+
six==1.16.0
|
50 |
+
smmap==5.0.1
|
51 |
+
sniffio==1.3.1
|
52 |
+
streamlit==1.39.0
|
53 |
+
tenacity==8.5.0
|
54 |
+
toml==0.10.2
|
55 |
+
tornado==6.4.1
|
56 |
+
typing_extensions==4.12.2
|
57 |
+
tzdata==2024.2
|
58 |
+
urllib3==2.2.3
|