Agents.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  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 langchain_google_genai import ChatGoogleGenerativeAI
  12. from tools import excel_code_interpreter
  13. from paddleocr import PaddleOCR
  14. from function import (
  15. preparer_image_zoom_hd,
  16. extraire_donnees_ocr,
  17. nettoyage_sortie_ocr,
  18. formater_donnees_section,
  19. sauvegarder_fichier_unique,
  20. to_points ,
  21. build_lines,
  22. cluster_lines ,
  23. merge_close_lines
  24. )
  25. from clean_DBSCAN import (
  26. transform_to_clean_markdown
  27. )
  28. load_dotenv()
  29. tools = [excel_code_interpreter]
  30. class AgentState(TypedDict) :
  31. messages : Annotated[Sequence[BaseMessage],add_messages]
  32. pdf_path : str
  33. pages : List[int]
  34. page : List[int]
  35. entreprise_name : str
  36. section_name : str
  37. lignes : str
  38. markdown : str
  39. model_gemini = ChatGoogleGenerativeAI(model="gemini-2.5-flash")
  40. model_llama = ChatGroq(model="llama-3.3-70b-versatile")
  41. model_openai = ChatOpenAI(model="gpt-4o-mini" , temperature=0.2)
  42. model_ai = model_llama.bind_tools(tools)
  43. ocr = PaddleOCR(use_angle_cls=True, lang='fr', det_limit_side_len=10000, show_log=False)
  44. def agent_ocr(state: AgentState):
  45. pdf_path = state.get("pdf_path")
  46. # On récupère la page unique envoyée par le main
  47. page_val = state.get("page")
  48. if page_val is None:
  49. raise ValueError("page est None dans state")
  50. # Conversion en entier au cas où
  51. try:
  52. page_index = int(page_val) - 1
  53. except TypeError:
  54. # Si c'est une liste [45], on prend le premier élément
  55. page_index = int(page_val[0]) - 1
  56. all_points = []
  57. texte_accumule = []
  58. try:
  59. # Traitement de la page unique
  60. img_finale = preparer_image_zoom_hd(pdf_path, page_index)
  61. raw_data = extraire_donnees_ocr(img_finale, ocr)
  62. data_propre = nettoyage_sortie_ocr(raw_data)
  63. # Structure spatiale
  64. points = to_points(data_propre)
  65. all_points.extend(points)
  66. # Texte pour le LLM
  67. lignes_page = formater_donnees_section(data_propre, page_index)
  68. texte_accumule.extend(lignes_page)
  69. print(f" Page {page_index + 1} traitée par l'OCR.")
  70. except Exception as e:
  71. print(f"Erreur lors de l'OCR Page {page_index + 1}: {e}")
  72. raise e
  73. # DBSCAN et Markdown
  74. clusters = cluster_lines(all_points, eps=0.5)
  75. lignes = build_lines(clusters)
  76. lignes = merge_close_lines(lignes)
  77. markdown = transform_to_clean_markdown(lignes)
  78. contenu_final = "\n".join(texte_accumule)
  79. return {
  80. "points" : points ,
  81. "messages": [HumanMessage(content=f"Voici les données OCR brutes :\n{contenu_final}")],
  82. "clusters" : clusters ,
  83. "lignes" : lignes ,
  84. "markdown": markdown
  85. }
  86. def agent_extracteur(state: AgentState):
  87. prompt = """
  88. Tu es un extracteur de tableaux QRT (Solvabilité II).
  89. Objectif : convertir le tableau markdown en JSON sans AUCUNE modification.
  90. Règles STRICTES :
  91. - La position de chaque valeur dans le tableau est ABSOLUE et NE DOIT PAS être modifiée.
  92. - Si une colonne contient 0, garde 0. Ne déplace jamais une valeur vers une autre colonne.
  93. - Utilise Rxxxx (lignes) et Cxxxx (colonnes) comme clés directement.
  94. - Copie STRICTEMENT les valeurs dans leur colonne exacte.
  95. - Supprime uniquement les espaces dans les nombres : "3 297 388" → 3297388.
  96. - Une colonne à 0 reste à 0, même si une valeur non-nulle existe dans une colonne adjacente.
  97. - Structure attendue : {"Cxxxx": {"Rxxxx": valeur, ...}, ...}
  98. ⚠️ CONTRAINTE DE SORTIE :
  99. - Réponds UNIQUEMENT avec l'objet JSON.
  100. - PAS de markdown (```json), PAS de texte, PAS d'explications.
  101. - NE PAS réorganiser, NE PAS interpréter, NE PAS corriger les données.
  102. Tableau à convertir :
  103. {markdown}
  104. """
  105. markdown_content = state.get("markdown", "")
  106. if not markdown_content or str(markdown_content).strip() == "":
  107. backup_content = json.dumps(state.get("lignes", "Aucune donnée trouvée"))
  108. input_content = f"Note : Le markdown était vide. Voici les lignes brutes :\n{backup_content}"
  109. else:
  110. input_content = markdown_content
  111. msg = [
  112. SystemMessage(content=prompt),
  113. HumanMessage(content=json.dumps(input_content))
  114. ]
  115. response = model_llama.invoke(msg)
  116. return {"messages": [response]}
  117. def agent_builder(state: AgentState):
  118. print(f" Construction du fichier Excel pour : {state['entreprise_name']}...")
  119. try:
  120. import json
  121. import pandas as pd
  122. import os
  123. import re
  124. # 1. Extraction du contenu JSON pur
  125. content = state["messages"][-1].content
  126. # Nettoyage au cas où le modèle aurait ajouté des balises markdown malgré les consignes
  127. content = re.sub(r'```json|```', '', content).strip()
  128. data_json = json.loads(content)
  129. # 2. Gestion du chemin de sauvegarde (identique)
  130. base_outputs = os.path.join("..", "04 - Outputs")
  131. match = re.search(r'(\d{4})', state['entreprise_name'])
  132. annee = match.group(1) if match else "2025"
  133. nom_entreprise = state['entreprise_name'].split('_')[0].replace(" ", "_")
  134. target_folder = os.path.join(base_outputs, annee, nom_entreprise)
  135. if not os.path.exists(target_folder):
  136. os.makedirs(target_folder)
  137. output_file = os.path.join(target_folder, f"Rapport_{state['section_name']}.xlsx")
  138. # 3. Construction du DataFrame à partir de 'data_json' uniquement
  139. # On récupère l'ensemble unique de tous les Rxxxx présents dans toutes les colonnes
  140. all_rows = sorted(list(set(r for col in data_json.values() for r in col.keys())))
  141. all_cols = sorted(list(data_json.keys()))
  142. df = pd.DataFrame(index=all_rows, columns=all_cols)
  143. for col, row_values in data_json.items():
  144. for row, val in row_values.items():
  145. df.at[row, col] = val
  146. # 4. Exportation
  147. with pd.ExcelWriter(output_file, engine="xlsxwriter") as writer:
  148. df.to_excel(writer, sheet_name="QRT_Export", index=True)
  149. # Ajoute ici tes formats xlsxwriter si nécessaire
  150. success_msg = f"✓ Sauvegardé dans : {output_file}"
  151. print(success_msg)
  152. return {"messages": [HumanMessage(content=success_msg)]}
  153. except Exception as e:
  154. error_msg = f" Erreur construction Excel : {str(e)}"
  155. print(error_msg)
  156. return {"messages": [HumanMessage(content=error_msg)]}