avi080704 commited on
Commit
75a2650
·
verified ·
1 Parent(s): 7ec3bbc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +108 -68
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
- # Primary model first, then fallbacks used when rate-limited.
 
 
 
 
19
  GROQ_MODELS = [
20
  m.strip()
21
  for m in os.getenv(
22
  "GROQ_MODELS",
23
- "llama-3.3-70b-versatile,llama-3.1-8b-instant",
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 = 8
33
- TOOL_RESULT_MAX_CHARS = 3500
 
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', '')[:400]}"
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 = 3500) -> str:
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 = 3500) -> str:
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) # populates cache
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) > 4000:
204
- text = text[:4000] + " ...[truncated]"
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=800,
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}\nSaved to: {tmp.name}\n"
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[:3000]
291
 
292
  if suffix in {".xlsx", ".xls"}:
293
  try:
294
  df = pd.read_excel(tmp.name)
295
- return info + "\n--- preview (head 30) ---\n" + df.head(30).to_csv(index=False)
 
 
 
 
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[:8]]
304
- return info + "\n--- pdf text (first 8 pages) ---\n" + "\n".join(pages)[:3000]
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": 3500},
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 and return a text preview. Always call this first if the question references an attached file.",
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": 3500},
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", 3500))),
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", 3500))
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
- Workflow:
455
- - If the question references an attached file/image/audio/code/table, call get_task_file(task_id) first.
456
- - For audio (mp3/wav/m4a/ogg), then call transcribe_audio(task_id).
457
- - For images (png/jpg/gif/webp), then call view_image(task_id, question="...").
458
- - If the question references a YouTube link, call youtube_transcript(url).
459
- - Use web_search then fetch_url to verify facts from primary sources. Prefer official sites and Wikipedia.
460
- - Use wikipedia for well-known entities, places, and historical facts.
461
- - Use python for arithmetic, date math, sorting, set operations, parsing strings/CSV. Do NOT eyeball math.
462
- - Cross-check before answering. Don't guess. If two sources disagree, prefer the most authoritative.
463
-
464
- Answer formatting (CRITICAL — grader does an exact-match-style comparison):
465
- - Reply with ONLY the answer. No preamble, no explanation, no quotes, no trailing period.
 
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
- - If the question asks for a name, give just the name. If it asks "how many", give just the number.
471
- - If the question asks for a list in alphabetical order, sort it.
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
- # ---- Groq call with model fallback + 429 handling -------------------
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 rate limit; sleeping {wait}s (attempt {attempt + 1}/3)")
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
- # ---- Main entrypoint ------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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=1024)
556
  except Exception as e:
557
- return f"AGENT ERROR: {e}"
 
 
 
 
 
 
 
 
 
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=256)
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 isn't a list/sentence with internal periods.
659
- if text.endswith(".") and text.count(".") == 1 and " " not in text[-3:]:
660
  text = text[:-1]
661
 
662
  return text.strip()
663
 
664
 
665
  # ---------------------------------------------------------------------------
666
- # Answer cache so a failed run doesn't waste tokens
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` with your Groq API key (free at console.groq.com).
814
- 2. *Optional but recommended:* add `TAVILY_API_KEY` (free tier at tavily.com) for better search.
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
- Answers are cached locally so a failed submission can be retried without re-running the agent.
 
 
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