Explorar o código

Premier commit : Projet works !

Abdenbi IABBADENE hai 1 mes
achega
eb45845942
Modificáronse 12 ficheiros con 443 adicións e 0 borrados
  1. 5 0
      .gitignore
  2. 85 0
      Agents.py
  3. 29 0
      ReadMe.md
  4. BIN=BIN
      __pycache__/Agents.cpython-313.pyc
  5. BIN=BIN
      __pycache__/tools.cpython-313.pyc
  6. BIN=BIN
      __pycache__/workflow_Agent.cpython-313.pyc
  7. 76 0
      app.py
  8. BIN=BIN
      graph_workflow.png
  9. 24 0
      main.py
  10. 9 0
      requirements.txt
  11. 171 0
      tools.py
  12. 44 0
      workflow_Agent.py

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+data
+outputs
+.env
+old_outputs
+.vscode

+ 85 - 0
Agents.py

@@ -0,0 +1,85 @@
+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
+
+
+# Import de tes outils corrigés
+from tools import  excel_code_interpreter, convert_csv_to_excel  , generate_excel_chart , search_tool , inspect_data
+
+load_dotenv()
+
+# 1. Définition du State
+class AgentState(TypedDict):
+    messages: Annotated[Sequence[BaseMessage], add_messages]
+    current_df_path: Optional[str]
+    generated_charts : List[str]
+
+# 2. Configuration du Modèle et des Outils
+model_gpt = ChatOpenAI(model="o3-mini") 
+tools = [ search_tool  , excel_code_interpreter, convert_csv_to_excel , generate_excel_chart  , inspect_data ]
+model_with_tools = model_gpt.bind_tools(tools)
+
+
+
+
+# 3. Définition des Nœuds
+def agent_analyseur(state: AgentState):
+    prompt = (
+        "Tu es l'Analyseur Stratégique. Ton rôle est de décomposer la demande utilisateur en étapes techniques claires.\n"
+        "RÈGLES CRITIQUES :\n"
+        "1. Si le fichier est en .csv, ordonne IMMÉDIATEMENT sa conversion via 'convert_csv_to_excel'. Ne demande pas la permission.\n"
+        "2. Définis un plan d'action claire \n"
+        "3. Ne pose JAMAIS de questions sur les données (prix, stock). L'Exécuteur doit les trouver lui-même via les outils.\n"
+        "4. Sois concis : Ton message doit être une directive pour l'Exécuteur, pas une conversation avec l'utilisateur."
+    )
+    msg = [SystemMessage(content=prompt)] + state["messages"]
+    response = model_gpt.invoke(msg)
+    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"
+
+    # 2. On construit un prompt qui contient le VRAI chemin
+    prompt = (
+        f"Tu es un Data Scientist expert spécialisé en automatisation. Le fichier cible se trouve dans le dossier 'data/' : '{file_path}'.\n"
+        "PROTOCOLE D'ACTION IMPÉRATIF :\n"
+        "1. INSPECTION PRÉALABLE : Avant de générer le moindre code Python, appelle TOUJOURS 'inspect_data'.\n Tu dois connaître les colonnes réelles avant de coder.\n"
+        "2. ACTION D'ABORD : Si tu n'as pas les données externes, appelle 'search_tool' immédiatement.\n"
+        "3. ZÉRO SIMULATION : Il est strictement interdit d'inventer des prix ou des stocks. Si l'outil 'search_tool' donne une plage de prix (ex: 30-40€), calcule la moyenne (35€).\n"
+        "4. LOGIQUE PANDAS : Ne fais aucun calcul manuel. Utilise 'df' pour filtrer le produit exact (ex: df[df['Produit'] == 'Souris']).\n"
+        "5. VARIABLE RESULT : Ton script Python DOIT se terminer par 'result = ...'. C'est cette variable qui sera transmise au système.\n"
+        "6. PERSISTANCE : Si un graphique est demandé, utilise 'plt.savefig('outputs/nom_du_graphe.png')'.\n"
+        "Tu es un moteur d'exécution froid et précis."
+    )
+
+    # 3. Préparation des messages
+    messages = [SystemMessage(content=prompt)] + state["messages"]
+    
+    # 4. Appel du modèle avec les outils
+    response = model_with_tools.invoke(messages)
+    
+    return {"messages": [response]}
+
+
+
+def agent_reporter(state : AgentState) : 
+
+    prompt_reporter = (
+        "Tu es l'Agent de Reporting. Ton rôle est de traduire les résultats techniques en une synthèse claire.\n"
+        "CONSIGNES :\n"
+        "1. FIDÉLITÉ : Ne parle que des données réellement trouvées et calculées par l'Exécuteur.\n"
+        "2. STRUCTURE : Présente les faits marquants, puis les chiffres clés, puis les livrables (graphiques/fichiers).\n"
+        "3. ADAPTATION : Si l'analyse porte sur des ventes, utilise un vocabulaire commercial. Si c'est de l'actuariat, utilise un vocabulaire technique.\n"
+        "4. NEUTRALITÉ : Ne propose pas de texte de remplissage si les données sont manquantes. Sois factuel."
+    )
+    messages = [SystemMessage(content=prompt_reporter)] + state["messages"]
+
+    response = model_gpt.invoke(messages) 
+    return{"messages" : [response]}

+ 29 - 0
ReadMe.md

@@ -0,0 +1,29 @@
+
+
+## Analyse de document Excel ou CSV
+
+ici tu explique quelles sont les etapes pour analyser ton fichier 
+
+tu explique cle process de creation des agents, pour chaque agent( on explique son role ou son fonctionnement)
+
+Pour les Agents tu expliques les differents framework d'agent que tu as étudié et faire un comparatif brève.
+
+
+le workflow des Agents
+
+
+## Les LLM utililsés
+
+Présenter de préference 3 LLMs (OpenAI, Claude, Gemini, OpenSource-Hugging face) avec les couts et comment tu fais la connexion avec les differents Agents
+
+Configuration des Prompt des Agents (vpour après voir comment faire du prompt engenering pour optimiser les prompts)
+
+
+## Presenter le chatbot UI 
+
+Présentation des Resultats 
+
+(générer un Excel avec les graphs, avec TCD)
+
+
+

BIN=BIN
__pycache__/Agents.cpython-313.pyc


BIN=BIN
__pycache__/tools.cpython-313.pyc


BIN=BIN
__pycache__/workflow_Agent.cpython-313.pyc


+ 76 - 0
app.py

@@ -0,0 +1,76 @@
+import streamlit as st
+import os
+from workflow_Agent import app # Ton graphe compilé
+import glob
+
+st.set_page_config(page_title="Dataltist AI Assistant", layout="wide")
+st.title("📊 Dataltist BI Chat")
+
+# --- 1. Gestion du Fichier ---
+with st.sidebar:
+    st.header("Données")
+    uploaded_file = st.file_uploader("Charge ton fichier (CSV/Excel)", type=["csv", "xlsx"])
+    
+    if uploaded_file:
+        # On sauvegarde le fichier dans le dossier 'data'
+        file_path = os.path.join("data", uploaded_file.name)
+        with open(file_path, "wb") as f:
+            f.write(uploaded_file.getbuffer())
+        st.success(f"Fichier {uploaded_file.name} chargé !")
+
+# --- 2. Interface de Chat ---
+if "messages" not in st.session_state:
+    st.session_state.messages = []
+
+if st.sidebar.button("🗑️ Effacer la discussion"):
+    st.session_state.messages = []
+    st.rerun()
+
+# Affichage de l'historique
+for message in st.session_state.messages:
+    with st.chat_message(message["role"]):
+        st.markdown(message["content"])
+
+# --- 3. Interaction avec les Agents ---
+if prompt := st.chat_input("Pose ta question sur tes données..."):
+    # 1. Affichage du message utilisateur
+    st.session_state.messages.append({"role": "user", "content": prompt})
+    with st.chat_message("user"):
+        st.markdown(prompt)
+
+    # 2. Appel des agents
+    with st.chat_message("assistant"):
+        with st.status("Analyse en cours par les agents Dataltist...", expanded=True) as status:
+            
+            # On définit les entrées pour ton graphe
+            inputs = {
+                "messages": [("user", prompt)],
+                "current_df_path": file_path  # Le chemin vers le CSV chargé
+            }
+            
+            # Exécution du workflow
+            final_state = app.invoke(inputs)
+            
+            # RÉCUPÉRATION : On prend le message du DERNIER agent (le Reporter)
+            response_text = final_state["messages"][-1].content
+            st.markdown(response_text)
+            
+            # --- AFFICHAGE DES GRAPHIQUES ---
+            st.subheader("📊 Visualisations générées")
+            
+            # On récupère tous les fichiers .png dans le dossier outputs
+            list_of_plots = glob.glob("outputs/*.png")
+            
+            if list_of_plots:
+                # On crée des colonnes si on a plusieurs graphiques
+                cols = st.columns(len(list_of_plots))
+                for idx, plot_path in enumerate(list_of_plots):
+                    with cols[idx]:
+                        st.image(plot_path, use_container_width=True, caption=os.path.basename(plot_path))
+            else:
+                st.info("Aucun graphique n'a été généré pour cette analyse.")
+
+            status.update(label="Analyse terminée !", state="complete")
+        
+    # 3. Sauvegarde dans l'historique de session
+    st.session_state.messages.append({"role": "assistant", "content": response_text})

BIN=BIN
graph_workflow.png


+ 24 - 0
main.py

@@ -0,0 +1,24 @@
+from langchain_core.messages import HumanMessage
+from workflow_Agent import app
+
+if __name__ == "__main__":
+    inputs = {
+        "messages" : [HumanMessage(content = " Je veux savoir qui les meilleurs sales personne dans ma campany , egalement les produit et categorie les mieux vendu , tu me fais des graphs  beau avec seaborn egalement pour me montrer ca , et un graph ou tu les crois pour voir le produit le plus vendu par categorie par les top vendeurs  ")] , 
+        #"messages": [HumanMessage(content="Cherche la tandance de croissance du marché des Ecrans gaming ( ecran dans le dataset ) pour 2026 , puis calucle une Demande_Prevue dans mon dataset (Formule : Stock actuel * (1 + %croissance_trouvé)) , Identifie si le stock actuel d'Ecrans est suffisant ou s'il y a un risque de rupture , Génère un graphique de type 'Gauge' ou un 'Bar chart' comparant le Stock Actuel vs Demande Prévue. ")],
+        "current_df_path": "ventes_materiel_informatique.csv"
+    }
+
+    print("--- Début de l'exécution ---")
+    for event in app.stream(inputs):
+        for node, value in event.items():
+            print(f"\n[Nœud : {node}]")
+            if "messages" in value:
+                last_msg = value["messages"][-1]
+                # Affiche le texte ou l'appel d'outil
+                content = last_msg.content if last_msg.content else f"Appel d'outil: {last_msg.tool_calls}"
+                print(content)
+
+
+import matplotlib
+matplotlib.use('Agg')  # À placer AVANT d'importer pyplot
+import matplotlib.pyplot as plt

+ 9 - 0
requirements.txt

@@ -0,0 +1,9 @@
+langchain-openai
+langchain-community
+langgraph
+pandas
+openpyxl
+matplotlib
+seaborn
+python-dotenv
+duckduckgo-search

+ 171 - 0
tools.py

@@ -0,0 +1,171 @@
+import os
+import pandas as pd
+from langchain_core.tools import tool
+from langchain_community.tools import DuckDuckGoSearchRun
+import matplotlib.pyplot as plt
+import seaborn as sns
+
+
+import matplotlib
+matplotlib.use('Agg')  # Force Matplotlib à ne pas ouvrir de fenêtre graphique
+
+@tool
+def convert_csv_to_excel(csv_path: str):
+    """Convertit un fichier CSV en Excel (.xlsx)."""
+    if not os.path.exists(csv_path):
+        return f"Désolé, je ne trouve pas le fichier '{csv_path}'. Vérifiez qu'il est bien présent dans le dossier du projet."
+    
+    try:
+        df = pd.read_csv(csv_path)
+        new_path = csv_path.replace(".csv", ".xlsx")
+        df.to_excel(new_path, index=False)
+        return f"Succès : Le fichier a été converti en {new_path}"
+    
+    except Exception as e:
+        return f"Erreur lors de la lecture du CSV : {str(e)}"
+    
+
+
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+
+
+@tool
+def inspect_data(file_name: str):
+    """
+    Explore le fichier situé dans le dossier 'data/' pour retourner les colonnes et un aperçu.
+    Passer uniquement le nom du fichier (ex: 'data.csv').
+    """
+    # 1. On nettoie le nom du fichier au cas où l'IA ajoute des guillemets
+    clean_name = file_name.strip().replace("'", "").replace('"', "")
+    
+    # 2. On construit le chemin ABSOLU vers le dossier data
+    # On suppose que ton script est à la racine du projet
+    data_path = os.path.join(os.getcwd(), "data", clean_name)
+
+    try:
+        # 3. Vérification de l'existence du fichier
+        if not os.path.exists(data_path):
+            return f"Erreur : Le fichier '{clean_name}' est introuvable dans le dossier data/."
+
+        # 4. Lecture selon l'extension (Note : correction de 'endiwith' en 'endswith')
+        if data_path.lower().endswith(".csv"):
+            df = pd.read_csv(data_path) # Utilisation de la variable data_path, PAS de la chaîne "file_path"
+        elif data_path.lower().endswith((".xlsx", ".xls")):
+            df = pd.read_excel(data_path)
+        else:
+            return "Format de fichier non supporté. Utilisez .csv ou .xlsx"
+
+        # 5. Extraction des infos
+        columns_names = df.columns.tolist()
+        preview = df.head(3).to_string()
+
+        return f"Colonnes trouvées : {columns_names}\n\nAperçu des données :\n{preview}"
+    
+    except Exception as e:
+        return f"Erreur lors de l'inspection : {str(e)}"
+
+
+
+
+
+@tool
+def excel_code_interpreter(file_path: str, code: str):
+    """Exécute du code Python sur le fichier (CSV ou Excel) chargé dans 'df'."""
+    # Nettoyage du nom de fichier
+    file_name = os.path.basename(file_path.strip().replace("'", "").replace('"', ""))
+    
+    # 1. Définition de la priorité : Dossier 'data' d'abord
+    data_folder_path = os.path.join(BASE_DIR, "data", file_name)
+    root_path = os.path.join(BASE_DIR, file_name)
+    
+    # Choix du chemin
+    if os.path.exists(data_folder_path):
+        full_path = data_folder_path
+    elif os.path.exists(root_path):
+        full_path = root_path
+    else:
+        # Si rien n'est trouvé, on aide l'agent avec un message clair
+        files_in_data = os.listdir(os.path.join(BASE_DIR, "data")) if os.path.exists(os.path.join(BASE_DIR, "data")) else "Dossier data absent"
+        return f"ERREUR : Fichier '{file_name}' introuvable. Contenu de 'data/': {files_in_data}"
+
+
+    
+    try:
+        # Lecture selon l'extension
+        if file_name.endswith('.csv'):
+            df = pd.read_csv(full_path)
+        else:
+            df = pd.read_excel(full_path)
+
+        safe_globals={"__builtins__" :{}}
+
+        # Injection des bibliothèques pour l'IA
+        local_vars = {"df": df, "pd": pd, "plt": plt, "result": None}
+        exec(code, safe_globals , local_vars)
+        
+        return str(local_vars.get("result", "Exécution terminée."))
+    except Exception as e:
+        return f"ERREUR PYTHON : {str(e)}"
+    
+
+@tool
+def generate_excel_chart(file_path: str, code: str, output_image: str = "chart.png"):
+    """
+    Exécute du code Python pour générer un graphique Matplotlib/Seaborn à partir du fichier Excel.
+    Le DataFrame est chargé dans 'df'.
+    IMPORTANT : Le code doit finir par plt.savefig(output_image).
+    """
+    # Force le mode sans interface graphique (important pour les scripts automatisés)
+    plt.switch_backend('Agg')
+    plt.clf() # Nettoie les anciens graphiques en mémoire
+    
+    full_path = os.path.join(BASE_DIR, file_path)
+    try:
+        df = pd.read_excel(full_path)
+        
+        # Environnement d'exécution pour l'IA
+        local_vars = {
+            "df": df, 
+            "plt": plt, 
+            "sns": sns, 
+            "output_image": output_image
+        }
+        
+        exec(code, {}, local_vars)
+        
+        if os.path.exists(output_image):
+            return f"Succès : Graphique généré et enregistré sous '{output_image}'."
+        else:
+            return "Erreur : Le code a été exécuté mais aucun fichier image n'a été créé."
+            
+    except Exception as e:
+        return f"Erreur lors de la génération du graphique : {str(e)}"
+
+
+ddg = DuckDuckGoSearchRun()
+@tool 
+def search_tool(query : str) : 
+    """Recherche sur le web. Limité pour économiser les tokens."""
+    try:
+        results = ddg.run(query)
+        
+        if not results:
+            return "Aucun résultat trouvé."
+            
+        # 1. On nettoie les espaces superflus pour gagner des tokens
+        clean_results = " ".join(results.split())
+        
+        # 2. On limite intelligemment (ex: 1200 chars pour plus de contexte)
+        # Mais on s'assure de ne pas couper un mot au milieu
+        limit = 1200
+        if len(clean_results) <= limit:
+            return clean_results
+        
+        return clean_results[:limit] + "... [Résultat tronqué pour économie]"
+    except Exception as e:
+        return f"Erreur lors de la recherche : {str(e)}"
+
+
+
+
+

+ 44 - 0
workflow_Agent.py

@@ -0,0 +1,44 @@
+from langgraph.graph import StateGraph , START , END 
+from langgraph.prebuilt import ToolNode, tools_condition
+from Agents import AgentState , agent_analyseur , agent_executor , tools , agent_reporter
+
+
+# 1. Initialisation du graphe avec l'état personnalisé
+workflow = StateGraph(AgentState)
+
+# 2. Ajout des nœuds
+workflow.add_node("analyseur", agent_analyseur)
+workflow.add_node("executor", agent_executor)
+workflow.add_node("reporter", agent_reporter)
+workflow.add_node("tools", ToolNode(tools))
+
+# 3. Définition des arêtes stables
+workflow.add_edge(START, "analyseur")
+workflow.add_edge("analyseur", "executor")
+workflow.add_edge("tools", "executor")  
+workflow.add_edge("reporter", END)      
+
+# 4. Logique de routage personnalisée
+def router(state: AgentState):
+    last_message = state["messages"][-1]
+    # Si l'exécuteur demande un outil, on y va
+    if last_message.tool_calls:
+        return "tools"
+    # Sinon, cela signifie qu'il a fini son analyse technique -> on passe au rapport
+    return "reporter"
+
+# On applique la condition sur l'executor
+workflow.add_conditional_edges(
+    "executor",
+    router,
+    {"tools": "tools", "reporter": "reporter"}
+)
+
+# 5. Compilation
+app = workflow.compile()
+
+# Pour sauvegarder l'image du workflow
+with open("graph_workflow.png", "wb") as f:
+    f.write(app.get_graph().draw_mermaid_png())
+
+print("Graphique du workflow généré sous : graph_workflow.png")