Hennessy2025 commited on
Commit
7f4cee7
·
verified ·
1 Parent(s): 0fad4af

Upload 6 files

Browse files
Files changed (6) hide show
  1. agent.py +538 -0
  2. app.py +197 -0
  3. gemini_agent.py +131 -0
  4. requirements.txt +24 -0
  5. run.py +203 -0
  6. tools.py +311 -0
agent.py ADDED
@@ -0,0 +1,538 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from typing import TypedDict, List, Dict, Any, Optional
4
+ from urllib.parse import urlparse
5
+ from langgraph.graph import StateGraph, START, END, MessagesState
6
+ from langchain.agents import create_tool_calling_agent, ConversationalAgent, AgentExecutor, initialize_agent, create_react_agent
7
+ from langchain_google_genai import ChatGoogleGenerativeAI
8
+ from langchain_groq import ChatGroq
9
+ from langchain_core.tools import tool, Tool
10
+ from langchain_core.messages import HumanMessage, SystemMessage
11
+ from langchain.memory import ConversationBufferMemory
12
+ from langchain_core.prompts import ChatPromptTemplate, PromptTemplate
13
+ from langgraph.prebuilt import ToolNode
14
+ from langgraph.prebuilt import tools_condition
15
+
16
+ # 1. Web Browsing
17
+ from langchain_community.tools import DuckDuckGoSearchResults
18
+ from langchain_community.document_loaders import ImageCaptionLoader
19
+ import requests, time, yt_dlp
20
+ import pandas as pd
21
+ from pathlib import Path
22
+ from bs4 import BeautifulSoup
23
+ from langchain_community.tools import WikipediaQueryRun
24
+ from langchain_community.utilities import WikipediaAPIWrapper, DuckDuckGoSearchAPIWrapper
25
+ from langchain_community.document_loaders import YoutubeLoader
26
+ from langchain_community.document_loaders import UnstructuredExcelLoader
27
+ from langchain_community.document_loaders import AssemblyAIAudioTranscriptLoader
28
+ from langchain.text_splitter import CharacterTextSplitter
29
+ from langchain_community.utilities import GoogleSerperAPIWrapper
30
+
31
+ load_dotenv()
32
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
33
+
34
+ @tool
35
+ def duckduck_websearch(query: str) -> str:
36
+ """Allows search through DuckDuckGo.
37
+ Args:
38
+ query: what you want to search
39
+ """
40
+ try:
41
+ # search = DuckDuckGoSearchResults()
42
+ # results = search.invoke(query)
43
+ search = search = DuckDuckGoSearchAPIWrapper(max_results=5)
44
+ results = search.run(query)
45
+ if not results or results.strip() == "":
46
+ return "No search results found."
47
+
48
+ return results
49
+ except Exception as e:
50
+ print(str(e))
51
+ print('Try to use request method for duckcudckgo Search')
52
+ base_url = "https://html.duckduckgo.com/html"
53
+ params = {"q": query}
54
+ response = requests.get(base_url, params=params, timeout=10)
55
+ soup = BeautifulSoup(response.text, 'html.parser')
56
+ for result in soup.find_all('div', {'class': 'result'}):
57
+ title = result.find('a', {'class': 'result__a'})
58
+ snippet = result.find('a', {'class': 'result__snippet'})
59
+ if title and snippet:
60
+ results.append({
61
+ 'title': title.get_text(),
62
+ 'snippet': snippet.get_text(),
63
+ 'url': title.get('href')
64
+ })
65
+
66
+ # Format results
67
+ formatted_results = []
68
+ for r in results[:10]: # Limit to top 5 results
69
+ formatted_results.append(f"[{r['title']}]({r['url']})\n{r['snippet']}\n")
70
+
71
+ return "## Search Results\n\n" + "\n".join(formatted_results)
72
+
73
+ @tool
74
+ def serper_websearch(query: str) -> str:
75
+ """Allows search through Serper.
76
+ Args:
77
+ query: what you want to search
78
+ """
79
+ search = GoogleSerperAPIWrapper(serper_api_key=os.getenv("SERPER_API_KEY"))
80
+ results = search.run(query)
81
+ return results
82
+
83
+ @tool
84
+ def visit_webpage(url: str) -> str:
85
+ """Fetches raw HTML content of a web page.
86
+ Args:
87
+ url: the webpage url
88
+ """
89
+ try:
90
+ response = requests.get(url, timeout=5)
91
+ return response.text[:5000]
92
+ except Exception as e:
93
+ return f"[ERROR fetching {url}]: {str(e)}"
94
+
95
+ @tool
96
+ def wiki_search(query: str) -> str:
97
+ """Wiki search tools.
98
+ Args:
99
+ query: what you want to wiki
100
+ """
101
+ api_wrapper = WikipediaAPIWrapper(top_k_results=1, doc_content_chars_max=100)
102
+ wikipediatool = WikipediaQueryRun(api_wrapper=api_wrapper)
103
+ return wikipediatool.run({"query": query})
104
+
105
+ @tool
106
+ def text_splitter(text: str) -> List[str]:
107
+ """Splits text into chunks using LangChain's CharacterTextSplitter.
108
+ Args:
109
+ text: A string of text to split.
110
+ """
111
+ splitter = CharacterTextSplitter(chunk_size=450, chunk_overlap=10)
112
+ return splitter.split_text(text)
113
+
114
+ @tool
115
+ def youtube_transcript(video_url: str) -> str:
116
+ """Fetched youtube transcript
117
+ Args:
118
+ video_url: YouTube video url
119
+ """
120
+ try:
121
+ loader = YoutubeLoader.from_youtube_url(video_url)
122
+ # video_id = video_url.split("v=")[-1].split("&")[0]
123
+ # transcript = YouTubeTranscriptApi.get_transcript(video_id)
124
+ return loader.load()
125
+ except Exception as e:
126
+ return f"Error fetching transcript: {str(e)}"
127
+
128
+ # 4. File Reading
129
+ @tool
130
+ def read_file(task_id: str) -> str:
131
+ """First download the file, then read its content
132
+ Args:
133
+ dir: the task_id
134
+ """
135
+ file_url = f'{DEFAULT_API_URL}/files/{task_id}'
136
+ r = requests.get(file_url, timeout=15, allow_redirects=True)
137
+ with open('temp', "wb") as fp:
138
+ fp.write(r.content)
139
+ with open('temp') as f:
140
+ return f.read()
141
+
142
+ @tool
143
+ def excel_read(task_id: str) -> str:
144
+ """First download the excel file, then read its content
145
+ Args:
146
+ dir: the task_id
147
+ """
148
+ try:
149
+ file_url = f'{DEFAULT_API_URL}/files/{task_id}'
150
+ r = requests.get(file_url, timeout=15, allow_redirects=True)
151
+ with open('temp.xlsx', "wb") as fp:
152
+ fp.write(r.content)
153
+ # Read the Excel file
154
+ df = pd.read_excel('temp.xlsx')
155
+ # Run various analyses based on the query
156
+ result = (
157
+ f"Excel file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
158
+ )
159
+ result += f"Columns: {', '.join(df.columns)}\n\n"
160
+ # Add summary statistics
161
+ result += "Summary statistics:\n"
162
+ result += str(df.describe())
163
+ return result
164
+ except Exception as e:
165
+ return f"Error analyzing Excel file: {str(e)}"
166
+
167
+ @tool
168
+ def csv_read(task_id: str) -> str:
169
+ """First download the csv file, then read its content
170
+ Args:
171
+ dir: the task_id
172
+ """
173
+ try:
174
+ file_url = f'{DEFAULT_API_URL}/files/{task_id}'
175
+ r = requests.get(file_url, timeout=15, allow_redirects=True)
176
+ with open('temp.csv', "wb") as fp:
177
+ fp.write(r.content)
178
+ # Read the CSV file
179
+ df = pd.read_csv(temp.csv)
180
+ # Run various analyses based on the query
181
+ result = (
182
+ f"Excel file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
183
+ )
184
+ result += f"Columns: {', '.join(df.columns)}\n\n"
185
+ # Add summary statistics
186
+ result += "Summary statistics:\n"
187
+ result += str(df.describe())
188
+ return result
189
+ except Exception as e:
190
+ return f"Error analyzing CSV file: {str(e)}"
191
+
192
+ @tool
193
+ def mp3_listen(task_id: str) -> str:
194
+ """First download the mp3 file, then listen to it
195
+ Args:
196
+ dir: the task_id
197
+ """
198
+ file_url = f'{DEFAULT_API_URL}/files/{task_id}'
199
+ r = requests.get(file_url, timeout=15, allow_redirects=True)
200
+ with open('temp.mp3', "wb") as fp:
201
+ fp.write(r.content)
202
+ loader = AssemblyAIAudioTranscriptLoader(file_path="temp.mp3", api_key=os.getenv("AssemblyAI_API_KEY"))
203
+ docs = loader.load()
204
+ contents = [doc.page_content for doc in docs]
205
+ return "\n".join(contents)
206
+
207
+ # 5. Image Open
208
+ @tool
209
+ def image_caption(dir: str) -> str:
210
+ """Understand the content of the provided image
211
+ Args:
212
+ dir: the image url link
213
+ """
214
+ loader = ImageCaptionLoader(images=[dir])
215
+ metadata = loader.load()
216
+ return metadata[0].page_content
217
+
218
+ # 2. Coding
219
+ from langchain_experimental.tools import PythonREPLTool
220
+ @tool
221
+ def run_python(code: str):
222
+ """ Run the given python code
223
+ Args:
224
+ code: the python code
225
+ """
226
+ return PythonREPLTool().run(code)
227
+
228
+ @tool
229
+ def multiply(a: float, b: float) -> float:
230
+ """Multiply two numbers.
231
+ Args:
232
+ a: first float
233
+ b: second float
234
+ """
235
+ return a * b
236
+
237
+ @tool
238
+ def add(a: float, b: float) -> float:
239
+ """Add two numbers.
240
+ Args:
241
+ a: first float
242
+ b: second float
243
+ """
244
+ return a + b
245
+
246
+ @tool
247
+ def subtract(a: float, b: float) -> float:
248
+ """Subtract two numbers.
249
+ Args:
250
+ a: first float
251
+ b: second float
252
+ """
253
+ return a - b
254
+
255
+ @tool
256
+ def divide(a: float, b: float) -> float:
257
+ """Divide two numbers.
258
+ Args:
259
+ a: first float
260
+ b: second float
261
+ """
262
+ if b == 0:
263
+ raise ValueError("Cannot divide by zero.")
264
+ return a / b
265
+
266
+ # 3. Multi-Modality
267
+ # - multiply: multiply two numbers, A and B
268
+ # - add: add two numbers, A and B
269
+ # - subtract: Subtract A by B with passing A as the first argument
270
+ # - divide: Divide A by B with passing A as the first argument
271
+
272
+
273
+
274
+ # ("human", f"Question: {question}\nReport to validate: {final_answer}")
275
+ class BasicAgent:
276
+ def __init__(self):
277
+ self.model = ChatGoogleGenerativeAI(
278
+ model="gemini-2.0-flash-lite",
279
+ temperature=0,
280
+ max_tokens=1024,
281
+ candidate_count=1,
282
+ google_api_key=os.getenv("GEMINI_API_KEY"),
283
+ )
284
+ # System Prompt for few shot prompting
285
+ self.sys_prompt = """"
286
+ You are a general AI assistant. I will ask you a question. Report your thoughts, and finish your answer with the following template:
287
+ FINAL ANSWER: [YOUR FINAL ANSWER].
288
+ YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separared list of numbers and/or strings.
289
+ 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.
290
+ If you are asked for a string, don't use articles, neither abbreviations (eg. for cities), and write the digits in plain text unless specified otherwise.
291
+ If you are asked for a comma separated list, apply the above rules depending of whether the element to put in the list is a number or a string.
292
+
293
+ You have access to the following tools:
294
+ - serper_websearch: web search the content of the query by passing the query as input with Serper Search Engine
295
+ - duckduck_websearch: web search the content of the query by passing the query as input with DuckDuckGo Search Engine
296
+ - visit_webpage: visit the given webpage url by passing the url as input
297
+ - wiki_search: wiki search the content of the query by passing the query as input if the question asks for wiki search it
298
+ - text_splitter: split text into chunks
299
+ - youtube_transcript: fetch the transcript of the Youtube video by passing the video url as input if the question asks for watching a Youtube video
300
+ - read_file: read the content of the attached file by passing the TASK-ID as input
301
+ - excel_read: read the content of the attached excel file by passing the TASK-ID as input
302
+ - csv_read: read the content of the attached csv file by passing the TASK-ID as input
303
+ - mp3_listen: listen to the content of the attached mp3 file by passing the TASK-ID as input
304
+ - image_caption: understand the visual content of the attached image by passing the TASK-ID as input
305
+ - run_python: run the python code
306
+
307
+ If Task ID is included in the question, remember to call the relevant read tools [ie. read_file, excel_read, csv_read, mp3_listen, image_caption]
308
+ Note: python_tool is called when the question mentions the term "Python" or any math calculation.
309
+ """
310
+ # self.tools = [duckduck_websearch, serper_websearch, visit_webpage, wiki_search, text_splitter, self._analyze_video, youtube_transcript, read_file, excel_read, csv_read, mp3_listen, image_caption, run_python]
311
+ self.tools = [
312
+ Tool(
313
+ name="duckduck_websearch",
314
+ func=duckduck_websearch,
315
+ description="Search the web for information with DuckDuckGo"
316
+ ),
317
+ Tool(
318
+ name="serper_websearch",
319
+ func=serper_websearch,
320
+ description="Search the web for information with Serper"
321
+ ),
322
+ Tool(
323
+ name="visit_webpage",
324
+ func=visit_webpage,
325
+ description="Directly visit the webpage"
326
+ ),
327
+ Tool(
328
+ name="wiki_search",
329
+ func=wiki_search,
330
+ description="Search the information on Wikipedia"
331
+ ),
332
+ Tool(
333
+ name="text_splitter",
334
+ func=text_splitter,
335
+ description="Split text into chunks"
336
+ ),
337
+ Tool(
338
+ name="analyze_video",
339
+ func=self._analyze_video,
340
+ description="Analyze YouTube video content directly"
341
+ ),
342
+ Tool(
343
+ name="youtube_transcript",
344
+ func=youtube_transcript,
345
+ description="Fetch the transcript of YouTube video"
346
+ ),
347
+ Tool(
348
+ name="read_file",
349
+ func=read_file,
350
+ description="Read the file content"
351
+ ),
352
+ Tool(
353
+ name="excel_read",
354
+ func=excel_read,
355
+ description="Read the content of Excel file"
356
+ ),
357
+ Tool(
358
+ name="csv_read",
359
+ func=csv_read,
360
+ description="Read the content of CSV file"
361
+ ),
362
+ Tool(
363
+ name='mp3_listen',
364
+ func=mp3_listen,
365
+ description="Listen to the MP3 file"
366
+ ),
367
+ Tool(
368
+ name="image_caption",
369
+ func=image_caption,
370
+ description="Understand the image content"
371
+ ),
372
+ Tool(
373
+ name="run_python",
374
+ func=run_python,
375
+ description="Run Python code"
376
+ )
377
+ ]
378
+ # Setup memory
379
+ self.memory = ConversationBufferMemory(
380
+ memory_key="chat_history",
381
+ return_messages=True
382
+ )
383
+ self.agent = self.__setup_agent__()
384
+ # self.prompt = ChatPromptTemplate.from_messages([
385
+ # ("system", self.sys_prompt),
386
+ # ("human", "{input}")
387
+ # ])
388
+
389
+ # self.agent = initialize_agent(
390
+ # tools=self.tools,
391
+ # llm=self.model,
392
+ # agent="zero-shot-react-description", # ReAct agent type
393
+ # verbose=True,
394
+ # system_prompt=self.prompt,
395
+ # handle_parsing_errors=True,
396
+ # max_iterations=30
397
+ # # "Check your output and make sure it conforms, use the Action/Action Input syntax"
398
+ # )
399
+ print("BasicAgent initialized.")
400
+
401
+ def __call__(self, task: dict) -> str:
402
+ task_id, question, file_name = task["task_id"], task["question"], task["file_name"]
403
+ print(f"Agent received question (first 50 chars): {question[:50]}...")
404
+
405
+ if file_name == "" or file_name is None:
406
+ question = question
407
+ else:
408
+ question = f"{question} with TASK-ID: {task_id}"
409
+ # fixed_answer = self.agent.run(f'{question} with TASK-ID: {task_id}')
410
+ fixed_answer = "This is a default answer."
411
+
412
+
413
+ max_retries = 5
414
+ base_sleep = 1
415
+ for attempt in range(max_retries):
416
+ try:
417
+ fixed_answer = self.agent.run(question)
418
+ print(f"Agent returning fixed answer: {fixed_answer}")
419
+ time.sleep(60)
420
+ return fixed_answer
421
+ except Exception as e:
422
+ sleep_time = base_sleep * (attempt + 1)
423
+ if attempt < max_retries - 1:
424
+ print(str(e))
425
+ print(f"Attempt {attempt + 1} failed. Retrying in {sleep_time} seconds...")
426
+ time.sleep(sleep_time)
427
+ continue
428
+ return f"Error processing query after {max_retries} attempts: {str(e)}"
429
+ return fixed_answer
430
+
431
+ @tool
432
+ def _analyze_video(self, url: str) -> str:
433
+ """Analyze video content using Gemini's video understanding capabilities."""
434
+ try:
435
+ # Validate URL
436
+ parsed_url = urlparse(url)
437
+ if not all([parsed_url.scheme, parsed_url.netloc]):
438
+ return "Please provide a valid video URL with http:// or https:// prefix."
439
+
440
+ # Check if it's a YouTube URL
441
+ if 'youtube.com' not in url and 'youtu.be' not in url:
442
+ return "Only YouTube videos are supported at this time."
443
+
444
+ try:
445
+ # Configure yt-dlp with minimal extraction
446
+ ydl_opts = {
447
+ 'quiet': True,
448
+ 'no_warnings': True,
449
+ 'extract_flat': True,
450
+ 'no_playlist': True,
451
+ 'youtube_include_dash_manifest': False
452
+ }
453
+
454
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
455
+ try:
456
+ # Try basic info extraction
457
+ info = ydl.extract_info(url, download=False, process=False)
458
+ if not info:
459
+ return "Could not extract video information."
460
+
461
+ title = info.get('title', 'Unknown')
462
+ description = info.get('description', '')
463
+
464
+ # Create a detailed prompt with available metadata
465
+ prompt = f"""Please analyze this YouTube video:
466
+ Title: {title}
467
+ URL: {url}
468
+ Description: {description}
469
+ Please provide a detailed analysis focusing on:
470
+ 1. Main topic and key points from the title and description
471
+ 2. Expected visual elements and scenes
472
+ 3. Overall message or purpose
473
+ 4. Target audience"""
474
+
475
+ # Use the LLM with proper message format
476
+ messages = [HumanMessage(content=prompt)]
477
+ response = self.model.invoke(messages)
478
+ return response.content if hasattr(response, 'content') else str(response)
479
+
480
+ except Exception as e:
481
+ if 'Sign in to confirm' in str(e):
482
+ return "This video requires age verification or sign-in. Please provide a different video URL."
483
+ return f"Error accessing video: {str(e)}"
484
+
485
+ except Exception as e:
486
+ return f"Error extracting video info: {str(e)}"
487
+
488
+ except Exception as e:
489
+ return f"Error analyzing video: {str(e)}"
490
+
491
+ def __setup_agent__(self) -> AgentExecutor:
492
+ PREFIX = """
493
+ You are a general AI assistant that can use various tools to answer question. I will ask you a question. Report your thoughts, and finish your answer with the following template:
494
+ FINAL ANSWER: [YOUR FINAL ANSWER].
495
+ YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separared list of numbers and/or strings.
496
+ 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.
497
+ If you are asked for a string, don't use articles, neither abbreviations (eg. for cities), and write the digits in plain text unless specified otherwise.
498
+ If you are asked for a comma separated list, apply the above rules depending of whether the element to put in the list is a number or a string.
499
+
500
+ NOTE:
501
+ - If Task ID is included in the question, remember to call the relevant read tools [ie. read_file, excel_read, csv_read, mp3_listen, image_caption]
502
+ - python_tool is called when the question mentions the term "Python" or any math calculation.
503
+ """
504
+ FORMAT_INSTRUCTIONS = """
505
+ To use a tool, use the following format:
506
+ Thought: Do I need to use a tool? Yes
507
+ Action: the action to take, should be one of [{tool_names}]
508
+ Action Input: the input to the action
509
+ Observation: the result of the action
510
+ When you have a response to say to the Human, or if you do not need to use a tool, you MUST use the format:
511
+ Thought: Do I need to use a tool? No
512
+ Final Answer: [your response here]
513
+ Begin! Remember to ALWAYS include 'Thought:', 'Action:', 'Action Input:', and 'Final Answer:' in your responses.
514
+ """
515
+ SUFFIX = """
516
+ Previous conversation history:
517
+ {chat_history}
518
+ New question: {input}
519
+ {agent_scratchpad}
520
+ """
521
+ agent = ConversationalAgent.from_llm_and_tools(
522
+ llm=self.model,
523
+ tools=self.tools,
524
+ prefix=PREFIX,
525
+ format_instructions=FORMAT_INSTRUCTIONS,
526
+ suffix=SUFFIX,
527
+ input_variables=["input", "chat_history", "agent_scratchpad", "tool_names"],
528
+ handle_parsing_errors=True
529
+ )
530
+ return AgentExecutor.from_agent_and_tools(
531
+ agent=agent,
532
+ tools=self.tools,
533
+ memory=self.memory,
534
+ max_iterations=30,
535
+ verbose=True,
536
+ handle_parsing_errors=True,
537
+ # return_only_outputs=True # This ensures we only get the final output
538
+ )
app.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ 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
+ from gemini_agent import GEMINI_AGENT
12
+ class BasicAgent:
13
+ def __init__(self):
14
+ self.agent = GEMINI_AGENT()
15
+
16
+ def __call__(self, task: dict) -> str:
17
+ try:
18
+ response = self.agent.run(task)
19
+ return response
20
+ except Exception as e:
21
+ print(f"Error is raised: {str(e)}")
22
+ return "Agent could not complete this task"
23
+
24
+ def run_and_submit_all( profile: gr.OAuthProfile | None):
25
+ """
26
+ Fetches all questions, runs the BasicAgent on them, submits all answers,
27
+ and displays the results.
28
+ """
29
+ # --- Determine HF Space Runtime URL and Repo URL ---
30
+ space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
31
+
32
+ if profile:
33
+ username= f"{profile.username}"
34
+ print(f"User logged in: {username}")
35
+ else:
36
+ print("User not logged in.")
37
+ return "Please Login to Hugging Face with the button.", None
38
+
39
+ api_url = DEFAULT_API_URL
40
+ questions_url = f"{api_url}/questions"
41
+ submit_url = f"{api_url}/submit"
42
+
43
+ # 1. Instantiate Agent ( modify this part to create your agent)
44
+ try:
45
+ agent = BasicAgent()
46
+ except Exception as e:
47
+ print(f"Error instantiating agent: {e}")
48
+ return f"Error initializing agent: {e}", None
49
+ # 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)
50
+ agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
51
+ print(agent_code)
52
+
53
+ # 2. Fetch Questions
54
+ print(f"Fetching questions from: {questions_url}")
55
+ try:
56
+ response = requests.get(questions_url, timeout=15)
57
+ response.raise_for_status()
58
+ questions_data = response.json()
59
+ if not questions_data:
60
+ print("Fetched questions list is empty.")
61
+ return "Fetched questions list is empty or invalid format.", None
62
+ print(f"Fetched {len(questions_data)} questions.")
63
+ except requests.exceptions.RequestException as e:
64
+ print(f"Error fetching questions: {e}")
65
+ return f"Error fetching questions: {e}", None
66
+ except requests.exceptions.JSONDecodeError as e:
67
+ print(f"Error decoding JSON response from questions endpoint: {e}")
68
+ print(f"Response text: {response.text[:500]}")
69
+ return f"Error decoding server response for questions: {e}", None
70
+ except Exception as e:
71
+ print(f"An unexpected error occurred fetching questions: {e}")
72
+ return f"An unexpected error occurred fetching questions: {e}", None
73
+
74
+ # 3. Run your Agent
75
+ results_log = []
76
+ answers_payload = []
77
+ print(f"Running agent on {len(questions_data)} questions...")
78
+ for item in questions_data:
79
+ task_id = item.get("task_id")
80
+ question_text = item.get("question")
81
+ if not task_id or question_text is None:
82
+ print(f"Skipping item with missing task_id or question: {item}")
83
+ continue
84
+ try:
85
+ # submitted_answer = agent(question_text)
86
+ submitted_answer = agent(item) # dictionary that consists: task_id, question, Level, file_name
87
+ answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
88
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
89
+ except Exception as e:
90
+ print(f"Error running agent on task {task_id}: {e}")
91
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
92
+
93
+ if not answers_payload:
94
+ print("Agent did not produce any answers to submit.")
95
+ return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
96
+
97
+ # 4. Prepare Submission
98
+ submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
99
+ status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
100
+ print(status_update)
101
+
102
+ # 5. Submit
103
+ print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
104
+ try:
105
+ response = requests.post(submit_url, json=submission_data, timeout=60)
106
+ response.raise_for_status()
107
+ result_data = response.json()
108
+ final_status = (
109
+ f"Submission Successful!\n"
110
+ f"User: {result_data.get('username')}\n"
111
+ f"Overall Score: {result_data.get('score', 'N/A')}% "
112
+ f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
113
+ f"Message: {result_data.get('message', 'No message received.')}"
114
+ )
115
+ print("Submission successful.")
116
+ results_df = pd.DataFrame(results_log)
117
+ return final_status, results_df
118
+ except requests.exceptions.HTTPError as e:
119
+ error_detail = f"Server responded with status {e.response.status_code}."
120
+ try:
121
+ error_json = e.response.json()
122
+ error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
123
+ except requests.exceptions.JSONDecodeError:
124
+ error_detail += f" Response: {e.response.text[:500]}"
125
+ status_message = f"Submission Failed: {error_detail}"
126
+ print(status_message)
127
+ results_df = pd.DataFrame(results_log)
128
+ return status_message, results_df
129
+ except requests.exceptions.Timeout:
130
+ status_message = "Submission Failed: The request timed out."
131
+ print(status_message)
132
+ results_df = pd.DataFrame(results_log)
133
+ return status_message, results_df
134
+ except requests.exceptions.RequestException as e:
135
+ status_message = f"Submission Failed: Network error - {e}"
136
+ print(status_message)
137
+ results_df = pd.DataFrame(results_log)
138
+ return status_message, results_df
139
+ except Exception as e:
140
+ status_message = f"An unexpected error occurred during submission: {e}"
141
+ print(status_message)
142
+ results_df = pd.DataFrame(results_log)
143
+ return status_message, results_df
144
+
145
+
146
+ # --- Build Gradio Interface using Blocks ---
147
+ with gr.Blocks() as demo:
148
+ gr.Markdown("# Basic Agent Evaluation Runner")
149
+ gr.Markdown(
150
+ """
151
+ **Instructions:**
152
+ 1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ...
153
+ 2. Log in to your Hugging Face account using the button below. This uses your HF username for submission.
154
+ 3. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score.
155
+ ---
156
+ **Disclaimers:**
157
+ 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).
158
+ 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.
159
+ """
160
+ )
161
+
162
+ gr.LoginButton()
163
+
164
+ run_button = gr.Button("Run Evaluation & Submit All Answers")
165
+
166
+ status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
167
+ # Removed max_rows=10 from DataFrame constructor
168
+ results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
169
+
170
+ run_button.click(
171
+ fn=run_and_submit_all,
172
+ outputs=[status_output, results_table]
173
+ )
174
+
175
+ if __name__ == "__main__":
176
+ print("\n" + "-"*30 + " App Starting " + "-"*30)
177
+ # Check for SPACE_HOST and SPACE_ID at startup for information
178
+ space_host_startup = os.getenv("SPACE_HOST")
179
+ space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
180
+
181
+ if space_host_startup:
182
+ print(f"✅ SPACE_HOST found: {space_host_startup}")
183
+ print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
184
+ else:
185
+ print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
186
+
187
+ if space_id_startup: # Print repo URLs if SPACE_ID is found
188
+ print(f"✅ SPACE_ID found: {space_id_startup}")
189
+ print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
190
+ print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
191
+ else:
192
+ print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")
193
+
194
+ print("-"*(60 + len(" App Starting ")) + "\n")
195
+
196
+ print("Launching Gradio Interface for Basic Agent Evaluation...")
197
+ demo.launch(debug=True, share=False)
gemini_agent.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ from dotenv import load_dotenv
4
+ from typing import TypedDict, Annotated, Optional
5
+
6
+ from langgraph.prebuilt import ToolNode, tools_condition
7
+ from langgraph.graph import StateGraph, START
8
+ from langgraph.graph.message import add_messages
9
+
10
+ from langchain_core.messages import AnyMessage, SystemMessage, HumanMessage
11
+ from langchain_google_genai import ChatGoogleGenerativeAI
12
+
13
+ from tools import *
14
+
15
+ load_dotenv()
16
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
17
+
18
+ class AgentState(TypedDict):
19
+ """Agent state for the graph."""
20
+ input_file: Optional[str]
21
+ messages: Annotated[list[AnyMessage], add_messages]
22
+
23
+
24
+ class GEMINI_AGENT:
25
+ def __init__(self):
26
+ self.llm = ChatGoogleGenerativeAI(
27
+ model="gemini-2.0-flash-lite",
28
+ temperature=0,
29
+ max_tokens=1024,
30
+ google_api_key=os.getenv("GEMINI_API_KEY"),
31
+ )
32
+
33
+ self.tools = [
34
+ duckduck_websearch,
35
+ serper_websearch,
36
+ visit_webpage,
37
+ wiki_search,
38
+ youtube_viewer,
39
+ text_splitter,
40
+ read_file,
41
+ excel_read,
42
+ csv_read,
43
+ mp3_listen,
44
+ image_caption,
45
+ run_python,
46
+ multiply,
47
+ add,
48
+ subtract,
49
+ divide
50
+ ]
51
+
52
+ self.llm_with_tools = self.llm.bind_tools(self.tools)
53
+ self.app = self._graph_compile()
54
+
55
+ def _graph_compile(self):
56
+ builder = StateGraph(AgentState)
57
+ # Define nodes: these do the work
58
+ builder.add_node("assistant", self._assistant)
59
+ builder.add_node("tools", ToolNode(self.tools))
60
+ # Define edges: these determine how the control flow moves
61
+ builder.add_edge(START, "assistant")
62
+ builder.add_conditional_edges(
63
+ "assistant",
64
+ tools_condition,
65
+ )
66
+ builder.add_edge("tools", "assistant")
67
+ react_graph = builder.compile()
68
+ return react_graph
69
+
70
+ def _assistant(self, state: AgentState):
71
+ sys_msg = SystemMessage(
72
+ content=
73
+ """
74
+ You are a helpful assistant tasked with answering questions using a set of tools. When given a question, follow these steps:
75
+ 1. Create a clear, step-by-step plan to solve the question.
76
+ 2. If a tool is necessary, select the most appropriate tool based on its functionality. If one tool isn't working, use another with similar functionality.
77
+ 3. Execute your plan and provide the response in the following format:
78
+
79
+ FINAL ANSWER: [YOUR FINAL ANSWER]
80
+
81
+ Your final answer should be:
82
+
83
+ - A number (without commas or units unless explicitly requested),
84
+ - A short string (avoid articles, abbreviations, and use plain text for digits unless otherwise specified),
85
+ - A comma-separated list (apply the formatting rules above for each element, with exactly one space after each comma).
86
+
87
+ Ensure that your answer is concise and follows the task instructions strictly. If the answer is more complex, break it down in a way that follows the format.
88
+ Begin your response with "FINAL ANSWER: " followed by the answer, and nothing else.
89
+ """
90
+ )
91
+
92
+ return {
93
+ "messages": [self.llm_with_tools.invoke([sys_msg] + state["messages"])],
94
+ "input_file": state["input_file"]
95
+ }
96
+
97
+ def extract_after_final_answer(self, text):
98
+ keyword = "FINAL ANSWER: "
99
+ index = text.find(keyword)
100
+ if index != -1:
101
+ return text[index + len(keyword):]
102
+ else:
103
+ return ""
104
+
105
+ def run(self, task: dict):
106
+ task_id, question, file_name = task["task_id"], task["question"], task["file_name"]
107
+ print(f"Agent received question (first 50 chars): {question[:50]}...")
108
+
109
+ if file_name == "" or file_name is None:
110
+ question_text = question
111
+ else:
112
+ question_text = f'{question} with TASK-ID: {task_id}'
113
+ messages = [HumanMessage(content=question_text)]
114
+
115
+ max_retries = 5
116
+ base_sleep = 1
117
+ for attempt in range(max_retries):
118
+ try:
119
+ response = self.app.invoke({"messages": messages, "input_file": None})
120
+ final_ans = self.extract_after_final_answer(response['messages'][-1].content)
121
+ time.sleep(60) # avoid rate limit
122
+ return final_ans
123
+ except Exception as e:
124
+ sleep_time = base_sleep * (attempt + 1)
125
+ if attempt < max_retries - 1:
126
+ print(str(e))
127
+ print(f"Attempt {attempt + 1} failed. Retrying in {sleep_time} seconds...")
128
+ time.sleep(sleep_time)
129
+ continue
130
+ return f"Error processing query after {max_retries} attempts: {str(e)}"
131
+ return "This is a default answer."
requirements.txt ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio
2
+ requests
3
+ python-dotenv
4
+ langchain
5
+ langchain-google-genai
6
+ langchain-core
7
+ langchain-community
8
+ pandas
9
+ langgraph
10
+ langchain-anthropic
11
+ pypdf
12
+ youtube-transcript-api
13
+ wikipedia
14
+ duckduckgo-search
15
+ transformers
16
+ langchain-experimental
17
+ unstructured
18
+ openpyxl
19
+ assemblyai
20
+ langchain-groq
21
+ torch
22
+ yt-dlp
23
+ beautifulsoup4
24
+ google-genai
run.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ 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
+ # from smolagents import CodeAgent, DuckDuckGoSearchTool, OpenAIServerModel, VLLMModel, HfApiModel
14
+ # class BasicAgent:
15
+ # def __init__(self):
16
+ # # model = OpenAIServerModel(model_id="gpt-4o")
17
+ # # model = VLLMModel(model_id="HuggingFaceTB/SmolLM-135M-Instruct")
18
+ # model = HfApiModel("Qwen/Qwen2.5-Coder-32B-Instruct")
19
+ # self.agent = CodeAgent(tools=[DuckDuckGoSearchTool()], model=model)
20
+ # print("BasicAgent initialized.")
21
+ # def __call__(self, question: str) -> str:
22
+ # print(f"Agent received question (first 50 chars): {question[:50]}...")
23
+ # fixed_answer = self.agent.run(question)
24
+ # # fixed_answer = "This is a default answer."
25
+ # print(f"Agent returning fixed answer: {fixed_answer}")
26
+ # return fixed_answer
27
+ from agent import BasicAgent
28
+
29
+ def run_and_submit_all( profile: gr.OAuthProfile | None):
30
+ """
31
+ Fetches all questions, runs the BasicAgent on them, submits all answers,
32
+ and displays the results.
33
+ """
34
+ # --- Determine HF Space Runtime URL and Repo URL ---
35
+ space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
36
+
37
+ if profile:
38
+ username= f"{profile.username}"
39
+ print(f"User logged in: {username}")
40
+ else:
41
+ print("User not logged in.")
42
+ return "Please Login to Hugging Face with the button.", None
43
+
44
+ api_url = DEFAULT_API_URL
45
+ questions_url = f"{api_url}/questions"
46
+ submit_url = f"{api_url}/submit"
47
+
48
+ # 1. Instantiate Agent ( modify this part to create your agent)
49
+ try:
50
+ agent = BasicAgent()
51
+ except Exception as e:
52
+ print(f"Error instantiating agent: {e}")
53
+ return f"Error initializing agent: {e}", None
54
+ # 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)
55
+ agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
56
+ print(agent_code)
57
+
58
+ # 2. Fetch Questions
59
+ print(f"Fetching questions from: {questions_url}")
60
+ try:
61
+ response = requests.get(questions_url, timeout=15)
62
+ response.raise_for_status()
63
+ questions_data = response.json()
64
+ if not questions_data:
65
+ print("Fetched questions list is empty.")
66
+ return "Fetched questions list is empty or invalid format.", None
67
+ print(f"Fetched {len(questions_data)} questions.")
68
+ except requests.exceptions.RequestException as e:
69
+ print(f"Error fetching questions: {e}")
70
+ return f"Error fetching questions: {e}", None
71
+ except requests.exceptions.JSONDecodeError as e:
72
+ print(f"Error decoding JSON response from questions endpoint: {e}")
73
+ print(f"Response text: {response.text[:500]}")
74
+ return f"Error decoding server response for questions: {e}", None
75
+ except Exception as e:
76
+ print(f"An unexpected error occurred fetching questions: {e}")
77
+ return f"An unexpected error occurred fetching questions: {e}", None
78
+
79
+ # 3. Run your Agent
80
+ results_log = []
81
+ answers_payload = []
82
+ print(f"Running agent on {len(questions_data)} questions...")
83
+ for item in questions_data:
84
+ task_id = item.get("task_id")
85
+ question_text = item.get("question")
86
+ if not task_id or question_text is None:
87
+ print(f"Skipping item with missing task_id or question: {item}")
88
+ continue
89
+ try:
90
+ submitted_answer = agent(question_text)
91
+ answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
92
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
93
+ except Exception as e:
94
+ print(f"Error running agent on task {task_id}: {e}")
95
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
96
+
97
+ if not answers_payload:
98
+ print("Agent did not produce any answers to submit.")
99
+ return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
100
+
101
+ # 4. Prepare Submission
102
+ submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
103
+ status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
104
+ print(status_update)
105
+
106
+ # 5. Submit
107
+ print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
108
+ try:
109
+ response = requests.post(submit_url, json=submission_data, timeout=60)
110
+ response.raise_for_status()
111
+ result_data = response.json()
112
+ final_status = (
113
+ f"Submission Successful!\n"
114
+ f"User: {result_data.get('username')}\n"
115
+ f"Overall Score: {result_data.get('score', 'N/A')}% "
116
+ f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
117
+ f"Message: {result_data.get('message', 'No message received.')}"
118
+ )
119
+ print("Submission successful.")
120
+ results_df = pd.DataFrame(results_log)
121
+ return final_status, results_df
122
+ except requests.exceptions.HTTPError as e:
123
+ error_detail = f"Server responded with status {e.response.status_code}."
124
+ try:
125
+ error_json = e.response.json()
126
+ error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
127
+ except requests.exceptions.JSONDecodeError:
128
+ error_detail += f" Response: {e.response.text[:500]}"
129
+ status_message = f"Submission Failed: {error_detail}"
130
+ print(status_message)
131
+ results_df = pd.DataFrame(results_log)
132
+ return status_message, results_df
133
+ except requests.exceptions.Timeout:
134
+ status_message = "Submission Failed: The request timed out."
135
+ print(status_message)
136
+ results_df = pd.DataFrame(results_log)
137
+ return status_message, results_df
138
+ except requests.exceptions.RequestException as e:
139
+ status_message = f"Submission Failed: Network error - {e}"
140
+ print(status_message)
141
+ results_df = pd.DataFrame(results_log)
142
+ return status_message, results_df
143
+ except Exception as e:
144
+ status_message = f"An unexpected error occurred during submission: {e}"
145
+ print(status_message)
146
+ results_df = pd.DataFrame(results_log)
147
+ return status_message, results_df
148
+
149
+
150
+ # --- Build Gradio Interface using Blocks ---
151
+ with gr.Blocks() as demo:
152
+ gr.Markdown("# Basic Agent Evaluation Runner")
153
+ gr.Markdown(
154
+ """
155
+ **Instructions:**
156
+
157
+ 1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ...
158
+ 2. Log in to your Hugging Face account using the button below. This uses your HF username for submission.
159
+ 3. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score.
160
+
161
+ ---
162
+ **Disclaimers:**
163
+ 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).
164
+ 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.
165
+ """
166
+ )
167
+
168
+ gr.LoginButton()
169
+
170
+ run_button = gr.Button("Run Evaluation & Submit All Answers")
171
+
172
+ status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
173
+ # Removed max_rows=10 from DataFrame constructor
174
+ results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
175
+
176
+ run_button.click(
177
+ fn=run_and_submit_all,
178
+ outputs=[status_output, results_table]
179
+ )
180
+
181
+ if __name__ == "__main__":
182
+ print("\n" + "-"*30 + " App Starting " + "-"*30)
183
+ # Check for SPACE_HOST and SPACE_ID at startup for information
184
+ space_host_startup = os.getenv("SPACE_HOST")
185
+ space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
186
+
187
+ if space_host_startup:
188
+ print(f"✅ SPACE_HOST found: {space_host_startup}")
189
+ print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
190
+ else:
191
+ print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
192
+
193
+ if space_id_startup: # Print repo URLs if SPACE_ID is found
194
+ print(f"✅ SPACE_ID found: {space_id_startup}")
195
+ print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
196
+ print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
197
+ else:
198
+ print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")
199
+
200
+ print("-"*(60 + len(" App Starting ")) + "\n")
201
+
202
+ print("Launching Gradio Interface for Basic Agent Evaluation...")
203
+ demo.launch(debug=True, share=False)
tools.py ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import requests
4
+ import pandas as pd
5
+ from typing import List
6
+ from dotenv import load_dotenv
7
+
8
+ from google import genai
9
+ from google.genai import types
10
+
11
+ from langchain_core.tools import tool
12
+ from langchain.document_loaders import WebBaseLoader
13
+ from langchain_experimental.tools import PythonREPLTool
14
+ from langchain.text_splitter import CharacterTextSplitter
15
+ from langchain_community.tools import DuckDuckGoSearchResults
16
+ from langchain_community.retrievers import WikipediaRetriever
17
+ from langchain_community.utilities import GoogleSerperAPIWrapper
18
+ from langchain_community.document_loaders import ImageCaptionLoader, AssemblyAIAudioTranscriptLoader
19
+
20
+
21
+ load_dotenv()
22
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
23
+
24
+
25
+ def duckduck_websearch(query: str) -> str:
26
+ """
27
+ Performs a web search using the given query, downloads the content of two relevant web pages,
28
+ and returns their combined content as a raw string.
29
+
30
+ This is useful when the task requires analysis of web page content, such as retrieving poems,
31
+ changelogs, or other textual resources.
32
+
33
+ Args:
34
+ query (str): The search query.
35
+
36
+ Returns:
37
+ str: The combined raw text content of the two retrieved web pages.
38
+ """
39
+ search_engine = DuckDuckGoSearchResults(output_format="list", num_results=2)
40
+ page_urls = [url["link"] for url in search_engine(query)]
41
+
42
+ loader = WebBaseLoader(web_paths=(page_urls))
43
+ docs = loader.load()
44
+
45
+ combined_text = "\n\n".join(doc.page_content[:15000] for doc in docs)
46
+
47
+ # Clean up excessive newlines, spaces and strip leading/trailing whitespace
48
+ cleaned_text = re.sub(r'\n{3,}', '\n\n', combined_text).strip()
49
+ cleaned_text = re.sub(r'[ \t]{6,}', ' ', cleaned_text)
50
+
51
+ # Strip leading/trailing whitespace
52
+ cleaned_text = cleaned_text.strip()
53
+ return cleaned_text
54
+
55
+
56
+ def serper_websearch(query: str) -> str:
57
+ """
58
+ Performs a web search using the given query with SERPER Search Engine
59
+
60
+ Args:
61
+ query (str): The search query.
62
+
63
+ Returns:
64
+ str: the search result
65
+ """
66
+ search = GoogleSerperAPIWrapper(serper_api_key=os.getenv("SERPER_API_KEY"))
67
+ results = search.run(query)
68
+ return results
69
+
70
+ def visit_webpage(url: str) -> str:
71
+ """
72
+ Fetches raw HTML content of a web page.
73
+
74
+ Args:
75
+ url: the webpage url
76
+
77
+ Returns:
78
+ str: The combined raw text content of the webpage
79
+ """
80
+ try:
81
+ response = requests.get(url, timeout=5)
82
+ return response.text[:5000]
83
+ except Exception as e:
84
+ return f"[ERROR fetching {url}]: {str(e)}"
85
+
86
+ def wiki_search(query: str) -> str:
87
+ """
88
+ Searches for a Wikipedia articles using the provided query and returns the content of the corresponding Wikipedia pages.
89
+
90
+ Args:
91
+ query (str): The search term to look up on Wikipedia.
92
+
93
+ Returns:
94
+ str: The text content of the Wikipedia articles related to the query.
95
+ """
96
+ retriever = WikipediaRetriever()
97
+ docs = retriever.invoke(query)
98
+ combined_text = "\n\n".join(doc.page_content for doc in docs)
99
+ return combined_text
100
+
101
+ def youtube_viewer(youtube_url: str, question: str) -> str:
102
+ """
103
+ Analyzes a YouTube video from the provided URL and returns an answer
104
+ to the given question based on the analysis results.
105
+
106
+ Args:
107
+ youtube_url (str): The URL of the YouTube video, in the format
108
+ "https://www.youtube.com/...".
109
+ question (str): A question related to the content of the video.
110
+
111
+ Returns:
112
+ str: An answer to the question based on the video's content.
113
+ """
114
+ client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))
115
+ response = client.models.generate_content(
116
+ model='models/gemini-2.5-flash-preview-04-17',
117
+ contents=types.Content(
118
+ parts=[
119
+ types.Part(
120
+ file_data=types.FileData(file_uri=youtube_url)
121
+ ),
122
+ types.Part(text=question)
123
+ ]
124
+ )
125
+ )
126
+ return response.text
127
+
128
+ def text_splitter(text: str) -> List[str]:
129
+ """
130
+ Splits text into chunks using LangChain's CharacterTextSplitter.
131
+
132
+ Args:
133
+ text: A string of text to split.
134
+
135
+ Returns:
136
+ List[str]: a list of split text
137
+ """
138
+ splitter = CharacterTextSplitter(chunk_size=450, chunk_overlap=10)
139
+ return splitter.split_text(text)
140
+
141
+ def read_file(task_id: str) -> str:
142
+ """
143
+ First download the file, then read its content
144
+
145
+ Args:
146
+ dir: the task_id
147
+
148
+ Returns:
149
+ str: the file content
150
+ """
151
+ file_url = f'{DEFAULT_API_URL}/files/{task_id}'
152
+ r = requests.get(file_url, timeout=15, allow_redirects=True)
153
+ with open('temp', "wb") as fp:
154
+ fp.write(r.content)
155
+ with open('temp') as f:
156
+ return f.read()
157
+
158
+ def excel_read(task_id: str) -> str:
159
+ """
160
+ First download the excel file, then read its content
161
+
162
+ Args:
163
+ dir: the task_id
164
+
165
+ Returns:
166
+ str: the content of excel file
167
+ """
168
+ try:
169
+ file_url = f'{DEFAULT_API_URL}/files/{task_id}'
170
+ r = requests.get(file_url, timeout=15, allow_redirects=True)
171
+ with open('temp.xlsx', "wb") as fp:
172
+ fp.write(r.content)
173
+ # Read the Excel file
174
+ df = pd.read_excel('temp.xlsx')
175
+ # Run various analyses based on the query
176
+ result = (
177
+ f"Excel file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
178
+ )
179
+ result += f"Columns: {', '.join(df.columns)}\n\n"
180
+ # Add summary statistics
181
+ result += "Summary statistics:\n"
182
+ result += str(df.describe())
183
+ return result
184
+ except Exception as e:
185
+ return f"Error analyzing Excel file: {str(e)}"
186
+
187
+ def csv_read(task_id: str) -> str:
188
+ """
189
+ First download the csv file, then read its content
190
+
191
+ Args:
192
+ dir: the task_id
193
+
194
+ Returns:
195
+ str: the content of csv file
196
+ """
197
+ try:
198
+ file_url = f'{DEFAULT_API_URL}/files/{task_id}'
199
+ r = requests.get(file_url, timeout=15, allow_redirects=True)
200
+ with open('temp.csv', "wb") as fp:
201
+ fp.write(r.content)
202
+ # Read the CSV file
203
+ df = pd.read_csv('temp.csv')
204
+ # Run various analyses based on the query
205
+ result = (
206
+ f"Excel file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
207
+ )
208
+ result += f"Columns: {', '.join(df.columns)}\n\n"
209
+ # Add summary statistics
210
+ result += "Summary statistics:\n"
211
+ result += str(df.describe())
212
+ return result
213
+ except Exception as e:
214
+ return f"Error analyzing CSV file: {str(e)}"
215
+
216
+
217
+ def mp3_listen(task_id: str) -> str:
218
+ """
219
+ First download the mp3 file, then listen to it
220
+
221
+ Args:
222
+ dir: the task_id
223
+
224
+ Returns:
225
+ str: the content of mp3 file
226
+ """
227
+ file_url = f'{DEFAULT_API_URL}/files/{task_id}'
228
+ r = requests.get(file_url, timeout=15, allow_redirects=True)
229
+ with open('temp.mp3', "wb") as fp:
230
+ fp.write(r.content)
231
+ loader = AssemblyAIAudioTranscriptLoader(file_path="temp.mp3", api_key=os.getenv("AssemblyAI_API_KEY"))
232
+ docs = loader.load()
233
+ contents = [doc.page_content for doc in docs]
234
+ return "\n".join(contents)
235
+
236
+
237
+ def image_caption(dir: str) -> str:
238
+ """
239
+ Understand the content of the provided image
240
+
241
+ Args:
242
+ dir: the image url link
243
+
244
+ Returns:
245
+ str: the image caption
246
+ """
247
+ loader = ImageCaptionLoader(images=[dir])
248
+ metadata = loader.load()
249
+ return metadata[0].page_content
250
+
251
+
252
+ def run_python(code: str):
253
+ """ Run the given python code
254
+
255
+ Args:
256
+ code: the python code
257
+ """
258
+ return PythonREPLTool().run(code)
259
+
260
+ def multiply(a: float, b: float) -> float:
261
+ """
262
+ Multiply two numbers.
263
+
264
+ Args:
265
+ a: first float
266
+ b: second float
267
+
268
+ Returns:
269
+ float: the multiplication of a and b
270
+ """
271
+ return a * b
272
+
273
+ def add(a: float, b: float) -> float:
274
+ """
275
+ Add two numbers.
276
+
277
+ Args:
278
+ a: first float
279
+ b: second float
280
+
281
+ Returns:
282
+ float: the sum of a and b
283
+ """
284
+ return a + b
285
+
286
+ def subtract(a: float, b: float) -> float:
287
+ """
288
+ Subtract two numbers.
289
+
290
+ Args:
291
+ a: first float
292
+ b: second float
293
+
294
+ Returns:
295
+ float: the result after a subtracted by b
296
+ """
297
+ return a - b
298
+
299
+ def divide(a: float, b: float) -> float:
300
+ """Divide two numbers.
301
+
302
+ Args:
303
+ a: first float
304
+ b: second float
305
+
306
+ Returns:
307
+ float: the result after a divided by b
308
+ """
309
+ if b == 0:
310
+ raise ValueError("Cannot divide by zero.")
311
+ return a / b