|
|
@@ -0,0 +1,262 @@
|
|
|
+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
|
|
|
+
|
|
|
+OLLAMA_BASE_URL = os.getenv("OLLAMA_URL", "http://ollama1-server:11434")
|
|
|
+
|
|
|
+local_llm = ChatOllama(
|
|
|
+ model="deepseek-coder-v2:16b-lite-instruct-q4_K_M",
|
|
|
+ base_url=OLLAMA_BASE_URL,
|
|
|
+ format="json",
|
|
|
+ temperature=0
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+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)
|
|
|
+ response = local_llm.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)]}
|
|
|
+
|