Agents.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  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
  6. from langgraph.graph.message import add_messages
  7. from langchain_google_genai import ChatGoogleGenerativeAI
  8. from langchain_groq import ChatGroq
  9. from langfuse import get_client
  10. import pandas as pd
  11. from langfuse.langchain import CallbackHandler
  12. # Initialize Langfuse CallbackHandler for Langchain (tracing)
  13. langfuse_handler = CallbackHandler()
  14. # Import de tes outils corrigés
  15. from tools import excel_code_interpreter, search_tool , inspect_data
  16. langfuse = get_client()
  17. load_dotenv()
  18. # 1. Définition du State
  19. class AgentState(TypedDict):
  20. messages: Annotated[Sequence[BaseMessage], add_messages]
  21. current_df_path: Optional[str]
  22. generated_charts : List[str]
  23. # 2. Configuration du Modèle et des Outils
  24. model =ChatGroq(model="llama-3.3-70b-versatile")
  25. #model_llama=ChatGoogleGenerativeAI(model="gemini-2.5-flash")
  26. model_gpt = ChatOpenAI(model="gpt-5.3-codex" )
  27. model_gpt_5 = ChatOpenAI(model="o4-mini" )
  28. tools = [ search_tool , excel_code_interpreter, inspect_data ]
  29. model_with_tools = model_gpt.bind_tools(tools)
  30. # 3. Définition des Nœuds
  31. def agent_analyseur(state: AgentState):
  32. # --- PHASE D'INSPECTION AUTOMATIQUE ---
  33. # On récupère le chemin du fichier depuis le state
  34. file_path = state.get("current_df_path")
  35. inspection_info = ""
  36. if file_path and os.path.exists(file_path):
  37. try:
  38. # Lecture des 5 premières lignes pour comprendre la structure
  39. df_temp = pd.read_csv(file_path, nrows=5) if file_path.endswith('.csv') else pd.read_excel(file_path, nrows=5)
  40. columns_list = df_temp.columns.tolist()
  41. sample_data = df_temp.head(2).to_string()
  42. inspection_info = f"\n\n### DONNÉES RÉELLES DU FICHIER :\n- Colonnes détectées : {columns_list}\n- Aperçu des données :\n{sample_data}"
  43. except Exception as e:
  44. inspection_info = f"\n\n⚠️ Erreur lors de l'inspection du fichier : {e}"
  45. # --- CONSTRUCTION DU PROMPT AVEC LES DONNÉES RÉELLES ---
  46. prompt = (
  47. "Tu es l'Analyseur Stratégique de Dataltist. Ton rôle est de définir le 'QUOI' faire, pas le 'COMMENT'.\n\n"
  48. "### RÈGLES D'OR :\n"
  49. "1. PAS DE CODE : Ne génère JAMAIS de blocs de code Python (```python). C'est le rôle de l'Exécuteur.\n"
  50. "2. PLAN D'ACTION : Liste les étapes logiques en utilisant les noms de colonnes exacts.\n"
  51. "3. INSPECTION : Base-toi uniquement sur les colonnes détectées ci-dessous.\n"
  52. "4. CONCISION : Sois une tour de contrôle, donne des ordres clairs et précis.\n"
  53. f"{inspection_info}"
  54. )
  55. config_analyseur = {
  56. "callbacks": [langfuse_handler],
  57. "metadata": {"agent_name": "Analyseur"},
  58. "tags": ["Dataltist", "Planning"]
  59. }
  60. msg = [SystemMessage(content=prompt)] + state["messages"]
  61. response = model.invoke(msg , config=config_analyseur)
  62. # On retourne la réponse de l'IA qui contient maintenant le plan basé sur les vraies colonnes
  63. return {"messages": [response]}
  64. def agent_executor(state: AgentState):
  65. # 1. On récupère le chemin actuel depuis le State
  66. # Si current_df_path est None, on met data.xlsx par défaut
  67. file_path = state.get("current_df_path") or "data.xlsx"
  68. clean_path = file_path.replace("\\", "/")
  69. # 2. On construit un prompt qui contient le VRAI chemin
  70. prompt = (
  71. f"Tu es un Data Scientist expert spécialisé en automatisation. Le fichier cible est disponible ici : '{file_path}'.\n\n"
  72. "PROTOCOLE D'ACTION IMPÉRATIF :\n"
  73. "1. CONFIANCE STRATÉGIQUE : Utilise les noms de colonnes fournis par l'Analyseur ( Obligatoire , change pas ).\n"
  74. "2. RECHERCHE EXTERNE : Utilise 'search_tool' si des données manquent.\n"
  75. "3. ZÉRO SIMULATION : Ne jamais inventer de données.\n"
  76. "4. LOGIQUE PANDAS : Utilise 'df' pour filtrer les données exactes.\n"
  77. "5. CODE AUTONOME : Ton script Python doit être complet et prêt à l'exécution. \n"
  78. f" - Tu DOIS impérativement définir la variable `file_path = '{clean_path}'` au début de ton code.\n"
  79. " - Inclus tous les imports (pandas as pd, matplotlib.pyplot as plt, etc.).\n"
  80. " - Termine impérativement par 'result = ...'.\n"
  81. "6. PERSISTANCE : Utilise 'plt.savefig('outputs/nom_du_graphe.png')' et ferme la figure avec 'plt.close()'.\n\n"
  82. "Tu es un moteur d'exécution froid et précis. Pas de discours.\n"
  83. "STRUCTURE DE RÉPONSE :\n"
  84. "- 'Statut : Code exécuté avec succès.'\n"
  85. "- 'Fichiers : [liste des .png générés]'\n"
  86. "- 'Validation : Les données de [Colonnes] ont été traitées.'\n"
  87. )
  88. # 3. Préparation des messages
  89. messages = [SystemMessage(content=prompt)] + state["messages"]
  90. config_executor= {
  91. "callbacks": [langfuse_handler],
  92. "metadata": {"agent_name": "executor"},
  93. "tags": ["Dataltist", "executor"]
  94. }
  95. # 4. Appel du modèle avec les outils
  96. response = model_with_tools.invoke(messages , config= config_executor )
  97. return {"messages": [response]}
  98. def agent_reporter(state : AgentState) :
  99. prompt_reporter = (
  100. "Tu es l'Agent de Reporting de Dataltist.\n"
  101. "Ton rôle : Convertir les logs de l'Exécuteur en une synthèse factuelle pour l'utilisateur.\n\n"
  102. "RÈGLES CRITIQUES (ANTI-SILENCE) :\n"
  103. "- DISPONIBILITÉ : Tu dois TOUJOURS générer une réponse, même pour confirmer une erreur ou une absence de données.\n"
  104. "- STRUCTURE : Utilise des tirets pour lister les points clés.\n"
  105. "- SYNTHÈSE : Si l'Exécuteur donne 'result = 42', écris 'Résultat : 42'.\n"
  106. "- FICHIERS : Liste uniquement le nom des fichiers (ex: graphe_incendie.png), JAMAIS le chemin 'outputs/'.\n\n"
  107. "FORMAT IMPÉRATIF :\n"
  108. "1. Résumé des données traitées.\n"
  109. "2. Valeurs calculées (moyennes, totaux, etc.).\n"
  110. "3. Liste des visuels générés (si présents).\n\n"
  111. "4. L'affichage doit etre claire , tu laisse les espaces .\n\n"
  112. "INTERDICTIONS :\n"
  113. "- Pas de politesse inutile ('Voici le rapport...', 'J'espère que...').\n"
  114. "- Pas de répétition des étapes techniques de l'agent.\n"
  115. "- Pas de phrases vides si l'exécution a échoué : explique brièvement l'échec."
  116. "RÈGLES D'AFFICHAGE DES FICHIERS :\n"
  117. "1. CITATION : Si l'Exécuteur a généré des fichiers (images ou données), cite-les obligatoirement.\n"
  118. "2. FORMAT : Utilise TOUJOURS ce format exact pour introduire un fichier : \n"
  119. " '📊 Visualisation générée : [nom_du_fichier.png]'\n"
  120. " '📥 Fichier disponible : [nom_du_fichier.csv/xlsx]'\n"
  121. "3. SANS CHEMIN : Ne mentionne JAMAIS le dossier 'outputs/' ou 'data/'. Utilise uniquement le nom final du fichier.\n\n"
  122. )
  123. messages = [SystemMessage(content=prompt_reporter)] + state["messages"]
  124. response = model.invoke(messages )
  125. return{"messages" : [response]}