czrrr commited on
Commit
3c69123
·
verified ·
1 Parent(s): 19b57a8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +363 -7
app.py CHANGED
@@ -35,6 +35,7 @@ ATTACHMENT_CACHE = {}
35
  WEBPAGE_CACHE = {}
36
  SEARCH_CACHE = {}
37
  SEARCH_LOCK = threading.Lock()
 
38
 
39
 
40
  def compact_error(exc: Exception) -> str:
@@ -42,6 +43,67 @@ def compact_error(exc: Exception) -> str:
42
  return message or repr(exc)
43
 
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  def focus_text(text: str, query: str, max_chars: int = 5_000) -> str:
46
  """Selects high-signal passages locally, before text reaches the LLM."""
47
  text = re.sub(r"\r\n?", "\n", str(text or ""))
@@ -1464,6 +1526,8 @@ class BasicAgent:
1464
  AnalyzeGaiaImageTool(),
1465
  CalculatorTool(),
1466
  ]
 
 
1467
 
1468
  # GLM returns native tool calls. ToolCallingAgent handles that
1469
  # structured format without parsing generated Python code.
@@ -1502,6 +1566,9 @@ TOOL ROUTING POLICY:
1502
  Research carefully, prefer primary or official sources, and cross-check
1503
  uncertain facts. A search snippet alone is insufficient when the source page
1504
  can be opened. Never invent a tool, use subprocess, or use shell commands.
 
 
 
1505
  Do not repeat nearly identical searches. Stop as soon as primary evidence
1506
  answers the exact question. Never pass an entire task as a webpage query; use
1507
  only names, identifiers, and the target fact. Use at most four tool calls, then
@@ -1558,7 +1625,232 @@ include reasoning, explanations, labels, Markdown, citations, or the words
1558
  )
1559
  print(f"Fallback Gemini habilitado: {fallback_name}")
1560
 
1561
- def _run_with_failover(self, task_context: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1562
  try:
1563
  return self.agent.run(task_context, reset=True), False
1564
  except Exception as exc:
@@ -1580,10 +1872,20 @@ include reasoning, explanations, labels, Markdown, citations, or the words
1580
  )
1581
  )
1582
  if retryable_provider_error and self.gemini_fallback_agent:
 
1583
  print(
1584
  "Cerebras indisponível; executando a questão com o "
1585
  f"fallback Gemini. Motivo: {compact_error(exc)}"
1586
  )
 
 
 
 
 
 
 
 
 
1587
  return (
1588
  self.gemini_fallback_agent.run(task_context, reset=True),
1589
  True,
@@ -1601,6 +1903,8 @@ include reasoning, explanations, labels, Markdown, citations, or the words
1601
  if not question:
1602
  raise ValueError("Digite uma pergunta para testar o agente.")
1603
  SEARCH_CACHE.clear()
 
 
1604
 
1605
  if task_id:
1606
  attachment_name = get_task_file_name(task_id)
@@ -1640,8 +1944,26 @@ include reasoning, explanations, labels, Markdown, citations, or the words
1640
  )
1641
  else:
1642
  task_context = question
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1643
  try:
1644
- result, used_gemini_fallback = self._run_with_failover(task_context)
 
 
 
 
1645
  except Exception as exc:
1646
  error_text = str(exc)
1647
  if (
@@ -1672,6 +1994,9 @@ include reasoning, explanations, labels, Markdown, citations, or the words
1672
  ))
1673
  )
1674
  )
 
 
 
1675
  if (
1676
  invalid_candidate
1677
  and self.gemini_fallback_agent
@@ -1681,9 +2006,21 @@ include reasoning, explanations, labels, Markdown, citations, or the words
1681
  "Cerebras terminou sem resposta final; repetindo a questão "
1682
  "com o fallback Gemini."
1683
  )
1684
- result = self.gemini_fallback_agent.run(task_context, reset=True)
1685
  used_gemini_fallback = True
1686
- candidate = self.enforce_direct_answer(question, str(result))
 
 
 
 
 
 
 
 
 
 
 
 
1687
  invalid_candidate = (
1688
  not candidate
1689
  or candidate.strip().lower() in {"none", "null"}
@@ -1700,6 +2037,9 @@ include reasoning, explanations, labels, Markdown, citations, or the words
1700
  ))
1701
  )
1702
  )
 
 
 
1703
  if invalid_candidate:
1704
  raise RuntimeError(
1705
  "O agente esgotou as etapas sem produzir uma resposta final. "
@@ -1712,6 +2052,7 @@ include reasoning, explanations, labels, Markdown, citations, or the words
1712
  question=question,
1713
  candidate=candidate,
1714
  task_id=task_id,
 
1715
  )
1716
 
1717
  @staticmethod
@@ -1957,7 +2298,11 @@ include reasoning, explanations, labels, Markdown, citations, or the words
1957
  return self.deterministic_answer_cleanup(cleaned)
1958
 
1959
  def review_answer_with_gemini(
1960
- self, question: str, candidate: str, task_id: str | None = None
 
 
 
 
1961
  ) -> str:
1962
  """Revisa com Gemini e sempre preserva a resposta primária se falhar."""
1963
  gemini_api_key = os.getenv("GEMINI_API_KEY")
@@ -1977,9 +2322,13 @@ Review this GAIA exact-match candidate. Preserve it unless a correction is
1977
  clearly necessary. The final_answer must contain only the requested value,
1978
  without explanation, label, Markdown, or citation. Respect requested numeric,
1979
  currency, name, list separator/order, quote, or chess notation formats.
 
 
1980
  Task ID: {task_id or "test"}
1981
  Question: {question}
1982
  Candidate: {candidate}
 
 
1983
  """.strip()
1984
 
1985
  try:
@@ -2331,7 +2680,10 @@ def run_all_evaluation_questions(
2331
  try:
2332
  updated[task_id] = agent(question, task_id)
2333
  except Exception as exc:
2334
- updated[task_id] = f"ERROR: {exc}"
 
 
 
2335
  failures += 1
2336
 
2337
  dataframe = review_dataframe(questions, updated)
@@ -2433,7 +2785,11 @@ def run_agent_only(profile: gr.OAuthProfile | None):
2433
  try:
2434
  answer = agent(question, task_id)
2435
  except Exception as exc:
2436
- answer = f"ERROR: {exc}"
 
 
 
 
2437
 
2438
  results.append(
2439
  {
 
35
  WEBPAGE_CACHE = {}
36
  SEARCH_CACHE = {}
37
  SEARCH_LOCK = threading.Lock()
38
+ RUN_STATE = threading.local()
39
 
40
 
41
  def compact_error(exc: Exception) -> str:
 
43
  return message or repr(exc)
44
 
45
 
46
+ def reset_run_evidence():
47
+ RUN_STATE.evidence = []
48
+
49
+
50
+ def record_run_evidence(tool_name: str, value) -> str:
51
+ text = str(value or "").strip()
52
+ if text:
53
+ evidence = getattr(RUN_STATE, "evidence", None)
54
+ if evidence is None:
55
+ evidence = []
56
+ RUN_STATE.evidence = evidence
57
+ evidence.append(
58
+ {
59
+ "tool": str(tool_name),
60
+ "text": text[:5_000],
61
+ }
62
+ )
63
+ return value
64
+
65
+
66
+ def current_run_evidence(max_chars: int = 14_000) -> str:
67
+ items = getattr(RUN_STATE, "evidence", []) or []
68
+ blocks = []
69
+ used = 0
70
+ for index, item in enumerate(items, start=1):
71
+ block = f"[{index}. {item['tool']}]\n{item['text']}"
72
+ remaining = max_chars - used
73
+ if remaining <= 100:
74
+ break
75
+ blocks.append(block[:remaining])
76
+ used += len(blocks[-1]) + 2
77
+ return "\n\n".join(blocks)
78
+
79
+
80
+ def instrument_tool(tool: Tool) -> Tool:
81
+ """Records tool observations for deterministic review and failover."""
82
+ original_forward = tool.forward
83
+
84
+ def recorded_forward(*args, **kwargs):
85
+ result = original_forward(*args, **kwargs)
86
+ return record_run_evidence(tool.name, result)
87
+
88
+ tool.forward = recorded_forward
89
+ return tool
90
+
91
+
92
+ def concise_query_from_question(question: str, max_terms: int = 14) -> str:
93
+ stopwords = {
94
+ "about", "answer", "attached", "could", "from", "give", "have",
95
+ "into", "just", "number", "please", "provide", "question", "same",
96
+ "that", "their", "there", "these", "this", "under", "what", "when",
97
+ "where", "which", "with", "would", "your",
98
+ }
99
+ terms = [
100
+ token
101
+ for token in re.findall(r"[A-Za-z0-9][A-Za-z0-9'._-]{2,}", question or "")
102
+ if token.casefold() not in stopwords
103
+ ]
104
+ return " ".join(terms[:max_terms])
105
+
106
+
107
  def focus_text(text: str, query: str, max_chars: int = 5_000) -> str:
108
  """Selects high-signal passages locally, before text reaches the LLM."""
109
  text = re.sub(r"\r\n?", "\n", str(text or ""))
 
1526
  AnalyzeGaiaImageTool(),
1527
  CalculatorTool(),
1528
  ]
1529
+ agent_tools = [instrument_tool(tool) for tool in agent_tools]
1530
+ self.tools_by_name = {tool.name: tool for tool in agent_tools}
1531
 
1532
  # GLM returns native tool calls. ToolCallingAgent handles that
1533
  # structured format without parsing generated Python code.
 
1566
  Research carefully, prefer primary or official sources, and cross-check
1567
  uncertain facts. A search snippet alone is insufficient when the source page
1568
  can be opened. Never invent a tool, use subprocess, or use shell commands.
1569
+ When CONTROLLER-PRECOLLECTED EVIDENCE is present, use it first and do not
1570
+ repeat its exact tool call. If it directly answers the task, immediately call
1571
+ final_answer instead of researching again.
1572
  Do not repeat nearly identical searches. Stop as soon as primary evidence
1573
  answers the exact question. Never pass an entire task as a webpage query; use
1574
  only names, identifiers, and the target fact. Use at most four tool calls, then
 
1625
  )
1626
  print(f"Fallback Gemini habilitado: {fallback_name}")
1627
 
1628
+ def _precollect_deterministic_evidence(
1629
+ self,
1630
+ question: str,
1631
+ task_id: str | None,
1632
+ attachment_name: str = "",
1633
+ ) -> str:
1634
+ """Runs mandatory/specialized tools by rule before an LLM can choose."""
1635
+ suffix = Path(attachment_name).suffix.lower()
1636
+ query = concise_query_from_question(question)
1637
+ route = ""
1638
+
1639
+ if task_id and suffix in {
1640
+ ".mp3", ".wav", ".flac", ".m4a", ".ogg", ".webm", ".mp4"
1641
+ }:
1642
+ self.tools_by_name["transcribe_gaia_audio"].forward(task_id=task_id)
1643
+ route = "audio attachment -> transcribe_gaia_audio"
1644
+ elif task_id and suffix in {
1645
+ ".png", ".jpg", ".jpeg", ".webp", ".gif"
1646
+ }:
1647
+ self.tools_by_name["analyze_gaia_image"].forward(
1648
+ task_id=task_id,
1649
+ question=question,
1650
+ )
1651
+ route = "image attachment -> analyze_gaia_image"
1652
+ elif task_id and suffix in {".xlsx", ".xlsm", ".csv", ".tsv"}:
1653
+ self.tools_by_name["query_gaia_spreadsheet"].forward(
1654
+ task_id=task_id,
1655
+ operation="describe",
1656
+ sheet="",
1657
+ column="",
1658
+ filters="",
1659
+ )
1660
+ route = "spreadsheet attachment -> query_gaia_spreadsheet(describe)"
1661
+ elif task_id and attachment_name:
1662
+ self.tools_by_name["inspect_gaia_attachment"].forward(
1663
+ task_id=task_id,
1664
+ query=query,
1665
+ )
1666
+ route = "document attachment -> inspect_gaia_attachment"
1667
+
1668
+ youtube_match = re.search(
1669
+ r"https?://(?:www\.)?(?:youtube\.com/watch\?v=|youtu\.be/)"
1670
+ r"[A-Za-z0-9_-]{11}",
1671
+ question,
1672
+ flags=re.I,
1673
+ )
1674
+ spoken_cues = (
1675
+ "say", "said", "says", "speak", "spoken", "quote", "transcript",
1676
+ "according to the video", "what does", "what did",
1677
+ )
1678
+ if (
1679
+ not route
1680
+ and youtube_match
1681
+ and any(cue in question.lower() for cue in spoken_cues)
1682
+ ):
1683
+ self.tools_by_name["youtube_transcript"].forward(
1684
+ url=youtube_match.group(0),
1685
+ query=query,
1686
+ )
1687
+ route = "spoken YouTube question -> youtube_transcript"
1688
+
1689
+ team_aliases = {
1690
+ "yankee": "Yankees", "red sox": "Red Sox",
1691
+ "oriole": "Orioles", "ray": "Rays", "blue jay": "Blue Jays",
1692
+ "white sox": "White Sox", "guardian": "Guardians",
1693
+ "cleveland indian": "Indians", "tiger": "Tigers",
1694
+ "royal": "Royals", "twin": "Twins", "astro": "Astros",
1695
+ "angel": "Angels", "athletic": "Athletics", "mariner": "Mariners",
1696
+ "ranger": "Rangers", "brave": "Braves", "marlin": "Marlins",
1697
+ "met": "Mets", "phillie": "Phillies", "national": "Nationals",
1698
+ "cub": "Cubs", "red": "Reds", "brewer": "Brewers",
1699
+ "pirate": "Pirates", "cardinal": "Cardinals",
1700
+ "diamondback": "Diamondbacks", "rockie": "Rockies",
1701
+ "dodger": "Dodgers", "padre": "Padres", "giant": "Giants",
1702
+ }
1703
+ lower_question = question.lower()
1704
+ years = re.findall(r"\b(?:18|19|20)\d{2}\b", question)
1705
+ matched_team = next(
1706
+ (
1707
+ canonical
1708
+ for alias, canonical in team_aliases.items()
1709
+ if re.search(rf"\b{re.escape(alias)}s?\b", lower_question)
1710
+ ),
1711
+ None,
1712
+ )
1713
+ stat_cues = [
1714
+ ("most walks", "walks"), ("least walks", "walks"),
1715
+ ("walks", "walks"), ("at bats", "at bats"),
1716
+ ("home runs", "home runs"), ("stolen bases", "stolen bases"),
1717
+ ("strikeouts", "strikeouts"), ("hits", "hits"),
1718
+ ("runs batted", "rbi"), ("rbi", "rbi"), ("ops", "ops"),
1719
+ ("obp", "obp"), ("slugging", "slg"), ("average", "average"),
1720
+ ]
1721
+ matched_stat = next(
1722
+ (stat for cue, stat in stat_cues if cue in lower_question),
1723
+ None,
1724
+ )
1725
+ if not route and matched_team and years and matched_stat:
1726
+ self.tools_by_name["mlb_stats"].forward(
1727
+ team=matched_team,
1728
+ season=int(years[0]),
1729
+ sort_stat=matched_stat,
1730
+ )
1731
+ route = "MLB statistics question -> official MLB Stats API"
1732
+
1733
+ if route:
1734
+ print(f"Rota determinística: {route}")
1735
+ return route
1736
+
1737
+ @staticmethod
1738
+ def _invalid_candidate(candidate: str) -> bool:
1739
+ value = str(candidate or "").strip()
1740
+ lowered = value.lower()
1741
+ if not value or lowered in {"none", "null", "n/a"}:
1742
+ return True
1743
+ if len(value) > 1_000:
1744
+ return True
1745
+ tool_syntax = (
1746
+ bool(re.search(
1747
+ r"""["']type["']\s*:\s*["']function["']""",
1748
+ value,
1749
+ flags=re.I,
1750
+ ))
1751
+ and bool(re.search(
1752
+ r"""["']arguments["']\s*:""",
1753
+ value,
1754
+ flags=re.I,
1755
+ ))
1756
+ )
1757
+ unfinished_cues = (
1758
+ "call: ", "calling tool", "let's search", "lets search",
1759
+ "let's do a search", "web_search(", "wikipedia_search(",
1760
+ "visit_webpage(", "query_web_table(", "i need to search",
1761
+ )
1762
+ return tool_syntax or any(cue in lowered for cue in unfinished_cues)
1763
+
1764
+ def _gemini_answer_from_evidence(
1765
+ self,
1766
+ question: str,
1767
+ evidence: str,
1768
+ task_id: str | None = None,
1769
+ ) -> str:
1770
+ """Produces one answer from existing evidence without running tools."""
1771
+ gemini_api_key = os.getenv("GEMINI_API_KEY")
1772
+ if not gemini_api_key:
1773
+ raise RuntimeError(
1774
+ "GEMINI_API_KEY não está configurada para o fallback."
1775
+ )
1776
+ model = os.getenv(
1777
+ "GAIA_GEMINI_FALLBACK_MODEL", DEFAULT_GEMINI_REVIEW_MODEL
1778
+ )
1779
+ if not model.lower().startswith("gemini-"):
1780
+ model = DEFAULT_GEMINI_REVIEW_MODEL
1781
+ prompt = f"""
1782
+ Answer this GAIA task using ONLY the collected evidence below. Do not call or
1783
+ suggest tools and do not perform another search. If the evidence is sufficient,
1784
+ return only the exact requested value in final_answer. If it is insufficient,
1785
+ set sufficient_evidence to false and leave final_answer empty.
1786
+
1787
+ Task ID: {task_id or "test"}
1788
+ Question: {question}
1789
+
1790
+ COLLECTED EVIDENCE:
1791
+ {evidence}
1792
+ """.strip()
1793
+ response = requests.post(
1794
+ (
1795
+ "https://generativelanguage.googleapis.com/v1beta/models/"
1796
+ f"{model}:generateContent"
1797
+ ),
1798
+ headers={
1799
+ "x-goog-api-key": gemini_api_key,
1800
+ "Content-Type": "application/json",
1801
+ },
1802
+ json={
1803
+ "contents": [
1804
+ {"role": "user", "parts": [{"text": prompt}]}
1805
+ ],
1806
+ "generationConfig": {
1807
+ "maxOutputTokens": 512,
1808
+ "thinkingConfig": {"thinkingLevel": "minimal"},
1809
+ "responseMimeType": "application/json",
1810
+ "responseJsonSchema": {
1811
+ "type": "object",
1812
+ "properties": {
1813
+ "final_answer": {"type": "string"},
1814
+ "sufficient_evidence": {"type": "boolean"},
1815
+ },
1816
+ "required": [
1817
+ "final_answer",
1818
+ "sufficient_evidence",
1819
+ ],
1820
+ "additionalProperties": False,
1821
+ },
1822
+ },
1823
+ },
1824
+ timeout=(WEBPAGE_CONNECT_TIMEOUT, 45),
1825
+ )
1826
+ response.raise_for_status()
1827
+ payload = response.json()
1828
+ parts = payload["candidates"][0]["content"]["parts"]
1829
+ content = "".join(
1830
+ str(part.get("text") or "") for part in parts
1831
+ ).strip()
1832
+ data = json.loads(content)
1833
+ if not data.get("sufficient_evidence"):
1834
+ raise RuntimeError(
1835
+ "O Gemini informou que as evidências coletadas ainda são "
1836
+ "insuficientes. A resposta não foi salva."
1837
+ )
1838
+ answer = self.enforce_direct_answer(
1839
+ question, str(data.get("final_answer") or "")
1840
+ )
1841
+ if self._invalid_candidate(answer):
1842
+ raise RuntimeError(
1843
+ "O fallback Gemini não produziu uma resposta final válida."
1844
+ )
1845
+ print(f"Gemini respondeu usando as evidências existentes: {answer}")
1846
+ return answer
1847
+
1848
+ def _run_with_failover(
1849
+ self,
1850
+ task_context: str,
1851
+ question: str,
1852
+ task_id: str | None,
1853
+ ):
1854
  try:
1855
  return self.agent.run(task_context, reset=True), False
1856
  except Exception as exc:
 
1872
  )
1873
  )
1874
  if retryable_provider_error and self.gemini_fallback_agent:
1875
+ evidence = current_run_evidence()
1876
  print(
1877
  "Cerebras indisponível; executando a questão com o "
1878
  f"fallback Gemini. Motivo: {compact_error(exc)}"
1879
  )
1880
+ if evidence:
1881
+ return (
1882
+ self._gemini_answer_from_evidence(
1883
+ question=question,
1884
+ evidence=evidence,
1885
+ task_id=task_id,
1886
+ ),
1887
+ True,
1888
+ )
1889
  return (
1890
  self.gemini_fallback_agent.run(task_context, reset=True),
1891
  True,
 
1903
  if not question:
1904
  raise ValueError("Digite uma pergunta para testar o agente.")
1905
  SEARCH_CACHE.clear()
1906
+ reset_run_evidence()
1907
+ attachment_name = ""
1908
 
1909
  if task_id:
1910
  attachment_name = get_task_file_name(task_id)
 
1944
  )
1945
  else:
1946
  task_context = question
1947
+ route = self._precollect_deterministic_evidence(
1948
+ question=question,
1949
+ task_id=task_id,
1950
+ attachment_name=attachment_name,
1951
+ )
1952
+ precollected = current_run_evidence()
1953
+ if precollected:
1954
+ task_context += (
1955
+ "\n\nCONTROLLER-PRECOLLECTED EVIDENCE:\n"
1956
+ f"{precollected}\n\n"
1957
+ f"Controller route: {route}. Do not repeat the same tool call; "
1958
+ "a new spreadsheet calculation is allowed after describe. "
1959
+ "If this evidence answers the question, call final_answer now."
1960
+ )
1961
  try:
1962
+ result, used_gemini_fallback = self._run_with_failover(
1963
+ task_context=task_context,
1964
+ question=question,
1965
+ task_id=task_id,
1966
+ )
1967
  except Exception as exc:
1968
  error_text = str(exc)
1969
  if (
 
1994
  ))
1995
  )
1996
  )
1997
+ invalid_candidate = (
1998
+ invalid_candidate or self._invalid_candidate(candidate)
1999
+ )
2000
  if (
2001
  invalid_candidate
2002
  and self.gemini_fallback_agent
 
2006
  "Cerebras terminou sem resposta final; repetindo a questão "
2007
  "com o fallback Gemini."
2008
  )
2009
+ evidence = current_run_evidence()
2010
  used_gemini_fallback = True
2011
+ if evidence:
2012
+ candidate = self._gemini_answer_from_evidence(
2013
+ question=question,
2014
+ evidence=evidence,
2015
+ task_id=task_id,
2016
+ )
2017
+ else:
2018
+ result = self.gemini_fallback_agent.run(
2019
+ task_context, reset=True
2020
+ )
2021
+ candidate = self.enforce_direct_answer(
2022
+ question, str(result)
2023
+ )
2024
  invalid_candidate = (
2025
  not candidate
2026
  or candidate.strip().lower() in {"none", "null"}
 
2037
  ))
2038
  )
2039
  )
2040
+ invalid_candidate = (
2041
+ invalid_candidate or self._invalid_candidate(candidate)
2042
+ )
2043
  if invalid_candidate:
2044
  raise RuntimeError(
2045
  "O agente esgotou as etapas sem produzir uma resposta final. "
 
2052
  question=question,
2053
  candidate=candidate,
2054
  task_id=task_id,
2055
+ evidence=current_run_evidence(),
2056
  )
2057
 
2058
  @staticmethod
 
2298
  return self.deterministic_answer_cleanup(cleaned)
2299
 
2300
  def review_answer_with_gemini(
2301
+ self,
2302
+ question: str,
2303
+ candidate: str,
2304
+ task_id: str | None = None,
2305
+ evidence: str = "",
2306
  ) -> str:
2307
  """Revisa com Gemini e sempre preserva a resposta primária se falhar."""
2308
  gemini_api_key = os.getenv("GEMINI_API_KEY")
 
2322
  clearly necessary. The final_answer must contain only the requested value,
2323
  without explanation, label, Markdown, or citation. Respect requested numeric,
2324
  currency, name, list separator/order, quote, or chess notation formats.
2325
+ Use only the collected evidence when it is present. Do not invent facts and do
2326
+ not suggest or call another tool.
2327
  Task ID: {task_id or "test"}
2328
  Question: {question}
2329
  Candidate: {candidate}
2330
+ Collected evidence:
2331
+ {evidence or "(none; format-check the candidate only)"}
2332
  """.strip()
2333
 
2334
  try:
 
2680
  try:
2681
  updated[task_id] = agent(question, task_id)
2682
  except Exception as exc:
2683
+ print(
2684
+ f"Questão {task_id} falhou e não foi salva: "
2685
+ f"{compact_error(exc)}"
2686
+ )
2687
  failures += 1
2688
 
2689
  dataframe = review_dataframe(questions, updated)
 
2785
  try:
2786
  answer = agent(question, task_id)
2787
  except Exception as exc:
2788
+ print(
2789
+ f"Questão {task_id} falhou e foi omitida da avaliação local: "
2790
+ f"{compact_error(exc)}"
2791
+ )
2792
+ continue
2793
 
2794
  results.append(
2795
  {