| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- 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
- 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="deepseek-coder-v2:16b-lite-instruct-q4_K_M",
- 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
- )
- 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 : Commence par les imports (pandas, matplotlib.pyplot, etc.).\n"
- f"2. Chargement : df = pd.read_csv('data/{file_name}')\n"
-
- "3. SYNTAXE (Apostrophes) : Utilise TOUJOURS des doubles guillemets (\"\") pour les labels et titres.\n"
- " INTERDIT : plt.title('L'analyse') | VALIDE : plt.title(\"L'analyse\")\n\n"
-
- "4. GRAPHIQUES (SAUVEGARDE OBLIGATOIRE) :\n"
- " - Ne JAMAIS utiliser plt.show().\n"
- " - Utilise TOUJOURS : plt.savefig('outputs/nom_du_graphe.png')\n"
- " - Assure-toi que le dossier 'outputs/' est utilisé pour la sauvegarde.\n\n"
-
- "5. SORTIE : Termine ton code par une variable 'result' contenant le résumé texte des données "
- "ou la confirmation du fichier généré (ex: result = \"Graphique sauvegardé : ventes.png\").\n\n"
-
- "### EXEMPLE DE FORMAT ATTENDU ###\n"
- "{\n"
- " \"tool\": \"excel_code_interpreter\",\n"
- " \"tool_input\": {\n"
- f" \"file_path\": \"data/{file_name}\",\n"
- " \"code\": \"import pandas as pd\\nimport matplotlib.pyplot as plt\\n# ... code ...\\nplt.savefig('outputs/chart.png')\\nresult = 'Analyse terminée'\"\n"
- " }\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 = (
- "Tu es l'Agent de Reporting de Dataltist.\n"
- "Ton rôle : Convertir les logs de l'Exécuteur en une synthèse factuelle pour l'utilisateur.\n\n"
- "RÈGLES CRITIQUES (ANTI-SILENCE) :\n"
- "- DISPONIBILITÉ : Tu dois TOUJOURS générer une réponse, même pour confirmer une erreur ou une absence de données.\n"
- "- STRUCTURE : Utilise des tirets pour lister les points clés.\n"
- "- SYNTHÈSE : Si l'Exécuteur donne 'result = 42', écris 'Résultat : 42'.\n"
- "- FICHIERS : Liste uniquement le nom des fichiers (ex: graphe_incendie.png), JAMAIS le chemin 'outputs/'.\n\n"
- "FORMAT IMPÉRATIF :\n"
- "1. Résumé des données traitées.\n"
- "2. Valeurs calculées (moyennes, totaux, etc.).\n"
- "3. Liste des visuels générés (si présents).\n\n"
- "4. L'affichage doit etre claire , tu laisse les espaces .\n\n"
- "INTERDICTIONS :\n"
- "- Pas de politesse inutile ('Voici le rapport...', 'J'espère que...').\n"
- "- Pas de répétition des étapes techniques de l'agent.\n"
- "- Pas de phrases vides si l'exécution a échoué : explique brièvement l'échec."
- "RÈGLES D'AFFICHAGE DES FICHIERS :\n"
- "1. CITATION : Si l'Exécuteur a généré des fichiers (images ou données), cite-les obligatoirement.\n"
- "2. FORMAT : Utilise TOUJOURS ce format exact pour introduire un fichier : \n"
- " '📊 Visualisation générée : [nom_du_fichier.png]'\n"
- " '📥 Fichier disponible : [nom_du_fichier.csv/xlsx]'\n"
- "3. SANS CHEMIN : Ne mentionne JAMAIS le dossier 'outputs/' ou 'data/'. Utilise uniquement le nom final du fichier.\n\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_reporter)] + cleaned_messages
- return {"messages": [local_llm_chat.invoke(final_messages)]}
|