Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -15,23 +15,27 @@ import pandas as pd
|
|
| 15 |
|
| 16 |
# --- Constants ---
|
| 17 |
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
GROQ_MODELS = [
|
| 20 |
m.strip()
|
| 21 |
for m in os.getenv(
|
| 22 |
"GROQ_MODELS",
|
| 23 |
-
"llama-3.
|
| 24 |
).split(",")
|
| 25 |
if m.strip()
|
| 26 |
]
|
| 27 |
-
# Vision-capable Groq model (free tier).
|
| 28 |
GROQ_VISION_MODEL = os.getenv("GROQ_VISION_MODEL", "meta-llama/llama-4-scout-17b-16e-instruct")
|
| 29 |
-
# Groq Whisper model for audio.
|
| 30 |
GROQ_WHISPER_MODEL = os.getenv("GROQ_WHISPER_MODEL", "whisper-large-v3-turbo")
|
| 31 |
|
| 32 |
-
MAX_TOOL_ITERATIONS =
|
| 33 |
-
TOOL_RESULT_MAX_CHARS =
|
|
|
|
| 34 |
ANSWER_CACHE_PATH = os.getenv("ANSWER_CACHE_PATH", "answers_cache.json")
|
|
|
|
| 35 |
|
| 36 |
# Track downloaded task files so vision/audio tools can re-use them by task_id.
|
| 37 |
_TASK_FILE_CACHE: dict[str, dict] = {}
|
|
@@ -58,21 +62,20 @@ def tool_web_search(query: str, max_results: int = 5) -> str:
|
|
| 58 |
lines.append(f"Answer: {res['answer']}")
|
| 59 |
for r in res.get("results", [])[:max_results]:
|
| 60 |
lines.append(
|
| 61 |
-
f"- {r.get('title', '')}\n {r.get('url', '')}\n {r.get('content', '')[:
|
| 62 |
)
|
| 63 |
if lines:
|
| 64 |
return "\n".join(lines)
|
| 65 |
except Exception as e:
|
| 66 |
print(f"tavily search failed, falling back to DDG: {e}")
|
| 67 |
|
| 68 |
-
# Fallback: DuckDuckGo
|
| 69 |
try:
|
| 70 |
from duckduckgo_search import DDGS
|
| 71 |
results = []
|
| 72 |
with DDGS() as ddgs:
|
| 73 |
for r in ddgs.text(query, max_results=max_results):
|
| 74 |
results.append(
|
| 75 |
-
f"- {r.get('title', '')}\n {r.get('href', '')}\n {r.get('body', '')}"
|
| 76 |
)
|
| 77 |
if not results:
|
| 78 |
return "No results."
|
|
@@ -81,7 +84,7 @@ def tool_web_search(query: str, max_results: int = 5) -> str:
|
|
| 81 |
return f"web_search error: {e}"
|
| 82 |
|
| 83 |
|
| 84 |
-
def tool_fetch_url(url: str, max_chars: int =
|
| 85 |
"""Fetch a URL and return readable text (HTML stripped)."""
|
| 86 |
try:
|
| 87 |
from bs4 import BeautifulSoup
|
|
@@ -138,7 +141,7 @@ def tool_python(code: str) -> str:
|
|
| 138 |
out = buf.getvalue().strip()
|
| 139 |
if not out and "result" in local_ns:
|
| 140 |
out = str(local_ns["result"])
|
| 141 |
-
return out or "(no output)"
|
| 142 |
except Exception as e:
|
| 143 |
return f"python error: {e}\n{traceback.format_exc(limit=2)}"
|
| 144 |
|
|
@@ -158,7 +161,7 @@ def _extract_youtube_id(url: str) -> str | None:
|
|
| 158 |
return None
|
| 159 |
|
| 160 |
|
| 161 |
-
def tool_youtube_transcript(url: str, max_chars: int =
|
| 162 |
"""Fetch the transcript of a YouTube video by URL or ID."""
|
| 163 |
try:
|
| 164 |
from youtube_transcript_api import YouTubeTranscriptApi
|
|
@@ -166,7 +169,6 @@ def tool_youtube_transcript(url: str, max_chars: int = 3500) -> str:
|
|
| 166 |
try:
|
| 167 |
data = YouTubeTranscriptApi.get_transcript(vid, languages=["en", "en-US", "en-GB"])
|
| 168 |
except Exception:
|
| 169 |
-
# Try any available language
|
| 170 |
tlist = YouTubeTranscriptApi.list_transcripts(vid)
|
| 171 |
t = next(iter(tlist), None)
|
| 172 |
data = t.fetch() if t else []
|
|
@@ -183,13 +185,12 @@ def tool_transcribe_audio(task_id: str) -> str:
|
|
| 183 |
"""Transcribe an audio file attached to a GAIA task using Groq Whisper."""
|
| 184 |
try:
|
| 185 |
from groq import Groq
|
| 186 |
-
# Make sure the file is downloaded.
|
| 187 |
info = _TASK_FILE_CACHE.get(task_id)
|
| 188 |
if not info:
|
| 189 |
-
tool_get_task_file(task_id)
|
| 190 |
info = _TASK_FILE_CACHE.get(task_id)
|
| 191 |
if not info or not os.path.exists(info.get("path", "")):
|
| 192 |
-
return "transcribe_audio error: no local file for task"
|
| 193 |
|
| 194 |
client = Groq(api_key=os.getenv("GROQ_API_KEY"))
|
| 195 |
with open(info["path"], "rb") as f:
|
|
@@ -200,8 +201,8 @@ def tool_transcribe_audio(task_id: str) -> str:
|
|
| 200 |
)
|
| 201 |
text = tr if isinstance(tr, str) else getattr(tr, "text", str(tr))
|
| 202 |
text = text.strip()
|
| 203 |
-
if len(text) >
|
| 204 |
-
text = text[:
|
| 205 |
return text or "(empty transcript)"
|
| 206 |
except Exception as e:
|
| 207 |
return f"transcribe_audio error: {e}"
|
|
@@ -216,7 +217,7 @@ def tool_view_image(task_id: str, question: str = "") -> str:
|
|
| 216 |
tool_get_task_file(task_id)
|
| 217 |
info = _TASK_FILE_CACHE.get(task_id)
|
| 218 |
if not info or not os.path.exists(info.get("path", "")):
|
| 219 |
-
return "view_image error: no local file for task"
|
| 220 |
|
| 221 |
suffix = os.path.splitext(info["path"])[1].lower().lstrip(".")
|
| 222 |
if suffix == "jpg":
|
|
@@ -246,7 +247,7 @@ def tool_view_image(task_id: str, question: str = "") -> str:
|
|
| 246 |
}
|
| 247 |
],
|
| 248 |
temperature=0.0,
|
| 249 |
-
max_tokens=
|
| 250 |
)
|
| 251 |
return (resp.choices[0].message.content or "").strip()
|
| 252 |
except Exception as e:
|
|
@@ -257,6 +258,11 @@ def tool_get_task_file(task_id: str, api_url: str = DEFAULT_API_URL) -> str:
|
|
| 257 |
"""Download the file attached to a task and return a text preview."""
|
| 258 |
try:
|
| 259 |
resp = requests.get(f"{api_url}/files/{task_id}", timeout=30)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
resp.raise_for_status()
|
| 261 |
ctype = resp.headers.get("Content-Type", "")
|
| 262 |
cdisp = resp.headers.get("Content-Disposition", "")
|
|
@@ -264,7 +270,6 @@ def tool_get_task_file(task_id: str, api_url: str = DEFAULT_API_URL) -> str:
|
|
| 264 |
fname = fname_match.group(1) if fname_match else f"{task_id}"
|
| 265 |
suffix = os.path.splitext(fname)[1].lower()
|
| 266 |
|
| 267 |
-
# Save to temp for tools that need a path
|
| 268 |
tmp = tempfile.NamedTemporaryFile(prefix=f"{task_id}_", suffix=suffix, delete=False)
|
| 269 |
tmp.write(resp.content)
|
| 270 |
tmp.close()
|
|
@@ -277,22 +282,24 @@ def tool_get_task_file(task_id: str, api_url: str = DEFAULT_API_URL) -> str:
|
|
| 277 |
}
|
| 278 |
|
| 279 |
info = (
|
| 280 |
-
f"File: {fname}\nContent-Type: {ctype}\
|
| 281 |
-
f"Size: {len(resp.content)} bytes\n"
|
| 282 |
)
|
| 283 |
|
| 284 |
-
# Try to give a readable preview
|
| 285 |
if suffix in {".txt", ".md", ".csv", ".json", ".py", ".tsv", ".log", ".xml", ".html"}:
|
| 286 |
try:
|
| 287 |
text = resp.content.decode("utf-8", errors="replace")
|
| 288 |
except Exception:
|
| 289 |
text = resp.text
|
| 290 |
-
return info + "\n--- preview ---\n" + text[:
|
| 291 |
|
| 292 |
if suffix in {".xlsx", ".xls"}:
|
| 293 |
try:
|
| 294 |
df = pd.read_excel(tmp.name)
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
except Exception as e:
|
| 297 |
return info + f"\n(excel parse error: {e})"
|
| 298 |
|
|
@@ -300,16 +307,16 @@ def tool_get_task_file(task_id: str, api_url: str = DEFAULT_API_URL) -> str:
|
|
| 300 |
try:
|
| 301 |
from pypdf import PdfReader
|
| 302 |
reader = PdfReader(tmp.name)
|
| 303 |
-
pages = [p.extract_text() or "" for p in reader.pages[:
|
| 304 |
-
return info + "\n--- pdf text
|
| 305 |
except Exception as e:
|
| 306 |
return info + f"\n(pdf parse error: {e})"
|
| 307 |
|
| 308 |
if suffix in {".mp3", ".wav", ".m4a", ".ogg", ".flac", ".webm"}:
|
| 309 |
-
return info + "\nThis is an audio file. Call transcribe_audio to read it."
|
| 310 |
|
| 311 |
if suffix in {".png", ".jpg", ".jpeg", ".gif", ".webp"}:
|
| 312 |
-
return info + "\nThis is an image. Call view_image to inspect it."
|
| 313 |
|
| 314 |
return info + "\n(binary file; no preview)"
|
| 315 |
except Exception as e:
|
|
@@ -344,7 +351,7 @@ TOOLS_SPEC = [
|
|
| 344 |
"type": "object",
|
| 345 |
"properties": {
|
| 346 |
"url": {"type": "string"},
|
| 347 |
-
"max_chars": {"type": "integer", "default":
|
| 348 |
},
|
| 349 |
"required": ["url"],
|
| 350 |
},
|
|
@@ -369,7 +376,7 @@ TOOLS_SPEC = [
|
|
| 369 |
"type": "function",
|
| 370 |
"function": {
|
| 371 |
"name": "python",
|
| 372 |
-
"description": "Execute a short Python snippet for math, dates, parsing CSV, list/string work. Use print() or assign to `result`.",
|
| 373 |
"parameters": {
|
| 374 |
"type": "object",
|
| 375 |
"properties": {"code": {"type": "string"}},
|
|
@@ -381,7 +388,7 @@ TOOLS_SPEC = [
|
|
| 381 |
"type": "function",
|
| 382 |
"function": {
|
| 383 |
"name": "get_task_file",
|
| 384 |
-
"description": "Download the file attached to a GAIA task by task_id
|
| 385 |
"parameters": {
|
| 386 |
"type": "object",
|
| 387 |
"properties": {"task_id": {"type": "string"}},
|
|
@@ -425,7 +432,7 @@ TOOLS_SPEC = [
|
|
| 425 |
"type": "object",
|
| 426 |
"properties": {
|
| 427 |
"url": {"type": "string"},
|
| 428 |
-
"max_chars": {"type": "integer", "default":
|
| 429 |
},
|
| 430 |
"required": ["url"],
|
| 431 |
},
|
|
@@ -435,14 +442,14 @@ TOOLS_SPEC = [
|
|
| 435 |
|
| 436 |
TOOL_FUNCTIONS = {
|
| 437 |
"web_search": lambda args: tool_web_search(args["query"], int(args.get("max_results", 5))),
|
| 438 |
-
"fetch_url": lambda args: tool_fetch_url(args["url"], int(args.get("max_chars",
|
| 439 |
"wikipedia": lambda args: tool_wikipedia(args["query"], int(args.get("sentences", 4))),
|
| 440 |
"python": lambda args: tool_python(args["code"]),
|
| 441 |
"get_task_file": lambda args: tool_get_task_file(args["task_id"]),
|
| 442 |
"transcribe_audio": lambda args: tool_transcribe_audio(args["task_id"]),
|
| 443 |
"view_image": lambda args: tool_view_image(args["task_id"], args.get("question", "")),
|
| 444 |
"youtube_transcript": lambda args: tool_youtube_transcript(
|
| 445 |
-
args["url"], int(args.get("max_chars",
|
| 446 |
),
|
| 447 |
}
|
| 448 |
|
|
@@ -451,24 +458,27 @@ SYSTEM_PROMPT = """You are a careful research agent answering GAIA benchmark que
|
|
| 451 |
|
| 452 |
Tools: web_search, fetch_url, wikipedia, python, get_task_file, transcribe_audio, view_image, youtube_transcript.
|
| 453 |
|
| 454 |
-
|
| 455 |
-
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
- If
|
| 459 |
-
-
|
| 460 |
-
-
|
| 461 |
-
- Use python for arithmetic, date math, sorting,
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
|
|
|
| 466 |
- Do NOT include the words "FINAL ANSWER", "Answer:", or any label.
|
| 467 |
- Numbers: digits only, no commas, no units, no $ sign — UNLESS the question asks for the unit.
|
|
|
|
| 468 |
- Strings: no leading articles ("the", "a") unless required; spell out, no abbreviations; write digits as digits.
|
|
|
|
| 469 |
- Lists: comma-separated, single space after each comma, applying the rules above to each element.
|
| 470 |
-
-
|
| 471 |
-
-
|
| 472 |
"""
|
| 473 |
|
| 474 |
|
|
@@ -489,12 +499,10 @@ class GroqAgent:
|
|
| 489 |
)
|
| 490 |
self.client = Groq(api_key=api_key)
|
| 491 |
self.models = list(GROQ_MODELS)
|
| 492 |
-
# Track models that hit a daily-token cap; skip them for the rest of the run.
|
| 493 |
self.exhausted_models: set[str] = set()
|
| 494 |
print(f"GroqAgent initialized with models={self.models}")
|
| 495 |
|
| 496 |
-
|
| 497 |
-
def _chat(self, messages, use_tools: bool = True, max_tokens: int = 1024):
|
| 498 |
last_error: Exception | None = None
|
| 499 |
for model in self.models:
|
| 500 |
if model in self.exhausted_models:
|
|
@@ -515,7 +523,14 @@ class GroqAgent:
|
|
| 515 |
msg = str(e)
|
| 516 |
last_error = e
|
| 517 |
is_429 = "429" in msg or "rate_limit" in msg.lower()
|
|
|
|
| 518 |
is_tpd = "per day" in msg.lower() or "tpd" in msg.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 519 |
if is_429 and is_tpd:
|
| 520 |
print(f"[{model}] daily token limit exhausted; switching model.")
|
| 521 |
self.exhausted_models.add(model)
|
|
@@ -523,7 +538,7 @@ class GroqAgent:
|
|
| 523 |
if is_429:
|
| 524 |
wait = self._parse_retry_seconds(msg)
|
| 525 |
wait = min(max(wait, 2), 30)
|
| 526 |
-
print(f"[{model}] 429
|
| 527 |
time.sleep(wait)
|
| 528 |
continue
|
| 529 |
print(f"[{model}] API error: {e}")
|
|
@@ -539,7 +554,21 @@ class GroqAgent:
|
|
| 539 |
seconds = float(m.group(2)) if m.group(2) else 0.0
|
| 540 |
return minutes * 60 + seconds
|
| 541 |
|
| 542 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
def __call__(self, question: str, task_id: str | None = None) -> str:
|
| 544 |
user_content = question
|
| 545 |
if task_id:
|
|
@@ -552,9 +581,18 @@ class GroqAgent:
|
|
| 552 |
|
| 553 |
for step in range(MAX_TOOL_ITERATIONS):
|
| 554 |
try:
|
| 555 |
-
resp = self._chat(messages, use_tools=True, max_tokens=
|
| 556 |
except Exception as e:
|
| 557 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 558 |
|
| 559 |
msg = resp.choices[0].message
|
| 560 |
tool_calls = getattr(msg, "tool_calls", None)
|
|
@@ -619,7 +657,7 @@ class GroqAgent:
|
|
| 619 |
}
|
| 620 |
)
|
| 621 |
try:
|
| 622 |
-
resp = self._chat(messages, use_tools=False, max_tokens=
|
| 623 |
return self._postprocess_answer(
|
| 624 |
(resp.choices[0].message.content or "").strip(), question
|
| 625 |
)
|
|
@@ -639,13 +677,10 @@ class GroqAgent:
|
|
| 639 |
text,
|
| 640 |
flags=re.IGNORECASE,
|
| 641 |
)
|
| 642 |
-
# If model wrapped answer in code fence or quotes.
|
| 643 |
text = text.strip("`")
|
| 644 |
if len(text) >= 2 and text[0] == text[-1] and text[0] in {'"', "'"}:
|
| 645 |
text = text[1:-1].strip()
|
| 646 |
|
| 647 |
-
# If the question is "how many" / numeric and the model returned a sentence,
|
| 648 |
-
# try to extract a single number.
|
| 649 |
q_lower = question.lower()
|
| 650 |
wants_number = bool(
|
| 651 |
re.search(r"\bhow many\b|\bhow much\b|\bwhat number\b|\bcount\b", q_lower)
|
|
@@ -655,15 +690,15 @@ class GroqAgent:
|
|
| 655 |
if m:
|
| 656 |
text = m.group(0)
|
| 657 |
|
| 658 |
-
# Strip a single trailing period if the text
|
| 659 |
-
if text.endswith(".") and
|
| 660 |
text = text[:-1]
|
| 661 |
|
| 662 |
return text.strip()
|
| 663 |
|
| 664 |
|
| 665 |
# ---------------------------------------------------------------------------
|
| 666 |
-
# Answer cache
|
| 667 |
# ---------------------------------------------------------------------------
|
| 668 |
def _load_cache() -> dict:
|
| 669 |
try:
|
|
@@ -748,6 +783,9 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
|
|
| 748 |
results_log.append(
|
| 749 |
{"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer}
|
| 750 |
)
|
|
|
|
|
|
|
|
|
|
| 751 |
|
| 752 |
if not answers_payload:
|
| 753 |
return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
|
|
@@ -810,14 +848,16 @@ with gr.Blocks() as demo:
|
|
| 810 |
gr.Markdown(
|
| 811 |
"""
|
| 812 |
**Setup**
|
| 813 |
-
1. Add a Space secret named `GROQ_API_KEY`
|
| 814 |
-
2. *Optional but recommended:*
|
| 815 |
-
3. Optional env vars: `GROQ_MODELS`, `GROQ_VISION_MODEL`, `GROQ_WHISPER_MODEL`.
|
| 816 |
4. Log in to Hugging Face below and click **Run Evaluation & Submit All Answers**.
|
| 817 |
|
| 818 |
Tools: `web_search`, `fetch_url`, `wikipedia`, `python`, `get_task_file`,
|
| 819 |
`transcribe_audio`, `view_image`, `youtube_transcript`.
|
| 820 |
-
|
|
|
|
|
|
|
| 821 |
"""
|
| 822 |
)
|
| 823 |
|
|
|
|
| 15 |
|
| 16 |
# --- Constants ---
|
| 17 |
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
| 18 |
+
|
| 19 |
+
# Free-tier Groq TPM limits (per minute):
|
| 20 |
+
# llama-3.1-8b-instant -> 30,000 TPM (lots of headroom)
|
| 21 |
+
# llama-3.3-70b-versatile -> 6,000 TPM (tight; one big tool result busts it)
|
| 22 |
+
# Default order: 8b first to avoid 413s, fall back to 70b only when necessary.
|
| 23 |
GROQ_MODELS = [
|
| 24 |
m.strip()
|
| 25 |
for m in os.getenv(
|
| 26 |
"GROQ_MODELS",
|
| 27 |
+
"llama-3.1-8b-instant,llama-3.3-70b-versatile",
|
| 28 |
).split(",")
|
| 29 |
if m.strip()
|
| 30 |
]
|
|
|
|
| 31 |
GROQ_VISION_MODEL = os.getenv("GROQ_VISION_MODEL", "meta-llama/llama-4-scout-17b-16e-instruct")
|
|
|
|
| 32 |
GROQ_WHISPER_MODEL = os.getenv("GROQ_WHISPER_MODEL", "whisper-large-v3-turbo")
|
| 33 |
|
| 34 |
+
MAX_TOOL_ITERATIONS = 5
|
| 35 |
+
TOOL_RESULT_MAX_CHARS = 1800 # smaller -> stays under 6K TPM for 70b fallback
|
| 36 |
+
HISTORY_TRIM_AFTER = 6 # trim old tool turns once we have this many messages
|
| 37 |
ANSWER_CACHE_PATH = os.getenv("ANSWER_CACHE_PATH", "answers_cache.json")
|
| 38 |
+
INTER_QUESTION_SLEEP = float(os.getenv("INTER_QUESTION_SLEEP", "2"))
|
| 39 |
|
| 40 |
# Track downloaded task files so vision/audio tools can re-use them by task_id.
|
| 41 |
_TASK_FILE_CACHE: dict[str, dict] = {}
|
|
|
|
| 62 |
lines.append(f"Answer: {res['answer']}")
|
| 63 |
for r in res.get("results", [])[:max_results]:
|
| 64 |
lines.append(
|
| 65 |
+
f"- {r.get('title', '')}\n {r.get('url', '')}\n {r.get('content', '')[:300]}"
|
| 66 |
)
|
| 67 |
if lines:
|
| 68 |
return "\n".join(lines)
|
| 69 |
except Exception as e:
|
| 70 |
print(f"tavily search failed, falling back to DDG: {e}")
|
| 71 |
|
|
|
|
| 72 |
try:
|
| 73 |
from duckduckgo_search import DDGS
|
| 74 |
results = []
|
| 75 |
with DDGS() as ddgs:
|
| 76 |
for r in ddgs.text(query, max_results=max_results):
|
| 77 |
results.append(
|
| 78 |
+
f"- {r.get('title', '')}\n {r.get('href', '')}\n {r.get('body', '')[:300]}"
|
| 79 |
)
|
| 80 |
if not results:
|
| 81 |
return "No results."
|
|
|
|
| 84 |
return f"web_search error: {e}"
|
| 85 |
|
| 86 |
|
| 87 |
+
def tool_fetch_url(url: str, max_chars: int = 1800) -> str:
|
| 88 |
"""Fetch a URL and return readable text (HTML stripped)."""
|
| 89 |
try:
|
| 90 |
from bs4 import BeautifulSoup
|
|
|
|
| 141 |
out = buf.getvalue().strip()
|
| 142 |
if not out and "result" in local_ns:
|
| 143 |
out = str(local_ns["result"])
|
| 144 |
+
return (out or "(no output)")[:1500]
|
| 145 |
except Exception as e:
|
| 146 |
return f"python error: {e}\n{traceback.format_exc(limit=2)}"
|
| 147 |
|
|
|
|
| 161 |
return None
|
| 162 |
|
| 163 |
|
| 164 |
+
def tool_youtube_transcript(url: str, max_chars: int = 2500) -> str:
|
| 165 |
"""Fetch the transcript of a YouTube video by URL or ID."""
|
| 166 |
try:
|
| 167 |
from youtube_transcript_api import YouTubeTranscriptApi
|
|
|
|
| 169 |
try:
|
| 170 |
data = YouTubeTranscriptApi.get_transcript(vid, languages=["en", "en-US", "en-GB"])
|
| 171 |
except Exception:
|
|
|
|
| 172 |
tlist = YouTubeTranscriptApi.list_transcripts(vid)
|
| 173 |
t = next(iter(tlist), None)
|
| 174 |
data = t.fetch() if t else []
|
|
|
|
| 185 |
"""Transcribe an audio file attached to a GAIA task using Groq Whisper."""
|
| 186 |
try:
|
| 187 |
from groq import Groq
|
|
|
|
| 188 |
info = _TASK_FILE_CACHE.get(task_id)
|
| 189 |
if not info:
|
| 190 |
+
tool_get_task_file(task_id)
|
| 191 |
info = _TASK_FILE_CACHE.get(task_id)
|
| 192 |
if not info or not os.path.exists(info.get("path", "")):
|
| 193 |
+
return "transcribe_audio error: no local file for task (file may not exist for this task_id)"
|
| 194 |
|
| 195 |
client = Groq(api_key=os.getenv("GROQ_API_KEY"))
|
| 196 |
with open(info["path"], "rb") as f:
|
|
|
|
| 201 |
)
|
| 202 |
text = tr if isinstance(tr, str) else getattr(tr, "text", str(tr))
|
| 203 |
text = text.strip()
|
| 204 |
+
if len(text) > 3500:
|
| 205 |
+
text = text[:3500] + " ...[truncated]"
|
| 206 |
return text or "(empty transcript)"
|
| 207 |
except Exception as e:
|
| 208 |
return f"transcribe_audio error: {e}"
|
|
|
|
| 217 |
tool_get_task_file(task_id)
|
| 218 |
info = _TASK_FILE_CACHE.get(task_id)
|
| 219 |
if not info or not os.path.exists(info.get("path", "")):
|
| 220 |
+
return "view_image error: no local file for task (file may not exist for this task_id)"
|
| 221 |
|
| 222 |
suffix = os.path.splitext(info["path"])[1].lower().lstrip(".")
|
| 223 |
if suffix == "jpg":
|
|
|
|
| 247 |
}
|
| 248 |
],
|
| 249 |
temperature=0.0,
|
| 250 |
+
max_tokens=600,
|
| 251 |
)
|
| 252 |
return (resp.choices[0].message.content or "").strip()
|
| 253 |
except Exception as e:
|
|
|
|
| 258 |
"""Download the file attached to a task and return a text preview."""
|
| 259 |
try:
|
| 260 |
resp = requests.get(f"{api_url}/files/{task_id}", timeout=30)
|
| 261 |
+
if resp.status_code == 404:
|
| 262 |
+
return (
|
| 263 |
+
"NO_FILE: This task has no attached file. Do not call get_task_file again. "
|
| 264 |
+
"Answer using web_search / wikipedia / python / your own knowledge."
|
| 265 |
+
)
|
| 266 |
resp.raise_for_status()
|
| 267 |
ctype = resp.headers.get("Content-Type", "")
|
| 268 |
cdisp = resp.headers.get("Content-Disposition", "")
|
|
|
|
| 270 |
fname = fname_match.group(1) if fname_match else f"{task_id}"
|
| 271 |
suffix = os.path.splitext(fname)[1].lower()
|
| 272 |
|
|
|
|
| 273 |
tmp = tempfile.NamedTemporaryFile(prefix=f"{task_id}_", suffix=suffix, delete=False)
|
| 274 |
tmp.write(resp.content)
|
| 275 |
tmp.close()
|
|
|
|
| 282 |
}
|
| 283 |
|
| 284 |
info = (
|
| 285 |
+
f"File: {fname}\nContent-Type: {ctype}\nSize: {len(resp.content)} bytes\n"
|
|
|
|
| 286 |
)
|
| 287 |
|
|
|
|
| 288 |
if suffix in {".txt", ".md", ".csv", ".json", ".py", ".tsv", ".log", ".xml", ".html"}:
|
| 289 |
try:
|
| 290 |
text = resp.content.decode("utf-8", errors="replace")
|
| 291 |
except Exception:
|
| 292 |
text = resp.text
|
| 293 |
+
return info + "\n--- preview ---\n" + text[:2500]
|
| 294 |
|
| 295 |
if suffix in {".xlsx", ".xls"}:
|
| 296 |
try:
|
| 297 |
df = pd.read_excel(tmp.name)
|
| 298 |
+
# Show full table to allow exact summing.
|
| 299 |
+
csv = df.to_csv(index=False)
|
| 300 |
+
if len(csv) > 2500:
|
| 301 |
+
csv = csv[:2500] + "\n...[truncated]"
|
| 302 |
+
return info + "\n--- excel as csv ---\n" + csv
|
| 303 |
except Exception as e:
|
| 304 |
return info + f"\n(excel parse error: {e})"
|
| 305 |
|
|
|
|
| 307 |
try:
|
| 308 |
from pypdf import PdfReader
|
| 309 |
reader = PdfReader(tmp.name)
|
| 310 |
+
pages = [p.extract_text() or "" for p in reader.pages[:6]]
|
| 311 |
+
return info + "\n--- pdf text ---\n" + "\n".join(pages)[:2500]
|
| 312 |
except Exception as e:
|
| 313 |
return info + f"\n(pdf parse error: {e})"
|
| 314 |
|
| 315 |
if suffix in {".mp3", ".wav", ".m4a", ".ogg", ".flac", ".webm"}:
|
| 316 |
+
return info + "\nThis is an audio file. Call transcribe_audio(task_id) to read it."
|
| 317 |
|
| 318 |
if suffix in {".png", ".jpg", ".jpeg", ".gif", ".webp"}:
|
| 319 |
+
return info + "\nThis is an image. Call view_image(task_id, question='...') to inspect it."
|
| 320 |
|
| 321 |
return info + "\n(binary file; no preview)"
|
| 322 |
except Exception as e:
|
|
|
|
| 351 |
"type": "object",
|
| 352 |
"properties": {
|
| 353 |
"url": {"type": "string"},
|
| 354 |
+
"max_chars": {"type": "integer", "default": 1800},
|
| 355 |
},
|
| 356 |
"required": ["url"],
|
| 357 |
},
|
|
|
|
| 376 |
"type": "function",
|
| 377 |
"function": {
|
| 378 |
"name": "python",
|
| 379 |
+
"description": "Execute a short Python snippet for math, dates, parsing CSV, list/string work, reversing text. Use print() or assign to `result`.",
|
| 380 |
"parameters": {
|
| 381 |
"type": "object",
|
| 382 |
"properties": {"code": {"type": "string"}},
|
|
|
|
| 388 |
"type": "function",
|
| 389 |
"function": {
|
| 390 |
"name": "get_task_file",
|
| 391 |
+
"description": "Download the file attached to a GAIA task by task_id. ONLY call when the question explicitly mentions an attached file/image/audio/Excel/PDF/code. Returns NO_FILE if none exists.",
|
| 392 |
"parameters": {
|
| 393 |
"type": "object",
|
| 394 |
"properties": {"task_id": {"type": "string"}},
|
|
|
|
| 432 |
"type": "object",
|
| 433 |
"properties": {
|
| 434 |
"url": {"type": "string"},
|
| 435 |
+
"max_chars": {"type": "integer", "default": 2500},
|
| 436 |
},
|
| 437 |
"required": ["url"],
|
| 438 |
},
|
|
|
|
| 442 |
|
| 443 |
TOOL_FUNCTIONS = {
|
| 444 |
"web_search": lambda args: tool_web_search(args["query"], int(args.get("max_results", 5))),
|
| 445 |
+
"fetch_url": lambda args: tool_fetch_url(args["url"], int(args.get("max_chars", 1800))),
|
| 446 |
"wikipedia": lambda args: tool_wikipedia(args["query"], int(args.get("sentences", 4))),
|
| 447 |
"python": lambda args: tool_python(args["code"]),
|
| 448 |
"get_task_file": lambda args: tool_get_task_file(args["task_id"]),
|
| 449 |
"transcribe_audio": lambda args: tool_transcribe_audio(args["task_id"]),
|
| 450 |
"view_image": lambda args: tool_view_image(args["task_id"], args.get("question", "")),
|
| 451 |
"youtube_transcript": lambda args: tool_youtube_transcript(
|
| 452 |
+
args["url"], int(args.get("max_chars", 2500))
|
| 453 |
),
|
| 454 |
}
|
| 455 |
|
|
|
|
| 458 |
|
| 459 |
Tools: web_search, fetch_url, wikipedia, python, get_task_file, transcribe_audio, view_image, youtube_transcript.
|
| 460 |
|
| 461 |
+
Decide tools by reading the question carefully:
|
| 462 |
+
- ONLY call get_task_file if the question literally says "attached file", "attached image", ".mp3", ".xlsx", ".pdf", ".py", "the file", "the image", "the audio", "the recording", or similar. If get_task_file returns NO_FILE, do NOT call it again — answer from web research.
|
| 463 |
+
- If the question references an audio file/recording, call get_task_file then transcribe_audio.
|
| 464 |
+
- If it references an image, call get_task_file then view_image with a focused question.
|
| 465 |
+
- If it contains a YouTube URL, call youtube_transcript(url) directly. (No need for get_task_file.)
|
| 466 |
+
- If the question text looks reversed (e.g. starts with strange punctuation like ".rewsna" or seems like backwards English), use python to reverse it: `result = "<text>"[::-1]` then answer that reversed question.
|
| 467 |
+
- For factual lookups, prefer wikipedia first for entities/people/places, web_search + fetch_url for everything else.
|
| 468 |
+
- Use python for ALL arithmetic, sums, date math, sorting, alphabetizing. Never compute by hand.
|
| 469 |
+
|
| 470 |
+
Be concise with tool calls — you have at most 5 tool turns, so plan well.
|
| 471 |
+
|
| 472 |
+
Answer formatting (CRITICAL — grader does an exact-match comparison):
|
| 473 |
+
- Reply with ONLY the answer. No preamble, no explanation, no quotes, no trailing period, no markdown.
|
| 474 |
- Do NOT include the words "FINAL ANSWER", "Answer:", or any label.
|
| 475 |
- Numbers: digits only, no commas, no units, no $ sign — UNLESS the question asks for the unit.
|
| 476 |
+
- Currency with "two decimal places": e.g. "89706.00" not "$89,706" not "89706".
|
| 477 |
- Strings: no leading articles ("the", "a") unless required; spell out, no abbreviations; write digits as digits.
|
| 478 |
+
- For names: just the requested form (first name only / last name only / full name) — read the question carefully.
|
| 479 |
- Lists: comma-separated, single space after each comma, applying the rules above to each element.
|
| 480 |
+
- For "alphabetical order" lists, sort them.
|
| 481 |
+
- For "ascending order" numeric lists, sort numerically.
|
| 482 |
"""
|
| 483 |
|
| 484 |
|
|
|
|
| 499 |
)
|
| 500 |
self.client = Groq(api_key=api_key)
|
| 501 |
self.models = list(GROQ_MODELS)
|
|
|
|
| 502 |
self.exhausted_models: set[str] = set()
|
| 503 |
print(f"GroqAgent initialized with models={self.models}")
|
| 504 |
|
| 505 |
+
def _chat(self, messages, use_tools: bool = True, max_tokens: int = 800):
|
|
|
|
| 506 |
last_error: Exception | None = None
|
| 507 |
for model in self.models:
|
| 508 |
if model in self.exhausted_models:
|
|
|
|
| 523 |
msg = str(e)
|
| 524 |
last_error = e
|
| 525 |
is_429 = "429" in msg or "rate_limit" in msg.lower()
|
| 526 |
+
is_413 = "413" in msg or "too large" in msg.lower()
|
| 527 |
is_tpd = "per day" in msg.lower() or "tpd" in msg.lower()
|
| 528 |
+
if is_413:
|
| 529 |
+
# Request is too big for this model's TPM bucket.
|
| 530 |
+
# Trim history aggressively and try the next model.
|
| 531 |
+
print(f"[{model}] 413 too large; will trim and try next model.")
|
| 532 |
+
last_error = e
|
| 533 |
+
break
|
| 534 |
if is_429 and is_tpd:
|
| 535 |
print(f"[{model}] daily token limit exhausted; switching model.")
|
| 536 |
self.exhausted_models.add(model)
|
|
|
|
| 538 |
if is_429:
|
| 539 |
wait = self._parse_retry_seconds(msg)
|
| 540 |
wait = min(max(wait, 2), 30)
|
| 541 |
+
print(f"[{model}] 429; sleeping {wait}s (attempt {attempt + 1}/3)")
|
| 542 |
time.sleep(wait)
|
| 543 |
continue
|
| 544 |
print(f"[{model}] API error: {e}")
|
|
|
|
| 554 |
seconds = float(m.group(2)) if m.group(2) else 0.0
|
| 555 |
return minutes * 60 + seconds
|
| 556 |
|
| 557 |
+
@staticmethod
|
| 558 |
+
def _trim_messages(messages: list) -> list:
|
| 559 |
+
"""Keep system + user(0) + last 4 turns. Older tool/assistant turns get summarized."""
|
| 560 |
+
if len(messages) <= HISTORY_TRIM_AFTER:
|
| 561 |
+
return messages
|
| 562 |
+
head = messages[:2] # system + first user
|
| 563 |
+
tail = messages[-4:]
|
| 564 |
+
# Summarize what was dropped so model has continuity.
|
| 565 |
+
dropped = len(messages) - len(head) - len(tail)
|
| 566 |
+
summary = {
|
| 567 |
+
"role": "user",
|
| 568 |
+
"content": f"[Note: {dropped} earlier tool turns omitted to save tokens. Continue with the latest results.]",
|
| 569 |
+
}
|
| 570 |
+
return head + [summary] + tail
|
| 571 |
+
|
| 572 |
def __call__(self, question: str, task_id: str | None = None) -> str:
|
| 573 |
user_content = question
|
| 574 |
if task_id:
|
|
|
|
| 581 |
|
| 582 |
for step in range(MAX_TOOL_ITERATIONS):
|
| 583 |
try:
|
| 584 |
+
resp = self._chat(self._trim_messages(messages), use_tools=True, max_tokens=800)
|
| 585 |
except Exception as e:
|
| 586 |
+
# Try one more time with no tools, heavily trimmed, to at least get a guess.
|
| 587 |
+
print(f"chat failed: {e} — trying no-tool fallback.")
|
| 588 |
+
try:
|
| 589 |
+
resp = self._chat(
|
| 590 |
+
[messages[0], messages[1]],
|
| 591 |
+
use_tools=False,
|
| 592 |
+
max_tokens=200,
|
| 593 |
+
)
|
| 594 |
+
except Exception as e2:
|
| 595 |
+
return f"AGENT ERROR: {e2}"
|
| 596 |
|
| 597 |
msg = resp.choices[0].message
|
| 598 |
tool_calls = getattr(msg, "tool_calls", None)
|
|
|
|
| 657 |
}
|
| 658 |
)
|
| 659 |
try:
|
| 660 |
+
resp = self._chat(self._trim_messages(messages), use_tools=False, max_tokens=200)
|
| 661 |
return self._postprocess_answer(
|
| 662 |
(resp.choices[0].message.content or "").strip(), question
|
| 663 |
)
|
|
|
|
| 677 |
text,
|
| 678 |
flags=re.IGNORECASE,
|
| 679 |
)
|
|
|
|
| 680 |
text = text.strip("`")
|
| 681 |
if len(text) >= 2 and text[0] == text[-1] and text[0] in {'"', "'"}:
|
| 682 |
text = text[1:-1].strip()
|
| 683 |
|
|
|
|
|
|
|
| 684 |
q_lower = question.lower()
|
| 685 |
wants_number = bool(
|
| 686 |
re.search(r"\bhow many\b|\bhow much\b|\bwhat number\b|\bcount\b", q_lower)
|
|
|
|
| 690 |
if m:
|
| 691 |
text = m.group(0)
|
| 692 |
|
| 693 |
+
# Strip a single trailing period if the text is short / single token.
|
| 694 |
+
if text.endswith(".") and " " not in text:
|
| 695 |
text = text[:-1]
|
| 696 |
|
| 697 |
return text.strip()
|
| 698 |
|
| 699 |
|
| 700 |
# ---------------------------------------------------------------------------
|
| 701 |
+
# Answer cache
|
| 702 |
# ---------------------------------------------------------------------------
|
| 703 |
def _load_cache() -> dict:
|
| 704 |
try:
|
|
|
|
| 783 |
results_log.append(
|
| 784 |
{"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer}
|
| 785 |
)
|
| 786 |
+
# Pace requests so per-minute Groq limits reset between questions.
|
| 787 |
+
if INTER_QUESTION_SLEEP > 0 and idx < len(questions_data):
|
| 788 |
+
time.sleep(INTER_QUESTION_SLEEP)
|
| 789 |
|
| 790 |
if not answers_payload:
|
| 791 |
return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
|
|
|
|
| 848 |
gr.Markdown(
|
| 849 |
"""
|
| 850 |
**Setup**
|
| 851 |
+
1. Add a Space secret named `GROQ_API_KEY` (free at console.groq.com).
|
| 852 |
+
2. *Optional but recommended:* `TAVILY_API_KEY` (free 1000/mo at tavily.com) for better search.
|
| 853 |
+
3. Optional env vars: `GROQ_MODELS`, `GROQ_VISION_MODEL`, `GROQ_WHISPER_MODEL`, `INTER_QUESTION_SLEEP`.
|
| 854 |
4. Log in to Hugging Face below and click **Run Evaluation & Submit All Answers**.
|
| 855 |
|
| 856 |
Tools: `web_search`, `fetch_url`, `wikipedia`, `python`, `get_task_file`,
|
| 857 |
`transcribe_audio`, `view_image`, `youtube_transcript`.
|
| 858 |
+
|
| 859 |
+
Tip: if you get rate-limit errors, the answers cache lets you click Run again to
|
| 860 |
+
re-attempt only the failed questions without re-querying ones that already worked.
|
| 861 |
"""
|
| 862 |
)
|
| 863 |
|