albertCHY commited on
Commit
95bd65f
·
verified ·
1 Parent(s): a9fa749

Update agent.py

Browse files
Files changed (1) hide show
  1. agent.py +235 -199
agent.py CHANGED
@@ -1,223 +1,259 @@
 
1
  import os
2
- import gradio as gr
 
3
  import requests
4
- import inspect
5
- import pandas as pd
6
-
7
- import spaces
8
- from agent import graph
9
- from langchain_core.messages import HumanMessage, SystemMessage
10
-
11
- # (Keep Constants as is)
12
- # --- Constants ---
13
- DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
14
-
15
- # --- ZeroGPU Bypass ---
16
- # This dummy function tricks the Space into thinking we are using the GPU, preventing crashes.
17
- @spaces.GPU(duration=1)
18
- def gpu_check():
19
- return "GPU available"
20
-
21
- # --- Basic Agent Definition ---
22
- # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
23
-
24
-
25
-
26
- class BasicAgent:
27
- def __init__(self):
28
- print("BasicAgent initialized.")
29
- def __call__(self, question: str) -> str:
30
- response = graph.invoke({
31
- "messages": [SystemMessage(content = """
32
- You are a helpful assistant tasked with answering questions using a set of tools.
33
- Now, I will ask you a question. Report your thoughts, and answer in the fewest words possible.
34
- If the answer is only one word, format it as such, no extra punctuation.
35
- If your answer is a list, use a comma delimited list. Ex "cornstarch, freshly squeezed lemon juice, granulated sugar" or "Myanmar, Indonesia"
36
- If your answer is a number, only respond with the number, nothing else.
37
- DO NOT USE SQUARE BRACKETS IN YOUR ANSWERS UNLESS DIRECTLY STATED.
38
- DO NOT START YOUR ANSWER WITH "YOUR FINAL ANSWER".
39
- YOUR ANSWER SHOULD ONLY BE THE ANSWER AND NOTHING ELSE.
40
- If you need to process any video or download any content, return "I don't have enough tools yet" since such tools haven't been provided.
41
- """), HumanMessage(content=question)],
42
- "file_path": None,
43
- "url": None,
44
- "task_id": None
45
- })
46
- answer = response["messages"][-1].text
47
- return answer
48
-
49
- def run_and_submit_all( profile: gr.OAuthProfile | None):
50
  """
51
- Fetches all questions, runs the BasicAgent on them, submits all answers,
52
- and displays the results.
 
 
53
  """
54
- # --- Determine HF Space Runtime URL and Repo URL ---
55
- space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
56
 
57
- if profile:
58
- username= f"{profile.username}"
59
- print(f"User logged in: {username}")
60
- else:
61
- print("User not logged in.")
62
- return "Please Login to Hugging Face with the button.", None
 
 
 
63
 
64
- api_url = DEFAULT_API_URL
65
- questions_url = f"{api_url}/questions"
66
- submit_url = f"{api_url}/submit"
 
 
 
 
 
 
67
 
68
- # 1. Instantiate Agent ( modify this part to create your agent)
69
- try:
70
- agent = BasicAgent()
71
- except Exception as e:
72
- print(f"Error instantiating agent: {e}")
73
- return f"Error initializing agent: {e}", None
74
- # 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)
75
- agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
76
- print(agent_code)
77
-
78
- # 2. Fetch Questions
79
- print(f"Fetching questions from: {questions_url}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  try:
81
- response = requests.get(questions_url, timeout=15)
82
- response.raise_for_status()
83
- questions_data = response.json()
84
- if not questions_data:
85
- print("Fetched questions list is empty.")
86
- return "Fetched questions list is empty or invalid format.", None
87
- print(f"Fetched {len(questions_data)} questions.")
88
- except requests.exceptions.RequestException as e:
89
- print(f"Error fetching questions: {e}")
90
- return f"Error fetching questions: {e}", None
91
- except requests.exceptions.JSONDecodeError as e:
92
- print(f"Error decoding JSON response from questions endpoint: {e}")
93
- print(f"Response text: {response.text[:500]}")
94
- return f"Error decoding server response for questions: {e}", None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  except Exception as e:
96
- print(f"An unexpected error occurred fetching questions: {e}")
97
- return f"An unexpected error occurred fetching questions: {e}", None
98
-
99
- # 3. Run your Agent
100
- results_log = []
101
- answers_payload = []
102
- print(f"Running agent on {len(questions_data)} questions...")
103
- for item in questions_data:
104
- task_id = item.get("task_id")
105
- question_text = item.get("question")
106
- if not task_id or question_text is None:
107
- print(f"Skipping item with missing task_id or question: {item}")
108
- continue
109
- try:
110
- submitted_answer = agent(question_text)
111
- answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
112
- results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
113
- except Exception as e:
114
- print(f"Error running agent on task {task_id}: {e}")
115
- results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
116
-
117
- if not answers_payload:
118
- print("Agent did not produce any answers to submit.")
119
- return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
120
-
121
- # 4. Prepare Submission
122
- submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
123
- status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
124
- print(status_update)
125
-
126
- # 5. Submit
127
- print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
128
  try:
129
- response = requests.post(submit_url, json=submission_data, timeout=60)
 
130
  response.raise_for_status()
131
- result_data = response.json()
132
- final_status = (
133
- f"Submission Successful!\n"
134
- f"User: {result_data.get('username')}\n"
135
- f"Overall Score: {result_data.get('score', 'N/A')}% "
136
- f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
137
- f"Message: {result_data.get('message', 'No message received.')}"
138
- )
139
- print("Submission successful.")
140
- results_df = pd.DataFrame(results_log)
141
- return final_status, results_df
142
- except requests.exceptions.HTTPError as e:
143
- error_detail = f"Server responded with status {e.response.status_code}."
144
- try:
145
- error_json = e.response.json()
146
- error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
147
- except requests.exceptions.JSONDecodeError:
148
- error_detail += f" Response: {e.response.text[:500]}"
149
- status_message = f"Submission Failed: {error_detail}"
150
- print(status_message)
151
- results_df = pd.DataFrame(results_log)
152
- return status_message, results_df
153
- except requests.exceptions.Timeout:
154
- status_message = "Submission Failed: The request timed out."
155
- print(status_message)
156
- results_df = pd.DataFrame(results_log)
157
- return status_message, results_df
158
- except requests.exceptions.RequestException as e:
159
- status_message = f"Submission Failed: Network error - {e}"
160
- print(status_message)
161
- results_df = pd.DataFrame(results_log)
162
- return status_message, results_df
163
- except Exception as e:
164
- status_message = f"An unexpected error occurred during submission: {e}"
165
- print(status_message)
166
- results_df = pd.DataFrame(results_log)
167
- return status_message, results_df
168
 
 
 
 
 
 
 
169
 
170
- # --- Build Gradio Interface using Blocks ---
171
- with gr.Blocks() as demo:
172
- gr.Markdown("# Basic Agent Evaluation Runner")
173
- gr.Markdown(
174
- """
175
- **Instructions:**
176
 
177
- 1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ...
178
- 2. Log in to your Hugging Face account using the button below. This uses your HF username for submission.
179
- 3. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score.
180
 
181
- ---
182
- **Disclaimers:**
183
- Once clicking on the "submit button, it can take quite some time ( this is the time for the agent to go through all the questions).
184
- This space provides a basic setup and is intentionally sub-optimal to encourage you to develop your own, more robust solution. For instance for the delay process of the submit button, a solution could be to cache the answers and submit in a seperate action or even to answer the questions in async.
185
- """
186
- )
187
 
188
- gr.LoginButton()
 
 
 
 
189
 
190
- run_button = gr.Button("Run Evaluation & Submit All Answers")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
- status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
193
- # Removed max_rows=10 from DataFrame constructor
194
- results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
195
 
196
- run_button.click(
197
- fn=run_and_submit_all,
198
- outputs=[status_output, results_table]
199
- )
 
 
 
 
 
 
200
 
201
- if __name__ == "__main__":
202
- print("\n" + "-"*30 + " App Starting " + "-"*30)
203
- # Check for SPACE_HOST and SPACE_ID at startup for information
204
- space_host_startup = os.getenv("SPACE_HOST")
205
- space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
 
 
 
 
 
 
 
206
 
207
- if space_host_startup:
208
- print(f"✅ SPACE_HOST found: {space_host_startup}")
209
- print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
210
- else:
211
- print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
212
 
213
- if space_id_startup: # Print repo URLs if SPACE_ID is found
214
- print(f"✅ SPACE_ID found: {space_id_startup}")
215
- print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
216
- print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
217
- else:
218
- print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")
219
 
220
- print("-"*(60 + len(" App Starting ")) + "\n")
 
 
221
 
222
- print("Launching Gradio Interface for Basic Agent Evaluation...")
223
- demo.launch(debug=True, share=False)
 
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
+ from dotenv import load_dotenv
16
+
17
+ # constants
18
+ API_URL = "https://agents-course-unit4-scoring.hf.space"
19
+ QUESTIONS_URL = f"{API_URL}/questions"
20
+ FILES_URL = f"{API_URL}/files"
21
+ SUBMIT_URL = f"{API_URL}/submit"
22
+ load_dotenv()
23
+
24
+ class AgentState(TypedDict):
25
+ messages: Annotated[list[AnyMessage], add_messages]
26
+ file_path: str | None
27
+ task_id: str | None
28
+ url: str | None
29
+
30
+ def build_gemini_llm():
31
+ if not os.environ.get("GOOGLE_API_KEY"):
32
+ raise ValueError("GOOGLE_API_KEY environment variable is not set.")
33
+ return ChatGoogleGenerativeAI(model = "gemini-3.7-flash", temperature = 0, max_output_tokens = 1025, include_thoughts=True)
34
+
35
+ @tool
36
+ def add_numbers(a: int, b: int) -> int:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  """
38
+ Adds two numbers and return the result.
39
+ Args:
40
+ a (int)
41
+ b (int)
42
  """
43
+ return a + b
 
44
 
45
+ @tool
46
+ def subtract_numbers(a: int, b: int) -> int:
47
+ """
48
+ Subtracts the second number from the first and return the result.
49
+ Args:
50
+ a (int)
51
+ b (int)
52
+ """
53
+ return a - b
54
 
55
+ @tool
56
+ def multiply_numbers(a: int, b: int) -> int:
57
+ """
58
+ Multiplies two numbers and return the result.
59
+ Args:
60
+ a (int)
61
+ b (int)
62
+ """
63
+ return a * b
64
 
65
+ @tool
66
+ def divide_numbers(a: int, b: int) -> float:
67
+ """
68
+ Divides the first number by the second and return the result.
69
+ Args:
70
+ a (int)
71
+ b (int)
72
+ """
73
+ return a / b
74
+
75
+ @tool
76
+ def search_web(query:str) -> str:
77
+ """
78
+ Searches the web for the answer to a given question or topic.
79
+ Args:
80
+ query (str): the question or topic to search for.
81
+ """
82
+ return DuckDuckGoSearchRun()
83
+
84
+ @tool
85
+ def extract_text_from_image(img_path: str) -> str:
86
+ """
87
+ Describe the image and extract any text in it.
88
+ Args:
89
+ img_path (str): the path to the image file.
90
+ """
91
+ all_text = ""
92
  try:
93
+ # Read image and encode as base64
94
+ with open(img_path, "rb") as image_file:
95
+ image_bytes = image_file.read()
96
+
97
+ image_base64 = base64.b64encode(image_bytes).decode("utf-8")
98
+
99
+ # Prepare the prompt including the base64 image data
100
+ message = [
101
+ HumanMessage(
102
+ content=[
103
+ {
104
+ "type": "text",
105
+ "text": (
106
+ "Describe the image and extract any text in it."
107
+ ),
108
+ },
109
+ {
110
+ "type": "image_url",
111
+ "image_url": {
112
+ "url": f"data:image/png;base64,{image_base64}"
113
+ },
114
+ },
115
+ ]
116
+ )
117
+ ]
118
+ response = model.invoke(message)
119
+ # Append extracted text
120
+ all_text += response.text + "\n\n"
121
+ return all_text.strip()
122
  except Exception as e:
123
+ # A butler should handle errors gracefully
124
+ error_msg = f"Error extracting text: {str(e)}"
125
+ print(error_msg)
126
+ return ""
127
+
128
+ @tool
129
+ def download_and_read_file(task_id: str) -> str:
130
+ """
131
+ Download and read the file attached to the GAIA task its contents.
132
+ Always call this first if there is a file attached to a GAIA Task.
133
+ Args:
134
+ task_id (str): The ID of the GAIA task.
135
+ Returns:
136
+ str: The contents of the file as a string.
137
+ """
138
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  try:
140
+ # Download the file from the GAIA API
141
+ response = requests.get(f"{FILES_URL}/{task_id}", timeout = 10)
142
  response.raise_for_status()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
+ # Determine the file type and read its contents
145
+ content_disposition = response.headers.get("content-disposition", "")
146
+ content_type = response.headers.get("content-type", "")
147
+ filename = None
148
+ if "filename=" in content_disposition:
149
+ filename = content_disposition.split("filename=")[1].strip('"')
150
 
151
+ if not filename:
152
+ filename = f"{task_id}.bin"
 
 
 
 
153
 
154
+ ext = Path(filename).suffix.lower()
 
 
155
 
156
+ if ext in(".txt", ".py", ".json", ".md", ".ymal", ".html", ".xml", ""):
157
+ return response.text
 
 
 
 
158
 
159
+ if ext == ".xlsx" or "xlsx" in content_type:
160
+ import pandas as pd
161
+ with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as file:
162
+ file.write(response.content)
163
+ temp_path = file.name
164
 
165
+ read_file = pd.read_excel(temp_path)
166
+ return read_file.to_string()
167
+ if ext == ".csv" or "csv" in content_type:
168
+ import pandas as pd
169
+ with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as file:
170
+ file.write(response.content)
171
+ temp_path = file.name
172
+ read_file = pd.read_csv(temp_path)
173
+ return read_file.to_string()
174
+ if ext == ".csv" or "csv" in content_type:
175
+ import pandas as pd
176
+ with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as file:
177
+ file.write(response.content)
178
+ temp_path = file.name
179
+ read_file = pd.read_csv(temp_path)
180
+ return read_file.to_string()
181
+ if ext == ".jpg" or ext == ".jpeg" or ext == ".png" or "image" in content_type:
182
+ from PIL import Image
183
+ with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as file:
184
+ file.write(response.content)
185
+ temp_path = file.name
186
+ return extract_text_from_image(temp_path)
187
+
188
+ # Unsupported file type
189
+ return (
190
+ f"Unsupported file type: {content_type}. "
191
+ "I downloaded the file successfully, but I don't know "
192
+ "how to extract its contents."
193
+ )
194
+ except requests.RequestException as e:
195
+ return f"Failed to download file: {e}"
196
+ except Exception as e: return f"Failed to read file: {e}"
197
+
198
+ except Exception as e:
199
+ return f"error downloading or reading file: {str(e)}"
200
+
201
+ return file_content
202
+
203
+ @tool
204
+ def wikipedia_search(query: str):
205
+ """
206
+ Search wikipedia for a query and return a max of three results.
207
+ Takes a string query as the search query
208
+ Args:
209
+ query (str): The search query.
210
+ """
211
+ try:
212
+ search_results = WikipediaLoader(query=query, load_max_docs=3).load()
213
+
214
+ if not search_results:
215
+ return f"No Wikipedia results found for {query}. Consider another query or try a web search."
216
+ return "\n\n---\n\n".join(
217
+ f"Title: {doc.metadata.get('title', 'Unknown')}\n"
218
+ f"Content: {doc.page_content}"
219
+ for doc in search_results
220
+ )
221
+ except Exception as e:
222
+ print(f"Wikipedia search failed for {query}: {e}")
223
+ return f"Wikipedia search failed for {query}. Try a web search instead."
224
 
 
 
 
225
 
226
+ model = build_gemini_llm()
227
+ tools = [
228
+ add_numbers,
229
+ subtract_numbers,
230
+ multiply_numbers,
231
+ divide_numbers,
232
+ extract_text_from_image,
233
+ search_web
234
+ ]
235
+ model_with_tools = model.bind_tools(tools)
236
 
237
+ def assistant(state: AgentState):
238
+ response = model_with_tools.invoke(state["messages"])
239
+ reasoning_tokens = response.usage_metadata["output_token_details"]["reasoning"]
240
+ print("Reasoning tokens used:", reasoning_tokens)
241
+ print(response.content[0].get("thinking"))
242
+ print("-" * 50)
243
+ return {
244
+ "messages": [response],
245
+ "file_path": state["file_path"],
246
+ "task_id": state["task_id"],
247
+ "url": state["url"]
248
+ }
249
 
250
+ builder = StateGraph(AgentState)
 
 
 
 
251
 
252
+ builder.add_node("assistant", assistant)
253
+ builder.add_node("tools", ToolNode(tools))
 
 
 
 
254
 
255
+ builder.add_edge(START, "assistant")
256
+ builder.add_conditional_edges("assistant", tools_condition)
257
+ builder.add_edge("tools", "assistant")
258
 
259
+ graph = builder.compile()