Agents.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import os
  2. from typing import Annotated , Sequence , TypedDict , Optional , List , Any
  3. from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
  4. from langgraph.graph.message import add_messages
  5. from dotenv import load_dotenv
  6. from langchain_openai import ChatOpenAI
  7. from langchain_groq import ChatGroq
  8. import pandas as pd
  9. from langfuse import get_client
  10. import json
  11. from tools import excel_code_interpreter
  12. from paddleocr import PaddleOCR
  13. from function import (
  14. preparer_image_zoom_hd,
  15. extraire_donnees_ocr,
  16. nettoyage_sortie_ocr,
  17. formater_donnees_section,
  18. sauvegarder_fichier_unique,
  19. to_points ,
  20. build_lines,
  21. cluster_lines ,
  22. merge_close_lines
  23. )
  24. from clean_DBSCAN import (
  25. transform_to_clean_markdown
  26. )
  27. load_dotenv()
  28. tools = [excel_code_interpreter]
  29. class AgentState(TypedDict) :
  30. messages : Annotated[Sequence[BaseMessage],add_messages]
  31. pdf_path : str
  32. pages : List[int]
  33. entreprise_name : str
  34. section_name : str
  35. markdown : str
  36. model_llama = ChatGroq(model="llama-3.3-70b-versatile")
  37. model_openai = ChatOpenAI(model="gpt-4.1" , temperature=0.2)
  38. model_ai = model_llama.bind_tools(tools)
  39. ocr = PaddleOCR(use_angle_cls=True, lang='fr', det_limit_side_len=10000, show_log=False)
  40. def agent_ocr(state: AgentState):
  41. pdf_path = state.get("pdf_path")
  42. pages = state.get("pages")
  43. texte_accumule = []
  44. all_points = []
  45. for page_index in pages:
  46. page_index = page_index -1
  47. try:
  48. img_finale = preparer_image_zoom_hd(pdf_path, page_index)
  49. raw_data = extraire_donnees_ocr(img_finale, ocr)
  50. data_propre = nettoyage_sortie_ocr(raw_data)
  51. # 🔥 garder structure brute
  52. points = to_points(data_propre)
  53. all_points.extend(points)
  54. # debug texte
  55. lignes_page = formater_donnees_section(data_propre, page_index)
  56. texte_accumule.extend(lignes_page)
  57. print(f"✅ Page {page_index + 1} traitée.")
  58. except Exception as e:
  59. print(f"❌ Erreur Page {page_index + 1}: {e}")
  60. # 🔥 DBSCAN GLOBAL (beaucoup plus robuste)
  61. clusters = cluster_lines(all_points, eps=0.5)
  62. lignes = build_lines(clusters)
  63. lignes = merge_close_lines(lignes)
  64. # 🔥 TEXTE pour LLM
  65. contenu_final = "\n".join(texte_accumule)
  66. message_ocr = HumanMessage(
  67. content=f"Voici les données OCR brutes :\n{contenu_final}"
  68. )
  69. markdown = transform_to_clean_markdown(lignes)
  70. return {
  71. "messages": [message_ocr],
  72. "lignes" : lignes,
  73. "markdown": markdown
  74. }
  75. def agent_extracteur(state:AgentState) :
  76. prompt = """
  77. Tu es un extracteur de tableaux QRT (Solvabilité II).
  78. Ton but est de transcrire les coordonnées spatiales OCR en une hiérarchie logique de données.
  79. Objectif : convertir le tableau en JSON sans perte d’information.
  80. Règles :
  81. - Utilise Rxxxx (lignes) et Cxxxx (colonnes) comme clés.
  82. - Copie STRICTEMENT les valeurs, sans calcul ni modification.
  83. - Supprime uniquement les espaces dans les nombres :
  84. "3 297 388" → 3297388
  85. - Ne tronque pas, n’arrondis pas, garde toute la précision.
  86. - Garde les 0.
  87. - Si pas de libellé pour un Rxxxx → "".
  88. - Optimise la structure (par lignes ou colonnes) pour réduire la taille.
  89. ⚠️ CONTRAINTE DE SORTIE (OBLIGATOIRE) :
  90. - Réponds UNIQUEMENT avec un objet JSON valide
  91. - NE PAS ajouter de texte avant ou après
  92. - NE PAS utiliser ```json ou ```
  93. - NE PAS commenter
  94. - NE PAS expliquer
  95. Sortie attendue (exemple strict) :
  96. {
  97. "labels": { "Rxxxx": "Rxxxx" },
  98. "data": {
  99. "Cxxxx": { "Rxxxx": 123456 }
  100. }
  101. }
  102. """
  103. msg = [
  104. SystemMessage(content=prompt),
  105. HumanMessage(content=json.dumps(state["markdown"]))
  106. ]
  107. response = model_llama.invoke(msg)
  108. return{"messages" : [response]}
  109. def agent_builder(state: AgentState):
  110. """
  111. Agent Builder (Version non-IA) :
  112. Prend le JSON de l'extracteur et génère physiquement le fichier Excel.
  113. """
  114. print(f"🏗️ Construction du fichier Excel pour : {state['entreprise_name']}...")
  115. try:
  116. # 1. Récupération du JSON depuis le dernier message de l'extracteur
  117. # On suppose que l'extracteur a renvoyé un dictionnaire ou une string JSON
  118. import json
  119. last_message_content = state["messages"][-1].content
  120. # Nettoyage si le LLM a mis des balises ```json
  121. if "```json" in last_message_content:
  122. last_message_content = last_message_content.split("```json")[1].split("```")[0].strip()
  123. payload = json.loads(last_message_content)
  124. labels = payload.get("labels", {})
  125. data_json = payload.get("data", {})
  126. # 2. Préparation de la structure du dossier
  127. folder = state['entreprise_name'].replace(" ", "_")
  128. if not os.path.exists(folder):
  129. os.makedirs(folder)
  130. output_file = os.path.join(folder, f"Rapport_{state['section_name']}.xlsx")
  131. # 3. Construction du DataFrame
  132. # On récupère toutes les colonnes et toutes les lignes uniques
  133. all_rows = list(labels.keys())
  134. all_cols = list(data_json.keys())
  135. # Création d'une matrice vide remplie de NaN
  136. df = pd.DataFrame(index=all_rows, columns=all_cols)
  137. # Remplissage des données
  138. for col, row_values in data_json.items():
  139. for row, val in row_values.items():
  140. df.at[row, col] = val
  141. # 4. Ajout de la colonne Libellé au début
  142. #df.insert(0, "Libellé", df.index.map(labels))
  143. # 5. Exportation avec formatage pro via XlsxWriter
  144. with pd.ExcelWriter(output_file, engine="xlsxwriter") as writer:
  145. df.to_excel(writer, sheet_name="QRT_Export", index=True)
  146. workbook = writer.book
  147. worksheet = writer.sheets["QRT_Export"]
  148. # --- Ajout de formats ---
  149. header_fmt = workbook.add_format({'bold': True, 'bg_color': '#1F3864', 'font_color': 'white', 'border': 1})
  150. num_fmt = workbook.add_format({'num_format': '#,##0', 'border': 1})
  151. label_fmt = workbook.add_format({'bg_color': '#D6E4F0', 'border': 1})
  152. # Appliquer les formats aux colonnes
  153. worksheet.set_column(1, 1, 60) # Libellé
  154. worksheet.set_column(0, 0, 15) # Code R
  155. worksheet.set_column(2, len(df.columns)+1, 18, num_fmt) # Valeurs
  156. success_msg = f"✓ Fichier Excel généré : {output_file} ({len(df)} lignes x {len(all_cols)} colonnes)"
  157. return {"messages": [HumanMessage(content=success_msg)]}
  158. except Exception as e:
  159. error_msg = f"❌ Erreur lors de la construction Excel : {str(e)}"
  160. print(error_msg)
  161. return {"messages": [HumanMessage(content=error_msg)]}