Agents.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  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 , ToolMessage
  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. OLLAMA_BASE_URL = os.getenv("OLLAMA_URL", "http://ollama1-server:11434")
  35. local_llm = ChatOllama(
  36. model="deepseek-coder-v2:16b-lite-instruct-q4_K_M",
  37. base_url=OLLAMA_BASE_URL,
  38. format="json",
  39. temperature=0
  40. )
  41. local_llm_fonction = OllamaFunctions(
  42. model="deepseek-coder-v2:16b-lite-instruct-q4_K_M",
  43. #model="llama3.1:8b",
  44. base_url=OLLAMA_BASE_URL,
  45. temperature=0,
  46. format="json",
  47. num_ctx=4096
  48. )
  49. local_llm_reporter = ChatOllama(
  50. model="llama3.1:8b",
  51. base_url=OLLAMA_BASE_URL,
  52. temperature=0,
  53. )
  54. model_with_tools = local_llm_fonction.bind_tools(tools)
  55. def agent_analyseur(state: AgentState):
  56. import os
  57. import pandas as pd
  58. # 1. PHASE D'INSPECTION (Identique)
  59. file_path = state.get("current_df_path")
  60. inspection_info = "Aucun fichier détecté."
  61. columns_list = []
  62. if file_path and os.path.exists(file_path):
  63. try:
  64. df_temp = pd.read_csv(file_path, nrows=5) if file_path.endswith('.csv') else pd.read_excel(file_path, nrows=5)
  65. columns_list = df_temp.columns.tolist()
  66. sample_data = df_temp.head(2).to_string()
  67. inspection_info = (
  68. f"COLONNES EXACTES : {columns_list}\n"
  69. f"APERÇU DES DONNÉES :\n{sample_data}"
  70. )
  71. except Exception as e:
  72. inspection_info = f"⚠️ Erreur lors de l'inspection : {e}"
  73. # 2. RÉCUPÉRATION DE LA QUERY
  74. user_query = state["messages"][-1].content if state["messages"] else "Pas de question."
  75. # 3. CONSTRUCTION DU PROMPT DYNAMIQUE (Comme l'Exécuteur)
  76. # On définit les règles métier ici
  77. prompt = (
  78. "### SYSTEM ROLE ###\n"
  79. "Tu es l'Analyseur de Dataltist. Ton rôle est de créer un plan d'action logique.\n"
  80. "Tu dois répondre EXCLUSIVEMENT au format JSON.\n\n"
  81. "### DONNÉES DISPONIBLES ###\n"
  82. f"{inspection_info}\n\n"
  83. "### RÈGLES CRITIQUES ###\n"
  84. "1. PAS DE CODE : Ne génère jamais de Python.\n"
  85. f"2. COLONNES : Utilise uniquement {columns_list}.\n"
  86. "3. PLAN : Détaille les étapes (Somme, Moyenne, Regroupement, etc.).\n\n"
  87. "### STRUCTURE JSON ATTENDUE ###\n"
  88. "{\n"
  89. " \"plan\": [\"étape 1\", \"étape 2\"],\n"
  90. " \"colonnes_utilisees\": [\"col1\", \"col2\"]\n"
  91. "}\n"
  92. )
  93. #
  94. # 4. PRÉPARATION DU MESSAGE UNIQUE (La méthode qui marche pour l'Exécuteur)
  95. # On fusionne le prompt et la question utilisateur dans un seul HumanMessage
  96. full_input = f"{prompt}\n\n### QUESTION UTILISATEUR ###\n{user_query}"
  97. final_messages = [HumanMessage(content=full_input)]
  98. config_analyseur = {
  99. "callbacks": [langfuse_handler],
  100. "metadata": {"agent_name": "Analyseur"}
  101. }
  102. # 5. APPEL DU MODÈLE
  103. # Note : On utilise local_llm (ou analyseur_llm si tu as séparé)
  104. # response = local_llm_chat.invoke(final_messages, config=config_analyseur)
  105. response = local_llm.invoke(final_messages, config=config_analyseur)
  106. return {"messages": [response]}
  107. def agent_executor(state: AgentState):
  108. # On récupère le nom exact du fichier depuis le state
  109. full_path = state.get("current_df_path") or "data/default.csv"
  110. file_name = os.path.basename(full_path) # Récupère 'mon_fichier.csv'
  111. # On construit le prompt en injectant le nom dynamiquement
  112. prompt = (
  113. "### SYSTEM ROLE ###\n"
  114. "Tu es un moteur d'exécution Python. Réponds EXCLUSIVEMENT au format JSON.\n\n"
  115. "### FICHIER ACTUEL ###\n"
  116. f"Fichier : '{file_name}' | Chemin : 'data/{file_name}'\n\n"
  117. "### INSTRUCTIONS DE CODE & SÉCURITÉ ###\n"
  118. "1. Imports : pandas, matplotlib.pyplot.\n"
  119. f"2. Chargement : df = pd.read_csv('data/{file_name}')\n"
  120. "3. SYNTAXE (Apostrophes) : Utilise TOUJOURS des doubles guillemets (\"\") pour les labels.\n\n"
  121. "4. GRAPHIQUES (SAUVEGARDE) :\n"
  122. " - INTERDIT : plt.show()\n"
  123. " - OBLIGATOIRE : plt.savefig('outputs/nom_du_graphe.png')\n\n"
  124. "5. DATA REPORTING (CRUCIAL POUR LE REPORTER) :\n"
  125. " - AVANT de générer le graphique, calcule les stats clés (Somme, Moyenne, Top 3).\n"
  126. " - Utilise print() pour afficher ces chiffres afin que le Reporter les reçoive.\n\n"
  127. "6. VARIABLE 'result' :\n"
  128. " - Termine ton code par une variable 'result' qui récapitule :\n"
  129. " 1. Les fichiers générés (ex: 'outputs/ventes.png')\n"
  130. " 2. Un résumé textuel des chiffres affichés par les graphiques.\n\n"
  131. "### EXEMPLE DE CODE INTERNE ATTENDU ###\n"
  132. "import pandas as pd\n"
  133. "import matplotlib.pyplot as plt\n"
  134. "df = pd.read_csv('...')\n"
  135. "stats = df.groupby('Pays')['Ventes'].sum()\n"
  136. "print(stats) # <--- Ceci permet au Reporter de voir les chiffres !\n"
  137. "plt.bar(stats.index, stats.values)\n"
  138. "plt.savefig('outputs/ventes_pays.png')\n"
  139. "result = f\"Graphique 'ventes_pays.png' généré. Chiffres clés : {stats.to_dict()}\"\n"
  140. )
  141. # Nettoyage des messages pour Ollama (comme avant)
  142. cleaned_messages = []
  143. for m in state["messages"]:
  144. if m.type == "tool":
  145. cleaned_messages.append(HumanMessage(content=f"Résultat : {m.content}"))
  146. else:
  147. cleaned_messages.append(m)
  148. # On place le prompt dynamique en premier
  149. final_messages = [HumanMessage(content=prompt)] + cleaned_messages
  150. return {"messages": [model_with_tools.invoke(final_messages)]}
  151. def agent_reporter(state : AgentState) :
  152. prompt_reporter = (
  153. ### RÔLE ###
  154. """ Tu es un Expert Consultant en Data Visualisation et Analyse Statistique.
  155. Ton objectif est de transformer des données techniques brutes en un rapport stratégique clair, élégant et actionnable.
  156. ### DIRECTIVES DE RÉDACTION ###
  157. 1. INTERPRÉTATION : Ne te contente pas de citer les chiffres. Explique ce qu'ils signifient Utilise correctement les chiffres retournés par les outils : assure-toi de bien les comprendre et de les avoir triés.
  158. 2. STRUCTURE : Utilise des titres (##), du gras (**), et des listes à puces pour la clarté.
  159. 3. VISUELS : Mentionne les graphiques générés avec l'émoji 📊.
  160. 4. TON : Professionnel, dynamique et rassurant.
  161. 5. PAS DE JSON : Ta réponse doit être uniquement du texte formaté en Markdown.
  162. ### STRUCTURE DU RAPPORT ###
  163. ## 📝 Synthèse de l'Analyse
  164. (Un paragraphe fluide qui résume la situation globale).
  165. ## 💡 Points Clés & Insights
  166. - **[Point 1]** : Explication...
  167. - **[Point 2]** : Explication...
  168. ## 📊 Visualisations Disponibles
  169. - [Nom du fichier.png] : Brève description de ce que le graphique démontre.
  170. ## 🚀 Recommandations si tu n'as
  171. (Une ou deux phrases sur la prochaine étape suggérée)."""
  172. )
  173. all_messages = state["messages"]
  174. # On ne garde que :
  175. # 1. Le premier message (User) pour le contexte
  176. # 2. Le DERNIER message de l'IA (le code qui a marché)
  177. # 3. Le DERNIER message de l'outil (les données réelles)
  178. important_messages = [
  179. all_messages[0], # La question de l'utilisateur
  180. [m for m in all_messages if isinstance(m, AIMessage)][-1], # Le dernier code
  181. [m for m in all_messages if isinstance(m, ToolMessage)][-1] # Le dernier résultat
  182. ]
  183. # Nettoyage des messages pour Ollama (comme avant)
  184. cleaned_messages = []
  185. for m in important_messages :
  186. if m.type == "tool":
  187. cleaned_messages.append(HumanMessage(content=f"Résultat : {m.content}"))
  188. else:
  189. cleaned_messages.append(m)
  190. # On place le prompt dynamique en premier
  191. final_messages = [HumanMessage(content=prompt_reporter)] + cleaned_messages
  192. return {"messages": [local_llm_reporter.invoke(final_messages)]}