Burcin commited on
Commit
800ba42
·
verified ·
1 Parent(s): 44a8857

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +626 -0
  2. requirements.txt +16 -0
app.py ADDED
@@ -0,0 +1,626 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ import requests
4
+ import pandas as pd
5
+ import asyncio
6
+ import time
7
+ from pathlib import Path
8
+
9
+ # LlamaIndex and tool imports
10
+ from llama_index.core.agent.workflow import AgentWorkflow, ReActAgent
11
+ from llama_index.core.tools import FunctionTool
12
+ from llama_index.tools.duckduckgo import DuckDuckGoSearchToolSpec
13
+ from llama_index.llms.azure_openai import AzureOpenAI
14
+ from youtube_transcript_api import YouTubeTranscriptApi, NoTranscriptFound
15
+ from bs4 import BeautifulSoup
16
+ import pdfplumber
17
+ import docx
18
+ import speech_recognition as sr
19
+ import base64
20
+
21
+ from io import BytesIO, StringIO
22
+ from dotenv import load_dotenv
23
+ load_dotenv()
24
+
25
+ # ------------------------------
26
+ # 0. Define Azure OpenAI LLM
27
+ # ------------------------------
28
+ api_key = os.getenv("AZURE_OPENAI_API_KEY")
29
+ azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
30
+ azure_api_version = os.getenv("AZURE_OPENAI_API_VERSION")
31
+ azure_deployment_name = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME")
32
+ azure_model_name = os.getenv("AZURE_OPENAI_MODEL_NAME")
33
+
34
+ llm = AzureOpenAI(
35
+ engine=azure_deployment_name,
36
+ model=azure_model_name,
37
+ temperature=0.0,
38
+ azure_endpoint=azure_endpoint,
39
+ api_key=api_key,
40
+ api_version=azure_api_version,
41
+ )
42
+
43
+ # ------------------------------
44
+ # 1. Helper Functions / Tools
45
+ # ------------------------------
46
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
47
+
48
+ # File parsing tool
49
+ def parse_file(file_url: str, file_type: str) -> str:
50
+ try:
51
+ # Download file
52
+ resp = requests.get(file_url, timeout=30)
53
+ resp.raise_for_status()
54
+ content = resp.content
55
+
56
+ # --- XLSX ---
57
+ if file_type == ".xlsx":
58
+ df = pd.read_excel(BytesIO(content))
59
+ return f"Excel Sheet Content:\n{df.to_string(index=False)}"
60
+
61
+ # --- CSV ---
62
+ if file_type == ".csv":
63
+ df = pd.read_csv(StringIO(content.decode()))
64
+ return f"CSV File Content:\n{df.to_string(index=False)}"
65
+
66
+ # --- TXT ---
67
+ if file_type == ".txt":
68
+ text = content.decode(errors='ignore')
69
+ return f"Text File Content:\n{text[:3500]}"
70
+
71
+ # --- PDF ---
72
+ if file_type == ".pdf" and pdfplumber:
73
+ with pdfplumber.open(BytesIO(content)) as pdf:
74
+ text = "\n".join(page.extract_text() or "" for page in pdf.pages)
75
+ return f"PDF Content (first 3500 chars):\n{text[:3500]}"
76
+
77
+ # --- DOCX ---
78
+ if file_type == ".docx" and docx:
79
+ d = docx.Document(BytesIO(content))
80
+ text = "\n".join(p.text for p in d.paragraphs)
81
+ return f"DOCX Content (first 3500 chars):\n{text[:3500]}"
82
+
83
+ # --- MP3 (Audio to Text) ---
84
+ if file_type == ".mp3" and sr:
85
+ # Save MP3 to local
86
+ mp3_path = "temp.mp3"
87
+ with open(mp3_path, "wb") as f:
88
+ f.write(content)
89
+ try:
90
+ # Convert MP3 to WAV using pydub if available
91
+ wav_path = "temp.wav"
92
+ try:
93
+ from pydub import AudioSegment
94
+ sound = AudioSegment.from_mp3(mp3_path)
95
+ sound.export(wav_path, format="wav")
96
+ audio_file = wav_path
97
+ except Exception:
98
+ audio_file = mp3_path # Try raw mp3 if conversion fails
99
+
100
+ recognizer = sr.Recognizer()
101
+ with sr.AudioFile(audio_file) as source:
102
+ audio = recognizer.record(source)
103
+ transcript = recognizer.recognize_google(audio)
104
+ # Clean up
105
+ if os.path.exists(mp3_path): os.remove(mp3_path)
106
+ if os.path.exists(wav_path): os.remove(wav_path)
107
+ return f"Audio Transcript:\n{transcript}"
108
+ except Exception as e:
109
+ if os.path.exists(mp3_path): os.remove(mp3_path)
110
+ if os.path.exists("temp.wav"): os.remove("temp.wav")
111
+ return f"Could not transcribe audio: {e}"
112
+
113
+ # --- Python file ---
114
+ if file_type == ".py":
115
+ text = content.decode(errors='ignore')
116
+ return f"Python Script Content:\n{text[:3500]}"
117
+
118
+ # --- Fallback ---
119
+ return f"File type {file_type} is not supported yet, or required package is missing."
120
+
121
+ except Exception as e:
122
+ return f"Failed to parse file: {e}"
123
+
124
+ # YouTube transcript tool
125
+ def get_youtube_transcript(url: str) -> str:
126
+ try:
127
+ video_id = url.split("v=")[-1]
128
+ transcript = YouTubeTranscriptApi.get_transcript(video_id)
129
+ return " ".join([e['text'] for e in transcript])
130
+ except Exception:
131
+ return "No transcript available."
132
+
133
+ # ------------ DuckDuckGo Search and Extract -------------------------
134
+ def scrape_text_from_url(url: str, max_chars=4000) -> str:
135
+ """Fetch and clean main text from a webpage (basic version)."""
136
+ try:
137
+ resp = requests.get(url, timeout=10)
138
+ soup = BeautifulSoup(resp.text, 'html.parser')
139
+ # Get visible text only, skip scripts/styles
140
+ text = ' '.join(soup.stripped_strings)
141
+ return text[:max_chars]
142
+ except Exception as e:
143
+ return f"Could not scrape {url}: {e}"
144
+
145
+ def duckduckgo_search_and_scrape(question: str) -> str:
146
+ """
147
+ Performs a DuckDuckGo search, scrapes the top relevant link, and returns the scraped content for LLM-based answering.
148
+ """
149
+ # Step 1: Search
150
+ ddg_spec = DuckDuckGoSearchToolSpec()
151
+ results = ddg_spec.duckduckgo_full_search(question)
152
+ if not results or not isinstance(results, list):
153
+ return "No search results found."
154
+
155
+ # Step 2: Find first Wikipedia or Discogs or similar music data site
156
+ for entry in results:
157
+ href = entry.get("href", "")
158
+ if href:
159
+ text = scrape_text_from_url(href)
160
+ # Step 3: Compose output for LLM or direct answer
161
+ return (
162
+ f"Here is content scraped from {href}:\n\n"
163
+ f"{text}\n\n"
164
+ "Based on this, please answer the original question."
165
+ )
166
+ # If no "trusted" link found, fallback to first result
167
+ text = scrape_text_from_url(results[0]["href"])
168
+ return (
169
+ f"Here is content scraped from {results[0]['href']}:\n\n"
170
+ f"{text}\n\n"
171
+ "Based on this, please answer the original question."
172
+ )
173
+
174
+ # ------------ Image Processing Tool Functions -------------------------
175
+ # MIME type mapping for images
176
+ MIME_MAP = {
177
+ '.jpg': 'jpeg',
178
+ '.jpeg': 'jpeg',
179
+ '.png': 'png',
180
+ '.bmp': 'bmp',
181
+ '.gif': 'gif',
182
+ '.webp': 'webp'
183
+ }
184
+
185
+ # 3. Image agent with enhanced capabilities
186
+ def process_image(file_url: str, question: str) -> str:
187
+ """
188
+ Download the image, send it to Azure's vision API, and return the reply text.
189
+ """
190
+ try:
191
+ print(f"Processing image via process_image function from URL: {file_url}")
192
+ resp = requests.get(file_url, timeout=30)
193
+ resp.raise_for_status()
194
+ raw = resp.content
195
+ # 2) Figure out the MIME type from headers (fallback to png)
196
+ mime = resp.headers.get("Content-Type", "image/png")
197
+ # 3) Build data URI
198
+ img_b64 = base64.b64encode(raw).decode()
199
+ data_uri = f"data:{mime};base64,{img_b64}"
200
+
201
+ print(f"Image downloaded and encoded successfully.")
202
+ from openai import AzureOpenAI
203
+ vision_client = AzureOpenAI(
204
+ api_key=api_key,
205
+ api_version=azure_api_version,
206
+ azure_endpoint=azure_endpoint,
207
+ )
208
+ messages = [
209
+ {"role": "system", "content": (
210
+ "You are a vision expert. Answer based *only* on the image content."
211
+ )},
212
+ {"role": "user", "content": [
213
+ {"type": "text", "text": question},
214
+ {"type": "image_url", "image_url": {"url": data_uri}}
215
+ ]},
216
+ ]
217
+ response = vision_client.chat.completions.create(
218
+ model=azure_model_name,
219
+ messages=messages,
220
+ temperature=0.0,
221
+ max_tokens=2000,
222
+ )
223
+ print(f"Vision API response received : {response.choices[0].message.content.strip()}")
224
+
225
+ return response.choices[0].message.content.strip()
226
+ except Exception as e:
227
+ return f"Vision API error: {e}"
228
+
229
+ # ------------------------------
230
+ # 2. BasicAgent Class Definition
231
+ # ------------------------------
232
+ class BasicAgent:
233
+ def __init__(self):
234
+ """Initialize the BasicAgent with all tools and agent workflow."""
235
+ self.llm = llm
236
+ self.api_url = DEFAULT_API_URL
237
+
238
+ # Initialize tools
239
+ self._setup_tools()
240
+
241
+ # Initialize agents
242
+ self._setup_agents()
243
+
244
+ # Initialize agent workflow
245
+ self._setup_workflow()
246
+
247
+ # Define routing instruction
248
+ self.routing_instruction = (
249
+ "You are a multi-agent AI system responsible for routing and answering diverse user questions.\n"
250
+ "You have access to the following specialized agents:\n"
251
+ "- File Parser Agent → handles structured documents like PDFs, DOCXs, CSVs, etc.\n"
252
+ "- YouTube Transcript Agent → answers questions about YouTube video content.\n"
253
+ "- Web Search Agent → retrieves general or real-time information using web search.\n"
254
+ "- Image Agent → analyzes image files and answer visual questions.\n\n"
255
+ "Your responsibilities:\n"
256
+ "1. Analyze the user question and any attached file if any.\n"
257
+ "2. Select and route the task to the most appropriate agent.\n"
258
+ "3. Return ONLY the final answer from the selected agent.\n"
259
+ "4. If the file type is image file(png,jpg,jpeg, webp, gif etc..), you must immediately forward the question and the provided image path to the Image Agent. Once you hand off to the Image Agent, that agent will call the image_processing tool to fetch and analyze the image.\n\n"
260
+ "5. For all other file types(pdf, docx, xlsx, txt, mp3, wav, mov etc..), use the File Parser Agent to extract content and answer the question.\n"
261
+ "Strict guidelines:\n"
262
+ "- NEVER reply with filler phrases like 'please wait', 'I will await Agent's response.' or 'awaiting response', wait until the response received from any agent.\n"
263
+ "- ALWAYS use the most appropriate agent based on the task.\n"
264
+ "- For ambiguous, encoded, or reversed questions, attempt to interpret and resolve them logically.\n"
265
+ "- Do NOT skip or ignore input unless it clearly violates safety policies.\n\n"
266
+ "Answer formatting:\n"
267
+ "- Final responses must end with: FINAL ANSWER: [your answer].\n"
268
+ "- The answer must be a clean string, number, or comma-separated list — no currency symbols, percentages, or unnecessary words.\n"
269
+ "- Do NOT include the phrase 'FINAL ANSWER:' in the output. Return only the final clean answer string.\n\n"
270
+ "Now analyze and respond to the user question appropriately."
271
+ )
272
+
273
+ def _setup_tools(self):
274
+ """Initialize all the tools."""
275
+ self.file_parser_tool = FunctionTool.from_defaults(parse_file)
276
+ self.youtube_transcript_tool = FunctionTool.from_defaults(get_youtube_transcript)
277
+
278
+ self.ddg_tool = FunctionTool.from_defaults(
279
+ fn=duckduckgo_search_and_scrape,
280
+ name="web_search",
281
+ description="Performs a DuckDuckGo search and scrapes the top relevant link and fetch the webpage content of the link to answer the question."
282
+ )
283
+
284
+ self.image_processing_tool = FunctionTool.from_defaults(
285
+ fn=process_image,
286
+ name="image_processing",
287
+ description="Downloads the image at `file_url` and answers `question` based on its visual content."
288
+ )
289
+
290
+ def _setup_agents(self):
291
+ """Initialize all the specialized agents."""
292
+ # File Parsing ReActAgent
293
+ self.file_agent = ReActAgent(
294
+ name="file_agent",
295
+ description="Expert at reading and extracting info from files",
296
+ system_prompt="""You are a precise file analyst.
297
+ Steps to follow:
298
+ 1. Check if there's a file URL in the question
299
+ 2. Use parse_file tool to examine the file
300
+ 3. For all file types except images, extract content and answer
301
+ 4. Never attempt to analyze images yourself""",
302
+ tools=[self.file_parser_tool],
303
+ llm=self.llm,
304
+ )
305
+
306
+ # YouTube ReActAgent
307
+ self.youtube_agent = ReActAgent(
308
+ name="youtube_agent",
309
+ description="Expert at extracting info from YouTube videos by transcript.",
310
+ system_prompt="You are a video analyst. For YouTube questions, fetch and summarize or quote the video transcript.",
311
+ tools=[self.youtube_transcript_tool],
312
+ llm=self.llm,
313
+ )
314
+
315
+ # DuckDuckGo Web Search ReActAgent
316
+ self.search_agent = ReActAgent(
317
+ name="websearch_agent",
318
+ description="Web search expert. ALWAYS use the web search tool for any question you receive. Do NOT just say you are searching.",
319
+ system_prompt=(
320
+ "You are a web researcher. For any question, always use the DuckDuckGo search tool to get an answer. "
321
+ "Never ask the user to wait. Do not simply state that you are searching. "
322
+ "Return the answer from the tool as your only reply."
323
+ ),
324
+ tools=[self.ddg_tool],
325
+ llm=self.llm,
326
+ )
327
+
328
+ # Image Agent
329
+ self.image_agent = ReActAgent(
330
+ name="image_agent",
331
+ description="Analyzes images and answers questions using the image_processing tool.",
332
+ system_prompt=(
333
+ "You are a vision specialist. For *every* user query involving an image, "
334
+ "you **must** issue exactly one tool call:\n\n"
335
+ "```\nAction: image_processing\n"
336
+ "Action Input: {\"file_url\": <url>, \"question\": <user question>}\n```"
337
+ "\nThen immediately return *only* the tool's output."
338
+ ),
339
+ tools=[self.image_processing_tool],
340
+ llm=self.llm,
341
+ )
342
+
343
+ def _setup_workflow(self):
344
+ """Initialize the agent workflow."""
345
+ self.agentflow = AgentWorkflow(
346
+ agents=[self.file_agent, self.youtube_agent, self.search_agent, self.image_agent],
347
+ root_agent=self.file_agent.name, # the file_agent will detect image types and delegate to image_agent
348
+ )
349
+
350
+ def _extract_final_answer(self, response_text: str) -> str:
351
+ """Extract the final answer from the response, removing 'FINAL ANSWER:' prefix if present."""
352
+ # Look for FINAL ANSWER: pattern and extract what comes after
353
+ if "FINAL ANSWER:" in response_text:
354
+ parts = response_text.split("FINAL ANSWER:", 1)
355
+ if len(parts) > 1:
356
+ return parts[1].strip()
357
+
358
+ # If no FINAL ANSWER: pattern found, return the full response stripped
359
+ return response_text.strip()
360
+
361
+ def __call__(self, question: str, task_id: str = None) -> str:
362
+ """
363
+ Main method to process a question and return an answer.
364
+ This method will be called by the evaluation system.
365
+
366
+ Args:
367
+ question (str): The question to answer
368
+ task_id (str, optional): Task ID for file retrieval
369
+
370
+ Returns:
371
+ str: The answer to the question
372
+ """
373
+ try:
374
+ # Check if there's a file associated with this question
375
+ # The evaluation system should provide file info in the question or via task_id
376
+ enhanced_question = question
377
+
378
+ # If task_id is provided, we might need to construct file URL
379
+ if task_id:
380
+ # This assumes the evaluation system follows the same pattern
381
+ file_url = f"{self.api_url}/files/{task_id}"
382
+ # You might need to adjust this logic based on how files are provided
383
+ enhanced_question += f"\nFile URL: {file_url}"
384
+
385
+ # Construct the full prompt with routing instructions
386
+ full_prompt = f"{self.routing_instruction}\n\nUser Question:\n{enhanced_question}"
387
+
388
+ # Run the agent workflow
389
+ response = asyncio.run(self.agentflow.run(user_msg=full_prompt))
390
+
391
+ # Extract and clean the final answer
392
+ final_answer = self._extract_final_answer(response.response.blocks[0].text)
393
+
394
+ return final_answer
395
+
396
+ except Exception as e:
397
+ print(f"Error in BasicAgent.__call__: {e}")
398
+ return f"Error processing question: {str(e)}"
399
+
400
+ # ------------------------------
401
+ # 3. Modified answer_questions_batch function (kept for reference)
402
+ # ------------------------------
403
+ async def answer_questions_batch(questions_data):
404
+ """
405
+ This function is kept for reference but is no longer used in the main flow.
406
+ The BasicAgent class now handles individual questions directly.
407
+ """
408
+ answers = []
409
+ agent = BasicAgent()
410
+
411
+ for question_data in questions_data:
412
+ question = question_data.get("question", "")
413
+ file_name = question_data.get("file_name", "")
414
+ task_id = question_data.get("task_id", "")
415
+
416
+ try:
417
+ # Let the BasicAgent handle the question processing
418
+ answer = agent(question, task_id)
419
+
420
+ answers.append({
421
+ "task_id": task_id,
422
+ "question": question,
423
+ "submitted_answer": answer
424
+ })
425
+
426
+ except Exception as e:
427
+ print(f"Error processing question {task_id}: {e}")
428
+ answers.append({
429
+ "task_id": task_id,
430
+ "question": question,
431
+ "submitted_answer": f"Error: {str(e)}"
432
+ })
433
+
434
+ time.sleep(1) # Rate limiting
435
+
436
+ return answers
437
+
438
+ def run_and_submit_all(profile: gr.OAuthProfile | None):
439
+ """
440
+ Fetches all questions, runs the BasicAgent on them, submits all answers,
441
+ and displays the results.
442
+ """
443
+ # --- Determine HF Space Runtime URL and Repo URL ---
444
+ space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
445
+
446
+ if profile:
447
+ username = f"{profile.username}"
448
+ print(f"User logged in: {username}")
449
+ else:
450
+ print("User not logged in.")
451
+ return "Please Login to Hugging Face with the button.", None
452
+
453
+ api_url = DEFAULT_API_URL
454
+ questions_url = f"{api_url}/questions"
455
+ submit_url = f"{api_url}/submit"
456
+
457
+ # 1. Instantiate Agent
458
+ try:
459
+ agent = BasicAgent()
460
+ print("BasicAgent instantiated successfully.")
461
+ except Exception as e:
462
+ print(f"Error instantiating agent: {e}")
463
+ return f"Error initializing agent: {e}", None
464
+
465
+ # In the case of an app running as a hugging Face space, this link points toward your codebase
466
+ agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
467
+ print(agent_code)
468
+
469
+ # 2. Fetch Questions
470
+ print(f"Fetching questions from: {questions_url}")
471
+ try:
472
+ response = requests.get(questions_url, timeout=15)
473
+ response.raise_for_status()
474
+ questions_data = response.json()
475
+ if not questions_data:
476
+ print("Fetched questions list is empty.")
477
+ return "Fetched questions list is empty or invalid format.", None
478
+ print(f"Fetched {len(questions_data)} questions.")
479
+ except requests.exceptions.RequestException as e:
480
+ print(f"Error fetching questions: {e}")
481
+ return f"Error fetching questions: {e}", None
482
+ except requests.exceptions.JSONDecodeError as e:
483
+ print(f"Error decoding JSON response from questions endpoint: {e}")
484
+ print(f"Response text: {response.text[:500]}")
485
+ return f"Error decoding server response for questions: {e}", None
486
+ except Exception as e:
487
+ print(f"An unexpected error occurred fetching questions: {e}")
488
+ return f"An unexpected error occurred fetching questions: {e}", None
489
+
490
+ # 3. Run your Agent
491
+ results_log = []
492
+ answers_payload = []
493
+ print(f"Running agent on {len(questions_data)} questions...")
494
+
495
+ for item in questions_data:
496
+ task_id = item.get("task_id")
497
+ question_text = item.get("question")
498
+ file_name = item.get("file_name", "")
499
+
500
+ if not task_id or question_text is None:
501
+ print(f"Skipping item with missing task_id or question: {item}")
502
+ continue
503
+
504
+ try:
505
+ # Prepare enhanced question with file information if present
506
+ enhanced_question = question_text
507
+ if file_name:
508
+ file_type = Path(file_name).suffix.lower().split("?")[0]
509
+ file_url = f"{api_url}/files/{task_id}"
510
+ enhanced_question += f"\nThis question relates to the file at {file_url} (filename: {file_name} and file type: {file_type}). Please analyze its contents using the appropriate tool."
511
+
512
+ # Call the agent
513
+ submitted_answer = agent(enhanced_question, task_id)
514
+
515
+ answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
516
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
517
+
518
+ except Exception as e:
519
+ print(f"Error running agent on task {task_id}: {e}")
520
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
521
+
522
+ if not answers_payload:
523
+ print("Agent did not produce any answers to submit.")
524
+ return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
525
+
526
+ # 4. Prepare Submission
527
+ submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
528
+ status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
529
+ print(status_update)
530
+
531
+ # 5. Submit
532
+ print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
533
+ try:
534
+ response = requests.post(submit_url, json=submission_data, timeout=60)
535
+ response.raise_for_status()
536
+ result_data = response.json()
537
+ final_status = (
538
+ f"Submission Successful!\n"
539
+ f"User: {result_data.get('username')}\n"
540
+ f"Overall Score: {result_data.get('score', 'N/A')}% "
541
+ f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
542
+ f"Message: {result_data.get('message', 'No message received.')}"
543
+ )
544
+ print("Submission successful.")
545
+ results_df = pd.DataFrame(results_log)
546
+ return final_status, results_df
547
+ except requests.exceptions.HTTPError as e:
548
+ error_detail = f"Server responded with status {e.response.status_code}."
549
+ try:
550
+ error_json = e.response.json()
551
+ error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
552
+ except requests.exceptions.JSONDecodeError:
553
+ error_detail += f" Response: {e.response.text[:500]}"
554
+ status_message = f"Submission Failed: {error_detail}"
555
+ print(status_message)
556
+ results_df = pd.DataFrame(results_log)
557
+ return status_message, results_df
558
+ except requests.exceptions.Timeout:
559
+ status_message = "Submission Failed: The request timed out."
560
+ print(status_message)
561
+ results_df = pd.DataFrame(results_log)
562
+ return status_message, results_df
563
+ except requests.exceptions.RequestException as e:
564
+ status_message = f"Submission Failed: Network error - {e}"
565
+ print(status_message)
566
+ results_df = pd.DataFrame(results_log)
567
+ return status_message, results_df
568
+ except Exception as e:
569
+ status_message = f"An unexpected error occurred during submission: {e}"
570
+ print(status_message)
571
+ results_df = pd.DataFrame(results_log)
572
+ return status_message, results_df
573
+
574
+ # --- Build Gradio Interface using Blocks ---
575
+ with gr.Blocks() as demo:
576
+ gr.Markdown("# Basic Agent Evaluation Runner")
577
+ gr.Markdown(
578
+ """
579
+ **Instructions:**
580
+
581
+ 1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ...
582
+ 2. Log in to your Hugging Face account using the button below. This uses your HF username for submission.
583
+ 3. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score.
584
+
585
+ ---
586
+ **Disclaimers:**
587
+ 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).
588
+ 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.
589
+ """
590
+ )
591
+
592
+ gr.LoginButton()
593
+
594
+ run_button = gr.Button("Run Evaluation & Submit All Answers")
595
+
596
+ status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
597
+ results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
598
+
599
+ run_button.click(
600
+ fn=run_and_submit_all,
601
+ outputs=[status_output, results_table]
602
+ )
603
+
604
+ if __name__ == "__main__":
605
+ print("\n" + "-"*30 + " App Starting " + "-"*30)
606
+ # Check for SPACE_HOST and SPACE_ID at startup for information
607
+ space_host_startup = os.getenv("SPACE_HOST")
608
+ space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
609
+
610
+ if space_host_startup:
611
+ print(f"✅ SPACE_HOST found: {space_host_startup}")
612
+ print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
613
+ else:
614
+ print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
615
+
616
+ if space_id_startup: # Print repo URLs if SPACE_ID is found
617
+ print(f"✅ SPACE_ID found: {space_id_startup}")
618
+ print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
619
+ print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
620
+ else:
621
+ print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")
622
+
623
+ print("-"*(60 + len(" App Starting ")) + "\n")
624
+
625
+ print("Launching Gradio Interface for Basic Agent Evaluation...")
626
+ demo.launch(debug=True, share=False)
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio
2
+ requests
3
+ torch==2.7.0
4
+ wikipedia==1.4.0
5
+ openpyxl==3.1.5
6
+ python-docx==1.1.2
7
+ youtube_transcript_api==1.0.3
8
+ llama-index
9
+ llama-index-tools-duckduckgo==0.3.0
10
+ SpeechRecognition==3.14.3
11
+ pdfplumber==0.11.6
12
+ docx==0.2.4
13
+ llama-index-embeddings-azure-openai==0.3.5
14
+ llama-index-llms-azure-openai==0.3.2
15
+ beautifulsoup4
16
+ python-dotenv