czrrr commited on
Commit
631e5ed
·
verified ·
1 Parent(s): 84be3ad

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +675 -101
app.py CHANGED
@@ -1,6 +1,9 @@
1
  import os
2
  import re
3
  import base64
 
 
 
4
  from io import BytesIO
5
  from pathlib import Path
6
  from zipfile import ZipFile
@@ -21,13 +24,93 @@ from smolagents import (
21
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
22
  RESULT_COLUMNS = ["Task ID", "Question", "Submitted Answer"]
23
  HTTP_TIMEOUT = 45
24
- MAX_EXTRACTED_CHARS = 35_000
25
  WEBPAGE_CONNECT_TIMEOUT = 8
26
  WEBPAGE_READ_TIMEOUT = 20
27
- MAX_WEBPAGE_CHARS = 8_000
28
  DEFAULT_MAIN_MODEL = "groq/qwen/qwen3.6-27b"
29
  DEFAULT_GROQ_REVIEW_MODEL = "groq/qwen/qwen3.6-27b"
30
  TASK_FILE_CACHE = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
 
33
  def clean_filename(value: str) -> str:
@@ -70,10 +153,15 @@ def filename_from_response(response: requests.Response, task_id: str) -> str:
70
  def download_gaia_attachment(task_id: str) -> tuple[bytes, str]:
71
  """Baixa um anexo pela API do curso, com fallback para o dataset oficial."""
72
  task_id = str(task_id).strip()
 
 
 
73
  course_url = f"{DEFAULT_API_URL}/files/{task_id}"
74
  response = requests.get(course_url, timeout=HTTP_TIMEOUT)
75
  if response.ok:
76
- return response.content, filename_from_response(response, task_id)
 
 
77
  if response.status_code != 404:
78
  response.raise_for_status()
79
 
@@ -114,7 +202,9 @@ def download_gaia_attachment(task_id: str) -> tuple[bytes, str]:
114
  filename=dataset_path,
115
  token=token,
116
  )
117
- return Path(local_path).read_bytes(), filename
 
 
118
  except Exception as exc:
119
  errors.append(str(exc))
120
 
@@ -151,7 +241,9 @@ def get_task_file_name(task_id: str) -> str:
151
  return TASK_FILE_CACHE.get(task_id, "")
152
 
153
 
154
- def extract_attachment_text(data: bytes, filename: str) -> str:
 
 
155
  """Extrai conteúdo legível dos formatos mais comuns do GAIA."""
156
  suffix = Path(filename).suffix.lower()
157
 
@@ -205,20 +297,10 @@ def extract_attachment_text(data: bytes, filename: str) -> str:
205
 
206
  text = BeautifulSoup(text, "html.parser").get_text("\n")
207
  elif suffix in {".mp3", ".wav", ".flac", ".m4a", ".ogg"}:
208
- from huggingface_hub import InferenceClient
209
-
210
- token = os.getenv("HF_TOKEN")
211
- if not token:
212
- text = "Audio transcription failed: HF_TOKEN is not configured."
213
- else:
214
- client = InferenceClient(api_key=token, provider="auto")
215
- asr_model = os.getenv(
216
- "GAIA_ASR_MODEL", "distil-whisper/distil-large-v3"
217
- )
218
- transcript = client.automatic_speech_recognition(
219
- data, model=asr_model
220
- )
221
- text = f"Audio transcript:\n{transcript.text}"
222
  elif suffix == ".zip":
223
  with ZipFile(BytesIO(data)) as archive:
224
  text = "Files inside ZIP:\n" + "\n".join(archive.namelist())
@@ -241,8 +323,16 @@ def extract_attachment_text(data: bytes, filename: str) -> str:
241
  text = text.strip()
242
  if not text:
243
  return f"The attachment {filename} was downloaded but contained no extractable text."
244
- if len(text) > MAX_EXTRACTED_CHARS:
245
- text = text[:MAX_EXTRACTED_CHARS] + "\n[content truncated]"
 
 
 
 
 
 
 
 
246
  return text
247
 
248
 
@@ -259,16 +349,26 @@ class OpenWebPageTool(Tool):
259
  "url": {
260
  "type": "string",
261
  "description": "The complete HTTP or HTTPS URL to open.",
262
- }
 
 
 
 
 
 
 
263
  }
264
  output_type = "string"
265
 
266
- def forward(self, url: str) -> str:
267
  from markdownify import markdownify
268
 
269
  url = str(url or "").strip()
 
270
  if not re.match(r"^https?://", url, flags=re.I):
271
  return "Invalid URL: visit_webpage requires a full HTTP/HTTPS URL."
 
 
272
 
273
  headers = {
274
  "User-Agent": (
@@ -358,47 +458,488 @@ class OpenWebPageTool(Tool):
358
  + "\n".join(source_links[-25:])
359
  )
360
  text = re.sub(r"\n{3,}", "\n\n", text).strip()
361
- if len(text) > MAX_WEBPAGE_CHARS:
362
- head_size = 5_000
363
- tail_size = MAX_WEBPAGE_CHARS - head_size
364
- text = (
365
- text[:head_size]
366
- + "\n\n[page middle omitted to conserve tokens]\n\n"
367
- + text[-tail_size:]
368
- )
369
- return text or "The page was retrieved but contained no text."
370
  except Exception as exc:
371
- errors.append(f"{target}: {exc}")
372
 
373
  return "Error fetching the webpage: " + " | ".join(errors)
374
 
375
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  class InspectGaiaAttachmentTool(Tool):
377
  name = "inspect_gaia_attachment"
378
  description = (
379
  "Downloads and reads the official attachment associated with a GAIA "
380
- "task. Use it when the question mentions an attached file, document, "
381
- "spreadsheet, table, PDF, image, or other supplied material."
382
  )
383
  inputs = {
384
  "task_id": {
385
  "type": "string",
386
  "description": "The exact GAIA task_id supplied in the user task.",
387
- }
 
 
 
 
388
  }
389
  output_type = "string"
390
 
391
- def forward(self, task_id: str) -> str:
392
  task_id = str(task_id).strip()
393
  if not task_id:
394
  return "No task_id was supplied."
395
 
396
  try:
397
  data, filename = download_gaia_attachment(task_id)
398
- extracted = extract_attachment_text(data, filename)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  return f"Attachment filename: {filename}\n\n{extracted}"
400
  except Exception as exc:
401
- return f"Could not inspect attachment for task {task_id}: {exc}"
 
 
 
402
 
403
 
404
  class YouTubeTranscriptTool(Tool):
@@ -412,11 +953,15 @@ class YouTubeTranscriptTool(Tool):
412
  "url": {
413
  "type": "string",
414
  "description": "Full YouTube URL or the 11-character video ID.",
415
- }
 
 
 
 
416
  }
417
  output_type = "string"
418
 
419
- def forward(self, url: str) -> str:
420
  from youtube_transcript_api import YouTubeTranscriptApi
421
 
422
  value = str(url or "").strip()
@@ -438,9 +983,16 @@ class YouTubeTranscriptTool(Tool):
438
  if text:
439
  lines.append(str(text))
440
  result = " ".join(lines).strip()
441
- return result or "The video has no available transcript."
 
 
 
 
442
  except Exception as exc:
443
- return f"Could not retrieve YouTube transcript: {exc}"
 
 
 
444
 
445
 
446
  class AnalyzeGaiaImageTool(Tool):
@@ -499,8 +1051,9 @@ class AnalyzeGaiaImageTool(Tool):
499
  "type": "text",
500
  "text": (
501
  "Analyze the supplied image carefully and "
502
- "answer this task. Explain visual evidence "
503
- "briefly so another agent can verify it:\n"
 
504
  f"{question}"
505
  ),
506
  },
@@ -514,7 +1067,7 @@ class AnalyzeGaiaImageTool(Tool):
514
  }
515
  ],
516
  "temperature": 0,
517
- "max_tokens": 600,
518
  },
519
  timeout=HTTP_TIMEOUT,
520
  )
@@ -522,7 +1075,10 @@ class AnalyzeGaiaImageTool(Tool):
522
  payload = response.json()
523
  return str(payload["choices"][0]["message"]["content"]).strip()
524
  except Exception as exc:
525
- return f"Could not analyze the GAIA image: {exc}"
 
 
 
526
 
527
 
528
  class BasicAgent:
@@ -559,7 +1115,7 @@ class BasicAgent:
559
  print(f"Modelo principal selecionado: {model_id}")
560
 
561
  web_search_tool = DuckDuckGoSearchTool(
562
- max_results=8, rate_limit=1.0
563
  )
564
  web_search_tool.description = (
565
  "Searches the public web and returns result titles, URLs, and short "
@@ -581,10 +1137,14 @@ class BasicAgent:
581
  agent_tools = [
582
  web_search_tool,
583
  visit_page_tool,
 
584
  wikipedia_tool,
585
  InspectGaiaAttachmentTool(),
 
 
586
  YouTubeTranscriptTool(),
587
  AnalyzeGaiaImageTool(),
 
588
  ]
589
 
590
  # Qwen returns native tool calls. ToolCallingAgent handles that
@@ -592,7 +1152,7 @@ class BasicAgent:
592
  self.agent = ToolCallingAgent(
593
  tools=agent_tools,
594
  model=self.model,
595
- max_steps=10,
596
  planning_interval=None,
597
  description=(
598
  "Agent designed to solve GAIA benchmark questions with "
@@ -605,22 +1165,25 @@ You are an expert AI assistant solving tasks from the GAIA benchmark.
605
 
606
  TOOL ROUTING POLICY:
607
  1. web_search discovers URLs and snippets. It does not read full pages.
608
- 2. visit_webpage opens and reads one exact URL. Use it after web_search to
609
- verify articles, tables, archives, papers, and linked primary sources.
610
- 3. wikipedia_search searches English Wikipedia. Use it when Wikipedia is
611
- explicitly mentioned or for encyclopedic facts. For revision-specific,
612
- nomination, archive, or table details, verify the exact page.
613
- 4. inspect_gaia_attachment reads the official file for the supplied task_id.
614
- Call it first for attached PDFs, spreadsheets, documents, audio, or code.
615
- 5. analyze_gaia_image reads image pixels. Use it for diagrams, screenshots,
616
- chess positions, or questions that visually depend on an attached image.
617
- 6. youtube_transcript retrieves spoken subtitles. Use it when asked what a
618
- person said. It cannot answer purely visual video questions.
 
619
 
620
  Research carefully, prefer primary or official sources, and cross-check
621
  uncertain facts. A search snippet alone is insufficient when the source page
622
  can be opened. Never invent a tool, use subprocess, or use shell commands.
623
- Do not repeat nearly identical searches; change the source or method.
 
 
624
 
625
  FINAL RESPONSE POLICY:
626
  Call the final_answer tool with only the requested value. Never write the
@@ -647,15 +1210,36 @@ include reasoning, explanations, labels, Markdown, citations, or the words
647
 
648
  if task_id:
649
  attachment_name = get_task_file_name(task_id)
650
- attachment_context = (
651
- f"Official attachment: {attachment_name}. "
652
- "Use the appropriate attachment tool."
653
- if attachment_name
654
- else (
655
- "Official attachment: NONE. Do not call "
656
- "inspect_gaia_attachment or analyze_gaia_image."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
657
  )
658
- )
659
  task_context = (
660
  f"GAIA task_id: {task_id}\n"
661
  f"{attachment_context}\n\nQuestion: {question}"
@@ -755,6 +1339,18 @@ include reasoning, explanations, labels, Markdown, citations, or the words
755
 
756
  text = cls.deterministic_answer_cleanup(text)
757
 
 
 
 
 
 
 
 
 
 
 
 
 
758
  quantity_question = (
759
  "how many" in question_lower
760
  or "numeric output" in question_lower
@@ -899,41 +1495,18 @@ include reasoning, explanations, labels, Markdown, citations, or the words
899
  if not reviewer_model.lower().startswith("groq/qwen/"):
900
  reviewer_model = DEFAULT_GROQ_REVIEW_MODEL
901
  review_prompt = f"""
902
- You are the mandatory final reviewer for a GAIA exact-match answer.
903
-
904
- Task ID: {task_id or "test"}
905
- Question:
906
- {question}
907
-
908
- Candidate answer:
909
- {candidate}
910
-
911
- Review the candidate for both likely correctness and exact requested format.
912
- Preserve it unless there is a clear factual, logical, ordering, spelling, or
913
- formatting error. Never add explanations to final_answer.
914
-
915
- Formatting rules:
916
- - If asked "how many", for a count, or for a numeric output: final_answer must
917
- contain only the number, unless the question explicitly requests currency,
918
- units, decimals, or another representation.
919
- - If asked for a first name, surname, city, country, or IOC code: return only
920
- that requested value.
921
- - If asked for a list: return only the list, with the exact requested separator,
922
- ordering, capitalization, and plurality.
923
- - If asked for a quote: return only the requested spoken words.
924
- - If asked for a chess move: return only algebraic notation.
925
- - Never include labels, Markdown, citations, rationale, or "FINAL ANSWER" inside
926
- the final_answer value.
927
-
928
- Return exactly these two XML-style fields:
929
  <final_answer>exact value to submit</final_answer>
930
- <review_note>brief reason, maximum 20 words</review_note>
931
-
932
- Do not return JSON. Do not wrap the fields in Markdown or a code block.
933
  """.strip()
934
 
935
  last_error = None
936
- for attempt in range(2):
937
  try:
938
  retry_instruction = (
939
  ""
@@ -950,7 +1523,8 @@ Do not return JSON. Do not wrap the fields in Markdown or a code block.
950
  }
951
  ],
952
  temperature=0,
953
- max_tokens=350,
 
954
  )
955
  content = str(response.choices[0].message.content).strip()
956
  content = re.sub(
 
1
  import os
2
  import re
3
  import base64
4
+ import ast
5
+ import math
6
+ import operator
7
  from io import BytesIO
8
  from pathlib import Path
9
  from zipfile import ZipFile
 
24
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
25
  RESULT_COLUMNS = ["Task ID", "Question", "Submitted Answer"]
26
  HTTP_TIMEOUT = 45
27
+ MAX_EXTRACTED_CHARS = 8_000
28
  WEBPAGE_CONNECT_TIMEOUT = 8
29
  WEBPAGE_READ_TIMEOUT = 20
30
+ MAX_WEBPAGE_CHARS = 6_000
31
  DEFAULT_MAIN_MODEL = "groq/qwen/qwen3.6-27b"
32
  DEFAULT_GROQ_REVIEW_MODEL = "groq/qwen/qwen3.6-27b"
33
  TASK_FILE_CACHE = {}
34
+ ATTACHMENT_CACHE = {}
35
+ WEBPAGE_CACHE = {}
36
+
37
+
38
+ def compact_error(exc: Exception) -> str:
39
+ message = str(exc).strip()
40
+ return message or repr(exc)
41
+
42
+
43
+ def focus_text(text: str, query: str, max_chars: int = 5_000) -> str:
44
+ """Selects high-signal passages locally, before text reaches the LLM."""
45
+ text = re.sub(r"\r\n?", "\n", str(text or ""))
46
+ text = re.sub(r"[ \t]+", " ", text)
47
+ text = re.sub(r"\n{3,}", "\n\n", text).strip()
48
+ if not text or len(text) <= max_chars:
49
+ return text
50
+
51
+ stopwords = {
52
+ "about", "after", "again", "also", "article", "attached", "before",
53
+ "could", "find", "from", "have", "into", "just", "mentions", "please",
54
+ "provide", "question", "should", "that", "their", "there", "these",
55
+ "this", "under", "what", "when", "where", "which", "with", "work",
56
+ "would", "your",
57
+ }
58
+ terms = {
59
+ token.lower()
60
+ for token in re.findall(r"[A-Za-z0-9][A-Za-z0-9._-]{2,}", query or "")
61
+ if token.lower() not in stopwords
62
+ }
63
+ blocks = [
64
+ block.strip()
65
+ for block in re.split(r"\n\s*\n", text)
66
+ if block.strip()
67
+ ]
68
+ if not blocks:
69
+ return text[:max_chars]
70
+
71
+ scored = []
72
+ for index, block in enumerate(blocks):
73
+ lowered = block.lower()
74
+ hits = sum(lowered.count(term) for term in terms)
75
+ exact_bonus = 4 if query and query.lower() in lowered else 0
76
+ signal_bonus = 2 if re.search(
77
+ r"\b(acknowledg|award|grant|answer|result|total|page|pages)\b",
78
+ lowered,
79
+ ) else 0
80
+ scored.append((hits * 3 + exact_bonus + signal_bonus, index))
81
+
82
+ best_indices = [
83
+ index
84
+ for score, index in sorted(scored, reverse=True)
85
+ if score > 0
86
+ ][:10]
87
+ if not best_indices:
88
+ return (
89
+ text[: max_chars * 2 // 3]
90
+ + "\n\n[content omitted]\n\n"
91
+ + text[-max_chars // 3 :]
92
+ )
93
+
94
+ selected = {}
95
+ used = 0
96
+ for best_index in best_indices:
97
+ # Reserve space for the matching block before optional neighbors.
98
+ for index in (best_index, best_index - 1, best_index + 1):
99
+ if (
100
+ index in selected
101
+ or not 0 <= index < len(blocks)
102
+ or used >= max_chars
103
+ ):
104
+ continue
105
+ block = blocks[index]
106
+ if index != best_index and len(block) > 1_000:
107
+ block = block[:1_000]
108
+ remaining = max_chars - used
109
+ if remaining < 100:
110
+ break
111
+ selected[index] = block[:remaining]
112
+ used += len(selected[index]) + 2
113
+ return "\n\n".join(selected[index] for index in sorted(selected)).strip()
114
 
115
 
116
  def clean_filename(value: str) -> str:
 
153
  def download_gaia_attachment(task_id: str) -> tuple[bytes, str]:
154
  """Baixa um anexo pela API do curso, com fallback para o dataset oficial."""
155
  task_id = str(task_id).strip()
156
+ if task_id in ATTACHMENT_CACHE:
157
+ return ATTACHMENT_CACHE[task_id]
158
+
159
  course_url = f"{DEFAULT_API_URL}/files/{task_id}"
160
  response = requests.get(course_url, timeout=HTTP_TIMEOUT)
161
  if response.ok:
162
+ result = (response.content, filename_from_response(response, task_id))
163
+ ATTACHMENT_CACHE[task_id] = result
164
+ return result
165
  if response.status_code != 404:
166
  response.raise_for_status()
167
 
 
202
  filename=dataset_path,
203
  token=token,
204
  )
205
+ result = (Path(local_path).read_bytes(), filename)
206
+ ATTACHMENT_CACHE[task_id] = result
207
+ return result
208
  except Exception as exc:
209
  errors.append(str(exc))
210
 
 
241
  return TASK_FILE_CACHE.get(task_id, "")
242
 
243
 
244
+ def extract_attachment_text(
245
+ data: bytes, filename: str, query: str = ""
246
+ ) -> str:
247
  """Extrai conteúdo legível dos formatos mais comuns do GAIA."""
248
  suffix = Path(filename).suffix.lower()
249
 
 
297
 
298
  text = BeautifulSoup(text, "html.parser").get_text("\n")
299
  elif suffix in {".mp3", ".wav", ".flac", ".m4a", ".ogg"}:
300
+ text = (
301
+ "Audio attachment detected. Use transcribe_gaia_audio with the "
302
+ "task_id instead of inspect_gaia_attachment."
303
+ )
 
 
 
 
 
 
 
 
 
 
304
  elif suffix == ".zip":
305
  with ZipFile(BytesIO(data)) as archive:
306
  text = "Files inside ZIP:\n" + "\n".join(archive.namelist())
 
323
  text = text.strip()
324
  if not text:
325
  return f"The attachment {filename} was downloaded but contained no extractable text."
326
+ tabular = suffix in {".xlsx", ".xlsm", ".csv", ".tsv"}
327
+ if query and not tabular:
328
+ text = focus_text(text, query, MAX_EXTRACTED_CHARS)
329
+ elif len(text) > MAX_EXTRACTED_CHARS:
330
+ head_size = MAX_EXTRACTED_CHARS * 2 // 3
331
+ text = (
332
+ text[:head_size]
333
+ + "\n\n[attachment middle omitted]\n\n"
334
+ + text[-(MAX_EXTRACTED_CHARS - head_size) :]
335
+ )
336
  return text
337
 
338
 
 
349
  "url": {
350
  "type": "string",
351
  "description": "The complete HTTP or HTTPS URL to open.",
352
+ },
353
+ "query": {
354
+ "type": "string",
355
+ "description": (
356
+ "Short terms describing the exact fact to find on the page. "
357
+ "Do not repeat the full task."
358
+ ),
359
+ },
360
  }
361
  output_type = "string"
362
 
363
+ def forward(self, url: str, query: str) -> str:
364
  from markdownify import markdownify
365
 
366
  url = str(url or "").strip()
367
+ query = str(query or "").strip()
368
  if not re.match(r"^https?://", url, flags=re.I):
369
  return "Invalid URL: visit_webpage requires a full HTTP/HTTPS URL."
370
+ if url in WEBPAGE_CACHE:
371
+ return focus_text(WEBPAGE_CACHE[url], query, MAX_WEBPAGE_CHARS)
372
 
373
  headers = {
374
  "User-Agent": (
 
458
  + "\n".join(source_links[-25:])
459
  )
460
  text = re.sub(r"\n{3,}", "\n\n", text).strip()
461
+ if text:
462
+ WEBPAGE_CACHE[url] = text[:50_000]
463
+ return focus_text(text, query, MAX_WEBPAGE_CHARS)
464
+ return "The page was retrieved but contained no text."
 
 
 
 
 
465
  except Exception as exc:
466
+ errors.append(f"{target}: {compact_error(exc)}")
467
 
468
  return "Error fetching the webpage: " + " | ".join(errors)
469
 
470
 
471
+ class ReadDocumentUrlTool(Tool):
472
+ name = "read_document_url"
473
+ description = (
474
+ "Downloads and reads a PDF, DOCX, CSV, or text document from an exact "
475
+ "public URL, then returns only passages relevant to the supplied query. "
476
+ "Use it for linked papers and reports; use visit_webpage for HTML."
477
+ )
478
+ inputs = {
479
+ "url": {
480
+ "type": "string",
481
+ "description": "Direct public URL of the document.",
482
+ },
483
+ "query": {
484
+ "type": "string",
485
+ "description": "Short terms for the exact fact to find.",
486
+ },
487
+ }
488
+ output_type = "string"
489
+
490
+ def forward(self, url: str, query: str) -> str:
491
+ url = str(url or "").strip()
492
+ query = str(query or "").strip()
493
+ if not re.match(r"^https?://", url, flags=re.I):
494
+ return "Invalid document URL."
495
+
496
+ try:
497
+ response = requests.get(
498
+ url,
499
+ headers={"User-Agent": "GAIA-Course-Agent/1.0"},
500
+ timeout=(WEBPAGE_CONNECT_TIMEOUT, HTTP_TIMEOUT),
501
+ allow_redirects=True,
502
+ )
503
+ response.raise_for_status()
504
+ if len(response.content) > 25 * 1024 * 1024:
505
+ return "Document exceeds the 25 MB safety limit."
506
+
507
+ content_type = response.headers.get("content-type", "").lower()
508
+ if "text/html" in content_type:
509
+ return (
510
+ "This URL returned HTML. Use visit_webpage with the same "
511
+ "URL and a short query."
512
+ )
513
+ filename = filename_from_response(response, "web_document")
514
+ if not Path(filename).suffix:
515
+ from urllib.parse import urlparse
516
+
517
+ filename = Path(urlparse(response.url).path).name or filename
518
+ extracted = extract_attachment_text(
519
+ response.content, filename, query=query
520
+ )
521
+ return f"Document: {filename}\n\n{extracted}"
522
+ except Exception as exc:
523
+ return f"Could not read document URL: {compact_error(exc)}"
524
+
525
+
526
+ class TranscribeGaiaAudioTool(Tool):
527
+ name = "transcribe_gaia_audio"
528
+ description = (
529
+ "Downloads and transcribes the official GAIA audio attachment. Use it "
530
+ "first whenever the attachment is MP3, WAV, FLAC, M4A, OGG, or WEBM. "
531
+ "Speech transcription uses the separate audio quota, not chat tokens."
532
+ )
533
+ inputs = {
534
+ "task_id": {
535
+ "type": "string",
536
+ "description": "Exact GAIA task_id associated with the audio.",
537
+ }
538
+ }
539
+ output_type = "string"
540
+
541
+ def forward(self, task_id: str) -> str:
542
+ task_id = str(task_id or "").strip()
543
+ try:
544
+ data, filename = download_gaia_attachment(task_id)
545
+ suffix = Path(filename).suffix.lower()
546
+ if suffix not in {
547
+ ".mp3", ".wav", ".flac", ".m4a", ".ogg", ".webm", ".mp4"
548
+ }:
549
+ return (
550
+ f"Attachment {filename} is not an audio file. Use "
551
+ "inspect_gaia_attachment."
552
+ )
553
+
554
+ errors = []
555
+ groq_api_key = os.getenv("GROQ_API_KEY")
556
+ if groq_api_key:
557
+ mime_types = {
558
+ ".mp3": "audio/mpeg",
559
+ ".wav": "audio/wav",
560
+ ".flac": "audio/flac",
561
+ ".m4a": "audio/mp4",
562
+ ".ogg": "audio/ogg",
563
+ ".webm": "audio/webm",
564
+ ".mp4": "video/mp4",
565
+ }
566
+ try:
567
+ response = requests.post(
568
+ "https://api.groq.com/openai/v1/audio/transcriptions",
569
+ headers={"Authorization": f"Bearer {groq_api_key}"},
570
+ files={
571
+ "file": (
572
+ filename,
573
+ data,
574
+ mime_types.get(suffix, "application/octet-stream"),
575
+ )
576
+ },
577
+ data={
578
+ "model": "whisper-large-v3-turbo",
579
+ "response_format": "json",
580
+ "temperature": "0",
581
+ "language": "en",
582
+ },
583
+ timeout=(WEBPAGE_CONNECT_TIMEOUT, 90),
584
+ )
585
+ response.raise_for_status()
586
+ transcript = str(response.json().get("text") or "").strip()
587
+ if transcript:
588
+ return (
589
+ f"Audio transcript ({filename}):\n"
590
+ f"{transcript[:6_000]}"
591
+ )
592
+ errors.append("Groq returned an empty transcript.")
593
+ except Exception as exc:
594
+ detail = compact_error(exc)
595
+ if "response" in locals() and response is not None:
596
+ detail += f" Response: {response.text[:500]}"
597
+ errors.append(f"Groq speech-to-text: {detail}")
598
+
599
+ hf_token = os.getenv("HF_TOKEN")
600
+ if hf_token:
601
+ try:
602
+ from huggingface_hub import InferenceClient
603
+
604
+ client = InferenceClient(api_key=hf_token, provider="auto")
605
+ transcript_result = client.automatic_speech_recognition(
606
+ data,
607
+ model=os.getenv(
608
+ "GAIA_ASR_MODEL", "openai/whisper-large-v3"
609
+ ),
610
+ )
611
+ transcript = str(
612
+ getattr(transcript_result, "text", transcript_result)
613
+ ).strip()
614
+ if transcript:
615
+ return (
616
+ f"Audio transcript ({filename}):\n"
617
+ f"{transcript[:6_000]}"
618
+ )
619
+ errors.append("Hugging Face returned an empty transcript.")
620
+ except Exception as exc:
621
+ errors.append(
622
+ f"Hugging Face speech-to-text: {compact_error(exc)}"
623
+ )
624
+
625
+ return "Audio transcription failed. " + " | ".join(errors)
626
+ except Exception as exc:
627
+ return (
628
+ f"Could not transcribe audio for task {task_id}: "
629
+ f"{compact_error(exc)}"
630
+ )
631
+
632
+
633
+ class CalculatorTool(Tool):
634
+ name = "calculator"
635
+ description = (
636
+ "Evaluates arithmetic locally without an LLM. Supports +, -, *, /, //, "
637
+ "**, %, parentheses, pi, e, sqrt, log, exp, sin, cos, tan, abs, and round."
638
+ )
639
+ inputs = {
640
+ "expression": {
641
+ "type": "string",
642
+ "description": "Arithmetic expression to evaluate.",
643
+ }
644
+ }
645
+ output_type = "string"
646
+
647
+ def forward(self, expression: str) -> str:
648
+ binary_ops = {
649
+ ast.Add: operator.add,
650
+ ast.Sub: operator.sub,
651
+ ast.Mult: operator.mul,
652
+ ast.Div: operator.truediv,
653
+ ast.FloorDiv: operator.floordiv,
654
+ ast.Mod: operator.mod,
655
+ ast.Pow: operator.pow,
656
+ }
657
+ unary_ops = {ast.UAdd: operator.pos, ast.USub: operator.neg}
658
+ functions = {
659
+ "abs": abs,
660
+ "round": round,
661
+ "sqrt": math.sqrt,
662
+ "log": math.log,
663
+ "exp": math.exp,
664
+ "sin": math.sin,
665
+ "cos": math.cos,
666
+ "tan": math.tan,
667
+ }
668
+ constants = {"pi": math.pi, "e": math.e}
669
+
670
+ def evaluate(node):
671
+ if isinstance(node, ast.Expression):
672
+ return evaluate(node.body)
673
+ if isinstance(node, ast.Constant) and isinstance(
674
+ node.value, (int, float)
675
+ ):
676
+ return node.value
677
+ if isinstance(node, ast.BinOp) and type(node.op) in binary_ops:
678
+ return binary_ops[type(node.op)](
679
+ evaluate(node.left), evaluate(node.right)
680
+ )
681
+ if isinstance(node, ast.UnaryOp) and type(node.op) in unary_ops:
682
+ return unary_ops[type(node.op)](evaluate(node.operand))
683
+ if isinstance(node, ast.Name) and node.id in constants:
684
+ return constants[node.id]
685
+ if (
686
+ isinstance(node, ast.Call)
687
+ and isinstance(node.func, ast.Name)
688
+ and node.func.id in functions
689
+ and not node.keywords
690
+ ):
691
+ return functions[node.func.id](
692
+ *(evaluate(argument) for argument in node.args)
693
+ )
694
+ raise ValueError("Unsupported expression.")
695
+
696
+ try:
697
+ parsed = ast.parse(str(expression), mode="eval")
698
+ result = evaluate(parsed)
699
+ return str(result)
700
+ except Exception as exc:
701
+ return f"Calculation failed: {compact_error(exc)}"
702
+
703
+
704
+ class QueryGaiaSpreadsheetTool(Tool):
705
+ name = "query_gaia_spreadsheet"
706
+ description = (
707
+ "Analyzes an attached XLSX, XLSM, CSV, or TSV locally. Use operation "
708
+ "'describe' first, then sum, mean, min, max, count, unique, or rows. "
709
+ "This avoids sending the entire spreadsheet to the language model."
710
+ )
711
+ inputs = {
712
+ "task_id": {
713
+ "type": "string",
714
+ "description": "Exact GAIA task_id for the spreadsheet.",
715
+ },
716
+ "operation": {
717
+ "type": "string",
718
+ "description": "describe, sum, mean, min, max, count, unique, or rows.",
719
+ },
720
+ "sheet": {
721
+ "type": "string",
722
+ "description": "Sheet name, or an empty string for the first sheet.",
723
+ },
724
+ "column": {
725
+ "type": "string",
726
+ "description": "Target column, or empty for describe/count rows.",
727
+ },
728
+ "filters": {
729
+ "type": "string",
730
+ "description": (
731
+ "Optional exact filters as column=value;column=value. "
732
+ "Use an empty string for no filter."
733
+ ),
734
+ },
735
+ }
736
+ output_type = "string"
737
+
738
+ def forward(
739
+ self,
740
+ task_id: str,
741
+ operation: str,
742
+ sheet: str,
743
+ column: str,
744
+ filters: str,
745
+ ) -> str:
746
+ try:
747
+ data, filename = download_gaia_attachment(str(task_id).strip())
748
+ suffix = Path(filename).suffix.lower()
749
+ if suffix in {".xlsx", ".xlsm"}:
750
+ tables = pd.read_excel(BytesIO(data), sheet_name=None)
751
+ elif suffix in {".csv", ".tsv"}:
752
+ separator = "\t" if suffix == ".tsv" else ","
753
+ tables = {
754
+ "data": pd.read_csv(
755
+ BytesIO(data),
756
+ sep=separator,
757
+ encoding_errors="replace",
758
+ )
759
+ }
760
+ else:
761
+ return f"Attachment {filename} is not a supported spreadsheet."
762
+
763
+ requested_sheet = str(sheet or "").strip()
764
+ sheet_name = next(iter(tables))
765
+ if requested_sheet:
766
+ matching_sheet = next(
767
+ (
768
+ name
769
+ for name in tables
770
+ if str(name).lower() == requested_sheet.lower()
771
+ ),
772
+ None,
773
+ )
774
+ if matching_sheet is None:
775
+ return (
776
+ f"Unknown sheet {requested_sheet}. Available: "
777
+ + ", ".join(map(str, tables))
778
+ )
779
+ sheet_name = matching_sheet
780
+
781
+ frame = tables[sheet_name].copy()
782
+ frame.columns = [str(value).strip() for value in frame.columns]
783
+ requested_column = str(column or "").strip()
784
+
785
+ for filter_expression in str(filters or "").split(";"):
786
+ filter_expression = filter_expression.strip()
787
+ if not filter_expression:
788
+ continue
789
+ if "=" not in filter_expression:
790
+ return f"Invalid filter: {filter_expression}"
791
+ filter_column, filter_value = (
792
+ part.strip() for part in filter_expression.split("=", 1)
793
+ )
794
+ actual_filter_column = next(
795
+ (
796
+ name
797
+ for name in frame.columns
798
+ if name.lower() == filter_column.lower()
799
+ ),
800
+ None,
801
+ )
802
+ if actual_filter_column is None:
803
+ return (
804
+ f"Unknown filter column {filter_column}. Columns: "
805
+ + ", ".join(frame.columns)
806
+ )
807
+ numeric_value = pd.to_numeric(
808
+ pd.Series([filter_value]), errors="coerce"
809
+ ).iloc[0]
810
+ numeric_column = pd.to_numeric(
811
+ frame[actual_filter_column], errors="coerce"
812
+ )
813
+ if pd.notna(numeric_value) and numeric_column.notna().any():
814
+ frame = frame[numeric_column == numeric_value]
815
+ else:
816
+ frame = frame[
817
+ frame[actual_filter_column]
818
+ .astype(str)
819
+ .str.strip()
820
+ .str.casefold()
821
+ == filter_value.casefold()
822
+ ]
823
+
824
+ operation = str(operation or "describe").strip().lower()
825
+ if operation == "describe":
826
+ preview = frame.head(5).to_csv(index=False)
827
+ return (
828
+ f"Workbook: {filename}\n"
829
+ f"Sheets: {', '.join(map(str, tables))}\n"
830
+ f"Selected sheet: {sheet_name}\n"
831
+ f"Rows: {len(frame)}; Columns: {len(frame.columns)}\n"
832
+ f"Column names: {', '.join(frame.columns)}\n"
833
+ f"First rows:\n{preview[:2_500]}"
834
+ )
835
+
836
+ if requested_column:
837
+ actual_column = next(
838
+ (
839
+ name
840
+ for name in frame.columns
841
+ if name.lower() == requested_column.lower()
842
+ ),
843
+ None,
844
+ )
845
+ if actual_column is None:
846
+ return (
847
+ f"Unknown target column {requested_column}. Columns: "
848
+ + ", ".join(frame.columns)
849
+ )
850
+ else:
851
+ actual_column = ""
852
+
853
+ if operation == "count":
854
+ result = (
855
+ int(frame[actual_column].notna().sum())
856
+ if actual_column
857
+ else len(frame)
858
+ )
859
+ elif operation in {"sum", "mean", "min", "max"}:
860
+ if not actual_column:
861
+ return f"Operation {operation} requires a target column."
862
+ series = pd.to_numeric(frame[actual_column], errors="coerce").dropna()
863
+ if series.empty:
864
+ return f"Column {actual_column} has no numeric values."
865
+ result = getattr(series, operation)()
866
+ elif operation == "unique":
867
+ if not actual_column:
868
+ return "Operation unique requires a target column."
869
+ values = frame[actual_column].dropna().astype(str).unique().tolist()
870
+ return ", ".join(values[:100])
871
+ elif operation == "rows":
872
+ columns = [actual_column] if actual_column else list(frame.columns)
873
+ return frame[columns].head(30).to_csv(index=False)[:4_000]
874
+ else:
875
+ return (
876
+ "Unknown operation. Use describe, sum, mean, min, max, "
877
+ "count, unique, or rows."
878
+ )
879
+
880
+ if hasattr(result, "item"):
881
+ result = result.item()
882
+ return (
883
+ f"Operation: {operation}; sheet: {sheet_name}; "
884
+ f"rows matched: {len(frame)}; result: {result}"
885
+ )
886
+ except Exception as exc:
887
+ return f"Spreadsheet analysis failed: {compact_error(exc)}"
888
+
889
+
890
  class InspectGaiaAttachmentTool(Tool):
891
  name = "inspect_gaia_attachment"
892
  description = (
893
  "Downloads and reads the official attachment associated with a GAIA "
894
+ "task. Use it for PDF, DOCX, text, code, or ZIP files. Do not use it "
895
+ "for audio, images, or spreadsheets; those have specialized tools."
896
  )
897
  inputs = {
898
  "task_id": {
899
  "type": "string",
900
  "description": "The exact GAIA task_id supplied in the user task.",
901
+ },
902
+ "query": {
903
+ "type": "string",
904
+ "description": "Short terms for the exact fact to extract.",
905
+ },
906
  }
907
  output_type = "string"
908
 
909
+ def forward(self, task_id: str, query: str) -> str:
910
  task_id = str(task_id).strip()
911
  if not task_id:
912
  return "No task_id was supplied."
913
 
914
  try:
915
  data, filename = download_gaia_attachment(task_id)
916
+ suffix = Path(filename).suffix.lower()
917
+ if suffix in {
918
+ ".mp3", ".wav", ".flac", ".m4a", ".ogg", ".webm", ".mp4"
919
+ }:
920
+ return (
921
+ f"Attachment {filename} is audio. Call "
922
+ "transcribe_gaia_audio with this task_id."
923
+ )
924
+ if suffix in {".png", ".jpg", ".jpeg", ".webp", ".gif"}:
925
+ return (
926
+ f"Attachment {filename} is an image. Call "
927
+ "analyze_gaia_image with this task_id and question."
928
+ )
929
+ if suffix in {".xlsx", ".xlsm", ".csv", ".tsv"}:
930
+ return (
931
+ f"Attachment {filename} is tabular. Call "
932
+ "query_gaia_spreadsheet with operation='describe' first."
933
+ )
934
+ extracted = extract_attachment_text(
935
+ data, filename, query=str(query or "")
936
+ )
937
  return f"Attachment filename: {filename}\n\n{extracted}"
938
  except Exception as exc:
939
+ return (
940
+ f"Could not inspect attachment for task {task_id}: "
941
+ f"{compact_error(exc)}"
942
+ )
943
 
944
 
945
  class YouTubeTranscriptTool(Tool):
 
953
  "url": {
954
  "type": "string",
955
  "description": "Full YouTube URL or the 11-character video ID.",
956
+ },
957
+ "query": {
958
+ "type": "string",
959
+ "description": "Short terms describing the spoken fact or quote.",
960
+ },
961
  }
962
  output_type = "string"
963
 
964
+ def forward(self, url: str, query: str) -> str:
965
  from youtube_transcript_api import YouTubeTranscriptApi
966
 
967
  value = str(url or "").strip()
 
983
  if text:
984
  lines.append(str(text))
985
  result = " ".join(lines).strip()
986
+ return (
987
+ focus_text(result, str(query or ""), 5_000)
988
+ if result
989
+ else "The video has no available transcript."
990
+ )
991
  except Exception as exc:
992
+ return (
993
+ "Could not retrieve YouTube transcript: "
994
+ f"{compact_error(exc)}"
995
+ )
996
 
997
 
998
  class AnalyzeGaiaImageTool(Tool):
 
1051
  "type": "text",
1052
  "text": (
1053
  "Analyze the supplied image carefully and "
1054
+ "answer this task. Return the likely exact "
1055
+ "answer plus at most one short evidence "
1056
+ "sentence:\n"
1057
  f"{question}"
1058
  ),
1059
  },
 
1067
  }
1068
  ],
1069
  "temperature": 0,
1070
+ "max_tokens": 300,
1071
  },
1072
  timeout=HTTP_TIMEOUT,
1073
  )
 
1075
  payload = response.json()
1076
  return str(payload["choices"][0]["message"]["content"]).strip()
1077
  except Exception as exc:
1078
+ return (
1079
+ "Could not analyze the GAIA image: "
1080
+ f"{compact_error(exc)}"
1081
+ )
1082
 
1083
 
1084
  class BasicAgent:
 
1115
  print(f"Modelo principal selecionado: {model_id}")
1116
 
1117
  web_search_tool = DuckDuckGoSearchTool(
1118
+ max_results=5, rate_limit=1.0
1119
  )
1120
  web_search_tool.description = (
1121
  "Searches the public web and returns result titles, URLs, and short "
 
1137
  agent_tools = [
1138
  web_search_tool,
1139
  visit_page_tool,
1140
+ ReadDocumentUrlTool(),
1141
  wikipedia_tool,
1142
  InspectGaiaAttachmentTool(),
1143
+ QueryGaiaSpreadsheetTool(),
1144
+ TranscribeGaiaAudioTool(),
1145
  YouTubeTranscriptTool(),
1146
  AnalyzeGaiaImageTool(),
1147
+ CalculatorTool(),
1148
  ]
1149
 
1150
  # Qwen returns native tool calls. ToolCallingAgent handles that
 
1152
  self.agent = ToolCallingAgent(
1153
  tools=agent_tools,
1154
  model=self.model,
1155
+ max_steps=7,
1156
  planning_interval=None,
1157
  description=(
1158
  "Agent designed to solve GAIA benchmark questions with "
 
1165
 
1166
  TOOL ROUTING POLICY:
1167
  1. web_search discovers URLs and snippets. It does not read full pages.
1168
+ 2. visit_webpage reads focused passages from HTML; always provide short query
1169
+ terms. read_document_url reads linked PDFs or documents by query.
1170
+ 3. transcribe_gaia_audio is the only tool for attached audio. Call it first
1171
+ for MP3, WAV, FLAC, M4A, OGG, WEBM, or MP4.
1172
+ 4. inspect_gaia_attachment handles attached PDF, DOCX, text, code, or ZIP.
1173
+ Pass short target terms. For XLSX/CSV use
1174
+ query_gaia_spreadsheet instead: describe columns, then run one calculation.
1175
+ Never send a whole table to the model.
1176
+ 5. analyze_gaia_image handles attached images. youtube_transcript handles
1177
+ spoken YouTube content. calculator evaluates arithmetic locally.
1178
+ 6. wikipedia_search is for Wikipedia or encyclopedic facts. Verify historical,
1179
+ nomination, revision, or archive details with an exact webpage.
1180
 
1181
  Research carefully, prefer primary or official sources, and cross-check
1182
  uncertain facts. A search snippet alone is insufficient when the source page
1183
  can be opened. Never invent a tool, use subprocess, or use shell commands.
1184
+ Do not repeat nearly identical searches. Stop as soon as primary evidence
1185
+ answers the exact question. Never pass an entire task as a webpage query; use
1186
+ only names, identifiers, and the target fact.
1187
 
1188
  FINAL RESPONSE POLICY:
1189
  Call the final_answer tool with only the requested value. Never write the
 
1210
 
1211
  if task_id:
1212
  attachment_name = get_task_file_name(task_id)
1213
+ attachment_suffix = Path(attachment_name).suffix.lower()
1214
+ if attachment_suffix in {
1215
+ ".mp3", ".wav", ".flac", ".m4a", ".ogg", ".webm", ".mp4"
1216
+ }:
1217
+ attachment_context = (
1218
+ f"Official attachment: {attachment_name}. Call "
1219
+ "transcribe_gaia_audio first."
1220
+ )
1221
+ elif attachment_suffix in {
1222
+ ".png", ".jpg", ".jpeg", ".webp", ".gif"
1223
+ }:
1224
+ attachment_context = (
1225
+ f"Official attachment: {attachment_name}. Call "
1226
+ "analyze_gaia_image with this task_id and question."
1227
+ )
1228
+ elif attachment_suffix in {".xlsx", ".xlsm", ".csv", ".tsv"}:
1229
+ attachment_context = (
1230
+ f"Official attachment: {attachment_name}. Call "
1231
+ "query_gaia_spreadsheet with operation='describe' first."
1232
+ )
1233
+ elif attachment_name:
1234
+ attachment_context = (
1235
+ f"Official attachment: {attachment_name}. Call "
1236
+ "inspect_gaia_attachment with this task_id and short target terms."
1237
+ )
1238
+ else:
1239
+ attachment_context = (
1240
+ "Official attachment: NONE. Do not call any GAIA "
1241
+ "attachment tool."
1242
  )
 
1243
  task_context = (
1244
  f"GAIA task_id: {task_id}\n"
1245
  f"{attachment_context}\n\nQuestion: {question}"
 
1339
 
1340
  text = cls.deterministic_answer_cleanup(text)
1341
 
1342
+ if "page numbers" in question_lower or "page number" in question_lower:
1343
+ page_groups = re.findall(
1344
+ r"\b(?:pages?|pp\.?)\s*(?:are|is|:|-)?\s*"
1345
+ r"(\d+(?:(?:\s*,\s*(?:and\s+)?|\s+and\s+|-)\d+)*)",
1346
+ text,
1347
+ flags=re.I,
1348
+ )
1349
+ if page_groups:
1350
+ pages = [int(value) for value in re.findall(r"\d+", page_groups[-1])]
1351
+ if pages:
1352
+ return ", ".join(str(value) for value in sorted(set(pages)))
1353
+
1354
  quantity_question = (
1355
  "how many" in question_lower
1356
  or "numeric output" in question_lower
 
1495
  if not reviewer_model.lower().startswith("groq/qwen/"):
1496
  reviewer_model = DEFAULT_GROQ_REVIEW_MODEL
1497
  review_prompt = f"""
1498
+ Review this GAIA exact-match candidate. Preserve it unless clearly wrong.
1499
+ Return only the requested value: no explanation, label, Markdown, or citation.
1500
+ Respect requested number, currency, name, list separator/order, quote, or chess
1501
+ notation format.
1502
+ Question: {question}
1503
+ Candidate: {candidate}
1504
+ Return:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1505
  <final_answer>exact value to submit</final_answer>
 
 
 
1506
  """.strip()
1507
 
1508
  last_error = None
1509
+ for attempt in range(1):
1510
  try:
1511
  retry_instruction = (
1512
  ""
 
1523
  }
1524
  ],
1525
  temperature=0,
1526
+ max_tokens=120,
1527
+ reasoning_effort="none",
1528
  )
1529
  content = str(response.choices[0].message.content).strip()
1530
  content = re.sub(