tools.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import os
  2. import pandas as pd
  3. from langchain_core.tools import tool
  4. from langchain_community.tools import DuckDuckGoSearchRun
  5. import matplotlib.pyplot as plt
  6. import seaborn as sns
  7. from langchain_core.messages import HumanMessage, AIMessage
  8. import sys
  9. import io
  10. import matplotlib
  11. matplotlib.use('Agg') # Force Matplotlib à ne pas ouvrir de fenêtre graphique
  12. @tool
  13. def convert_csv_to_excel(csv_path: str):
  14. """Convertit un fichier CSV en Excel (.xlsx)."""
  15. if not os.path.exists(csv_path):
  16. 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."
  17. try:
  18. df = pd.read_csv(csv_path)
  19. new_path = csv_path.replace(".csv", ".xlsx")
  20. df.to_excel(new_path, index=False)
  21. return f"Succès : Le fichier a été converti en {new_path}"
  22. except Exception as e:
  23. return f"Erreur lors de la lecture du CSV : {str(e)}"
  24. BASE_DIR = os.path.dirname(os.path.abspath(__file__))
  25. @tool
  26. def inspect_data(file_name: str):
  27. """
  28. Explore le fichier situé dans le dossier 'data/' pour retourner les colonnes et un aperçu.
  29. Passer uniquement le nom du fichier (ex: 'data.csv').
  30. """
  31. # 1. On nettoie le nom du fichier au cas où l'IA ajoute des guillemets
  32. clean_name = file_name.strip().replace("'", "").replace('"', "")
  33. # 2. On construit le chemin ABSOLU vers le dossier data
  34. # On suppose que ton script est à la racine du projet
  35. data_path = os.path.join(os.getcwd(), "data", clean_name)
  36. try:
  37. # 3. Vérification de l'existence du fichier
  38. if not os.path.exists(data_path):
  39. return f"Erreur : Le fichier '{clean_name}' est introuvable dans le dossier data/."
  40. # 4. Lecture selon l'extension (Note : correction de 'endiwith' en 'endswith')
  41. if data_path.lower().endswith(".csv"):
  42. df = pd.read_csv(data_path) # Utilisation de la variable data_path, PAS de la chaîne "file_path"
  43. elif data_path.lower().endswith((".xlsx", ".xls")):
  44. df = pd.read_excel(data_path)
  45. else:
  46. return "Format de fichier non supporté. Utilisez .csv ou .xlsx"
  47. # 5. Extraction des infos
  48. columns_names = df.columns.tolist()
  49. preview = df.head(3).to_string()
  50. return f"Colonnes trouvées : {columns_names}\n\nAperçu des données :\n{preview}"
  51. except Exception as e:
  52. return f"Erreur lors de l'inspection : {str(e)}"
  53. @tool
  54. def excel_code_interpreter(file_path: str, code: str):
  55. """Exécute du code Python sur le fichier (CSV ou Excel) chargé dans 'df'."""
  56. import warnings
  57. warnings.filterwarnings("ignore")
  58. # 1. Nettoyage et construction du chemin
  59. file_name = os.path.basename(file_path.strip().replace("'", "").replace('"', ""))
  60. # Assure-toi que BASE_DIR est défini globalement ou remplace-le par os.getcwd()
  61. data_folder_path = os.path.join(BASE_DIR, "data", file_name)
  62. root_path = os.path.join(BASE_DIR, file_name)
  63. if os.path.exists(data_folder_path):
  64. full_path = data_folder_path
  65. elif os.path.exists(root_path):
  66. full_path = root_path
  67. else:
  68. return f"ERREUR : Fichier '{file_name}' introuvable."
  69. # 2. CAPTURE DE LA CONSOLE (stdout)
  70. # On crée un tampon pour intercepter les print()
  71. buffer = io.StringIO()
  72. old_stdout = sys.stdout # On sauvegarde la sortie d'origine (ton terminal)
  73. sys.stdout = buffer # On redirige vers notre variable
  74. try:
  75. # Lecture du fichier
  76. df = pd.read_csv(full_path) if file_name.endswith('.csv') else pd.read_excel(full_path)
  77. # 3. Préparation du contexte d'exécution
  78. context = {
  79. "df": df,
  80. "pd": pd,
  81. "plt": plt,
  82. "os": os,
  83. "result": None,
  84. "__builtins__": __builtins__
  85. }
  86. # 4. Exécution du code généré par l'IA
  87. exec(code, context)
  88. # 5. Récupération des données capturées
  89. output_console = buffer.getvalue()
  90. final_result_variable = context.get("result", "")
  91. # On remet le système à la normale
  92. sys.stdout = old_stdout
  93. # 6. ON FUSIONNE TOUT POUR LE REPORTER
  94. # C'est ce bloc de texte que le Reporter va recevoir dans Langfuse
  95. full_report = f"--- RÉSULTATS DE LA CONSOLE ---\n{output_console}\n"
  96. full_report += f"--- RÉSUMÉ FINAL ---\n{final_result_variable}"
  97. return full_report
  98. except Exception as e:
  99. # En cas d'erreur, on n'oublie pas de remettre le stdout à la normale
  100. sys.stdout = old_stdout
  101. return f"ERREUR PYTHON : {str(e)}"
  102. ddg = DuckDuckGoSearchRun()
  103. @tool
  104. def search_tool(query : str) :
  105. """Recherche sur le web. Limité pour économiser les tokens."""
  106. try:
  107. results = ddg.run(query)
  108. if not results:
  109. return "Aucun résultat trouvé."
  110. # 1. On nettoie les espaces superflus pour gagner des tokens
  111. clean_results = " ".join(results.split())
  112. # 2. On limite intelligemment (ex: 1200 chars pour plus de contexte)
  113. # Mais on s'assure de ne pas couper un mot au milieu
  114. limit = 1200
  115. if len(clean_results) <= limit:
  116. return clean_results
  117. return clean_results[:limit] + "... [Résultat tronqué pour économie]"
  118. except Exception as e:
  119. return f"Erreur lors de la recherche : {str(e)}"
  120. def call_tool_and_format_for_ollama(tool_output):
  121. """
  122. Cette fonction transforme la sortie brute de l'outil
  123. en un format que ton Llama 3.1 8B local peut comprendre.
  124. """
  125. # Au lieu de renvoyer un ToolMessage (qui fait planter Ollama)
  126. # On crée un message "Observation"
  127. observation_message = HumanMessage(
  128. content=f"OBSERVATION DE L'OUTIL :\n{tool_output}\n\nUtilise ces données pour continuer ton analyse."
  129. )
  130. return observation_message
  131. import re
  132. import json
  133. def robust_json_parse(text):
  134. try:
  135. cleaned = text.strip()
  136. # Extraction JSON
  137. match = re.search(r"\{.*\}", cleaned, re.DOTALL)
  138. if not match:
  139. raise ValueError("Aucun JSON détecté")
  140. json_str = match.group()
  141. # Parsing direct
  142. try:
  143. return json.loads(json_str)
  144. except json.JSONDecodeError:
  145. pass
  146. # Auto-fix
  147. json_str = json_str.replace("\n", " ")
  148. json_str = re.sub(r",\s*}", "}", json_str)
  149. json_str = re.sub(r",\s*]", "]", json_str)
  150. return json.loads(json_str)
  151. except Exception as e:
  152. raise ValueError(f"Parsing JSON échoué : {e}\nRAW:\n{text}")