gaia-agent-final / src /agent.py
Wiktor
Fix: bump gradio to 5.29.0 for Python 3.13 / huggingface_hub 1.x compatibility
476893a
Raw History Blame Contribute Delete
4.72 kB
"""Core agent logic.
Given one GAIA question object, produce a single submittable answer string.
The agent:
1. inspects the question and any attached file
2. downloads the attached file (via ``task_id``) when present
3. lets the model gather evidence with tool use (web search) and the file
4. produces a final answer
5. normalizes that answer for exact-match scoring
The returned value is **only** the answer text — no explanation, no markdown,
no "FINAL ANSWER" label. Output discipline is enforced twice: once by the
system prompt, and once by ``normalizer.finalize`` as a safety net.
Two entry points:
* :func:`run_agent` — returns just the normalized answer (used by the app).
* :func:`run_agent_detailed` — returns an :class:`AgentResult` carrying the raw
model output alongside the normalized answer, for local debugging.
Model selection is delegated to ``llm.get_runner`` so the model can be swapped
(Anthropic / OpenAI) via configuration without touching this file. File handling
lives in ``files.py``; this module only maps a ``FileContext`` to model blocks.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
from . import config, files, normalizer, prompts
from .llm import get_runner
@dataclass
class AgentResult:
"""One question's run, with both raw and normalized output for debugging."""
task_id: str
question: str
raw_output: str
submitted_answer: str
file_kind: Optional[str] = None
file_summary: Optional[str] = None
def run_agent(question: Dict[str, Any]) -> str:
"""Answer one GAIA question and return the normalized answer string.
``question`` is a question dict from ``scoring_api.get_questions`` /
``get_random_question``. It must contain ``task_id`` and ``question``; a
non-empty ``file_name`` indicates an attachment to fetch.
"""
return run_agent_detailed(question).submitted_answer
def run_agent_detailed(question: Dict[str, Any]) -> AgentResult:
"""Answer one question, returning raw + normalized output and file info.
The raw model output is preserved verbatim (never mutated) so a debug
workflow can compare it against the normalized submission.
"""
task_id = str(question.get("task_id", ""))
question_text = question.get("question", "")
file_name = question.get("file_name") or ""
# --- Steps 1 & 2: download/inspect the file and assemble model input ---
ctx = files.load_file(task_id, file_name) if file_name else None
has_file = ctx is not None and ctx.is_readable
task_prompt = prompts.build_task_prompt(question_text, has_file=has_file)
user_content: List[Dict[str, Any]] = [{"type": "text", "text": task_prompt}]
if ctx is not None:
user_content.extend(_blocks_from_file_context(ctx))
# --- Steps 3-5: run the agent, then enforce exact-match formatting ---
# >>> The model call (and its tool loop) lives behind the runner. <<<
system_prompt = prompts.get_system_prompt(short=config.SHORT_PROMPT)
runner = get_runner()
raw_output = runner.run(system_prompt, user_content)
# finalize() never mutates raw_output; it returns a cleaned copy.
submitted_answer = normalizer.finalize(raw_output)
return AgentResult(
task_id=task_id,
question=question_text,
raw_output=raw_output,
submitted_answer=submitted_answer,
file_kind=ctx.kind if ctx else None,
file_summary=ctx.summary if ctx else None,
)
def _blocks_from_file_context(ctx: files.FileContext) -> List[Dict[str, Any]]:
"""Map a provider-neutral FileContext to Anthropic content blocks.
Images become vision blocks, PDFs become document blocks, and text-like
files (txt/csv/json) become a text block prefixed with the file summary.
Unreadable files contribute a short note so the model knows what's missing.
"""
if ctx.kind == "image" and ctx.data_b64:
return [{
"type": "image",
"source": {
"type": "base64",
"media_type": ctx.media_type or "image/png",
"data": ctx.data_b64,
},
}]
if ctx.kind == "pdf" and ctx.data_b64:
return [{
"type": "document",
"source": {
"type": "base64",
"media_type": "application/pdf",
"data": ctx.data_b64,
},
}]
if ctx.text is not None: # text / csv / json
return [{"type": "text", "text": f"{ctx.summary}\n\n{ctx.text}"}]
# Missing / unsupported: a note keeps the model informed without crashing.
return [{"type": "text", "text": f"[{ctx.summary}]"}]