avi080704 commited on
Commit
3361e36
·
verified ·
1 Parent(s): 6d91d29

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +274 -43
app.py CHANGED
@@ -3,9 +3,11 @@ import re
3
  import json
4
  import io
5
  import time
 
6
  import traceback
7
  import contextlib
8
  import tempfile
 
9
 
10
  import gradio as gr
11
  import requests
@@ -14,7 +16,6 @@ import pandas as pd
14
  # --- Constants ---
15
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
16
  # Primary model first, then fallbacks used when rate-limited.
17
- # llama-3.1-8b-instant has much higher daily token quota on the free tier.
18
  GROQ_MODELS = [
19
  m.strip()
20
  for m in os.getenv(
@@ -23,16 +24,48 @@ GROQ_MODELS = [
23
  ).split(",")
24
  if m.strip()
25
  ]
26
- MAX_TOOL_ITERATIONS = 6
27
- TOOL_RESULT_MAX_CHARS = 3000
 
 
 
 
 
28
  ANSWER_CACHE_PATH = os.getenv("ANSWER_CACHE_PATH", "answers_cache.json")
29
 
 
 
 
30
 
31
  # ---------------------------------------------------------------------------
32
  # Tool implementations
33
  # ---------------------------------------------------------------------------
34
  def tool_web_search(query: str, max_results: int = 5) -> str:
35
- """DuckDuckGo text search. Returns a short list of titles + snippets + URLs."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  try:
37
  from duckduckgo_search import DDGS
38
  results = []
@@ -48,7 +81,7 @@ def tool_web_search(query: str, max_results: int = 5) -> str:
48
  return f"web_search error: {e}"
49
 
50
 
51
- def tool_fetch_url(url: str, max_chars: int = 3000) -> str:
52
  """Fetch a URL and return readable text (HTML stripped)."""
53
  try:
54
  from bs4 import BeautifulSoup
@@ -110,6 +143,116 @@ def tool_python(code: str) -> str:
110
  return f"python error: {e}\n{traceback.format_exc(limit=2)}"
111
 
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  def tool_get_task_file(task_id: str, api_url: str = DEFAULT_API_URL) -> str:
114
  """Download the file attached to a task and return a text preview."""
115
  try:
@@ -126,7 +269,17 @@ def tool_get_task_file(task_id: str, api_url: str = DEFAULT_API_URL) -> str:
126
  tmp.write(resp.content)
127
  tmp.close()
128
 
129
- info = f"File: {fname}\nContent-Type: {ctype}\nSaved to: {tmp.name}\nSize: {len(resp.content)} bytes\n"
 
 
 
 
 
 
 
 
 
 
130
 
131
  # Try to give a readable preview
132
  if suffix in {".txt", ".md", ".csv", ".json", ".py", ".tsv", ".log", ".xml", ".html"}:
@@ -152,11 +305,11 @@ def tool_get_task_file(task_id: str, api_url: str = DEFAULT_API_URL) -> str:
152
  except Exception as e:
153
  return info + f"\n(pdf parse error: {e})"
154
 
155
- if suffix in {".mp3", ".wav", ".m4a", ".ogg"}:
156
- return info + "\n(audio file; no transcription tool available)"
157
 
158
  if suffix in {".png", ".jpg", ".jpeg", ".gif", ".webp"}:
159
- return info + "\n(image file; no vision tool available)"
160
 
161
  return info + "\n(binary file; no preview)"
162
  except Exception as e:
@@ -171,7 +324,7 @@ TOOLS_SPEC = [
171
  "type": "function",
172
  "function": {
173
  "name": "web_search",
174
- "description": "Search the web with DuckDuckGo. Returns titles, URLs, and snippets.",
175
  "parameters": {
176
  "type": "object",
177
  "properties": {
@@ -191,7 +344,7 @@ TOOLS_SPEC = [
191
  "type": "object",
192
  "properties": {
193
  "url": {"type": "string"},
194
- "max_chars": {"type": "integer", "default": 3000},
195
  },
196
  "required": ["url"],
197
  },
@@ -216,7 +369,7 @@ TOOLS_SPEC = [
216
  "type": "function",
217
  "function": {
218
  "name": "python",
219
- "description": "Execute a short Python snippet for math, string parsing, or data work. Use print() or assign to `result`.",
220
  "parameters": {
221
  "type": "object",
222
  "properties": {"code": {"type": "string"}},
@@ -228,7 +381,7 @@ TOOLS_SPEC = [
228
  "type": "function",
229
  "function": {
230
  "name": "get_task_file",
231
- "description": "Download the file attached to a GAIA task by task_id and return a text preview.",
232
  "parameters": {
233
  "type": "object",
234
  "properties": {"task_id": {"type": "string"}},
@@ -236,35 +389,86 @@ TOOLS_SPEC = [
236
  },
237
  },
238
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  ]
240
 
241
  TOOL_FUNCTIONS = {
242
  "web_search": lambda args: tool_web_search(args["query"], int(args.get("max_results", 5))),
243
- "fetch_url": lambda args: tool_fetch_url(args["url"], int(args.get("max_chars", 3000))),
244
  "wikipedia": lambda args: tool_wikipedia(args["query"], int(args.get("sentences", 4))),
245
  "python": lambda args: tool_python(args["code"]),
246
  "get_task_file": lambda args: tool_get_task_file(args["task_id"]),
 
 
 
 
 
247
  }
248
 
249
 
250
  SYSTEM_PROMPT = """You are a careful research agent answering GAIA benchmark questions.
251
 
252
- You have tools: web_search, fetch_url, wikipedia, python, get_task_file.
253
 
254
  Workflow:
255
- - If the question references an attached file, image, audio, code, or table, call get_task_file with the task_id first.
256
- - Use web_search then fetch_url to verify facts from primary sources.
257
- - Use wikipedia for well-known entities or historical facts.
258
- - Use python for arithmetic, date math, string/list manipulation, or parsing CSV data.
259
- - Cross-check before answering. Avoid guessing.
260
-
261
- Answer formatting (critical, the grader does EXACT string match):
262
- - Reply with ONLY the answer. No preamble, no explanation, no quotes, no trailing period unless part of the answer.
263
- - Do NOT include the words "FINAL ANSWER" or any label.
264
- - Numbers: digits only, no commas, no units, no $ sign, unless the question asks for the unit.
265
- - Strings: no leading articles ("the", "a") unless required, no abbreviations, write digits as digits.
 
 
 
266
  - Lists: comma-separated, single space after each comma, applying the rules above to each element.
267
  - If the question asks for a name, give just the name. If it asks "how many", give just the number.
 
268
  """
269
 
270
 
@@ -313,7 +517,6 @@ class GroqAgent:
313
  is_429 = "429" in msg or "rate_limit" in msg.lower()
314
  is_tpd = "per day" in msg.lower() or "tpd" in msg.lower()
315
  if is_429 and is_tpd:
316
- # Daily quota gone — switch model permanently for this run.
317
  print(f"[{model}] daily token limit exhausted; switching model.")
318
  self.exhausted_models.add(model)
319
  break
@@ -323,14 +526,12 @@ class GroqAgent:
323
  print(f"[{model}] 429 rate limit; sleeping {wait}s (attempt {attempt + 1}/3)")
324
  time.sleep(wait)
325
  continue
326
- # Non-429 error: don't retry on the same model.
327
  print(f"[{model}] API error: {e}")
328
  break
329
  raise RuntimeError(f"All Groq models failed. Last error: {last_error}")
330
 
331
  @staticmethod
332
  def _parse_retry_seconds(error_msg: str) -> float:
333
- # Examples in Groq error: "Please try again in 7m18.912s." or "in 12.3s"
334
  m = re.search(r"in\s+(?:(\d+)m)?([\d.]+)s", error_msg)
335
  if not m:
336
  return 5.0
@@ -360,7 +561,7 @@ class GroqAgent:
360
 
361
  if not tool_calls:
362
  answer = (msg.content or "").strip()
363
- return self._postprocess_answer(answer)
364
 
365
  messages.append(
366
  {
@@ -387,7 +588,7 @@ class GroqAgent:
387
  except json.JSONDecodeError:
388
  args = {}
389
  fn = TOOL_FUNCTIONS.get(name)
390
- print(f"[tool] {name}({args})")
391
  if fn is None:
392
  result = f"unknown tool: {name}"
393
  else:
@@ -419,19 +620,46 @@ class GroqAgent:
419
  )
420
  try:
421
  resp = self._chat(messages, use_tools=False, max_tokens=256)
422
- return self._postprocess_answer((resp.choices[0].message.content or "").strip())
 
 
423
  except Exception as e:
424
  return f"AGENT ERROR: {e}"
425
 
426
  @staticmethod
427
- def _postprocess_answer(text: str) -> str:
428
  if not text:
429
  return ""
430
  text = text.strip()
431
- text = re.sub(r"^(final answer|answer)\s*:\s*", "", text, flags=re.IGNORECASE)
432
- if len(text) >= 2 and text[0] == text[-1] and text[0] in {'"', "'", "`"}:
 
 
 
 
 
 
 
 
 
433
  text = text[1:-1].strip()
434
- return text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
 
436
 
437
  # ---------------------------------------------------------------------------
@@ -531,7 +759,6 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
531
  }
532
  print(f"Submitting {len(answers_payload)} answers for user '{username}'...")
533
 
534
- # Retry submission a few times — the leaderboard's HF dataset write is flaky.
535
  last_error = None
536
  for attempt in range(3):
537
  try:
@@ -583,12 +810,14 @@ with gr.Blocks() as demo:
583
  gr.Markdown(
584
  """
585
  **Setup**
586
- 1. Add a Space secret named `GROQ_API_KEY` with your Groq API key.
587
- 2. Optional: set `GROQ_MODELS` (comma-separated, default `llama-3.3-70b-versatile,llama-3.1-8b-instant`).
588
- 3. Log in to Hugging Face below and click **Run Evaluation & Submit All Answers**.
589
-
590
- Tools available to the agent: `web_search`, `fetch_url`, `wikipedia`, `python`, `get_task_file`.
591
- Answers are cached locally, so a failed submission can be retried without re-running the agent.
 
 
592
  """
593
  )
594
 
@@ -618,6 +847,8 @@ if __name__ == "__main__":
618
 
619
  if not os.getenv("GROQ_API_KEY"):
620
  print("⚠️ GROQ_API_KEY is not set. Set it before running evaluation.")
 
 
621
 
622
  print("-" * (60 + len(" App Starting ")) + "\n")
623
  demo.launch(debug=True, share=False)
 
3
  import json
4
  import io
5
  import time
6
+ import base64
7
  import traceback
8
  import contextlib
9
  import tempfile
10
+ from urllib.parse import urlparse, parse_qs
11
 
12
  import gradio as gr
13
  import requests
 
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(
 
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] = {}
38
+
39
 
40
  # ---------------------------------------------------------------------------
41
  # Tool implementations
42
  # ---------------------------------------------------------------------------
43
  def tool_web_search(query: str, max_results: int = 5) -> str:
44
+ """Web search. Tries Tavily first (if TAVILY_API_KEY set), falls back to DuckDuckGo."""
45
+ tavily_key = os.getenv("TAVILY_API_KEY")
46
+ if tavily_key:
47
+ try:
48
+ from tavily import TavilyClient
49
+ client = TavilyClient(api_key=tavily_key)
50
+ res = client.search(
51
+ query=query,
52
+ max_results=max_results,
53
+ search_depth="basic",
54
+ include_answer=True,
55
+ )
56
+ lines = []
57
+ if res.get("answer"):
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 = []
 
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
 
143
  return f"python error: {e}\n{traceback.format_exc(limit=2)}"
144
 
145
 
146
+ def _extract_youtube_id(url: str) -> str | None:
147
+ try:
148
+ u = urlparse(url)
149
+ if "youtu.be" in u.netloc:
150
+ return u.path.lstrip("/").split("/")[0] or None
151
+ if "youtube.com" in u.netloc:
152
+ if u.path == "/watch":
153
+ return parse_qs(u.query).get("v", [None])[0]
154
+ if u.path.startswith("/embed/") or u.path.startswith("/shorts/"):
155
+ return u.path.split("/")[2]
156
+ except Exception:
157
+ pass
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
165
+ vid = _extract_youtube_id(url) or url.strip()
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 []
173
+ text = " ".join(seg.get("text", "") for seg in data).strip()
174
+ text = re.sub(r"\s+", " ", text)
175
+ if len(text) > max_chars:
176
+ text = text[:max_chars] + " ...[truncated]"
177
+ return text or "(empty transcript)"
178
+ except Exception as e:
179
+ return f"youtube_transcript error: {e}"
180
+
181
+
182
+ 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:
196
+ tr = client.audio.transcriptions.create(
197
+ file=(os.path.basename(info["path"]), f.read()),
198
+ model=GROQ_WHISPER_MODEL,
199
+ response_format="text",
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}"
208
+
209
+
210
+ def tool_view_image(task_id: str, question: str = "") -> str:
211
+ """Describe / answer a question about an image attached to a GAIA task using Groq vision."""
212
+ try:
213
+ from groq import Groq
214
+ info = _TASK_FILE_CACHE.get(task_id)
215
+ if not info:
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":
223
+ suffix = "jpeg"
224
+ if suffix not in {"png", "jpeg", "gif", "webp"}:
225
+ return f"view_image error: unsupported image type .{suffix}"
226
+
227
+ with open(info["path"], "rb") as f:
228
+ b64 = base64.b64encode(f.read()).decode("ascii")
229
+ data_url = f"data:image/{suffix};base64,{b64}"
230
+
231
+ prompt = (
232
+ question.strip()
233
+ or "Describe this image in detail, including any text, numbers, or symbols visible."
234
+ )
235
+
236
+ client = Groq(api_key=os.getenv("GROQ_API_KEY"))
237
+ resp = client.chat.completions.create(
238
+ model=GROQ_VISION_MODEL,
239
+ messages=[
240
+ {
241
+ "role": "user",
242
+ "content": [
243
+ {"type": "text", "text": prompt},
244
+ {"type": "image_url", "image_url": {"url": data_url}},
245
+ ],
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:
253
+ return f"view_image error: {e}"
254
+
255
+
256
  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:
 
269
  tmp.write(resp.content)
270
  tmp.close()
271
 
272
+ _TASK_FILE_CACHE[task_id] = {
273
+ "path": tmp.name,
274
+ "name": fname,
275
+ "ctype": ctype,
276
+ "size": len(resp.content),
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"}:
 
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:
 
324
  "type": "function",
325
  "function": {
326
  "name": "web_search",
327
+ "description": "Search the web (Tavily preferred, DuckDuckGo fallback). Returns titles, URLs, snippets.",
328
  "parameters": {
329
  "type": "object",
330
  "properties": {
 
344
  "type": "object",
345
  "properties": {
346
  "url": {"type": "string"},
347
+ "max_chars": {"type": "integer", "default": 3500},
348
  },
349
  "required": ["url"],
350
  },
 
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
  "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"}},
 
389
  },
390
  },
391
  },
392
+ {
393
+ "type": "function",
394
+ "function": {
395
+ "name": "transcribe_audio",
396
+ "description": "Transcribe an attached audio file (mp3/wav/m4a/ogg/flac) for the given task_id.",
397
+ "parameters": {
398
+ "type": "object",
399
+ "properties": {"task_id": {"type": "string"}},
400
+ "required": ["task_id"],
401
+ },
402
+ },
403
+ },
404
+ {
405
+ "type": "function",
406
+ "function": {
407
+ "name": "view_image",
408
+ "description": "Inspect an attached image (png/jpg/gif/webp) using a vision model. Pass a focused question for best results.",
409
+ "parameters": {
410
+ "type": "object",
411
+ "properties": {
412
+ "task_id": {"type": "string"},
413
+ "question": {"type": "string"},
414
+ },
415
+ "required": ["task_id"],
416
+ },
417
+ },
418
+ },
419
+ {
420
+ "type": "function",
421
+ "function": {
422
+ "name": "youtube_transcript",
423
+ "description": "Fetch the transcript text of a YouTube video given its URL or ID.",
424
+ "parameters": {
425
+ "type": "object",
426
+ "properties": {
427
+ "url": {"type": "string"},
428
+ "max_chars": {"type": "integer", "default": 3500},
429
+ },
430
+ "required": ["url"],
431
+ },
432
+ },
433
+ },
434
  ]
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
 
449
 
450
  SYSTEM_PROMPT = """You are a careful research agent answering GAIA benchmark questions.
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
 
 
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)
522
  break
 
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}")
530
  break
531
  raise RuntimeError(f"All Groq models failed. Last error: {last_error}")
532
 
533
  @staticmethod
534
  def _parse_retry_seconds(error_msg: str) -> float:
 
535
  m = re.search(r"in\s+(?:(\d+)m)?([\d.]+)s", error_msg)
536
  if not m:
537
  return 5.0
 
561
 
562
  if not tool_calls:
563
  answer = (msg.content or "").strip()
564
+ return self._postprocess_answer(answer, question)
565
 
566
  messages.append(
567
  {
 
588
  except json.JSONDecodeError:
589
  args = {}
590
  fn = TOOL_FUNCTIONS.get(name)
591
+ print(f"[tool] {name}({str(args)[:200]})")
592
  if fn is None:
593
  result = f"unknown tool: {name}"
594
  else:
 
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
+ )
626
  except Exception as e:
627
  return f"AGENT ERROR: {e}"
628
 
629
  @staticmethod
630
+ def _postprocess_answer(text: str, question: str = "") -> str:
631
  if not text:
632
  return ""
633
  text = text.strip()
634
+
635
+ # Drop common labels.
636
+ text = re.sub(
637
+ r"^(final\s*answer|answer|the\s*answer\s*is)\s*[:\-]?\s*",
638
+ "",
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)
652
+ )
653
+ if wants_number and not re.fullmatch(r"-?\d+(\.\d+)?", text):
654
+ m = re.search(r"-?\d+(?:\.\d+)?", text.replace(",", ""))
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
  # ---------------------------------------------------------------------------
 
759
  }
760
  print(f"Submitting {len(answers_payload)} answers for user '{username}'...")
761
 
 
762
  last_error = None
763
  for attempt in range(3):
764
  try:
 
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
 
 
847
 
848
  if not os.getenv("GROQ_API_KEY"):
849
  print("⚠️ GROQ_API_KEY is not set. Set it before running evaluation.")
850
+ if not os.getenv("TAVILY_API_KEY"):
851
+ print("ℹ️ TAVILY_API_KEY not set — search will use DuckDuckGo (less reliable).")
852
 
853
  print("-" * (60 + len(" App Starting ")) + "\n")
854
  demo.launch(debug=True, share=False)