Agents.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import os
  2. from typing import Annotated, Sequence, TypedDict, Optional , List
  3. from dotenv import load_dotenv
  4. from langchain_openai import ChatOpenAI
  5. from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage , AIMessage
  6. from langgraph.graph.message import add_messages
  7. from langchain_groq import ChatGroq
  8. from langfuse import get_client
  9. import pandas as pd
  10. from langfuse.langchain import CallbackHandler
  11. import json
  12. from langchain_community.chat_models import ChatOllama
  13. # Initialize Langfuse CallbackHandler for Langchain (tracing)
  14. langfuse_handler = CallbackHandler()
  15. # Import de tes outils corrigés
  16. from tools import excel_code_interpreter, search_tool , inspect_data , call_tool_and_format_for_ollama , robust_json_parse
  17. langfuse = get_client()
  18. load_dotenv()
  19. # 1. Définition du State
  20. class AgentState(TypedDict):
  21. messages: Annotated[Sequence[BaseMessage], add_messages]
  22. current_df_path: Optional[str]
  23. Data_colomns: Optional[str]
  24. generated_charts : List[str]
  25. # 2. Configuration du Modèle et des Outils
  26. #model =ChatGroq(model="llama-3.3-70b-versatile")
  27. #model_llama=ChatGoogleGenerativeAI(model="gemini-2.5-flash")
  28. #model_gpt = ChatOpenAI(model="gpt-5.3-codex" )
  29. model_gpt_5 = ChatOpenAI(model="o4-mini" )
  30. tools = [ search_tool , excel_code_interpreter, inspect_data ]
  31. # Local LLM :
  32. from langchain_experimental.llms.ollama_functions import OllamaFunctions
  33. from langchain_community.chat_models import ChatOllama
  34. # On crée une instance standard pour l'Analyseur
  35. analyseur_llm = ChatOllama(
  36. model="deepseek-coder-v2:16b-lite-instruct-q4_K_M",
  37. temperature=0,
  38. format="json" # On garde le format JSON ici
  39. )
  40. local_llm = OllamaFunctions(
  41. model="deepseek-coder-v2:16b-lite-instruct-q4_K_M",
  42. #model="llama3.1:8b",
  43. temperature=0,
  44. format="json",
  45. num_ctx=4096
  46. )
  47. model_with_tools = local_llm.bind_tools(tools)
  48. def agent_analyseur(state: AgentState):
  49. import os
  50. import pandas as pd
  51. # 1. PHASE D'INSPECTION (Identique)
  52. file_path = state.get("current_df_path")
  53. inspection_info = "Aucun fichier détecté."
  54. columns_list = []
  55. if file_path and os.path.exists(file_path):
  56. try:
  57. df_temp = pd.read_csv(file_path, nrows=5) if file_path.endswith('.csv') else pd.read_excel(file_path, nrows=5)
  58. columns_list = df_temp.columns.tolist()
  59. sample_data = df_temp.head(2).to_string()
  60. inspection_info = (
  61. f"COLONNES EXACTES : {columns_list}\n"
  62. f"APERÇU DES DONNÉES :\n{sample_data}"
  63. )
  64. except Exception as e:
  65. inspection_info = f"⚠️ Erreur lors de l'inspection : {e}"
  66. # 2. RÉCUPÉRATION DE LA QUERY
  67. user_query = state["messages"][-1].content if state["messages"] else "Pas de question."
  68. # 3. CONSTRUCTION DU PROMPT DYNAMIQUE (Comme l'Exécuteur)
  69. # On définit les règles métier ici
  70. prompt = (
  71. "### SYSTEM ROLE ###\n"
  72. "Tu es l'Analyseur de Dataltist. Ton rôle est de créer un plan d'action logique.\n"
  73. "Tu dois répondre EXCLUSIVEMENT au format JSON.\n\n"
  74. "### DONNÉES DISPONIBLES ###\n"
  75. f"{inspection_info}\n\n"
  76. "### RÈGLES CRITIQUES ###\n"
  77. "1. PAS DE CODE : Ne génère jamais de Python.\n"
  78. f"2. COLONNES : Utilise uniquement {columns_list}.\n"
  79. "3. PLAN : Détaille les étapes (Somme, Moyenne, Regroupement, etc.).\n\n"
  80. "### STRUCTURE JSON ATTENDUE ###\n"
  81. "{\n"
  82. " \"plan\": [\"étape 1\", \"étape 2\"],\n"
  83. " \"colonnes_utilisees\": [\"col1\", \"col2\"]\n"
  84. "}\n"
  85. )
  86. #
  87. # 4. PRÉPARATION DU MESSAGE UNIQUE (La méthode qui marche pour l'Exécuteur)
  88. # On fusionne le prompt et la question utilisateur dans un seul HumanMessage
  89. full_input = f"{prompt}\n\n### QUESTION UTILISATEUR ###\n{user_query}"
  90. final_messages = [HumanMessage(content=full_input)]
  91. config_analyseur = {
  92. "callbacks": [langfuse_handler],
  93. "metadata": {"agent_name": "Analyseur"}
  94. }
  95. # 5. APPEL DU MODÈLE
  96. # Note : On utilise local_llm (ou analyseur_llm si tu as séparé)
  97. response = local_llm.invoke(final_messages, config=config_analyseur)
  98. return {"messages": [response]}
  99. def agent_executor(state: AgentState):
  100. # On récupère le nom exact du fichier depuis le state
  101. full_path = state.get("current_df_path") or "data/default.csv"
  102. file_name = os.path.basename(full_path) # Récupère 'mon_fichier.csv'
  103. # On construit le prompt en injectant le nom dynamiquement
  104. prompt = (
  105. "### SYSTEM ROLE ###\n"
  106. "Tu es un moteur d'exécution Python. Réponds EXCLUSIVEMENT en JSON.\n\n"
  107. "### FICHIER ACTUEL (IMPORTANT) ###\n"
  108. f"Tu dois travailler UNIQUEMENT sur le fichier : '{file_name}'\n"
  109. f"Le chemin à utiliser dans ton code est : 'data/{file_name}'\n\n"
  110. "### INSTRUCTIONS DE CODE ###\n"
  111. "1. Commence ton code par l'import des librairies.\n"
  112. f"2. Charge le fichier avec : df = pd.read_csv('data/{file_name}') (ou read_excel).\n"
  113. "3. Termine TOUJOURS par une variable 'result' contenant ton analyse.\n\n"
  114. "### EXEMPLE DE FORMAT ATTENDU ###\n"
  115. "{\n"
  116. " \"tool\": \"excel_code_interpreter\",\n"
  117. " \"tool_input\": {\n"
  118. f" \"file_path\": \"data/{file_name}\",\n"
  119. " \"code\": \"import pandas as pd\\n# Ton code ici...\"\n"
  120. " }\n"
  121. "}"
  122. )
  123. # Nettoyage des messages pour Ollama (comme avant)
  124. cleaned_messages = []
  125. for m in state["messages"]:
  126. if m.type == "tool":
  127. cleaned_messages.append(HumanMessage(content=f"Résultat : {m.content}"))
  128. else:
  129. cleaned_messages.append(m)
  130. # On place le prompt dynamique en premier
  131. final_messages = [HumanMessage(content=prompt)] + cleaned_messages
  132. return {"messages": [model_with_tools.invoke(final_messages)]}
  133. def agent_reporter(state : AgentState) :
  134. prompt_reporter = (
  135. "Tu es l'Agent de Reporting de Dataltist.\n"
  136. "Ton rôle : Convertir les logs de l'Exécuteur en une synthèse factuelle pour l'utilisateur.\n\n"
  137. "RÈGLES CRITIQUES (ANTI-SILENCE) :\n"
  138. "- DISPONIBILITÉ : Tu dois TOUJOURS générer une réponse, même pour confirmer une erreur ou une absence de données.\n"
  139. "- STRUCTURE : Utilise des tirets pour lister les points clés.\n"
  140. "- SYNTHÈSE : Si l'Exécuteur donne 'result = 42', écris 'Résultat : 42'.\n"
  141. "- FICHIERS : Liste uniquement le nom des fichiers (ex: graphe_incendie.png), JAMAIS le chemin 'outputs/'.\n\n"
  142. "FORMAT IMPÉRATIF :\n"
  143. "1. Résumé des données traitées.\n"
  144. "2. Valeurs calculées (moyennes, totaux, etc.).\n"
  145. "3. Liste des visuels générés (si présents).\n\n"
  146. "4. L'affichage doit etre claire , tu laisse les espaces .\n\n"
  147. "INTERDICTIONS :\n"
  148. "- Pas de politesse inutile ('Voici le rapport...', 'J'espère que...').\n"
  149. "- Pas de répétition des étapes techniques de l'agent.\n"
  150. "- Pas de phrases vides si l'exécution a échoué : explique brièvement l'échec."
  151. "RÈGLES D'AFFICHAGE DES FICHIERS :\n"
  152. "1. CITATION : Si l'Exécuteur a généré des fichiers (images ou données), cite-les obligatoirement.\n"
  153. "2. FORMAT : Utilise TOUJOURS ce format exact pour introduire un fichier : \n"
  154. " '📊 Visualisation générée : [nom_du_fichier.png]'\n"
  155. " '📥 Fichier disponible : [nom_du_fichier.csv/xlsx]'\n"
  156. "3. SANS CHEMIN : Ne mentionne JAMAIS le dossier 'outputs/' ou 'data/'. Utilise uniquement le nom final du fichier.\n\n"
  157. )
  158. messages = [SystemMessage(content=prompt_reporter)] + state["messages"]
  159. response = local_llm.invoke(messages )
  160. return{"messages" : [response]}