Scott Cogan commited on
Commit
123066c
·
0 Parent(s):

fresh start

Browse files
Files changed (4) hide show
  1. Requirements.txt +9 -0
  2. app.py +405 -0
  3. my-final-assignment +1 -0
  4. utilities.py +17 -0
Requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ gradio
2
+ requests
3
+ openpyxl
4
+ langchain_google_genai
5
+ langchain
6
+ langgraph
7
+ pytube
8
+ google
9
+ google-genai
app.py ADDED
@@ -0,0 +1,405 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ import requests
4
+ import inspect
5
+ import pandas as pd
6
+ import asyncio
7
+ from langchain_google_genai.chat_models import ChatGoogleGenerativeAI
8
+ from typing import IO, Dict
9
+ from io import BytesIO
10
+ from langchain_core.messages import HumanMessage, SystemMessage
11
+ from langgraph.graph import MessagesState
12
+ from langgraph.graph import START, StateGraph
13
+ from langgraph.prebuilt import tools_condition
14
+ from langgraph.prebuilt import ToolNode
15
+ import base64
16
+ from google.ai.generativelanguage_v1beta.types import Tool as GenAITool
17
+ from google import genai
18
+ from google.genai import types
19
+ import os
20
+
21
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
22
+
23
+ # (Keep Constants as is)
24
+ # --- Constants ---
25
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
26
+ GEMINI_API_KEY = os.getenv("Gemini_API_key")
27
+ SERPER_API_KEY = os.getenv("SERPER_API_KEY")
28
+
29
+ # --- Basic Agent Definition ---
30
+ # Agent capabilities required: Search the web, listen to audio recordings, watch YouTube videos (process the footage, not the transcript), work with Excel spreadsheets
31
+ # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
32
+
33
+ def get_file(task_id: str) -> IO:
34
+ '''
35
+ Downloads the file associated with the given task_id, if one exists and is mapped.
36
+ If the question mentions an attachment, use this function.
37
+ Args:
38
+ task_id: Id of the question.
39
+ Returns:
40
+ The file associated with the question.
41
+ '''
42
+ file_request = requests.get(url=f'https://agents-course-unit4-scoring.hf.space/files/{task_id}')
43
+ file_request.raise_for_status()
44
+
45
+ return BytesIO(file_request.content)
46
+
47
+ def analyse_excel(task_id: str) -> Dict[str, float]:
48
+ '''
49
+ Analyzes the Excel file associated with the given task_id and returns the sum of each numeric column.
50
+ Args:
51
+ task_id: Id of the question.
52
+ Returns:
53
+ A dictionary with the sum of each numeric column.
54
+ '''
55
+ excel_file = get_file(task_id)
56
+ df = pd.read_excel(excel_file, sheet_name=0)
57
+
58
+ return df.select_dtypes(include='number').sum().to_dict()
59
+
60
+ def add_numbers(a: float, b: float) -> float:
61
+ '''
62
+ Adds two numbers together.
63
+ Args:
64
+ a: First number.
65
+ b: Second number.
66
+ Returns:
67
+ The sum of the two numbers.
68
+ '''
69
+ return a + b
70
+
71
+ def transcribe_audio(task_id: str) -> HumanMessage:
72
+ '''
73
+ Opens an audio file and returns its content as a string.
74
+ Args:
75
+ file: The audio file to be opened.
76
+ Returns:
77
+ The content of the audio file as a string.
78
+ '''
79
+ audio_file = get_file(task_id)
80
+ if audio_file is None:
81
+ raise ValueError("No audio file found for the given task_id.")
82
+ # Encode the audio file to base64
83
+ audio_file.seek(0) # Ensure the file pointer is at the beginning
84
+ encoded_audio = base64.b64encode(audio_file.read()).decode("utf-8")
85
+
86
+ return HumanMessage(
87
+ content=[
88
+ {"type": "text", "text": "Transcribe the audio."},
89
+ {
90
+ "type": "media",
91
+ "data": encoded_audio, # Use base64 string directly
92
+ "mime_type": "audio/mpeg",
93
+ },
94
+ ]
95
+ )
96
+
97
+ def python_code(task_id: str) -> str:
98
+ '''
99
+ Returns the Python code associated with the given task_id.
100
+ Args:
101
+ task_id: Id of the question.
102
+ Returns:
103
+ The Python code associated with the question.
104
+ '''
105
+ code_request = requests.get(url=f'https://agents-course-unit4-scoring.hf.space/files/{task_id}')
106
+ code_request.raise_for_status()
107
+
108
+ return code_request.text
109
+
110
+ def open_image(task_id: str) -> str:
111
+ '''
112
+ Opens an image file associated with the given task_id.
113
+ Args:
114
+ task_id: Id of the question.
115
+ Returns:
116
+ The base64 encoded string of the image file.
117
+ '''
118
+ image_file = get_file(task_id)
119
+ if image_file is None:
120
+ raise ValueError("No image file found for the given task_id.")
121
+
122
+ return base64.b64encode(image_file.read()).decode("utf-8")
123
+
124
+ def open_youtube_video(url: str, query:str) -> str:
125
+ '''
126
+ Answers a question about a video from the given URL.
127
+ Args:
128
+ url: The URL of the video file.
129
+ query: The question to be answered about the video.
130
+ Returns:
131
+ Answer to the question about the video.
132
+ '''
133
+
134
+ client = genai.Client(api_key=GOOGLE_API_KEY)
135
+
136
+ response = client.models.generate_content(
137
+ model='models/gemini-2.0-flash',
138
+ contents=types.Content(
139
+ parts=[
140
+ types.Part(
141
+ file_data=types.FileData(file_uri=url)
142
+ ),
143
+ types.Part(text=f'''{query} YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated
144
+ list of numbers and/or strings.''')
145
+ ]
146
+ )
147
+ )
148
+
149
+ return response.text
150
+
151
+ def google_search(query: str) -> str:
152
+ '''
153
+ Performs a Google search for the given query.
154
+ Args:
155
+ query: The search query.
156
+ Returns:
157
+ The search results as a string.
158
+ '''
159
+ llm = ChatGoogleGenerativeAI(
160
+ model="gemini-2.5-flash-preview-05-20",
161
+ max_tokens=8192,
162
+ temperature=0
163
+ )
164
+ response = llm.invoke(query,
165
+ tools=[GenAITool(google_search={})]
166
+ )
167
+
168
+ return response.content
169
+
170
+
171
+ class BasicAgent:
172
+ def __init__(self):
173
+ self.llm = ChatGoogleGenerativeAI(
174
+ model="gemini-2.5-flash-preview-05-20",
175
+ max_tokens=8192,
176
+ temperature=0
177
+ )
178
+ self.tools = [get_file, analyse_excel, add_numbers, transcribe_audio, python_code, open_image, open_youtube_video
179
+ , google_search
180
+ ]
181
+
182
+ self.agent = self.llm.bind_tools(self.tools)
183
+
184
+ self.sys_msg = SystemMessage('''You are a general AI assistant. I will ask you a question. Only provide YOUR FINAL ANSWER and nothing else.
185
+ YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings.
186
+ 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.
187
+ 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.
188
+ 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.
189
+ You have access to multiple tools and should use as many as you need to answer the question.
190
+ If you are asked to analyze an Excel file, use the 'analyse_excel' tool.
191
+ If you are asked to download a file, use the 'get_file' tool.
192
+ If you are asked to add two numbers, use the 'add_numbers' tool. If you need to add more than two numbers, use the 'add_numbers'
193
+ tool multiple times.
194
+ If you are asked to transcribe an audio file, use the 'transcribe_audio' tool.
195
+ If you are asked to run a Python code, use the 'python_code' tool.
196
+ If you are asked to open an image, use the 'open_image' tool.
197
+ If you were given a link with www.youtube.com, use the 'open_youtube_video' tool.
198
+ If the question requires a web search because your internal knowledge doesn't have the information, use the 'google_search' tool.
199
+ ''')
200
+
201
+ # Graph
202
+ self.builder = StateGraph(MessagesState)
203
+
204
+ # Define nodes: these do the work
205
+ self.builder.add_node("assistant", self.assistant)
206
+ self.builder.add_node("tools", ToolNode(self.tools))
207
+
208
+ # Define edges: these determine how the control flow moves
209
+ self.builder.add_edge(START, "assistant")
210
+ self.builder.add_conditional_edges(
211
+ "assistant",
212
+ # If the latest message (result) from assistant is a tool call -> tools_condition routes to tools
213
+ # If the latest message (result) from assistant is a not a tool call -> tools_condition routes to END
214
+ tools_condition,
215
+ )
216
+ self.builder.add_edge("tools", "assistant")
217
+ self.react_graph = self.builder.compile()
218
+
219
+ print("BasicAgent initialized.")
220
+
221
+ def assistant(self, state: MessagesState):
222
+ return {"messages": [self.agent.invoke([self.sys_msg] + state["messages"])]}
223
+
224
+ async def __call__(self, question: str, task_id: str) -> str:
225
+ print(f"Agent received question (first 50 chars): {question[:50]}...")
226
+ fixed_answer = "This is a default answer."
227
+
228
+ await asyncio.sleep(60)
229
+ messages = self.react_graph.invoke({"messages": f'Task id: {task_id}\n {question}'})
230
+ return messages["messages"][-1].content if messages["messages"] else fixed_answer
231
+
232
+
233
+ def run_and_submit_all( profile: gr.OAuthProfile | None):
234
+ """
235
+ Fetches all questions, runs the BasicAgent on them, submits all answers,
236
+ and displays the results.
237
+ """
238
+ # --- Determine HF Space Runtime URL and Repo URL ---
239
+ space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
240
+
241
+ if profile:
242
+ username= f"{profile.username}"
243
+ print(f"User logged in: {username}")
244
+ else:
245
+ print("User not logged in.")
246
+ return "Please Login to Hugging Face with the button.", None
247
+
248
+ api_url = DEFAULT_API_URL
249
+ questions_url = f"{api_url}/questions"
250
+ submit_url = f"{api_url}/submit"
251
+
252
+ # 1. Instantiate Agent ( modify this part to create your agent)
253
+ try:
254
+ agent = BasicAgent()
255
+ except Exception as e:
256
+ print(f"Error instantiating agent: {e}")
257
+ return f"Error initializing agent: {e}", None
258
+ # 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)
259
+ agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
260
+ print(agent_code)
261
+
262
+ # 2. Fetch Questions
263
+ print(f"Fetching questions from: {questions_url}")
264
+ try:
265
+ response = requests.get(questions_url, timeout=15)
266
+ response.raise_for_status()
267
+ questions_data = response.json()
268
+ if not questions_data:
269
+ print("Fetched questions list is empty.")
270
+ return "Fetched questions list is empty or invalid format.", None
271
+ print(f"Fetched {len(questions_data)} questions.")
272
+ except requests.exceptions.RequestException as e:
273
+ print(f"Error fetching questions: {e}")
274
+ return f"Error fetching questions: {e}", None
275
+ except requests.exceptions.JSONDecodeError as e:
276
+ print(f"Error decoding JSON response from questions endpoint: {e}")
277
+ print(f"Response text: {response.text[:500]}")
278
+ return f"Error decoding server response for questions: {e}", None
279
+ except Exception as e:
280
+ print(f"An unexpected error occurred fetching questions: {e}")
281
+ return f"An unexpected error occurred fetching questions: {e}", None
282
+
283
+ # 3. Run your Agent
284
+ results_log = []
285
+ answers_payload = []
286
+ print(f"Running agent on {len(questions_data)} questions...")
287
+ for item in questions_data:
288
+ task_id = item.get("task_id")
289
+ question_text = item.get("question")
290
+ if not task_id or question_text is None:
291
+ print(f"Skipping item with missing task_id or question: {item}")
292
+ continue
293
+ try:
294
+ submitted_answer = asyncio.run(agent(question_text, task_id))
295
+ answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
296
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
297
+ except Exception as e:
298
+ print(f"Error running agent on task {task_id}: {e}")
299
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
300
+
301
+ if not answers_payload:
302
+ print("Agent did not produce any answers to submit.")
303
+ return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
304
+
305
+ # 4. Prepare Submission
306
+ submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
307
+ status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
308
+ print(status_update)
309
+
310
+ # 5. Submit
311
+ print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
312
+ try:
313
+ response = requests.post(submit_url, json=submission_data, timeout=60)
314
+ response.raise_for_status()
315
+ result_data = response.json()
316
+ final_status = (
317
+ f"Submission Successful!\n"
318
+ f"User: {result_data.get('username')}\n"
319
+ f"Overall Score: {result_data.get('score', 'N/A')}% "
320
+ f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
321
+ f"Message: {result_data.get('message', 'No message received.')}"
322
+ )
323
+ print("Submission successful.")
324
+ results_df = pd.DataFrame(results_log)
325
+ return final_status, results_df
326
+ except requests.exceptions.HTTPError as e:
327
+ error_detail = f"Server responded with status {e.response.status_code}."
328
+ try:
329
+ error_json = e.response.json()
330
+ error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
331
+ except requests.exceptions.JSONDecodeError:
332
+ error_detail += f" Response: {e.response.text[:500]}"
333
+ status_message = f"Submission Failed: {error_detail}"
334
+ print(status_message)
335
+ results_df = pd.DataFrame(results_log)
336
+ return status_message, results_df
337
+ except requests.exceptions.Timeout:
338
+ status_message = "Submission Failed: The request timed out."
339
+ print(status_message)
340
+ results_df = pd.DataFrame(results_log)
341
+ return status_message, results_df
342
+ except requests.exceptions.RequestException as e:
343
+ status_message = f"Submission Failed: Network error - {e}"
344
+ print(status_message)
345
+ results_df = pd.DataFrame(results_log)
346
+ return status_message, results_df
347
+ except Exception as e:
348
+ status_message = f"An unexpected error occurred during submission: {e}"
349
+ print(status_message)
350
+ results_df = pd.DataFrame(results_log)
351
+ return status_message, results_df
352
+
353
+
354
+ # --- Build Gradio Interface using Blocks ---
355
+ with gr.Blocks() as demo:
356
+ gr.Markdown("# Basic Agent Evaluation Runner")
357
+ gr.Markdown(
358
+ """
359
+ **Instructions:**
360
+ 1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ...
361
+ 2. Log in to your Hugging Face account using the button below. This uses your HF username for submission.
362
+ 3. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score.
363
+ ---
364
+ **Disclaimers:**
365
+ 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).
366
+ 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.
367
+ """
368
+ )
369
+
370
+ gr.LoginButton()
371
+
372
+ run_button = gr.Button("Run Evaluation & Submit All Answers")
373
+
374
+ status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
375
+ # Removed max_rows=10 from DataFrame constructor
376
+ results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
377
+
378
+ run_button.click(
379
+ fn=run_and_submit_all,
380
+ outputs=[status_output, results_table]
381
+ )
382
+
383
+ if __name__ == "__main__":
384
+ print("\n" + "-"*30 + " App Starting " + "-"*30)
385
+ # Check for SPACE_HOST and SPACE_ID at startup for information
386
+ space_host_startup = os.getenv("SPACE_HOST")
387
+ space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
388
+
389
+ if space_host_startup:
390
+ print(f"✅ SPACE_HOST found: {space_host_startup}")
391
+ print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
392
+ else:
393
+ print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
394
+
395
+ if space_id_startup: # Print repo URLs if SPACE_ID is found
396
+ print(f"✅ SPACE_ID found: {space_id_startup}")
397
+ print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
398
+ print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
399
+ else:
400
+ print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")
401
+
402
+ print("-"*(60 + len(" App Starting ")) + "\n")
403
+
404
+ print("Launching Gradio Interface for Basic Agent Evaluation...")
405
+ demo.launch(debug=True, share=False)
my-final-assignment ADDED
@@ -0,0 +1 @@
 
 
1
+ Subproject commit 7a69f377adc049319143e7e01a89e6004c1fa4db
utilities.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from smolagents import tool
2
+ from typing import IO
3
+
4
+ @tool
5
+ def get_file(task_id: str) -> IO:
6
+ '''
7
+ Downloads the file associated with the given task_id, if one exists and is mapped.
8
+ If the question mentions an attachment, use this function.
9
+ Args:
10
+ task_id: Id of the question.
11
+ Returns:
12
+ The file associated with the question.
13
+ '''
14
+ file_request = requests.get(url=f'https://agents-course-unit4-scoring.hf.space/files/{task_id}')
15
+ file_request.raise_for_status()
16
+
17
+ return file_request