|
|
@@ -2,13 +2,15 @@ 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
|
|
|
-from langgraph.graph.message import add_messages
|
|
|
-from langchain_google_genai import ChatGoogleGenerativeAI
|
|
|
+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()
|
|
|
@@ -16,7 +18,7 @@ langfuse_handler = CallbackHandler()
|
|
|
|
|
|
|
|
|
# Import de tes outils corrigés
|
|
|
-from tools import excel_code_interpreter, search_tool , inspect_data
|
|
|
+from tools import excel_code_interpreter, search_tool , inspect_data , call_tool_and_format_for_ollama , robust_json_parse
|
|
|
|
|
|
langfuse = get_client()
|
|
|
load_dotenv()
|
|
|
@@ -25,104 +27,156 @@ load_dotenv()
|
|
|
class AgentState(TypedDict):
|
|
|
messages: Annotated[Sequence[BaseMessage], add_messages]
|
|
|
current_df_path: Optional[str]
|
|
|
- generated_charts : List[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 =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 = ChatOpenAI(model="gpt-5.3-codex" )
|
|
|
|
|
|
model_gpt_5 = ChatOpenAI(model="o4-mini" )
|
|
|
tools = [ search_tool , excel_code_interpreter, inspect_data ]
|
|
|
-model_with_tools = model_gpt.bind_tools(tools)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-# 3. Définition des Nœuds
|
|
|
+# 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
|
|
|
+analyseur_llm = ChatOllama(
|
|
|
+ model="deepseek-coder-v2:16b-lite-instruct-q4_K_M",
|
|
|
+ temperature=0,
|
|
|
+ format="json" # On garde le format JSON ici
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+local_llm = 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.bind_tools(tools)
|
|
|
+
|
|
|
def agent_analyseur(state: AgentState):
|
|
|
- # --- PHASE D'INSPECTION AUTOMATIQUE ---
|
|
|
- # On récupère le chemin du fichier depuis le state
|
|
|
+ import os
|
|
|
+ import pandas as pd
|
|
|
+
|
|
|
+ # 1. PHASE D'INSPECTION (Identique)
|
|
|
file_path = state.get("current_df_path")
|
|
|
- inspection_info = ""
|
|
|
-
|
|
|
+ inspection_info = "Aucun fichier détecté."
|
|
|
+ columns_list = []
|
|
|
+
|
|
|
if file_path and os.path.exists(file_path):
|
|
|
try:
|
|
|
- # Lecture des 5 premières lignes pour comprendre la structure
|
|
|
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"\n\n### DONNÉES RÉELLES DU FICHIER :\n- Colonnes détectées : {columns_list}\n- Aperçu des données :\n{sample_data}"
|
|
|
+ inspection_info = (
|
|
|
+ f"COLONNES EXACTES : {columns_list}\n"
|
|
|
+ f"APERÇU DES DONNÉES :\n{sample_data}"
|
|
|
+ )
|
|
|
except Exception as e:
|
|
|
- inspection_info = f"\n\n⚠️ Erreur lors de l'inspection du fichier : {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."
|
|
|
|
|
|
- # --- CONSTRUCTION DU PROMPT AVEC LES DONNÉES RÉELLES ---
|
|
|
+ # 3. CONSTRUCTION DU PROMPT DYNAMIQUE (Comme l'Exécuteur)
|
|
|
+ # On définit les règles métier ici
|
|
|
prompt = (
|
|
|
- "Tu es l'Analyseur Stratégique de Dataltist. Ton rôle est de définir le 'QUOI' faire, pas le 'COMMENT'.\n\n"
|
|
|
- "### RÈGLES D'OR :\n"
|
|
|
- "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"
|
|
|
- "2. PLAN D'ACTION : Liste les étapes logiques en utilisant les noms de colonnes exacts.\n"
|
|
|
- "3. INSPECTION : Base-toi uniquement sur les colonnes détectées ci-dessous.\n"
|
|
|
- "4. CONCISION : Sois une tour de contrôle, donne des ordres clairs et précis.\n"
|
|
|
- f"{inspection_info}"
|
|
|
- )
|
|
|
+ "### 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"},
|
|
|
- "tags": ["Dataltist", "Planning"]
|
|
|
+ "metadata": {"agent_name": "Analyseur"}
|
|
|
}
|
|
|
|
|
|
- msg = [SystemMessage(content=prompt)] + state["messages"]
|
|
|
- response = model.invoke(msg , config=config_analyseur)
|
|
|
-
|
|
|
- # On retourne la réponse de l'IA qui contient maintenant le plan basé sur les vraies colonnes
|
|
|
+ # 5. APPEL DU MODÈLE
|
|
|
+ # Note : On utilise local_llm (ou analyseur_llm si tu as séparé)
|
|
|
+ response = local_llm.invoke(final_messages, config=config_analyseur)
|
|
|
+
|
|
|
return {"messages": [response]}
|
|
|
|
|
|
|
|
|
|
|
|
-def agent_executor(state: AgentState):
|
|
|
- # 1. On récupère le chemin actuel depuis le State
|
|
|
- # Si current_df_path est None, on met data.xlsx par défaut
|
|
|
- file_path = state.get("current_df_path") or "data.xlsx"
|
|
|
- clean_path = file_path.replace("\\", "/")
|
|
|
|
|
|
- # 2. On construit un prompt qui contient le VRAI chemin
|
|
|
+
|
|
|
+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 = (
|
|
|
- f"Tu es un Data Scientist expert spécialisé en automatisation. Le fichier cible est disponible ici : '{file_path}'.\n\n"
|
|
|
- "PROTOCOLE D'ACTION IMPÉRATIF :\n"
|
|
|
- "1. CONFIANCE STRATÉGIQUE : Utilise les noms de colonnes fournis par l'Analyseur ( Obligatoire , change pas ).\n"
|
|
|
- "2. RECHERCHE EXTERNE : Utilise 'search_tool' si des données manquent.\n"
|
|
|
- "3. ZÉRO SIMULATION : Ne jamais inventer de données.\n"
|
|
|
- "4. LOGIQUE PANDAS : Utilise 'df' pour filtrer les données exactes.\n"
|
|
|
- "5. CODE AUTONOME : Ton script Python doit être complet et prêt à l'exécution. \n"
|
|
|
- f" - Tu DOIS impérativement définir la variable `file_path = '{clean_path}'` au début de ton code.\n"
|
|
|
- " - Inclus tous les imports (pandas as pd, matplotlib.pyplot as plt, etc.).\n"
|
|
|
- " - Termine impérativement par 'result = ...'.\n"
|
|
|
- "6. PERSISTANCE : Utilise 'plt.savefig('outputs/nom_du_graphe.png')' et ferme la figure avec 'plt.close()'.\n\n"
|
|
|
- "Tu es un moteur d'exécution froid et précis. Pas de discours.\n"
|
|
|
- "STRUCTURE DE RÉPONSE :\n"
|
|
|
- "- 'Statut : Code exécuté avec succès.'\n"
|
|
|
- "- 'Fichiers : [liste des .png générés]'\n"
|
|
|
- "- 'Validation : Les données de [Colonnes] ont été traitées.'\n"
|
|
|
+ "### SYSTEM ROLE ###\n"
|
|
|
+ "Tu es un moteur d'exécution Python. Réponds EXCLUSIVEMENT en JSON.\n\n"
|
|
|
+
|
|
|
+ "### FICHIER ACTUEL (IMPORTANT) ###\n"
|
|
|
+ f"Tu dois travailler UNIQUEMENT sur le fichier : '{file_name}'\n"
|
|
|
+ f"Le chemin à utiliser dans ton code est : 'data/{file_name}'\n\n"
|
|
|
+
|
|
|
+ "### INSTRUCTIONS DE CODE ###\n"
|
|
|
+ "1. Commence ton code par l'import des librairies.\n"
|
|
|
+ f"2. Charge le fichier avec : df = pd.read_csv('data/{file_name}') (ou read_excel).\n"
|
|
|
+ "3. Termine TOUJOURS par une variable 'result' contenant ton analyse.\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\\n# Ton code ici...\"\n"
|
|
|
+ " }\n"
|
|
|
+ "}"
|
|
|
)
|
|
|
|
|
|
- # 3. Préparation des messages
|
|
|
- messages = [SystemMessage(content=prompt)] + state["messages"]
|
|
|
+ # 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)
|
|
|
|
|
|
- config_executor= {
|
|
|
- "callbacks": [langfuse_handler],
|
|
|
- "metadata": {"agent_name": "executor"},
|
|
|
- "tags": ["Dataltist", "executor"]
|
|
|
- }
|
|
|
-
|
|
|
- # 4. Appel du modèle avec les outils
|
|
|
- response = model_with_tools.invoke(messages , config= config_executor )
|
|
|
-
|
|
|
- return {"messages": [response]}
|
|
|
+ # On place le prompt dynamique en premier
|
|
|
+ final_messages = [HumanMessage(content=prompt)] + cleaned_messages
|
|
|
+
|
|
|
+ return {"messages": [model_with_tools.invoke(final_messages)]}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -158,5 +212,5 @@ def agent_reporter(state : AgentState) :
|
|
|
)
|
|
|
messages = [SystemMessage(content=prompt_reporter)] + state["messages"]
|
|
|
|
|
|
- response = model.invoke(messages )
|
|
|
+ response = local_llm.invoke(messages )
|
|
|
return{"messages" : [response]}
|