Spaces:
Runtime error
Runtime error
Upload 2 files
Browse files- app.py +626 -0
- 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
|