@@ -67,14 +67,15 @@ def _initialize_openai_client(self) -> OpenAI:
6767
6868 return OpenAI (** client_kwargs )
6969
70- def ask_llm (self , message : dict , temperature : float = 0.0 ) -> str :
70+ def ask_llm (self , message : dict , response_format : dict = None , temperature : float = 0.0 ) -> str :
7171 """
7272 Interroge le LLM avec un prompt système et utilisateur.
7373
7474 Args:
7575 message:
7676 system_prompt: Prompt système pour définir le rôle du LLM
7777 user_prompt: Prompt utilisateur avec la question
78+ response_format: Format de réponse à utiliser
7879 temperature: Température pour la génération (0.0 = déterministe)
7980
8081 Returns:
@@ -84,7 +85,8 @@ def ask_llm(self, message: dict, temperature: float = 0.0) -> str:
8485 response = self .client .chat .completions .create (
8586 model = self .llm_model ,
8687 messages = message ,
87- temperature = temperature
88+ temperature = temperature ,
89+ response_format = response_format if response_format else None
8890 )
8991
9092 return response .choices [0 ].message .content .strip ()
@@ -114,13 +116,14 @@ def ask_llm(self, message: dict, temperature: float = 0.0) -> str:
114116 # logger.error(f"ERREUR lors de la seconde tentative LLM: {str(e2)}")
115117 return f"Erreur lors de l'appel au LLM: { str (e )[:100 ]} "
116118
117- def analyze_content (self , context : str , question : str , temperature : float = 0.0 ) -> str :
119+ def analyze_content (self , context : str , question : str , response_format : dict = None , temperature : float = 0.0 ) -> str :
118120 """
119121 Analyse le contexte fourni en utilisant l'API LLM.
120122
121123 Args:
122124 context: Contexte à analyser (texte complet ou liste de chunks)
123125 question: Question à poser au LLM
126+ response_format: Format de réponse à utiliser
124127 temperature: Température pour la génération (0.0 = déterministe)
125128
126129 Returns:
@@ -134,7 +137,7 @@ def analyze_content(self, context: str, question: str, temperature: float = 0.0)
134137 {"role" : "user" , "content" : user_prompt }
135138 ]
136139
137- return self .ask_llm (message , temperature )
140+ return self .ask_llm (message , response_format , temperature )
138141
139142# Fonction pour extraire le JSON de la réponse
140143def parse_json_response (response_text ):
@@ -156,34 +159,50 @@ def parse_json_response(response_text):
156159 return None , f"Erreur d'analyse: { str (e )} "
157160
158161# Fonction pour générer le prompt à partir des attributs à chercher
159- def get_prompt_from_attributes (dfAttributes : pd .DataFrame ):
162+ def get_prompt_from_attributes (df_attributes : pd .DataFrame ):
160163 question = """Extrait les informations clés et renvoie-les uniquement au format JSON spécifié, sans texte supplémentaire.
161164
162165 Format de réponse (commence par "{" et termine par "}") :
163166{
164167"""
165- for idx , row in dfAttributes .iterrows ():
168+ for idx , row in df_attributes .iterrows ():
166169 attr = row ["attribut" ]
167- if idx != dfAttributes .index [- 1 ]:
170+ if idx != df_attributes .index [- 1 ]:
168171 question += f""" "{ attr } ": "", \n """
169172 else :
170173 question += f""" "{ attr } ": "" \n """
171174 question += """}
172175
173176 Instructions d'extraction :\n \n """
174- for idx , row in dfAttributes .iterrows ():
177+ for idx , row in df_attributes .iterrows ():
175178 consigne = row ["consigne" ]
176- if idx != dfAttributes .index [- 1 ]:
179+ if idx != df_attributes .index [- 1 ]:
177180 question += f"""{ consigne } \n """
178181 else :
179182 question += f"""{ consigne } """
180183 return question
181184
185+ def create_response_format (df_attributes , classification ):
186+ l_output_field = select_attr (df_attributes , classification ).output_field .tolist ()
187+ response_format = {
188+ "type" : "json_schema" ,
189+ "json_schema" : {
190+ "name" : f"{ classification } " ,
191+ "strict" : True ,
192+ "schema" : {
193+ "type" : "object" ,
194+ "properties" : {output_field : {"type" : "string" } for output_field in l_output_field },
195+ "required" : list (df_attributes .output_field )
196+ }
197+ }
198+ }
199+ return response_format
200+
182201def df_analyze_content (api_key ,
183202 base_url ,
184203 llm_model ,
185204 df : pd .DataFrame ,
186- dfAttributes : pd .DataFrame ,
205+ df_attributes : pd .DataFrame ,
187206 temperature : float = 0.0 ,
188207 max_workers : int = 4 ,
189208 save_path : str = None ,
@@ -205,7 +224,7 @@ def df_analyze_content(api_key,
205224 dfResult ['llm_response' ] = None
206225 dfResult ['json_error' ] = None
207226
208- for attr in dfAttributes .attribut :
227+ for attr in df_attributes .attribut :
209228 dfResult [attr ] = None
210229
211230 llm_env = LLMEnvironment (
@@ -219,7 +238,8 @@ def process_row(idx):
219238 row = df .loc [idx ]
220239 classification = row ['classification' ]
221240 try :
222- question = get_prompt_from_attributes (select_attr (dfAttributes , classification ))
241+ question = get_prompt_from_attributes (select_attr (df_attributes , classification ))
242+ response_format = create_response_format (df_attributes , classification )
223243 context = row ['relevant_content' ]
224244
225245 if (context == "" ):
@@ -228,8 +248,8 @@ def process_row(idx):
228248 with log_execution_time (f"df_analyze_content({ row .filename } )" ):
229249 response = llm_env .analyze_content (context = context ,
230250 question = question ,
251+ response_format = response_format ,
231252 temperature = temperature )
232-
233253 data , error = parse_json_response (response )
234254
235255 result = {
@@ -238,7 +258,7 @@ def process_row(idx):
238258 }
239259
240260 if not error :
241- for attr in dfAttributes .attribut :
261+ for attr in df_attributes .attribut :
242262 result .update ({f'{ attr } ' : data .get (attr , '' )})
243263 else :
244264 print (f"Erreur lors de l'analyse du fichier { row ["filename" ]} : { error } " )
@@ -279,93 +299,3 @@ def process_row(idx):
279299def save_df_analyze_content_result (df : pd .DataFrame ):
280300 bulk_update_attachments (df , ['llm_response' , 'json_error' ])
281301
282-
283- def questionDesignation (row ):
284- context = ' ET ' .join (row ['infos' ])
285- question = f"""
286- Création d'une désignation et d'une description détaillée de la dépense
287-
288- Voici les informations disponibles sur la dépense :
289-
290- { context }
291- A partir de ces informations, tu dois produire :
292- 1. Un description, appelée "designation" : une description de la dépense en une phrase.
293- 2. Une description détaillée, appelée "description" : une description plus longue qui reprend tous les éléments importants fournis dans les informations ci-dessus.
294- 3. Tu dois IMPÉRATIVEMENT répondre UNIQUEMENT avec un JSON valide, sans aucun autre texte, avec cette structure exacte:
295- {{
296- "designation": la description en un paragraphe,
297- "description": la description detaillée à partir des éléments importants fournis dans les informations ci-dessus
298- }}
299- 4. Les informations seront fournies sous format JSON séparés par ET avec parfois de la redondance.
300- """
301- return question
302-
303- def df_create_designation (api_key ,
304- base_url ,
305- llm_model ,
306- df : pd .DataFrame ,
307- temperature : float = 0.0 ,
308- max_workers : int = 4 ,
309- save_path : str = None ,
310- directory_path : str = None ) -> pd .DataFrame :
311- dfResult = df [df ['json_error' ].isna ()].query ('nb_mot > 0' )\
312- .groupby ('num_EJ' )\
313- .agg ({
314- 'llm_response' : list
315- }).reset_index ()\
316- .rename (columns = {'llm_response' : 'infos' })\
317- .sort_values (by = 'num_EJ' , ascending = True )\
318- .copy ()
319- dfResult ['llm_response' ] = None
320- dfResult ['json_error' ] = None
321-
322- def process_row (idx ):
323- row = dfResult .iloc [idx ]
324- question = questionDesignation (row )
325- try :
326- llm_env = LLMEnvironment (
327- api_key = api_key ,
328- base_url = base_url ,
329- llm_model = llm_model
330- )
331- system_prompt = "Vous êtes un assistant IA qui analyse des documents juridiques."
332- message = [
333- {"role" : "system" , "content" : system_prompt },
334- {"role" : "user" , "content" : question }
335- ]
336- response = llm_env .ask_llm (message = message ,
337- temperature = temperature )
338- data , error = parse_json_response (response )
339-
340- result = {
341- 'llm_response' : response ,
342- 'json_error' : error
343- }
344-
345- if not error :
346- result .update ({
347- 'designation' : data .get ('designation' , '' ),
348- 'description' : data .get ('description' , '' )
349- })
350- else :
351- print (f"Erreur lors de l'analyse du fichier { row ['num_EJ' ]} : { error } " )
352- except Exception as e :
353- result = {
354- 'llm_response' : None ,
355- 'json_error' : f"Erreur lors de l'analyse: { str (e )} "
356- }
357- return idx , result
358-
359- with ThreadPoolExecutor (max_workers = max_workers ) as executor :
360- futures = [executor .submit (process_row , i ) for i in range (len (dfResult ))]
361-
362- for future in tqdm (futures , total = len (futures ), desc = "Traitement des désignations" ):
363- idx , result = future .result ()
364- for key , value in result .items ():
365- dfResult .at [idx , key ] = value
366-
367- if (save_path != None ):
368- dfResult .to_csv (f'{ save_path } /Designation_{ directory_path .split ("/" )[- 1 ]} _{ getDate ()} .csv' , index = False )
369- print (f"Liste des fichiers sauvegardées dans { save_path } /Designation_{ directory_path .split ("/" )[- 1 ]} _{ getDate ()} .csv" )
370-
371- return dfResult
0 commit comments