Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -14,7 +14,6 @@ from smolagents import (
|
|
| 14 |
DuckDuckGoSearchTool,
|
| 15 |
LiteLLMModel,
|
| 16 |
Tool,
|
| 17 |
-
VisitWebpageTool,
|
| 18 |
WikipediaSearchTool,
|
| 19 |
)
|
| 20 |
|
|
@@ -25,6 +24,7 @@ HTTP_TIMEOUT = 45
|
|
| 25 |
MAX_EXTRACTED_CHARS = 35_000
|
| 26 |
DEFAULT_HF_MODEL = "huggingface/openai/gpt-oss-120b"
|
| 27 |
DEFAULT_GEMINI_MODEL = "gemini/gemini-3.5-flash"
|
|
|
|
| 28 |
|
| 29 |
|
| 30 |
def clean_filename(value: str) -> str:
|
|
@@ -123,6 +123,31 @@ def download_gaia_attachment(task_id: str) -> tuple[bytes, str]:
|
|
| 123 |
)
|
| 124 |
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
def extract_attachment_text(data: bytes, filename: str) -> str:
|
| 127 |
"""Extrai conteúdo legível dos formatos mais comuns do GAIA."""
|
| 128 |
suffix = Path(filename).suffix.lower()
|
|
@@ -218,6 +243,74 @@ def extract_attachment_text(data: bytes, filename: str) -> str:
|
|
| 218 |
return text
|
| 219 |
|
| 220 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
class InspectGaiaAttachmentTool(Tool):
|
| 222 |
name = "inspect_gaia_attachment"
|
| 223 |
description = (
|
|
@@ -426,12 +519,6 @@ class BasicAgent:
|
|
| 426 |
gemini_api_key = os.getenv("GEMINI_API_KEY")
|
| 427 |
configured_model = os.getenv("GAIA_MODEL_ID")
|
| 428 |
|
| 429 |
-
if not gemini_api_key:
|
| 430 |
-
raise RuntimeError(
|
| 431 |
-
"O secret GEMINI_API_KEY é obrigatório porque todas as "
|
| 432 |
-
"respostas passam pela revisão final do Gemini."
|
| 433 |
-
)
|
| 434 |
-
|
| 435 |
model_id = configured_model or DEFAULT_HF_MODEL
|
| 436 |
|
| 437 |
if model_id.startswith("gemini/"):
|
|
@@ -466,12 +553,7 @@ class BasicAgent:
|
|
| 466 |
"snippets. Use it to discover candidate sources. It does NOT open "
|
| 467 |
"or read the full pages; call visit_webpage on a returned URL."
|
| 468 |
)
|
| 469 |
-
visit_page_tool =
|
| 470 |
-
visit_page_tool.description = (
|
| 471 |
-
"Opens one exact HTTP/HTTPS URL and returns the readable page "
|
| 472 |
-
"content. Use it after web_search when facts depend on the page "
|
| 473 |
-
"itself, a table, article text, or linked source."
|
| 474 |
-
)
|
| 475 |
wikipedia_tool = WikipediaSearchTool(
|
| 476 |
user_agent="GAIA-Course-Agent/1.0 (educational project)",
|
| 477 |
language="en",
|
|
@@ -490,14 +572,15 @@ class BasicAgent:
|
|
| 490 |
InspectGaiaAttachmentTool(),
|
| 491 |
YouTubeTranscriptTool(),
|
| 492 |
AnalyzeGaiaImageTool(),
|
| 493 |
-
ConsultGeminiTool(),
|
| 494 |
]
|
|
|
|
|
|
|
| 495 |
|
| 496 |
self.agent = CodeAgent(
|
| 497 |
tools=agent_tools,
|
| 498 |
model=self.model,
|
| 499 |
max_steps=10,
|
| 500 |
-
planning_interval=
|
| 501 |
additional_authorized_imports=[
|
| 502 |
"collections",
|
| 503 |
"datetime",
|
|
@@ -540,8 +623,18 @@ can be opened. Never invent a tool, use subprocess, or use shell commands.
|
|
| 540 |
Do not repeat nearly identical searches; change the source or method.
|
| 541 |
|
| 542 |
FINAL RESPONSE POLICY:
|
| 543 |
-
Call final_answer with only the requested value.
|
| 544 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 545 |
- Quantity/count: return only the number, unless units or currency are requested.
|
| 546 |
- Person: return only the requested name component.
|
| 547 |
- City/country/code: return only that value.
|
|
@@ -560,11 +653,23 @@ explanations, labels, Markdown, citations, or the words "FINAL ANSWER".
|
|
| 560 |
if not question:
|
| 561 |
raise ValueError("Digite uma pergunta para testar o agente.")
|
| 562 |
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
try:
|
| 569 |
result = self.agent.run(task_context, reset=True)
|
| 570 |
except Exception as exc:
|
|
@@ -653,6 +758,12 @@ explanations, labels, Markdown, citations, or the words "FINAL ANSWER".
|
|
| 653 |
self, question: str, candidate: str, task_id: str | None = None
|
| 654 |
) -> str:
|
| 655 |
"""Revisa obrigatoriamente conteúdo e formato antes de salvar a resposta."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 656 |
reviewer_model = os.getenv(
|
| 657 |
"GAIA_GEMINI_REVIEW_MODEL", DEFAULT_GEMINI_MODEL
|
| 658 |
)
|
|
@@ -748,10 +859,17 @@ Do not return JSON. Do not wrap the fields in Markdown or a code block.
|
|
| 748 |
except Exception as exc:
|
| 749 |
last_error = exc
|
| 750 |
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 755 |
|
| 756 |
|
| 757 |
def empty_results() -> pd.DataFrame:
|
|
|
|
| 14 |
DuckDuckGoSearchTool,
|
| 15 |
LiteLLMModel,
|
| 16 |
Tool,
|
|
|
|
| 17 |
WikipediaSearchTool,
|
| 18 |
)
|
| 19 |
|
|
|
|
| 24 |
MAX_EXTRACTED_CHARS = 35_000
|
| 25 |
DEFAULT_HF_MODEL = "huggingface/openai/gpt-oss-120b"
|
| 26 |
DEFAULT_GEMINI_MODEL = "gemini/gemini-3.5-flash"
|
| 27 |
+
TASK_FILE_CACHE = {}
|
| 28 |
|
| 29 |
|
| 30 |
def clean_filename(value: str) -> str:
|
|
|
|
| 123 |
)
|
| 124 |
|
| 125 |
|
| 126 |
+
def get_task_file_name(task_id: str) -> str:
|
| 127 |
+
"""Retorna o nome oficial do anexo ou uma string vazia."""
|
| 128 |
+
task_id = str(task_id or "").strip()
|
| 129 |
+
if not task_id:
|
| 130 |
+
return ""
|
| 131 |
+
if task_id in TASK_FILE_CACHE:
|
| 132 |
+
return TASK_FILE_CACHE[task_id]
|
| 133 |
+
|
| 134 |
+
try:
|
| 135 |
+
response = requests.get(
|
| 136 |
+
f"{DEFAULT_API_URL}/questions", timeout=HTTP_TIMEOUT
|
| 137 |
+
)
|
| 138 |
+
response.raise_for_status()
|
| 139 |
+
for item in response.json():
|
| 140 |
+
item_task_id = str(item.get("task_id", "")).strip()
|
| 141 |
+
TASK_FILE_CACHE[item_task_id] = str(
|
| 142 |
+
item.get("file_name") or ""
|
| 143 |
+
).strip()
|
| 144 |
+
except Exception as exc:
|
| 145 |
+
print(f"Could not load task attachment metadata: {exc}")
|
| 146 |
+
return ""
|
| 147 |
+
|
| 148 |
+
return TASK_FILE_CACHE.get(task_id, "")
|
| 149 |
+
|
| 150 |
+
|
| 151 |
def extract_attachment_text(data: bytes, filename: str) -> str:
|
| 152 |
"""Extrai conteúdo legível dos formatos mais comuns do GAIA."""
|
| 153 |
suffix = Path(filename).suffix.lower()
|
|
|
|
| 243 |
return text
|
| 244 |
|
| 245 |
|
| 246 |
+
class OpenWebPageTool(Tool):
|
| 247 |
+
name = "visit_webpage"
|
| 248 |
+
description = (
|
| 249 |
+
"Opens and reads one exact HTTP/HTTPS page. Use it after web_search to "
|
| 250 |
+
"verify article text, tables, archives, papers, and linked sources. "
|
| 251 |
+
"It automatically retries blocked HTML pages through a text mirror. "
|
| 252 |
+
"It is not a binary-file downloader."
|
| 253 |
+
)
|
| 254 |
+
inputs = {
|
| 255 |
+
"url": {
|
| 256 |
+
"type": "string",
|
| 257 |
+
"description": "The complete HTTP or HTTPS URL to open.",
|
| 258 |
+
}
|
| 259 |
+
}
|
| 260 |
+
output_type = "string"
|
| 261 |
+
|
| 262 |
+
def forward(self, url: str) -> str:
|
| 263 |
+
from markdownify import markdownify
|
| 264 |
+
|
| 265 |
+
url = str(url or "").strip()
|
| 266 |
+
if not re.match(r"^https?://", url, flags=re.I):
|
| 267 |
+
return "Invalid URL: visit_webpage requires a full HTTP/HTTPS URL."
|
| 268 |
+
|
| 269 |
+
headers = {
|
| 270 |
+
"User-Agent": (
|
| 271 |
+
"Mozilla/5.0 (compatible; GAIA-Course-Agent/1.0; "
|
| 272 |
+
"+https://huggingface.co/learn/agents-course)"
|
| 273 |
+
)
|
| 274 |
+
}
|
| 275 |
+
errors = []
|
| 276 |
+
targets = [url]
|
| 277 |
+
if "r.jina.ai/http" not in url:
|
| 278 |
+
without_scheme = re.sub(r"^https?://", "", url)
|
| 279 |
+
targets.append(f"https://r.jina.ai/http://{without_scheme}")
|
| 280 |
+
|
| 281 |
+
for target in targets:
|
| 282 |
+
try:
|
| 283 |
+
response = requests.get(
|
| 284 |
+
target,
|
| 285 |
+
headers=headers,
|
| 286 |
+
timeout=HTTP_TIMEOUT,
|
| 287 |
+
allow_redirects=True,
|
| 288 |
+
)
|
| 289 |
+
response.raise_for_status()
|
| 290 |
+
content_type = response.headers.get("content-type", "").lower()
|
| 291 |
+
if (
|
| 292 |
+
"text/" not in content_type
|
| 293 |
+
and "json" not in content_type
|
| 294 |
+
and "xml" not in content_type
|
| 295 |
+
):
|
| 296 |
+
return (
|
| 297 |
+
f"The URL returned binary content ({content_type}). "
|
| 298 |
+
"Use inspect_gaia_attachment for official GAIA files."
|
| 299 |
+
)
|
| 300 |
+
|
| 301 |
+
text = response.text
|
| 302 |
+
if "html" in content_type:
|
| 303 |
+
text = markdownify(text)
|
| 304 |
+
text = text.strip()
|
| 305 |
+
if len(text) > 30_000:
|
| 306 |
+
text = text[:30_000] + "\n[page truncated]"
|
| 307 |
+
return text or "The page was retrieved but contained no text."
|
| 308 |
+
except Exception as exc:
|
| 309 |
+
errors.append(f"{target}: {exc}")
|
| 310 |
+
|
| 311 |
+
return "Error fetching the webpage: " + " | ".join(errors)
|
| 312 |
+
|
| 313 |
+
|
| 314 |
class InspectGaiaAttachmentTool(Tool):
|
| 315 |
name = "inspect_gaia_attachment"
|
| 316 |
description = (
|
|
|
|
| 519 |
gemini_api_key = os.getenv("GEMINI_API_KEY")
|
| 520 |
configured_model = os.getenv("GAIA_MODEL_ID")
|
| 521 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
model_id = configured_model or DEFAULT_HF_MODEL
|
| 523 |
|
| 524 |
if model_id.startswith("gemini/"):
|
|
|
|
| 553 |
"snippets. Use it to discover candidate sources. It does NOT open "
|
| 554 |
"or read the full pages; call visit_webpage on a returned URL."
|
| 555 |
)
|
| 556 |
+
visit_page_tool = OpenWebPageTool()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
wikipedia_tool = WikipediaSearchTool(
|
| 558 |
user_agent="GAIA-Course-Agent/1.0 (educational project)",
|
| 559 |
language="en",
|
|
|
|
| 572 |
InspectGaiaAttachmentTool(),
|
| 573 |
YouTubeTranscriptTool(),
|
| 574 |
AnalyzeGaiaImageTool(),
|
|
|
|
| 575 |
]
|
| 576 |
+
if gemini_api_key:
|
| 577 |
+
agent_tools.append(ConsultGeminiTool())
|
| 578 |
|
| 579 |
self.agent = CodeAgent(
|
| 580 |
tools=agent_tools,
|
| 581 |
model=self.model,
|
| 582 |
max_steps=10,
|
| 583 |
+
planning_interval=None,
|
| 584 |
additional_authorized_imports=[
|
| 585 |
"collections",
|
| 586 |
"datetime",
|
|
|
|
| 623 |
Do not repeat nearly identical searches; change the source or method.
|
| 624 |
|
| 625 |
FINAL RESPONSE POLICY:
|
| 626 |
+
Call final_answer with only the requested value. The final action must always
|
| 627 |
+
be valid executable code inside the required code tags, for example:
|
| 628 |
+
<code>
|
| 629 |
+
final_answer("Claus")
|
| 630 |
+
</code>
|
| 631 |
+
or:
|
| 632 |
+
<code>
|
| 633 |
+
final_answer(5)
|
| 634 |
+
</code>
|
| 635 |
+
Never write the answer as plain text outside a final_answer tool call. Never
|
| 636 |
+
include reasoning, explanations, labels, Markdown, citations, or the words
|
| 637 |
+
"FINAL ANSWER" inside the submitted value.
|
| 638 |
- Quantity/count: return only the number, unless units or currency are requested.
|
| 639 |
- Person: return only the requested name component.
|
| 640 |
- City/country/code: return only that value.
|
|
|
|
| 653 |
if not question:
|
| 654 |
raise ValueError("Digite uma pergunta para testar o agente.")
|
| 655 |
|
| 656 |
+
if task_id:
|
| 657 |
+
attachment_name = get_task_file_name(task_id)
|
| 658 |
+
attachment_context = (
|
| 659 |
+
f"Official attachment: {attachment_name}. "
|
| 660 |
+
"Use the appropriate attachment tool."
|
| 661 |
+
if attachment_name
|
| 662 |
+
else (
|
| 663 |
+
"Official attachment: NONE. Do not call "
|
| 664 |
+
"inspect_gaia_attachment or analyze_gaia_image."
|
| 665 |
+
)
|
| 666 |
+
)
|
| 667 |
+
task_context = (
|
| 668 |
+
f"GAIA task_id: {task_id}\n"
|
| 669 |
+
f"{attachment_context}\n\nQuestion: {question}"
|
| 670 |
+
)
|
| 671 |
+
else:
|
| 672 |
+
task_context = question
|
| 673 |
try:
|
| 674 |
result = self.agent.run(task_context, reset=True)
|
| 675 |
except Exception as exc:
|
|
|
|
| 758 |
self, question: str, candidate: str, task_id: str | None = None
|
| 759 |
) -> str:
|
| 760 |
"""Revisa obrigatoriamente conteúdo e formato antes de salvar a resposta."""
|
| 761 |
+
if not self.gemini_api_key:
|
| 762 |
+
fallback = self.deterministic_answer_cleanup(candidate)
|
| 763 |
+
print("Gemini review status: SKIPPED — GEMINI_API_KEY is missing")
|
| 764 |
+
print(f"Primary answer preserved: {fallback}")
|
| 765 |
+
return fallback
|
| 766 |
+
|
| 767 |
reviewer_model = os.getenv(
|
| 768 |
"GAIA_GEMINI_REVIEW_MODEL", DEFAULT_GEMINI_MODEL
|
| 769 |
)
|
|
|
|
| 859 |
except Exception as exc:
|
| 860 |
last_error = exc
|
| 861 |
|
| 862 |
+
fallback = self.deterministic_answer_cleanup(candidate)
|
| 863 |
+
if not fallback:
|
| 864 |
+
raise RuntimeError(
|
| 865 |
+
"A revisão do Gemini falhou e a resposta primária estava vazia. "
|
| 866 |
+
f"Detalhe: {last_error}"
|
| 867 |
+
)
|
| 868 |
+
|
| 869 |
+
print(f"Candidate answer: {fallback}")
|
| 870 |
+
print("Gemini review status: FAILED — primary answer preserved")
|
| 871 |
+
print(f"Gemini review error: {last_error}")
|
| 872 |
+
return fallback
|
| 873 |
|
| 874 |
|
| 875 |
def empty_results() -> pd.DataFrame:
|