albertCHY commited on
Commit
149d5fe
·
verified ·
1 Parent(s): 0ddb708

Create agent.py

Browse files
Files changed (1) hide show
  1. agent.py +254 -0
agent.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import os
3
+ import io
4
+ import contextlib
5
+ import requests
6
+ from typing import TypedDict, Annotated
7
+ from langchain_core.messages import HumanMessage, AnyMessage
8
+ from langgraph.graph import START, StateGraph, add_messages
9
+ from langgraph.prebuilt import ToolNode, tools_condition
10
+ from langchain_community.tools import tool, DuckDuckGoSearchRun
11
+ from langchain_community.document_loaders import WikipediaLoader
12
+ from langchain_google_genai import ChatGoogleGenerativeAI
13
+ from pathlib import Path
14
+ import tempfile
15
+
16
+ # constants
17
+ API_URL = "https://agents-course-unit4-scoring.hf.space"
18
+ QUESTIONS_URL = f"{API_URL}/questions"
19
+ FILES_URL = f"{API_URL}/files"
20
+ SUBMIT_URL = f"{API_URL}/submit"
21
+
22
+ class AgentState(TypedDict):
23
+ messages: Annotated[list[AnyMessage], add_messages]
24
+ file_path: str | None
25
+ task_id: str | None
26
+ url: str | None
27
+
28
+ def build_gemini_llm():
29
+ if not os.environ.get("GOOGLE_API_KEY"):
30
+ raise ValueError("GOOGLE_API_KEY environment variable is not set.")
31
+ return ChatGoogleGenerativeAI(model = "gemini-3.7-flash", temeperature = 0, max_output_tokens = 1024)
32
+
33
+ @tool
34
+ def add_numbers(a: int, b: int) -> int:
35
+ """
36
+ Adds two numbers and return the result.
37
+ Args:
38
+ a (int)
39
+ b (int)
40
+ """
41
+ return a + b
42
+
43
+ @tool
44
+ def subtract_numbers(a: int, b: int) -> int:
45
+ """
46
+ Subtracts the second number from the first and return the result.
47
+ Args:
48
+ a (int)
49
+ b (int)
50
+ """
51
+ return a - b
52
+
53
+ @tool
54
+ def multiply_numbers(a: int, b: int) -> int:
55
+ """
56
+ Multiplies two numbers and return the result.
57
+ Args:
58
+ a (int)
59
+ b (int)
60
+ """
61
+ return a * b
62
+
63
+ @tool
64
+ def divide_numbers(a: int, b: int) -> float:
65
+ """
66
+ Divides the first number by the second and return the result.
67
+ Args:
68
+ a (int)
69
+ b (int)
70
+ """
71
+ return a / b
72
+
73
+ @tool
74
+ def search_web(query:str) -> str:
75
+ """
76
+ Searches the web for the answer to a given question or topic.
77
+ Args:
78
+ query (str): the question or topic to search for.
79
+ """
80
+ return DuckDuckGoSearchRun().run(query)
81
+
82
+ @tool
83
+ def extract_text_from_image(img_path: str) -> str:
84
+ """
85
+ Describe the image and extract any text in it.
86
+ Args:
87
+ img_path (str): the path to the image file.
88
+ """
89
+ all_text = ""
90
+ try:
91
+ # Read image and encode as base64
92
+ with open(img_path, "rb") as image_file:
93
+ image_bytes = image_file.read()
94
+
95
+ image_base64 = base64.b64encode(image_bytes).decode("utf-8")
96
+
97
+ # Prepare the prompt including the base64 image data
98
+ message = [
99
+ HumanMessage(
100
+ content=[
101
+ {
102
+ "type": "text",
103
+ "text": (
104
+ "Describe the image and extract any text in it."
105
+ ),
106
+ },
107
+ {
108
+ "type": "image_url",
109
+ "image_url": {
110
+ "url": f"data:image/png;base64,{image_base64}"
111
+ },
112
+ },
113
+ ]
114
+ )
115
+ ]
116
+ response = model.invoke(message)
117
+ # Append extracted text
118
+ all_text += response.text + "\n\n"
119
+ return all_text.strip()
120
+ except Exception as e:
121
+ # A butler should handle errors gracefully
122
+ error_msg = f"Error extracting text: {str(e)}"
123
+ print(error_msg)
124
+ return ""
125
+
126
+ @tool
127
+ def download_and_read_file(task_id: str) -> str:
128
+ """
129
+ Download and read the file attached to the GAIA task its contents.
130
+ Always call this first if there is a file attached to a GAIA Task.
131
+ Args:
132
+ task_id (str): The ID of the GAIA task.
133
+ Returns:
134
+ str: The contents of the file as a string.
135
+ """
136
+
137
+ try:
138
+ # Download the file from the GAIA API
139
+ response = requests.get(f"{FILES_URL}/{task_id}", timeout = 10)
140
+ response.raise_for_status()
141
+
142
+ # Determine the file type and read its contents
143
+ content_disposition = response.headers.get("content-disposition", "")
144
+ content_type = response.headers.get("content-type", "")
145
+ filename = None
146
+ if "filename=" in content_disposition:
147
+ filename = content_disposition.split("filename=")[1].strip('"')
148
+
149
+ if not filename:
150
+ filename = f"{task_id}.bin"
151
+
152
+ ext = Path(filename).suffix.lower()
153
+
154
+ if ext in(".txt", ".py", ".json", ".md", ".ymal", ".html", ".xml", ""):
155
+ return response.text
156
+
157
+ if ext == ".xlsx" or "xlsx" in content_type:
158
+ import pandas as pd
159
+ with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as file:
160
+ file.write(response.content)
161
+ temp_path = file.name
162
+
163
+ read_file = pd.read_excel(temp_path)
164
+ return read_file.to_string()
165
+ if ext == ".csv" or "csv" in content_type:
166
+ import pandas as pd
167
+ with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as file:
168
+ file.write(response.content)
169
+ temp_path = file.name
170
+ read_file = pd.read_csv(temp_path)
171
+ return read_file.to_string()
172
+ if ext == ".csv" or "csv" in content_type:
173
+ import pandas as pd
174
+ with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as file:
175
+ file.write(response.content)
176
+ temp_path = file.name
177
+ read_file = pd.read_csv(temp_path)
178
+ return read_file.to_string()
179
+ if ext == ".jpg" or ext == ".jpeg" or ext == ".png" or "image" in content_type:
180
+ from PIL import Image
181
+ with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as file:
182
+ file.write(response.content)
183
+ temp_path = file.name
184
+ return extract_text_from_image(temp_path)
185
+
186
+ # Unsupported file type
187
+ return (
188
+ f"Unsupported file type: {content_type}. "
189
+ "I downloaded the file successfully, but I don't know "
190
+ "how to extract its contents."
191
+ )
192
+ except requests.RequestException as e:
193
+ return f"Failed to download file: {e}"
194
+ except Exception as e: return f"Failed to read file: {e}"
195
+
196
+ except Exception as e:
197
+ return f"error downloading or reading file: {str(e)}"
198
+
199
+ return file_content
200
+
201
+ @tool
202
+ def wikipedia_search(query: str):
203
+ """
204
+ Search wikipedia for a query and return a max of three results.
205
+ Takes a string query as the search query
206
+ Args:
207
+ query (str): The search query.
208
+ """
209
+ try:
210
+ search_results = WikipediaLoader(query=query, load_max_docs=3).load()
211
+
212
+ if not search_results:
213
+ return f"No Wikipedia results found for {query}. Consider another query or try a web search."
214
+ return "\n\n---\n\n".join(
215
+ f"Title: {doc.metadata.get('title', 'Unknown')}\n"
216
+ f"Content: {doc.page_content}"
217
+ for doc in search_results
218
+ )
219
+ except Exception as e:
220
+ print(f"Wikipedia search failed for {query}: {e}")
221
+ return f"Wikipedia search failed for {query}. Try a web search instead."
222
+
223
+
224
+ model = build_gemini_llm()
225
+ tools = [
226
+ add_numbers,
227
+ subtract_numbers,
228
+ multiply_numbers,
229
+ divide_numbers,
230
+ extract_text_from_image,
231
+ wikipedia_search,
232
+ download_and_read_file,
233
+ search_web
234
+ ]
235
+ model_with_tools = model.bind_tools(tools)
236
+
237
+ def assistant(state: AgentState):
238
+ return {
239
+ "messages": state["messages"],
240
+ "file_path": state["file_path"],
241
+ "task_id": state["task_id"],
242
+ "url": state["url"]
243
+ }
244
+
245
+ builder = StateGraph(AgentState)
246
+
247
+ builder.add_node("assistant", assistant)
248
+ builder.add_node("tools", ToolNode(tools))
249
+
250
+ builder.add_edge(START, "assistant")
251
+ builder.add_conditional_edges("assistant", tools_condition)
252
+ builder.add_edge("tools", "assistant")
253
+
254
+ graph = builder.compile()