Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -2,8 +2,10 @@ 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
|
|
@@ -11,9 +13,7 @@ from zipfile import ZipFile
|
|
| 11 |
import gradio as gr
|
| 12 |
import pandas as pd
|
| 13 |
import requests
|
| 14 |
-
from litellm import completion
|
| 15 |
from smolagents import (
|
| 16 |
-
DuckDuckGoSearchTool,
|
| 17 |
LiteLLMModel,
|
| 18 |
Tool,
|
| 19 |
ToolCallingAgent,
|
|
@@ -28,11 +28,13 @@ 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 = "
|
| 32 |
-
|
| 33 |
TASK_FILE_CACHE = {}
|
| 34 |
ATTACHMENT_CACHE = {}
|
| 35 |
WEBPAGE_CACHE = {}
|
|
|
|
|
|
|
| 36 |
|
| 37 |
|
| 38 |
def compact_error(exc: Exception) -> str:
|
|
@@ -336,6 +338,56 @@ def extract_attachment_text(
|
|
| 336 |
return text
|
| 337 |
|
| 338 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
class OpenWebPageTool(Tool):
|
| 340 |
name = "visit_webpage"
|
| 341 |
description = (
|
|
@@ -528,7 +580,7 @@ class TranscribeGaiaAudioTool(Tool):
|
|
| 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 |
-
"
|
| 532 |
)
|
| 533 |
inputs = {
|
| 534 |
"task_id": {
|
|
@@ -551,78 +603,33 @@ class TranscribeGaiaAudioTool(Tool):
|
|
| 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 |
-
|
| 602 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 626 |
except Exception as exc:
|
| 627 |
return (
|
| 628 |
f"Could not transcribe audio for task {task_id}: "
|
|
@@ -1086,42 +1093,36 @@ class BasicAgent:
|
|
| 1086 |
print("Inicializando o agente GAIA...")
|
| 1087 |
|
| 1088 |
hf_token = os.getenv("HF_TOKEN")
|
| 1089 |
-
|
| 1090 |
configured_model = os.getenv("GAIA_MODEL_ID")
|
| 1091 |
|
| 1092 |
model_id = configured_model or DEFAULT_MAIN_MODEL
|
| 1093 |
-
if not model_id.lower().startswith("
|
| 1094 |
print(
|
| 1095 |
-
"GAIA_MODEL_ID não apontava para um modelo
|
| 1096 |
"e foi ignorado. "
|
| 1097 |
f"Usando {DEFAULT_MAIN_MODEL}."
|
| 1098 |
)
|
| 1099 |
model_id = DEFAULT_MAIN_MODEL
|
| 1100 |
-
if not
|
| 1101 |
raise RuntimeError(
|
| 1102 |
-
"O secret
|
| 1103 |
"Adicione a chave em Settings > Variables and secrets > Secrets."
|
| 1104 |
)
|
| 1105 |
|
| 1106 |
self.model = LiteLLMModel(
|
| 1107 |
model_id=model_id,
|
| 1108 |
-
api_key=
|
| 1109 |
temperature=0,
|
| 1110 |
-
max_tokens=
|
| 1111 |
-
requests_per_minute=
|
|
|
|
| 1112 |
)
|
| 1113 |
self.hf_token = hf_token
|
| 1114 |
self.model_id = model_id
|
| 1115 |
print(f"Modelo principal selecionado: {model_id}")
|
| 1116 |
|
| 1117 |
-
web_search_tool =
|
| 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 "
|
| 1122 |
-
"snippets. Use it to discover candidate sources. It does NOT open "
|
| 1123 |
-
"or read the full pages; call visit_webpage on a returned URL."
|
| 1124 |
-
)
|
| 1125 |
visit_page_tool = OpenWebPageTool()
|
| 1126 |
wikipedia_tool = WikipediaSearchTool(
|
| 1127 |
user_agent="GAIA-Course-Agent/1.0 (educational project)",
|
|
@@ -1147,12 +1148,13 @@ class BasicAgent:
|
|
| 1147 |
CalculatorTool(),
|
| 1148 |
]
|
| 1149 |
|
| 1150 |
-
#
|
| 1151 |
# structured format without parsing generated Python code.
|
| 1152 |
self.agent = ToolCallingAgent(
|
| 1153 |
tools=agent_tools,
|
| 1154 |
model=self.model,
|
| 1155 |
-
max_steps=
|
|
|
|
| 1156 |
planning_interval=None,
|
| 1157 |
description=(
|
| 1158 |
"Agent designed to solve GAIA benchmark questions with "
|
|
@@ -1207,6 +1209,7 @@ include reasoning, explanations, labels, Markdown, citations, or the words
|
|
| 1207 |
question = (question or "").strip()
|
| 1208 |
if not question:
|
| 1209 |
raise ValueError("Digite uma pergunta para testar o agente.")
|
|
|
|
| 1210 |
|
| 1211 |
if task_id:
|
| 1212 |
attachment_name = get_task_file_name(task_id)
|
|
@@ -1262,7 +1265,7 @@ include reasoning, explanations, labels, Markdown, citations, or the words
|
|
| 1262 |
) from exc
|
| 1263 |
raise
|
| 1264 |
candidate = self.enforce_direct_answer(question, str(result))
|
| 1265 |
-
return self.
|
| 1266 |
question=question,
|
| 1267 |
candidate=candidate,
|
| 1268 |
task_id=task_id,
|
|
@@ -1390,7 +1393,39 @@ include reasoning, explanations, labels, Markdown, citations, or the words
|
|
| 1390 |
if identifiers:
|
| 1391 |
return identifiers[-1].strip(" .,:;\"'")
|
| 1392 |
|
| 1393 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1394 |
question_lower.startswith("who ")
|
| 1395 |
or " who " in f" {question_lower} "
|
| 1396 |
or "first name" in question_lower
|
|
@@ -1478,103 +1513,99 @@ include reasoning, explanations, labels, Markdown, citations, or the words
|
|
| 1478 |
|
| 1479 |
return self.deterministic_answer_cleanup(cleaned)
|
| 1480 |
|
| 1481 |
-
def
|
| 1482 |
self, question: str, candidate: str, task_id: str | None = None
|
| 1483 |
) -> str:
|
| 1484 |
-
"""
|
| 1485 |
-
|
| 1486 |
-
if not
|
| 1487 |
fallback = self.enforce_direct_answer(question, candidate)
|
| 1488 |
-
print("
|
| 1489 |
print(f"Primary answer preserved: {fallback}")
|
| 1490 |
return fallback
|
| 1491 |
|
| 1492 |
reviewer_model = os.getenv(
|
| 1493 |
-
"
|
| 1494 |
)
|
| 1495 |
-
if not reviewer_model.lower().startswith("
|
| 1496 |
-
reviewer_model =
|
| 1497 |
review_prompt = f"""
|
| 1498 |
-
Review this GAIA exact-match candidate. Preserve it unless
|
| 1499 |
-
|
| 1500 |
-
|
| 1501 |
-
notation
|
|
|
|
| 1502 |
Question: {question}
|
| 1503 |
Candidate: {candidate}
|
| 1504 |
-
Return:
|
| 1505 |
-
<final_answer>exact value to submit</final_answer>
|
| 1506 |
""".strip()
|
| 1507 |
|
| 1508 |
-
|
| 1509 |
-
|
| 1510 |
-
|
| 1511 |
-
|
| 1512 |
-
""
|
| 1513 |
-
|
| 1514 |
-
|
| 1515 |
-
|
| 1516 |
-
|
| 1517 |
-
|
| 1518 |
-
|
| 1519 |
-
|
| 1520 |
{
|
| 1521 |
"role": "user",
|
| 1522 |
-
"
|
| 1523 |
}
|
| 1524 |
],
|
| 1525 |
-
|
| 1526 |
-
|
| 1527 |
-
|
| 1528 |
-
|
| 1529 |
-
|
| 1530 |
-
|
| 1531 |
-
|
| 1532 |
-
|
| 1533 |
-
|
| 1534 |
-
|
| 1535 |
-
|
| 1536 |
-
|
| 1537 |
-
|
| 1538 |
-
|
| 1539 |
-
|
| 1540 |
-
|
| 1541 |
-
flags=re.I | re.S,
|
| 1542 |
-
)
|
| 1543 |
-
if not answer_match:
|
| 1544 |
-
raise ValueError(
|
| 1545 |
-
"Groq did not return the <final_answer> field."
|
| 1546 |
-
)
|
| 1547 |
-
final_answer = self.enforce_direct_answer(
|
| 1548 |
-
question, answer_match.group(1)
|
| 1549 |
-
)
|
| 1550 |
-
if not final_answer:
|
| 1551 |
-
raise ValueError("Groq returned an empty final_answer.")
|
| 1552 |
-
|
| 1553 |
-
changed = final_answer != candidate
|
| 1554 |
-
note = (
|
| 1555 |
-
note_match.group(1).strip()
|
| 1556 |
-
if note_match
|
| 1557 |
-
else "Review completed without a note."
|
| 1558 |
-
)
|
| 1559 |
-
print(f"Candidate answer: {candidate}")
|
| 1560 |
-
print(f"Groq reviewed answer: {final_answer}")
|
| 1561 |
-
print(f"Groq changed answer: {changed}")
|
| 1562 |
-
print(f"Groq review note: {note}")
|
| 1563 |
-
return final_answer
|
| 1564 |
-
except Exception as exc:
|
| 1565 |
-
last_error = exc
|
| 1566 |
-
|
| 1567 |
-
fallback = self.enforce_direct_answer(question, candidate)
|
| 1568 |
-
if not fallback:
|
| 1569 |
-
raise RuntimeError(
|
| 1570 |
-
"A revisão do Groq falhou e a resposta primária estava vazia. "
|
| 1571 |
-
f"Detalhe: {last_error}"
|
| 1572 |
)
|
| 1573 |
-
|
| 1574 |
-
|
| 1575 |
-
|
| 1576 |
-
|
| 1577 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1578 |
|
| 1579 |
|
| 1580 |
def empty_results() -> pd.DataFrame:
|
|
|
|
| 2 |
import re
|
| 3 |
import base64
|
| 4 |
import ast
|
| 5 |
+
import json
|
| 6 |
import math
|
| 7 |
import operator
|
| 8 |
+
import threading
|
| 9 |
from io import BytesIO
|
| 10 |
from pathlib import Path
|
| 11 |
from zipfile import ZipFile
|
|
|
|
| 13 |
import gradio as gr
|
| 14 |
import pandas as pd
|
| 15 |
import requests
|
|
|
|
| 16 |
from smolagents import (
|
|
|
|
| 17 |
LiteLLMModel,
|
| 18 |
Tool,
|
| 19 |
ToolCallingAgent,
|
|
|
|
| 28 |
WEBPAGE_CONNECT_TIMEOUT = 8
|
| 29 |
WEBPAGE_READ_TIMEOUT = 20
|
| 30 |
MAX_WEBPAGE_CHARS = 6_000
|
| 31 |
+
DEFAULT_MAIN_MODEL = "cerebras/zai-glm-4.7"
|
| 32 |
+
DEFAULT_GEMINI_REVIEW_MODEL = "gemini-3.5-flash"
|
| 33 |
TASK_FILE_CACHE = {}
|
| 34 |
ATTACHMENT_CACHE = {}
|
| 35 |
WEBPAGE_CACHE = {}
|
| 36 |
+
SEARCH_CACHE = {}
|
| 37 |
+
SEARCH_LOCK = threading.Lock()
|
| 38 |
|
| 39 |
|
| 40 |
def compact_error(exc: Exception) -> str:
|
|
|
|
| 338 |
return text
|
| 339 |
|
| 340 |
|
| 341 |
+
class ConciseWebSearchTool(Tool):
|
| 342 |
+
name = "web_search"
|
| 343 |
+
description = (
|
| 344 |
+
"Searches the public web without an API key. Returns at most five "
|
| 345 |
+
"compact results. Use one precise query, then open the best source. "
|
| 346 |
+
"Identical repeated searches are skipped automatically."
|
| 347 |
+
)
|
| 348 |
+
inputs = {
|
| 349 |
+
"query": {
|
| 350 |
+
"type": "string",
|
| 351 |
+
"description": "One concise web search query.",
|
| 352 |
+
}
|
| 353 |
+
}
|
| 354 |
+
output_type = "string"
|
| 355 |
+
|
| 356 |
+
def forward(self, query: str) -> str:
|
| 357 |
+
from ddgs import DDGS
|
| 358 |
+
|
| 359 |
+
query = " ".join(str(query or "").split())
|
| 360 |
+
if not query:
|
| 361 |
+
return "Search query is empty."
|
| 362 |
+
cache_key = query.casefold()
|
| 363 |
+
|
| 364 |
+
# The lock also prevents parallel duplicate searches from both sending
|
| 365 |
+
# the same network request and duplicating a large observation.
|
| 366 |
+
with SEARCH_LOCK:
|
| 367 |
+
if cache_key in SEARCH_CACHE:
|
| 368 |
+
return (
|
| 369 |
+
"Duplicate search skipped; the identical results are "
|
| 370 |
+
"already present in this run. Open one of those URLs."
|
| 371 |
+
)
|
| 372 |
+
try:
|
| 373 |
+
raw_results = list(DDGS().text(query, max_results=5))
|
| 374 |
+
lines = []
|
| 375 |
+
for index, result in enumerate(raw_results[:5], start=1):
|
| 376 |
+
title = " ".join(str(result.get("title") or "").split())
|
| 377 |
+
url = str(result.get("href") or result.get("url") or "").strip()
|
| 378 |
+
snippet = " ".join(str(result.get("body") or "").split())
|
| 379 |
+
lines.append(
|
| 380 |
+
f"{index}. {title[:180]}\n"
|
| 381 |
+
f"URL: {url}\n"
|
| 382 |
+
f"Snippet: {snippet[:320]}"
|
| 383 |
+
)
|
| 384 |
+
output = "\n\n".join(lines) or "No search results found."
|
| 385 |
+
SEARCH_CACHE[cache_key] = output
|
| 386 |
+
return output
|
| 387 |
+
except Exception as exc:
|
| 388 |
+
return f"Web search failed: {compact_error(exc)}"
|
| 389 |
+
|
| 390 |
+
|
| 391 |
class OpenWebPageTool(Tool):
|
| 392 |
name = "visit_webpage"
|
| 393 |
description = (
|
|
|
|
| 580 |
description = (
|
| 581 |
"Downloads and transcribes the official GAIA audio attachment. Use it "
|
| 582 |
"first whenever the attachment is MP3, WAV, FLAC, M4A, OGG, or WEBM. "
|
| 583 |
+
"It uses Hugging Face speech recognition, not the chat model."
|
| 584 |
)
|
| 585 |
inputs = {
|
| 586 |
"task_id": {
|
|
|
|
| 603 |
"inspect_gaia_attachment."
|
| 604 |
)
|
| 605 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
hf_token = os.getenv("HF_TOKEN")
|
| 607 |
+
if not hf_token:
|
| 608 |
+
return "Audio transcription failed: HF_TOKEN is not configured."
|
| 609 |
+
try:
|
| 610 |
+
from huggingface_hub import InferenceClient
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
|
| 612 |
+
client = InferenceClient(api_key=hf_token, provider="auto")
|
| 613 |
+
transcript_result = client.automatic_speech_recognition(
|
| 614 |
+
data,
|
| 615 |
+
model=os.getenv(
|
| 616 |
+
"GAIA_ASR_MODEL", "openai/whisper-large-v3"
|
| 617 |
+
),
|
| 618 |
+
)
|
| 619 |
+
transcript = str(
|
| 620 |
+
getattr(transcript_result, "text", transcript_result)
|
| 621 |
+
).strip()
|
| 622 |
+
if not transcript:
|
| 623 |
+
return "Hugging Face returned an empty audio transcript."
|
| 624 |
+
return (
|
| 625 |
+
f"Audio transcript ({filename}):\n"
|
| 626 |
+
f"{transcript[:6_000]}"
|
| 627 |
+
)
|
| 628 |
+
except Exception as exc:
|
| 629 |
+
return (
|
| 630 |
+
"Hugging Face speech-to-text failed: "
|
| 631 |
+
f"{compact_error(exc)}"
|
| 632 |
+
)
|
| 633 |
except Exception as exc:
|
| 634 |
return (
|
| 635 |
f"Could not transcribe audio for task {task_id}: "
|
|
|
|
| 1093 |
print("Inicializando o agente GAIA...")
|
| 1094 |
|
| 1095 |
hf_token = os.getenv("HF_TOKEN")
|
| 1096 |
+
cerebras_api_key = os.getenv("CEREBRAS_API_KEY")
|
| 1097 |
configured_model = os.getenv("GAIA_MODEL_ID")
|
| 1098 |
|
| 1099 |
model_id = configured_model or DEFAULT_MAIN_MODEL
|
| 1100 |
+
if not model_id.lower().startswith("cerebras/"):
|
| 1101 |
print(
|
| 1102 |
+
"GAIA_MODEL_ID não apontava para um modelo Cerebras "
|
| 1103 |
"e foi ignorado. "
|
| 1104 |
f"Usando {DEFAULT_MAIN_MODEL}."
|
| 1105 |
)
|
| 1106 |
model_id = DEFAULT_MAIN_MODEL
|
| 1107 |
+
if not cerebras_api_key:
|
| 1108 |
raise RuntimeError(
|
| 1109 |
+
"O secret CEREBRAS_API_KEY não está configurado. "
|
| 1110 |
"Adicione a chave em Settings > Variables and secrets > Secrets."
|
| 1111 |
)
|
| 1112 |
|
| 1113 |
self.model = LiteLLMModel(
|
| 1114 |
model_id=model_id,
|
| 1115 |
+
api_key=cerebras_api_key,
|
| 1116 |
temperature=0,
|
| 1117 |
+
max_tokens=1_000,
|
| 1118 |
+
requests_per_minute=8,
|
| 1119 |
+
parallel_tool_calls=False,
|
| 1120 |
)
|
| 1121 |
self.hf_token = hf_token
|
| 1122 |
self.model_id = model_id
|
| 1123 |
print(f"Modelo principal selecionado: {model_id}")
|
| 1124 |
|
| 1125 |
+
web_search_tool = ConciseWebSearchTool()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1126 |
visit_page_tool = OpenWebPageTool()
|
| 1127 |
wikipedia_tool = WikipediaSearchTool(
|
| 1128 |
user_agent="GAIA-Course-Agent/1.0 (educational project)",
|
|
|
|
| 1148 |
CalculatorTool(),
|
| 1149 |
]
|
| 1150 |
|
| 1151 |
+
# GLM returns native tool calls. ToolCallingAgent handles that
|
| 1152 |
# structured format without parsing generated Python code.
|
| 1153 |
self.agent = ToolCallingAgent(
|
| 1154 |
tools=agent_tools,
|
| 1155 |
model=self.model,
|
| 1156 |
+
max_steps=4,
|
| 1157 |
+
max_tool_threads=1,
|
| 1158 |
planning_interval=None,
|
| 1159 |
description=(
|
| 1160 |
"Agent designed to solve GAIA benchmark questions with "
|
|
|
|
| 1209 |
question = (question or "").strip()
|
| 1210 |
if not question:
|
| 1211 |
raise ValueError("Digite uma pergunta para testar o agente.")
|
| 1212 |
+
SEARCH_CACHE.clear()
|
| 1213 |
|
| 1214 |
if task_id:
|
| 1215 |
attachment_name = get_task_file_name(task_id)
|
|
|
|
| 1265 |
) from exc
|
| 1266 |
raise
|
| 1267 |
candidate = self.enforce_direct_answer(question, str(result))
|
| 1268 |
+
return self.review_answer_with_gemini(
|
| 1269 |
question=question,
|
| 1270 |
candidate=candidate,
|
| 1271 |
task_id=task_id,
|
|
|
|
| 1393 |
if identifiers:
|
| 1394 |
return identifiers[-1].strip(" .,:;\"'")
|
| 1395 |
|
| 1396 |
+
list_question = (
|
| 1397 |
+
"comma" in question_lower
|
| 1398 |
+
or "list" in question_lower
|
| 1399 |
+
or "separated" in question_lower
|
| 1400 |
+
or "delimited" in question_lower
|
| 1401 |
+
)
|
| 1402 |
+
if (
|
| 1403 |
+
list_question
|
| 1404 |
+
and "last name" in question_lower
|
| 1405 |
+
and "before" in question_lower
|
| 1406 |
+
and "after" in question_lower
|
| 1407 |
+
):
|
| 1408 |
+
emphasized_names = [
|
| 1409 |
+
cls.deterministic_answer_cleanup(value).strip(" .,:;\"'")
|
| 1410 |
+
for value in bold_values
|
| 1411 |
+
]
|
| 1412 |
+
emphasized_names = [
|
| 1413 |
+
value
|
| 1414 |
+
for value in emphasized_names
|
| 1415 |
+
if value and len(value) <= 60 and " " not in value
|
| 1416 |
+
]
|
| 1417 |
+
if len(emphasized_names) >= 2:
|
| 1418 |
+
return ", ".join(emphasized_names[-2:])
|
| 1419 |
+
|
| 1420 |
+
name_pair = re.search(
|
| 1421 |
+
r"\b(?:are|were)\s+([A-Z][A-Za-z'’-]+)\s*"
|
| 1422 |
+
r"(?:,|and)\s*([A-Z][A-Za-z'’-]+)",
|
| 1423 |
+
text,
|
| 1424 |
+
)
|
| 1425 |
+
if name_pair:
|
| 1426 |
+
return f"{name_pair.group(1)}, {name_pair.group(2)}"
|
| 1427 |
+
|
| 1428 |
+
person_question = not list_question and (
|
| 1429 |
question_lower.startswith("who ")
|
| 1430 |
or " who " in f" {question_lower} "
|
| 1431 |
or "first name" in question_lower
|
|
|
|
| 1513 |
|
| 1514 |
return self.deterministic_answer_cleanup(cleaned)
|
| 1515 |
|
| 1516 |
+
def review_answer_with_gemini(
|
| 1517 |
self, question: str, candidate: str, task_id: str | None = None
|
| 1518 |
) -> str:
|
| 1519 |
+
"""Revisa com Gemini e sempre preserva a resposta primária se falhar."""
|
| 1520 |
+
gemini_api_key = os.getenv("GEMINI_API_KEY")
|
| 1521 |
+
if not gemini_api_key:
|
| 1522 |
fallback = self.enforce_direct_answer(question, candidate)
|
| 1523 |
+
print("Gemini review status: SKIPPED — GEMINI_API_KEY is missing")
|
| 1524 |
print(f"Primary answer preserved: {fallback}")
|
| 1525 |
return fallback
|
| 1526 |
|
| 1527 |
reviewer_model = os.getenv(
|
| 1528 |
+
"GAIA_GEMINI_REVIEW_MODEL", DEFAULT_GEMINI_REVIEW_MODEL
|
| 1529 |
)
|
| 1530 |
+
if not reviewer_model.lower().startswith("gemini-"):
|
| 1531 |
+
reviewer_model = DEFAULT_GEMINI_REVIEW_MODEL
|
| 1532 |
review_prompt = f"""
|
| 1533 |
+
Review this GAIA exact-match candidate. Preserve it unless a correction is
|
| 1534 |
+
clearly necessary. The final_answer must contain only the requested value,
|
| 1535 |
+
without explanation, label, Markdown, or citation. Respect requested numeric,
|
| 1536 |
+
currency, name, list separator/order, quote, or chess notation formats.
|
| 1537 |
+
Task ID: {task_id or "test"}
|
| 1538 |
Question: {question}
|
| 1539 |
Candidate: {candidate}
|
|
|
|
|
|
|
| 1540 |
""".strip()
|
| 1541 |
|
| 1542 |
+
try:
|
| 1543 |
+
response = requests.post(
|
| 1544 |
+
(
|
| 1545 |
+
"https://generativelanguage.googleapis.com/v1beta/models/"
|
| 1546 |
+
f"{reviewer_model}:generateContent"
|
| 1547 |
+
),
|
| 1548 |
+
headers={
|
| 1549 |
+
"x-goog-api-key": gemini_api_key,
|
| 1550 |
+
"Content-Type": "application/json",
|
| 1551 |
+
},
|
| 1552 |
+
json={
|
| 1553 |
+
"contents": [
|
| 1554 |
{
|
| 1555 |
"role": "user",
|
| 1556 |
+
"parts": [{"text": review_prompt}],
|
| 1557 |
}
|
| 1558 |
],
|
| 1559 |
+
"generationConfig": {
|
| 1560 |
+
"temperature": 0,
|
| 1561 |
+
"maxOutputTokens": 120,
|
| 1562 |
+
"responseMimeType": "application/json",
|
| 1563 |
+
"responseJsonSchema": {
|
| 1564 |
+
"type": "object",
|
| 1565 |
+
"properties": {
|
| 1566 |
+
"final_answer": {"type": "string"},
|
| 1567 |
+
"review_note": {"type": "string"},
|
| 1568 |
+
},
|
| 1569 |
+
"required": ["final_answer", "review_note"],
|
| 1570 |
+
"additionalProperties": False,
|
| 1571 |
+
},
|
| 1572 |
+
},
|
| 1573 |
+
},
|
| 1574 |
+
timeout=(WEBPAGE_CONNECT_TIMEOUT, 45),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1575 |
)
|
| 1576 |
+
response.raise_for_status()
|
| 1577 |
+
payload = response.json()
|
| 1578 |
+
parts = payload["candidates"][0]["content"]["parts"]
|
| 1579 |
+
content = "".join(
|
| 1580 |
+
str(part.get("text") or "") for part in parts
|
| 1581 |
+
).strip()
|
| 1582 |
+
review_data = json.loads(content)
|
| 1583 |
+
final_answer = self.enforce_direct_answer(
|
| 1584 |
+
question, str(review_data.get("final_answer") or "")
|
| 1585 |
+
)
|
| 1586 |
+
if not final_answer:
|
| 1587 |
+
raise ValueError("Gemini returned an empty final_answer.")
|
| 1588 |
+
|
| 1589 |
+
note = str(review_data.get("review_note") or "").strip()
|
| 1590 |
+
print(f"Candidate answer: {candidate}")
|
| 1591 |
+
print(f"Gemini reviewed answer: {final_answer}")
|
| 1592 |
+
print(f"Gemini changed answer: {final_answer != candidate}")
|
| 1593 |
+
print(f"Gemini review note: {note}")
|
| 1594 |
+
return final_answer
|
| 1595 |
+
except Exception as exc:
|
| 1596 |
+
fallback = self.enforce_direct_answer(question, candidate)
|
| 1597 |
+
if not fallback:
|
| 1598 |
+
raise RuntimeError(
|
| 1599 |
+
"Gemini review failed and the primary answer was empty. "
|
| 1600 |
+
f"Detail: {compact_error(exc)}"
|
| 1601 |
+
) from exc
|
| 1602 |
+
detail = compact_error(exc)
|
| 1603 |
+
if "response" in locals() and response is not None:
|
| 1604 |
+
detail += f" Response: {response.text[:500]}"
|
| 1605 |
+
print(f"Candidate answer: {fallback}")
|
| 1606 |
+
print("Gemini review status: FAILED — primary answer preserved")
|
| 1607 |
+
print(f"Gemini review error: {detail}")
|
| 1608 |
+
return fallback
|
| 1609 |
|
| 1610 |
|
| 1611 |
def empty_results() -> pd.DataFrame:
|