benfls commited on
Commit
062a4b0
·
1 Parent(s): 81917a3

first commit

Browse files
Files changed (8) hide show
  1. .env-example +6 -0
  2. README.md +44 -0
  3. agent.py +188 -0
  4. app.py +48 -20
  5. config.py +30 -0
  6. requirements.txt +7 -1
  7. tools/__init__.py +10 -0
  8. tools/web_tools.py +98 -0
.env-example ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Configurez vos clés API ici et renommez ce fichier en .env
2
+ OPENAI_API_KEY=votre_clé_api_openai_ici
3
+
4
+ # Autres configurations
5
+ # TEMPERATURE=0.7
6
+ # MAX_TOKENS=4096
README.md CHANGED
@@ -12,4 +12,48 @@ hf_oauth: true
12
  hf_oauth_expiration_minutes: 480
13
  ---
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
12
  hf_oauth_expiration_minutes: 480
13
  ---
14
 
15
+ # Agent IA avancé avec LangChain
16
+
17
+ Ce projet implémente un agent IA avancé utilisant LangChain et des outils personnalisés pour répondre aux questions de manière intelligente.
18
+
19
+ ## Fonctionnalités
20
+
21
+ - **Agent basé sur LangChain**: Utilise l'architecture d'agent de LangChain pour une réponse structurée et itérative aux questions
22
+ - **Outils intégrés**: Recherche web, récupération de contenu web, calcul, parsing JSON, et plus
23
+ - **Interface Gradio**: Interface utilisateur intuitive pour tester l'agent et soumettre des réponses
24
+ - **Mémorisation des conversations**: L'agent maintient un historique des interactions
25
+
26
+ ## Configuration
27
+
28
+ 1. Clonez ce dépôt
29
+ 2. Installez les dépendances: `pip install -r requirements.txt`
30
+ 3. Copiez `.env-example` en `.env` et configurez votre clé API OpenAI
31
+
32
+ ## Structure du projet
33
+
34
+ - `app.py`: Point d'entrée principal de l'application avec l'interface Gradio
35
+ - `agent.py`: Implémentation de l'agent avancé avec LangChain
36
+ - `config.py`: Configuration du projet
37
+ - `tools/`: Dossier contenant les outils personnalisés pour l'agent
38
+ - `web_tools.py`: Outils pour la recherche web et la récupération de contenu
39
+ - `utils.py`: Outils utilitaires (date, calculatrice, parsing JSON)
40
+
41
+ ## Utilisation
42
+
43
+ 1. Lancez l'application: `python app.py`
44
+ 2. Connectez-vous avec votre compte Hugging Face
45
+ 3. Utilisez l'onglet "Test de l'agent" pour tester des questions individuelles
46
+ 4. Utilisez l'onglet "Évaluation complète" pour soumettre toutes les réponses
47
+
48
+ ## Personnalisation
49
+
50
+ Vous pouvez personnaliser l'agent en:
51
+ - Ajoutant de nouveaux outils dans le dossier `tools/`
52
+ - Modifiant le message système dans `config.py`
53
+ - Ajustant les paramètres de génération (température, etc.)
54
+
55
+ ---
56
+
57
+ Créé pour le cours Hugging Face Agent Course - Final Assignment
58
+
59
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
agent.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from typing import List, Optional, Dict, Any
4
+
5
+ from langchain.agents import AgentExecutor
6
+ from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent
7
+ from langchain.schema import SystemMessage, HumanMessage
8
+ from langchain.prompts import MessagesPlaceholder
9
+ from langchain_openai import ChatOpenAI
10
+ from langchain.memory import ConversationBufferMemory
11
+ from langchain_core.messages import AIMessage
12
+ from langchain.callbacks.manager import CallbackManager
13
+ from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
14
+
15
+ from tools import WebSearchTool, WebContentTool, CurrentDateTool, JsonParserTool, CalculatorTool
16
+ import config
17
+
18
+ # Configuration du logging
19
+ logging.basicConfig(
20
+ level=logging.INFO,
21
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
22
+ handlers=[logging.StreamHandler()]
23
+ )
24
+ logger = logging.getLogger("Agent")
25
+
26
+ class AdvancedAgent:
27
+ """
28
+ Agent avancé utilisant LangChain et des outils personnalisés
29
+ pour répondre aux questions de manière plus intelligente.
30
+ """
31
+
32
+ def __init__(self,
33
+ model_name: str = config.DEFAULT_MODEL,
34
+ api_key: Optional[str] = None,
35
+ temperature: float = config.TEMPERATURE,
36
+ system_message: str = config.DEFAULT_SYSTEM_MESSAGE,
37
+ verbose: bool = True):
38
+ """
39
+ Initialise l'agent avec ses outils et sa configuration.
40
+
41
+ Args:
42
+ model_name: Nom du modèle à utiliser
43
+ api_key: Clé API OpenAI (prend celle de l'environnement si non spécifiée)
44
+ temperature: Valeur de température pour la génération de texte
45
+ system_message: Message système pour l'agent
46
+ verbose: Afficher les logs détaillés
47
+ """
48
+ self.model_name = model_name
49
+ self.temperature = temperature
50
+ self.system_message = system_message
51
+ self.verbose = verbose
52
+
53
+ # Utilise la clé API fournie ou celle des variables d'environnement
54
+ self.api_key = api_key or config.OPENAI_API_KEY
55
+ if not self.api_key:
56
+ logger.warning("Aucune clé API OpenAI trouvée. Certaines fonctionnalités peuvent ne pas fonctionner.")
57
+
58
+ # Initialisation du modèle
59
+ callback_manager = CallbackManager([StreamingStdOutCallbackHandler()]) if verbose else None
60
+ self.llm = ChatOpenAI(
61
+ model=self.model_name,
62
+ temperature=self.temperature,
63
+ api_key=self.api_key,
64
+ verbose=self.verbose,
65
+ callback_manager=callback_manager
66
+ )
67
+
68
+ # Charger les outils
69
+ self.tools = self._setup_tools()
70
+
71
+ # Initialiser la mémoire
72
+ self.memory = ConversationBufferMemory(
73
+ memory_key="chat_history",
74
+ return_messages=True
75
+ )
76
+
77
+ # Créer l'agent
78
+ self.agent = self._create_agent()
79
+
80
+ def _setup_tools(self) -> List:
81
+ """Configure et retourne les outils disponibles pour l'agent"""
82
+ tools = []
83
+
84
+ # Ajouter les outils selon la configuration
85
+ if config.ENABLE_WEB_SEARCH:
86
+ tools.append(WebSearchTool())
87
+ tools.append(WebContentTool())
88
+
89
+ if config.ENABLE_CALCULATOR:
90
+ tools.append(CalculatorTool())
91
+
92
+ if config.ENABLE_DATE_TOOL:
93
+ tools.append(CurrentDateTool())
94
+
95
+ # Toujours ajouter l'outil JSON parser
96
+ tools.append(JsonParserTool())
97
+
98
+ logger.info(f"Agent initialisé avec {len(tools)} outils")
99
+ return tools
100
+
101
+ def _create_agent(self) -> AgentExecutor:
102
+ """Crée et configure l'exécuteur d'agent"""
103
+
104
+ # Définir le prompt
105
+ prompt = [
106
+ SystemMessage(content=self.system_message),
107
+ MessagesPlaceholder(variable_name="chat_history"),
108
+ HumanMessage(content="{input}"),
109
+ MessagesPlaceholder(variable_name="agent_scratchpad"),
110
+ ]
111
+
112
+ # Créer l'agent
113
+ agent = OpenAIFunctionsAgent(
114
+ llm=self.llm,
115
+ tools=self.tools,
116
+ prompt=prompt
117
+ )
118
+
119
+ # Créer l'exécuteur d'agent
120
+ agent_executor = AgentExecutor(
121
+ agent=agent,
122
+ tools=self.tools,
123
+ memory=self.memory,
124
+ verbose=self.verbose,
125
+ max_iterations=10,
126
+ early_stopping_method="generate"
127
+ )
128
+
129
+ return agent_executor
130
+
131
+ def __call__(self, question: str) -> str:
132
+ """
133
+ Répond à une question en utilisant l'agent.
134
+
135
+ Args:
136
+ question: La question à laquelle répondre
137
+
138
+ Returns:
139
+ La réponse de l'agent
140
+ """
141
+ if not question.strip():
142
+ return "Veuillez poser une question."
143
+
144
+ try:
145
+ logger.info(f"Question reçue: {question[:50]}...")
146
+
147
+ # Exécuter l'agent
148
+ response = self.agent.invoke({"input": question})
149
+
150
+ # Extraire la réponse
151
+ answer = response.get("output", "Je n'ai pas pu générer de réponse.")
152
+
153
+ logger.info(f"Réponse générée: {answer[:50]}...")
154
+ return answer
155
+
156
+ except Exception as e:
157
+ logger.error(f"Erreur lors du traitement de la question: {str(e)}")
158
+ return f"Désolé, une erreur s'est produite: {str(e)}"
159
+
160
+ def reset_memory(self):
161
+ """Réinitialise la mémoire de l'agent"""
162
+ self.memory.clear()
163
+ logger.info("Mémoire de l'agent réinitialisée")
164
+
165
+ def get_last_interactions(self, count: int = 5) -> List[Dict[str, Any]]:
166
+ """
167
+ Retourne les dernières interactions de l'agent
168
+
169
+ Args:
170
+ count: Nombre d'interactions à retourner
171
+
172
+ Returns:
173
+ Liste des dernières interactions
174
+ """
175
+ history = self.memory.chat_memory.messages
176
+
177
+ interactions = []
178
+ for i in range(0, len(history), 2):
179
+ if i + 1 < len(history):
180
+ human_msg = history[i].content if hasattr(history[i], 'content') else str(history[i])
181
+ ai_msg = history[i+1].content if hasattr(history[i+1], 'content') else str(history[i+1])
182
+
183
+ interactions.append({
184
+ "question": human_msg,
185
+ "answer": ai_msg
186
+ })
187
+
188
+ return interactions[-count:]
app.py CHANGED
@@ -3,23 +3,34 @@ import gradio as gr
3
  import requests
4
  import inspect
5
  import pandas as pd
 
 
 
 
 
 
 
 
6
 
7
- # (Keep Constants as is)
8
  # --- Constants ---
9
- DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
10
-
11
- # --- Basic Agent Definition ---
12
- # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
13
- class BasicAgent:
14
- def __init__(self):
15
- print("BasicAgent initialized.")
16
- def __call__(self, question: str) -> str:
17
- print(f"Agent received question (first 50 chars): {question[:50]}...")
18
- fixed_answer = "This is a default answer."
19
- print(f"Agent returning fixed answer: {fixed_answer}")
20
- return fixed_answer
21
-
22
- def run_and_submit_all( profile: gr.OAuthProfile | None):
 
 
 
 
23
  """
24
  Fetches all questions, runs the BasicAgent on them, submits all answers,
25
  and displays the results.
@@ -38,13 +49,15 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
38
  questions_url = f"{api_url}/questions"
39
  submit_url = f"{api_url}/submit"
40
 
41
- # 1. Instantiate Agent ( modify this part to create your agent)
42
  try:
43
- agent = BasicAgent()
 
44
  except Exception as e:
45
  print(f"Error instantiating agent: {e}")
46
  return f"Error initializing agent: {e}", None
47
- # In the case of an app running as a hugging Face space, this link points toward your codebase ( usefull for others so please keep it public)
 
48
  agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
49
  print(agent_code)
50
 
@@ -140,9 +153,21 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
140
  return status_message, results_df
141
 
142
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  # --- Build Gradio Interface using Blocks ---
144
  with gr.Blocks() as demo:
145
- gr.Markdown("# Basic Agent Evaluation Runner")
146
  gr.Markdown(
147
  """
148
  **Instructions:**
@@ -192,5 +217,8 @@ if __name__ == "__main__":
192
 
193
  print("-"*(60 + len(" App Starting ")) + "\n")
194
 
195
- print("Launching Gradio Interface for Basic Agent Evaluation...")
 
 
 
196
  demo.launch(debug=True, share=False)
 
3
  import requests
4
  import inspect
5
  import pandas as pd
6
+ from dotenv import load_dotenv
7
+
8
+ # Importer notre agent avancé
9
+ from agent import AdvancedAgent
10
+ import config
11
+
12
+ # Charger les variables d'environnement
13
+ load_dotenv()
14
 
 
15
  # --- Constants ---
16
+ DEFAULT_API_URL = config.DEFAULT_API_URL
17
+
18
+ # Initialiser l'agent
19
+ agent = None
20
+
21
+ def initialize_agent():
22
+ """Initialise l'agent s'il n'est pas déjà initialisé"""
23
+ global agent
24
+ if agent is None:
25
+ try:
26
+ agent = AdvancedAgent(verbose=False)
27
+ return True
28
+ except Exception as e:
29
+ print(f"Erreur lors de l'initialisation de l'agent: {e}")
30
+ return False
31
+ return True
32
+
33
+ def run_and_submit_all(profile: gr.OAuthProfile | None):
34
  """
35
  Fetches all questions, runs the BasicAgent on them, submits all answers,
36
  and displays the results.
 
49
  questions_url = f"{api_url}/questions"
50
  submit_url = f"{api_url}/submit"
51
 
52
+ # 1. Instantiate Agent
53
  try:
54
+ if not initialize_agent():
55
+ return "Impossible d'initialiser l'agent. Vérifiez les logs pour plus d'informations.", None
56
  except Exception as e:
57
  print(f"Error instantiating agent: {e}")
58
  return f"Error initializing agent: {e}", None
59
+
60
+ # In the case of an app running as a hugging Face space, this link points toward your codebase
61
  agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
62
  print(agent_code)
63
 
 
153
  return status_message, results_df
154
 
155
 
156
+ # Fonction pour tester l'agent sur une seule question
157
+ def test_agent(question):
158
+ if not initialize_agent():
159
+ return "Impossible d'initialiser l'agent. Vérifiez les logs pour plus d'informations."
160
+
161
+ try:
162
+ answer = agent(question)
163
+ return answer
164
+ except Exception as e:
165
+ return f"Erreur: {str(e)}"
166
+
167
+
168
  # --- Build Gradio Interface using Blocks ---
169
  with gr.Blocks() as demo:
170
+ gr.Markdown("# Agent avec LangChain - Évaluation")
171
  gr.Markdown(
172
  """
173
  **Instructions:**
 
217
 
218
  print("-"*(60 + len(" App Starting ")) + "\n")
219
 
220
+ # Initialiser l'agent au démarrage
221
+ initialize_agent()
222
+
223
+ print("Launching Gradio Interface for Advanced Agent Evaluation...")
224
  demo.launch(debug=True, share=False)
config.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ # Charger les variables d'environnement
5
+ load_dotenv()
6
+
7
+ # Configuration de l'API
8
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
9
+
10
+ # Configuration de l'agent
11
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
12
+ DEFAULT_MODEL = "gpt-3.5-turbo" # Modèle par défaut
13
+
14
+ # Configurations diverses
15
+ MAX_TOKENS = 4096
16
+ TEMPERATURE = 0.7
17
+ RETRY_ATTEMPTS = 3
18
+
19
+ # Configuration des outils
20
+ ENABLE_WEB_SEARCH = True
21
+ ENABLE_CALCULATOR = True
22
+ ENABLE_DATE_TOOL = True
23
+
24
+ # Message système par défaut pour l'agent
25
+ DEFAULT_SYSTEM_MESSAGE = """You are a general AI assistant. I will ask you a question.
26
+ Report your thoughts, and finish your answer with the following template: FINAL ANSWER: [YOUR FINAL ANSWER].
27
+ YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
28
+ If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise.
29
+ If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise.
30
+ If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string. """
requirements.txt CHANGED
@@ -1,2 +1,8 @@
1
  gradio
2
- requests
 
 
 
 
 
 
 
1
  gradio
2
+ requests
3
+ langchain
4
+ langchain-openai
5
+ langchain-core
6
+ python-dotenv
7
+ bs4
8
+ pandas
tools/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from tools.web_tools import WebSearchTool, WebContentTool
2
+ from tools.utils import CurrentDateTool, JsonParserTool, CalculatorTool
3
+
4
+ __all__ = [
5
+ "WebSearchTool",
6
+ "WebContentTool",
7
+ "CurrentDateTool",
8
+ "JsonParserTool",
9
+ "CalculatorTool"
10
+ ]
tools/web_tools.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ from bs4 import BeautifulSoup
3
+ from typing import List, Dict, Any
4
+ from langchain.tools import BaseTool
5
+
6
+ class WebSearchTool(BaseTool):
7
+ name = "web_search"
8
+ description = "Recherche des informations sur le web à partir d'un terme de recherche"
9
+
10
+ def _run(self, query: str) -> str:
11
+ """Exécute une recherche web et retourne les résultats pertinents"""
12
+ try:
13
+ # Cette fonction simule une recherche web
14
+ # Dans un cas réel, vous pourriez utiliser l'API Google ou Bing
15
+ headers = {
16
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
17
+ }
18
+ search_url = f"https://www.google.com/search?q={query.replace(' ', '+')}"
19
+ response = requests.get(search_url, headers=headers)
20
+
21
+ if response.status_code != 200:
22
+ return f"Erreur lors de la recherche: {response.status_code}"
23
+
24
+ # Extraction des résultats avec BeautifulSoup
25
+ soup = BeautifulSoup(response.text, 'html.parser')
26
+ search_results = []
27
+
28
+ # Extrait les titres et descriptions des résultats
29
+ for result in soup.select("div.g"):
30
+ title_elem = result.select_one("h3")
31
+ if not title_elem:
32
+ continue
33
+
34
+ title = title_elem.get_text()
35
+ link = result.select_one("a")["href"] if result.select_one("a") else ""
36
+ snippet = result.select_one("div.VwiC3b")
37
+ description = snippet.get_text() if snippet else ""
38
+
39
+ if title and description:
40
+ search_results.append(f"Titre: {title}\nLien: {link}\nDescription: {description}\n---")
41
+
42
+ if not search_results:
43
+ return "Aucun résultat trouvé pour cette recherche."
44
+
45
+ return "\n".join(search_results[:3]) # Limite à 3 résultats
46
+
47
+ except Exception as e:
48
+ return f"Erreur lors de la recherche web: {str(e)}"
49
+
50
+ async def _arun(self, query: str) -> str:
51
+ """Version asynchrone de l'outil"""
52
+ # Implémentation asynchrone si nécessaire
53
+ return self._run(query)
54
+
55
+
56
+ class WebContentTool(BaseTool):
57
+ name = "fetch_web_content"
58
+ description = "Récupère le contenu d'une page web à partir d'une URL"
59
+
60
+ def _run(self, url: str) -> str:
61
+ """Récupère et nettoie le contenu d'une page web"""
62
+ try:
63
+ headers = {
64
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
65
+ }
66
+ response = requests.get(url, headers=headers)
67
+
68
+ if response.status_code != 200:
69
+ return f"Erreur lors de la récupération du contenu: {response.status_code}"
70
+
71
+ # Extraction du contenu avec BeautifulSoup
72
+ soup = BeautifulSoup(response.text, 'html.parser')
73
+
74
+ # Supprimer les scripts, styles et autres éléments non pertinents
75
+ for element in soup(['script', 'style', 'header', 'footer', 'nav']):
76
+ element.decompose()
77
+
78
+ # Extraire le texte principal
79
+ text = soup.get_text(separator='\n')
80
+
81
+ # Nettoyer le texte (espaces multiples, lignes vides)
82
+ lines = [line.strip() for line in text.split('\n') if line.strip()]
83
+ cleaned_text = '\n'.join(lines)
84
+
85
+ # Limiter la longueur du texte retourné
86
+ max_length = 5000
87
+ if len(cleaned_text) > max_length:
88
+ cleaned_text = cleaned_text[:max_length] + "... (contenu tronqué)"
89
+
90
+ return cleaned_text
91
+
92
+ except Exception as e:
93
+ return f"Erreur lors de la récupération du contenu web: {str(e)}"
94
+
95
+ async def _arun(self, url: str) -> str:
96
+ """Version asynchrone de l'outil"""
97
+ # Implémentation asynchrone si nécessaire
98
+ return self._run(url)