Skip to content

Commit e43b233

Browse files
Merge pull request #3 from betagouv/llm.json
llm json
2 parents 0e6ad20 + 441f996 commit e43b233

1 file changed

Lines changed: 34 additions & 104 deletions

File tree

app/processor/analyze_content.py

Lines changed: 34 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -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
140143
def 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+
182201
def 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):
279299
def 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

Comments
 (0)