Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1685 |
used_gemini_fallback = True
|
| 1686 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
{
|