José Ángel González commited on
Commit
25d44c1
·
1 Parent(s): 4b0a0b7

moved to langgraph

Browse files
Files changed (4) hide show
  1. agents/__init__.py +0 -1
  2. agents/llm_only.py +0 -25
  3. agents/react_agent.py +516 -99
  4. requirements.txt +17 -12
agents/__init__.py CHANGED
@@ -1,2 +1 @@
1
- from .llm_only import LLMOnly
2
  from .react_agent import ReactAgent
 
 
1
  from .react_agent import ReactAgent
agents/llm_only.py DELETED
@@ -1,25 +0,0 @@
1
- from openai import OpenAI
2
- from pydantic import BaseModel
3
-
4
- MODEL = "gpt-4o"
5
- SYSTEM_PROMPT = "You are a general AI assistant. I will ask you a question. Report your thoughts, and write your final answer. Your final answer should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. 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. 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. 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."
6
-
7
- class OutputSchema(BaseModel):
8
- thoughts: str
9
- final_answer: str
10
-
11
- class LLMOnly:
12
- def __init__(self):
13
- self.client = OpenAI()
14
-
15
- def __call__(self, question: str) -> str:
16
- response = self.client.beta.chat.completions.parse(
17
- model=MODEL,
18
- messages=[
19
- {"role": "system", "content": SYSTEM_PROMPT},
20
- {"role": "user", "content": question}
21
- ],
22
- response_format=OutputSchema
23
- )
24
- answer = response.choices[0].message.parsed
25
- return answer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
agents/react_agent.py CHANGED
@@ -1,132 +1,549 @@
1
- from smolagents import (
2
- OpenAIServerModel,
3
- CodeAgent,
4
- # DuckDuckGoSearchTool,
5
- GoogleSearchTool,
6
- PythonInterpreterTool,
7
- VisitWebpageTool,
8
- Tool,
9
- HfApiModel
10
- )
11
- from PIL import Image
12
- import requests
13
- from io import BytesIO
14
  import os
15
- from pathlib import Path
16
- import pandas as pd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
 
 
 
18
 
19
- # Utils
20
- def parse_image(content: bytes) -> Image:
21
- return Image.open(BytesIO(content)).convert("RGB")
22
 
 
 
 
 
23
 
24
- def parse_excel(content: bytes) -> str:
25
- return pd.read_excel(BytesIO(content)).to_markdown(index=False)
 
26
 
27
 
28
- def parse_text(content: str) -> str:
29
- return content
 
30
 
 
 
31
 
32
- def parse_mp3(content: bytes) -> str:
33
- # Faster than load & run a whisper model
34
- # by ourselves or using the SpechToTextTool
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  speech_to_text = Tool.from_space(
36
  "maguid28/TranscriptTool",
37
  name="transcription_tool",
38
  description="Transcribe speech to text",
39
  )
40
- with open("audio.mp3", "wb") as fw:
41
- fw.write(content)
42
- return speech_to_text("audio.mp3")
43
 
44
 
45
- def download(task_id: str) -> bytes:
46
- response = requests.get(FILE_URL.format(task_id=task_id))
47
- return response.content
48
 
 
 
49
 
50
- def get_type_and_parser(file_name: str) -> dict:
51
- path = Path(file_name)
52
- return EXTENSIONS[path.suffix]
 
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
- # Configs
56
- EXTENSIONS = {
57
- ".png": {"type": "image", "parser": parse_image},
58
- ".jpg": {"type": "image", "parser": parse_image},
59
- ".jpeg": {"type": "image", "parser": parse_image},
60
- ".xlsx": {"type": "document", "parser": parse_excel},
61
- ".txt": {"type": "document", "parser": parse_text},
62
- ".py": {"type": "document", "parser": parse_text},
63
- ".mp3": {"type": "audio", "parser": parse_mp3},
64
- }
65
- DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
66
- FILE_URL = f"{DEFAULT_API_URL}/files/{{task_id}}"
67
 
68
- # Tools
69
- TOOLS = [
70
- GoogleSearchTool(provider="serper"),
71
- PythonInterpreterTool(),
72
- VisitWebpageTool(max_output_length=5000),
73
- ]
74
- # DuckDuckGoSearchTool()
75
- AUTHORIZED_IMPORTS = ["json", "pandas", "numpy", "datetime", "requests", "bs4"]
76
 
 
 
77
 
78
- class ReactAgent:
79
- def __init__(self):
80
- #model = OpenAIServerModel(
81
- # model_id="gpt-4o",
82
- # api_key=os.environ["OPENAI_API_KEY"],
83
- # temperature=0,
84
- #)
85
- self.agent = CodeAgent(
86
- #model=model,
87
- model=HfApiModel(),
88
- tools=TOOLS,
89
- additional_authorized_imports=AUTHORIZED_IMPORTS,
90
- max_steps=20,
91
- verbosity_level=2,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  )
93
- self.steer_system_prompt()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
- def __call__(self, question: dict) -> str:
96
- file_name = question.get("file_name")
 
 
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  if file_name:
99
- content = download(question["task_id"])
100
- file_info = get_type_and_parser(file_name)
101
- parsed_content = file_info["parser"](content)
102
- user_question = question["question"]
103
- if file_info["type"] == "image":
104
- return self.agent.run(user_question, images=[parsed_content])
105
- print(parsed_content)
106
- user_question = (
107
- f"{user_question}\n"
108
- f"Here is the content of the file you have to consider to answer the question:\n"
109
- f"{parsed_content}"
110
- )
111
- return self.agent.run(user_question)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
- return self.agent.run(question["question"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
- def steer_system_prompt(self):
116
- prev_system_prompt = self.agent.system_prompt
117
- prompt_prefix = prev_system_prompt.split("Now Begin!")[0].strip()
118
- gaia_answer_rules = """\n\nYour final answer should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. 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. 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. 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."""
119
- gaia_answer_rules += """ You must wrap your final answer in the ```code``` block by using the `final_answer` tool or your mom will die."""
120
- system_prompt = prompt_prefix + gaia_answer_rules + "\n\nNow Begin!"
121
- self.agent.system_prompt = system_prompt
122
 
123
  if __name__ == "__main__":
124
- question4 = {
125
- "task_id": "99c9cc74-fdc8-46c6-8f8d-3ce2d3bfeea3",
126
- "question": 'Hi, I\'m making a pie but I could use some help with my shopping list. I have everything I need for the crust, but I\'m not sure about the filling. I got the recipe from my friend Aditi, but she left it as a voice memo and the speaker on my phone is buzzing so I can\'t quite make out what she\'s saying. Could you please listen to the recipe and list all of the ingredients that my friend described? I only want the ingredients for the filling, as I have everything I need to make my favorite pie crust. I\'ve attached the recipe as Strawberry pie.mp3.\n\nIn your response, please only list the ingredients, not any measurements. So if the recipe calls for "a pinch of salt" or "two cups of ripe strawberries" the ingredients on the list would be "salt" and "ripe strawberries".\n\nPlease format your response as a comma separated list of ingredients. Also, please alphabetize the ingredients.',
 
127
  "Level": "1",
128
- "file_name": "99c9cc74-fdc8-46c6-8f8d-3ce2d3bfeea3.mp3",
129
  }
130
-
131
- agent = ReactAgent()
132
- response = agent(question4)
 
1
+ from langchain_community.utilities import GoogleSerperAPIWrapper
2
+ from smolagents import PythonInterpreterTool
3
+ from langgraph.graph import MessagesState
4
+ from langchain_openai import ChatOpenAI
5
+ from langgraph.graph import START, StateGraph
6
+ from langgraph.prebuilt import tools_condition, ToolNode
7
+ from langchain_core.messages import SystemMessage
8
+ from openai import OpenAI
9
+ from smolagents import Tool
10
+ from typing import Optional
11
+ import tempfile
 
 
12
  import os
13
+ from urllib.parse import urlparse
14
+ from base64 import b64encode
15
+ import requests
16
+ from bs4 import BeautifulSoup
17
+ import re
18
+ import wikipediaapi
19
+
20
+ # Configs
21
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
22
+ FILE_URL = f"{DEFAULT_API_URL}/files/{{task_id}}"
23
+
24
+
25
+ # Tools
26
+ def search_tool(query: str) -> str:
27
+ """Search in Google and returns an string with title, link, and snippet for the top 10 results.
28
+
29
+ Args:
30
+ query: str
31
+
32
+ Returns:
33
+ Title, link, and snippet for the top 10 results
34
+ """
35
+ searcher = GoogleSerperAPIWrapper(k=10)
36
+ retries = 3
37
+ result = ""
38
+ while retries > 0:
39
+ try:
40
+ search_results = searcher.results(query)["organic"]
41
+ for row in search_results:
42
+ result += f"Title: {row['title']}\nSnippet: {row['snippet']}\nURL: {row['link']}\n\n"
43
+ return result
44
+ except Exception as e:
45
+ retries -= 1
46
+ return f"There was an error with Google search: {e}"
47
+
48
+
49
+ def save_file(content: str, filename: Optional[str]) -> str:
50
+ """
51
+ Save content to a temporary file and return the path.
52
+ Useful for processing files from the GAIA API.
53
+
54
+ Args:
55
+ content: The content to save to the file
56
+ filename: Optional filename, will generate a random name if not provided
57
+
58
+ Returns:
59
+ Path to the saved file
60
+ """
61
+ temp_dir = tempfile.gettempdir()
62
+ if filename is None:
63
+ temp_file = tempfile.NamedTemporaryFile(delete=False)
64
+ filepath = temp_file.name
65
+ else:
66
+ filepath = os.path.join(temp_dir, filename)
67
+
68
+ # Write content to the file
69
+ with open(filepath, "w") as f:
70
+ f.write(content)
71
+
72
+ return f"File saved to {filepath}. You can read this file to process its contents."
73
+
74
+
75
+ def download_file_from_task_id(task_id: str, filename: str) -> str:
76
+ """
77
+ Download a file for a GAIA task using `task_id` if `file_extension` of the task is specified in the prompt.
78
+
79
+ Args:
80
+ task_id: id of the task
81
+ filename: filename
82
+
83
+ Returns:
84
+ Path to the downloaded file
85
+ """
86
+ return download_file_from_url(FILE_URL.format(task_id=task_id), filename)
87
+
88
+
89
+ def download_file_from_url(url: str, filename: str) -> str:
90
+ """
91
+ Download a file from a URL and save it to a temporary location.
92
+
93
+ Args:
94
+ url: The URL to download from
95
+ filename: filename
96
+
97
+ Returns:
98
+ Path to the downloaded file
99
+ """
100
+ try:
101
+ # Parse URL to get filename if not provided
102
+ if not filename:
103
+ path = urlparse(url).path
104
+ filename = os.path.basename(path)
105
+ if not filename:
106
+ # Generate a random name if we couldn't extract one
107
+ import uuid
108
+
109
+ filename = f"downloaded_{uuid.uuid4().hex[:8]}"
110
 
111
+ # Create temporary file
112
+ temp_dir = tempfile.gettempdir()
113
+ filepath = os.path.join(temp_dir, filename)
114
 
115
+ # Download the file
116
+ response = requests.get(url, stream=True)
117
+ response.raise_for_status()
118
 
119
+ # Save the file
120
+ with open(filepath, "wb") as f:
121
+ for chunk in response.iter_content(chunk_size=8192):
122
+ f.write(chunk)
123
 
124
+ return f"File downloaded to {filepath}. You can now process this file."
125
+ except Exception as e:
126
+ return f"Error downloading file: {str(e)}"
127
 
128
 
129
+ def analyze_csv_file(file_path: str) -> str:
130
+ """
131
+ Analyze a CSV file using pandas and answer a question about it.
132
 
133
+ Args:
134
+ file_path: Path to the CSV file
135
 
136
+ Returns:
137
+ Analysis result or error message
138
+ """
139
+ try:
140
+ import pandas as pd
141
+
142
+ # Read the CSV file
143
+ df = pd.read_csv(file_path)
144
+
145
+ # Run various analyses based on the query
146
+ result = f"CSV file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
147
+ result += f"Columns: {', '.join(df.columns)}\n\n"
148
+
149
+ # Add summary statistics
150
+ result += "Summary statistics:\n"
151
+ result += str(df.describe())
152
+ result += "\n\n" + df.head(100)
153
+ return result
154
+ except ImportError:
155
+ return "Error: pandas is not installed. Please install it with 'pip install pandas'."
156
+ except Exception as e:
157
+ return f"Error analyzing CSV file: {str(e)}"
158
+
159
+
160
+ def analyze_excel_file(file_path: str) -> str:
161
+ """
162
+ Analyze an Excel file using pandas and answer a question about it.
163
+
164
+ Args:
165
+ file_path: Path to the Excel file
166
+
167
+ Returns:
168
+ Analysis result or error message
169
+ """
170
+ try:
171
+ import pandas as pd
172
+
173
+ # Read the Excel file
174
+ df = pd.read_excel(file_path)
175
+ print(df)
176
+ # Run various analyses based on the query
177
+ result = f"Excel file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
178
+ result += f"Columns: {', '.join(df.columns)}\n\n"
179
+
180
+ # Add summary statistics
181
+ result += "Summary statistics:\n"
182
+ result += str(df.describe())
183
+ result += "\n\n" + str(df.head(100))
184
+ return result
185
+ except ImportError:
186
+ return "Error: pandas and openpyxl are not installed. Please install them with 'pip install pandas openpyxl'."
187
+ except Exception as e:
188
+ return f"Error analyzing Excel file: {str(e)}"
189
+
190
+
191
+ def transcribe_speech(filename: str) -> str:
192
+ """Transcribe speech to text
193
+
194
+ Args:
195
+ filename: str
196
+
197
+ Returns:
198
+ Transcribed speech as string
199
+ """
200
  speech_to_text = Tool.from_space(
201
  "maguid28/TranscriptTool",
202
  name="transcription_tool",
203
  description="Transcribe speech to text",
204
  )
205
+ return f"The transcription is: {speech_to_text(filename)}"
 
 
206
 
207
 
208
+ def python_interpreter(code: str) -> str:
209
+ """A Python interpreter
 
210
 
211
+ Args:
212
+ code: str
213
 
214
+ Returns:
215
+ The output of the interpreter
216
+ """
217
+ import traceback
218
 
219
+ interpreter = PythonInterpreterTool(
220
+ authorized_imports=[
221
+ "json",
222
+ "pandas",
223
+ "numpy",
224
+ "datetime",
225
+ "requests",
226
+ "bs4",
227
+ ]
228
+ )
229
+ try:
230
+ return interpreter(code)
231
+ except Exception as e:
232
+ return f"There was an exception in the interpreter: {traceback.format_exc()}"
233
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
+ def reverse_text(text: str) -> str:
236
+ """Reverses a text written from right to left
 
 
 
 
 
 
237
 
238
+ Args:
239
+ text: a reversed text
240
 
241
+ Returns:
242
+ The text written from left to right
243
+ """
244
+ return f"The reversed text is: {text[::-1]}"
245
+
246
+
247
+ def visit_webpage(url: str) -> str:
248
+ """Visits a webpage and returns the content
249
+
250
+ Args:
251
+ url: url of the webpage
252
+
253
+ Returns:
254
+ The webpage content
255
+ """
256
+ retries = 3
257
+ while retries > 0:
258
+ try:
259
+ response = requests.get(
260
+ url,
261
+ headers={
262
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36"
263
+ },
264
+ )
265
+ html = response.content
266
+ soup = BeautifulSoup(html, "html.parser")
267
+ for tag in soup.find_all(
268
+ ["header", "footer", "nav", "section", "aside"]
269
+ ):
270
+ tag.decompose()
271
+
272
+ for tag in soup.find_all(["script", "style"]):
273
+ tag.decompose()
274
+
275
+ meaningful_texts = []
276
+ for tag in soup.find_all(["p", "span", "div"]):
277
+ text = tag.get_text(separator=" ", strip=True)
278
+ if text:
279
+ meaningful_texts.append(text)
280
+
281
+ # Join all texts nicely
282
+ final_text = " ".join(meaningful_texts)
283
+
284
+ # Clean multiple spaces
285
+ final_text = re.sub(r"\s+", " ", final_text)
286
+ return " ".join(final_text.split()[:3000])
287
+
288
+ except Exception as e:
289
+ retries -= 1
290
+
291
+ return f"There was an error visiting the webpage: {e}"
292
+
293
+
294
+ def image_understanding(filename: str, question: str) -> str:
295
+ """Answers some question on an image
296
+
297
+ Args:
298
+ filename: the name of the image file
299
+ question: a question about the image
300
+ """
301
+ client = OpenAI()
302
+ with open(filename, "rb") as fr:
303
+ image_bytes = fr.read()
304
+ b64_image = b64encode(image_bytes).decode("utf-8")
305
+ response = client.responses.create(
306
+ model="gpt-4o",
307
+ input=[
308
+ {
309
+ "role": "user",
310
+ "content": [
311
+ {"type": "input_text", "text": question},
312
+ {
313
+ "type": "input_image",
314
+ "image_url": f"data:image/png;base64,{b64_image}",
315
+ },
316
+ ],
317
+ }
318
+ ],
319
+ )
320
+ return response.output[0].content[0].text
321
+
322
+
323
+ def get_wikipedia_article(entity: str) -> str:
324
+ """Get the text from the Wikipedia article of an entity.
325
+
326
+ Args:
327
+ entity: the name of the entity. Only for entities existing in Wikipedia, e.g. use "Mercedes Sosa" instead of "Mercedes Sosa discography"
328
+
329
+ Returns:
330
+ The text of the Wikipedia article of the entity
331
+ """
332
+ try:
333
+ wiki_wiki = wikipediaapi.Wikipedia(
334
+ user_agent="GAIA Benchmark (jogonba2)",
335
+ language="en",
336
+ extract_format=wikipediaapi.ExtractFormat.WIKI,
337
  )
338
+ p_wiki = wiki_wiki.page(entity)
339
+ text = p_wiki.text
340
+ if not text:
341
+ return f"The article is empty for {entity}. Please, be sure that the entity appears in Wikipedia."
342
+ return " ".join(text.split(" ")[:3000])
343
+ except Exception as e:
344
+ return "There was an exception looking at Wikipedia: {e}"
345
+
346
+
347
+ """
348
+ Tool to reinforce the output format.
349
+ """
350
+
351
+
352
+ def prepare_final_answer(candidate_answer: str, question: str) -> str:
353
+ """Prepare your final answer according to the guidelines in the prompt.
354
+ This tool must be called always before giving the final anwer.
355
+
356
+ Args:
357
+ candidate_answer: a candidate answer
358
+ question: the user question to know how to prepare the final answer
359
+
360
+ Returns:
361
+ Your final answer
362
+ """
363
+ client = OpenAI()
364
+
365
+ system_prompt = """Your final answer should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
366
+ Here are more detailed instructions you must follow to write your final answer according to the provided question:
367
+ 1) If you are asked for a number (how much, how many, ...), you must write a number!. Don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise.
368
+ 2) 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.
369
+ 3) 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.
370
+
371
+ If you follow all these instructions perfectly, you will win 1,000,000 dollars, otherwise, your mom will die"""
372
+
373
+ user_prompt = f"Question: {question}\nCandidate answer: {candidate_answer}"
374
+ response = client.responses.create(
375
+ model="gpt-4o",
376
+ input=[
377
+ {
378
+ "role": "user",
379
+ "content": [
380
+ {"type": "input_text", "text": user_prompt},
381
+ ],
382
+ }
383
+ ],
384
+ )
385
+ return response.output[0].content[0].text
386
+
387
+
388
+ # Nodes
389
+ def assistant(state: MessagesState):
390
+ return {
391
+ "messages": [llm_with_tools.invoke([system_prompt] + state["messages"])]
392
+ }
393
+
394
 
395
+ # System message
396
+ system_prompt = SystemMessage(
397
+ content="""You are a general AI assistant being evaluated in the GAIA Benchmark.
398
+ I will ask you a question and you must reach your final answer by using a set of tools I provide to you. Please, when you are needed to pass file names to the tools, pass absolute paths.
399
 
400
+ Your final answer should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
401
+ Here are more detailed instructions you must follow to write your final answer:
402
+ 1) If you are asked for a number, you must write a number!. Don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise.
403
+ 2) 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.
404
+ 3) 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.
405
+
406
+ If you follow all these instructions perfectly, you will win 1,000,000 dollars, otherwise, your mom will die.
407
+
408
+ Let's start!
409
+ """
410
+ )
411
+
412
+ llm = ChatOpenAI(model="gpt-4o")
413
+ tools = [
414
+ search_tool,
415
+ save_file,
416
+ download_file_from_task_id,
417
+ download_file_from_url,
418
+ analyze_csv_file,
419
+ analyze_excel_file,
420
+ transcribe_speech,
421
+ python_interpreter,
422
+ visit_webpage,
423
+ # reverse_text,
424
+ image_understanding,
425
+ # get_wikipedia_article
426
+ # prepare_final_answer,
427
+ ]
428
+ llm_with_tools = llm.bind_tools(tools)
429
+
430
+ # Graph
431
+ builder = StateGraph(MessagesState)
432
+
433
+ # Define nodes: these do the work
434
+ builder.add_node("assistant", assistant)
435
+ builder.add_node("tools", ToolNode(tools))
436
+
437
+ # Define edges: these determine the control flow
438
+ builder.add_edge(START, "assistant")
439
+ builder.add_conditional_edges(
440
+ "assistant",
441
+ tools_condition,
442
+ )
443
+ builder.add_edge("tools", "assistant")
444
+ react_graph = builder.compile()
445
+
446
+
447
+ def print_stream(stream):
448
+ for s in stream:
449
+ message = s["messages"][-1]
450
+ if isinstance(message, tuple):
451
+ print(message)
452
+ else:
453
+ message.pretty_print()
454
+
455
+
456
+ class ReactAgent:
457
+ def __init__(self, verbose: bool = False):
458
+ self.graph = react_graph
459
+ self.verbose = verbose
460
+
461
+ def __call__(self, task: dict) -> str:
462
+ question = task["question"]
463
+ task_id = task["task_id"]
464
+ file_name = task.get("file_name")
465
+ file_ext = None
466
+ user_prompt = question
467
  if file_name:
468
+ file_ext = os.path.splitext(file_name)[-1].removeprefix(".")
469
+ user_prompt += f"\nTask ID: {task_id}\nFile extension: {file_ext}"
470
+
471
+ user_input = {"messages": [("user", user_prompt)]}
472
+ if self.verbose:
473
+ print_stream(self.graph.stream(user_input, stream_mode="values"))
474
+ else:
475
+ answer = self.graph.invoke(user_input)["messages"][-1].content
476
+ return self._clean_answer(answer)
477
+
478
+ def _clean_answer(self, answer: any) -> str:
479
+ """
480
+ Taken from `susmitsil`:
481
+ https://huggingface.co/spaces/susmitsil/FinalAgenticAssessment/blob/main/main_agent.py
482
+ Clean up the answer to remove common prefixes and formatting
483
+ that models often add but that can cause exact match failures.
484
+
485
+ Args:
486
+ answer: The raw answer from the model
487
+
488
+ Returns:
489
+ The cleaned answer as a string
490
+ """
491
+ # Convert non-string types to strings
492
+ if not isinstance(answer, str):
493
+ # Handle numeric types (float, int)
494
+ if isinstance(answer, float):
495
+ # Format floating point numbers properly
496
+ # Check if it's an integer value in float form (e.g., 12.0)
497
+ if answer.is_integer():
498
+ formatted_answer = str(int(answer))
499
+ else:
500
+ # For currency values that might need formatting
501
+ if abs(answer) >= 1000:
502
+ formatted_answer = f"${answer:,.2f}"
503
+ else:
504
+ formatted_answer = str(answer)
505
+ return formatted_answer
506
+ elif isinstance(answer, int):
507
+ return str(answer)
508
+ else:
509
+ # For any other type
510
+ return str(answer)
511
 
512
+ # Now we know answer is a string, so we can safely use string methods
513
+ # Normalize whitespace
514
+ answer = answer.strip()
515
+
516
+ # Remove common prefixes and formatting that models add
517
+ prefixes_to_remove = [
518
+ "The answer is ",
519
+ "Answer: ",
520
+ "Final answer: ",
521
+ "The result is ",
522
+ "To answer this question: ",
523
+ "Based on the information provided, ",
524
+ "According to the information: ",
525
+ ]
526
+
527
+ for prefix in prefixes_to_remove:
528
+ if answer.startswith(prefix):
529
+ answer = answer[len(prefix) :].strip()
530
+
531
+ # Remove quotes if they wrap the entire answer
532
+ if (answer.startswith('"') and answer.endswith('"')) or (
533
+ answer.startswith("'") and answer.endswith("'")
534
+ ):
535
+ answer = answer[1:-1].strip()
536
+
537
+ return answer
538
 
 
 
 
 
 
 
 
539
 
540
  if __name__ == "__main__":
541
+
542
+ task = {
543
+ "task_id": "8e867cd7-cff9-4e6c-867a-ff5ddc2550be",
544
+ "question": "How many studio albums were published by Mercedes Sosa between 2000 and 2009 (included)? You can use the latest 2022 version of english wikipedia.",
545
  "Level": "1",
546
+ "file_name": "",
547
  }
548
+ agent = ReactAgent(verbose=False)
549
+ print(agent(task))
 
requirements.txt CHANGED
@@ -1,13 +1,18 @@
1
  gradio
2
- requests
3
- openai
4
- langchain-openai
5
- langchain-community
6
- duckduckgo-search
7
- langchain
8
- pydantic
9
- smolagents
10
- pandas
11
- openpyxl
12
- tabulate
13
- bs4
 
 
 
 
 
 
1
  gradio
2
+ langchain>=0.1.0
3
+ langchain-core>=0.1.0
4
+ langchain-community>=0.0.10
5
+ langchain-google-genai>=0.0.6
6
+ google-generativeai>=0.3.0
7
+ python-dotenv>=1.0.0
8
+ google-api-python-client>=2.108.0
9
+ duckduckgo-search>=4.4
10
+ tiktoken>=0.5.2
11
+ google-cloud-speech>=2.24.0
12
+ requests>=2.31.0
13
+ pydub>=0.25.1
14
+ yt-dlp>=2023.12.30
15
+ smolagents>=0.1.3
16
+ wikipedia>=1.4.0
17
+ Pillow>=10.2.0
18
+ wikipedia-api>=0.6.0