import os from typing import Annotated, Sequence, TypedDict, Optional , List from dotenv import load_dotenv from langchain_openai import ChatOpenAI from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage , AIMessage , ToolMessage from langgraph.graph.message import add_messages from langchain_groq import ChatGroq from langfuse import get_client import pandas as pd from langfuse.langchain import CallbackHandler import json from langchain_community.chat_models import ChatOllama # Initialize Langfuse CallbackHandler for Langchain (tracing) langfuse_handler = CallbackHandler() # Import de tes outils corrigés from tools import excel_code_interpreter, search_tool , inspect_data , call_tool_and_format_for_ollama , robust_json_parse langfuse = get_client() load_dotenv() # 1. Définition du State class AgentState(TypedDict): messages: Annotated[Sequence[BaseMessage], add_messages] current_df_path: Optional[str] Data_colomns: Optional[str] generated_charts : List[str] # 2. Configuration du Modèle et des Outils #model =ChatGroq(model="llama-3.3-70b-versatile") #model_llama=ChatGoogleGenerativeAI(model="gemini-2.5-flash") #model_gpt = ChatOpenAI(model="gpt-5.3-codex" ) #model_gpt_5 = ChatOpenAI(model="o4-mini" ) tools = [ search_tool , excel_code_interpreter, inspect_data ] # Local LLM : from langchain_experimental.llms.ollama_functions import OllamaFunctions from langchain_community.chat_models import ChatOllama # On crée une instance standard pour l'Analyseur local_llm_chat = ChatOllama( model="llama3.1:8b", temperature=0, format="json" # On garde le format JSON ici ) local_llm_fonction = OllamaFunctions( model="deepseek-coder-v2:16b-lite-instruct-q4_K_M", #model="llama3.1:8b", temperature=0, format="json", num_ctx=4096 ) local_llm_reporter = ChatOllama( model="llama3.1:8b", temperature=0, ) model_with_tools = local_llm_fonction.bind_tools(tools) def agent_analyseur(state: AgentState): import os import pandas as pd # 1. PHASE D'INSPECTION (Identique) file_path = state.get("current_df_path") inspection_info = "Aucun fichier détecté." columns_list = [] if file_path and os.path.exists(file_path): try: df_temp = pd.read_csv(file_path, nrows=5) if file_path.endswith('.csv') else pd.read_excel(file_path, nrows=5) columns_list = df_temp.columns.tolist() sample_data = df_temp.head(2).to_string() inspection_info = ( f"COLONNES EXACTES : {columns_list}\n" f"APERÇU DES DONNÉES :\n{sample_data}" ) except Exception as e: inspection_info = f"⚠️ Erreur lors de l'inspection : {e}" # 2. RÉCUPÉRATION DE LA QUERY user_query = state["messages"][-1].content if state["messages"] else "Pas de question." # 3. CONSTRUCTION DU PROMPT DYNAMIQUE (Comme l'Exécuteur) # On définit les règles métier ici prompt = ( "### SYSTEM ROLE ###\n" "Tu es l'Analyseur de Dataltist. Ton rôle est de créer un plan d'action logique.\n" "Tu dois répondre EXCLUSIVEMENT au format JSON.\n\n" "### DONNÉES DISPONIBLES ###\n" f"{inspection_info}\n\n" "### RÈGLES CRITIQUES ###\n" "1. PAS DE CODE : Ne génère jamais de Python.\n" f"2. COLONNES : Utilise uniquement {columns_list}.\n" "3. PLAN : Détaille les étapes (Somme, Moyenne, Regroupement, etc.).\n\n" "### STRUCTURE JSON ATTENDUE ###\n" "{\n" " \"plan\": [\"étape 1\", \"étape 2\"],\n" " \"colonnes_utilisees\": [\"col1\", \"col2\"]\n" "}\n" ) # # 4. PRÉPARATION DU MESSAGE UNIQUE (La méthode qui marche pour l'Exécuteur) # On fusionne le prompt et la question utilisateur dans un seul HumanMessage full_input = f"{prompt}\n\n### QUESTION UTILISATEUR ###\n{user_query}" final_messages = [HumanMessage(content=full_input)] config_analyseur = { "callbacks": [langfuse_handler], "metadata": {"agent_name": "Analyseur"} } # 5. APPEL DU MODÈLE # Note : On utilise local_llm (ou analyseur_llm si tu as séparé) response = local_llm_chat.invoke(final_messages, config=config_analyseur) return {"messages": [response]} def agent_executor(state: AgentState): # On récupère le nom exact du fichier depuis le state full_path = state.get("current_df_path") or "data/default.csv" file_name = os.path.basename(full_path) # Récupère 'mon_fichier.csv' # On construit le prompt en injectant le nom dynamiquement prompt = ( "### SYSTEM ROLE ###\n" "Tu es un moteur d'exécution Python. Réponds EXCLUSIVEMENT au format JSON.\n\n" "### FICHIER ACTUEL ###\n" f"Fichier : '{file_name}' | Chemin : 'data/{file_name}'\n\n" "### INSTRUCTIONS DE CODE & SÉCURITÉ ###\n" "1. Imports : pandas, matplotlib.pyplot.\n" f"2. Chargement : df = pd.read_csv('data/{file_name}')\n" "3. SYNTAXE (Apostrophes) : Utilise TOUJOURS des doubles guillemets (\"\") pour les labels.\n\n" "4. GRAPHIQUES (SAUVEGARDE) :\n" " - INTERDIT : plt.show()\n" " - OBLIGATOIRE : plt.savefig('outputs/nom_du_graphe.png')\n\n" "5. DATA REPORTING (CRUCIAL POUR LE REPORTER) :\n" " - AVANT de générer le graphique, calcule les stats clés (Somme, Moyenne, Top 3).\n" " - Utilise print() pour afficher ces chiffres afin que le Reporter les reçoive.\n\n" "6. VARIABLE 'result' :\n" " - Termine ton code par une variable 'result' qui récapitule :\n" " 1. Les fichiers générés (ex: 'outputs/ventes.png')\n" " 2. Un résumé textuel des chiffres affichés par les graphiques.\n\n" "### EXEMPLE DE CODE INTERNE ATTENDU ###\n" "import pandas as pd\n" "import matplotlib.pyplot as plt\n" "df = pd.read_csv('...')\n" "stats = df.groupby('Pays')['Ventes'].sum()\n" "print(stats) # <--- Ceci permet au Reporter de voir les chiffres !\n" "plt.bar(stats.index, stats.values)\n" "plt.savefig('outputs/ventes_pays.png')\n" "result = f\"Graphique 'ventes_pays.png' généré. Chiffres clés : {stats.to_dict()}\"\n" ) # Nettoyage des messages pour Ollama (comme avant) cleaned_messages = [] for m in state["messages"]: if m.type == "tool": cleaned_messages.append(HumanMessage(content=f"Résultat : {m.content}")) else: cleaned_messages.append(m) # On place le prompt dynamique en premier final_messages = [HumanMessage(content=prompt)] + cleaned_messages return {"messages": [model_with_tools.invoke(final_messages)]} def agent_reporter(state : AgentState) : prompt_reporter = ( ### RÔLE ### """ Tu es un Expert Consultant en Data Visualisation et Analyse Statistique. Ton objectif est de transformer des données techniques brutes en un rapport stratégique clair, élégant et actionnable. ### DIRECTIVES DE RÉDACTION ### 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. 2. STRUCTURE : Utilise des titres (##), du gras (**), et des listes à puces pour la clarté. 3. VISUELS : Mentionne les graphiques générés avec l'émoji 📊. 4. TON : Professionnel, dynamique et rassurant. 5. PAS DE JSON : Ta réponse doit être uniquement du texte formaté en Markdown. ### STRUCTURE DU RAPPORT ### ## 📝 Synthèse de l'Analyse (Un paragraphe fluide qui résume la situation globale). ## 💡 Points Clés & Insights - **[Point 1]** : Explication... - **[Point 2]** : Explication... ## 📊 Visualisations Disponibles - [Nom du fichier.png] : Brève description de ce que le graphique démontre. ## 🚀 Recommandations si tu n'as (Une ou deux phrases sur la prochaine étape suggérée).""" ) all_messages = state["messages"] # On ne garde que : # 1. Le premier message (User) pour le contexte # 2. Le DERNIER message de l'IA (le code qui a marché) # 3. Le DERNIER message de l'outil (les données réelles) important_messages = [ all_messages[0], # La question de l'utilisateur [m for m in all_messages if isinstance(m, AIMessage)][-1], # Le dernier code [m for m in all_messages if isinstance(m, ToolMessage)][-1] # Le dernier résultat ] # Nettoyage des messages pour Ollama (comme avant) cleaned_messages = [] for m in important_messages : if m.type == "tool": cleaned_messages.append(HumanMessage(content=f"Résultat : {m.content}")) else: cleaned_messages.append(m) # On place le prompt dynamique en premier final_messages = [HumanMessage(content=prompt_reporter)] + cleaned_messages return {"messages": [local_llm_reporter.invoke(final_messages)]}