File size: 6,482 Bytes
058e8a6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91f0524
 
 
058e8a6
 
91f0524
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
058e8a6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91f0524
058e8a6
 
 
 
 
 
 
 
 
 
 
 
 
 
91f0524
058e8a6
 
91f0524
058e8a6
 
 
91f0524
058e8a6
 
91f0524
 
 
 
 
058e8a6
 
 
 
 
 
91f0524
 
 
 
 
 
 
 
 
 
 
 
 
 
 
058e8a6
 
 
 
 
 
 
 
 
91f0524
058e8a6
 
91f0524
 
 
 
 
 
 
 
 
 
 
 
 
 
058e8a6
91f0524
 
 
058e8a6
91f0524
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
"""GAIA agent: smolagents CodeAgent + HF Inference Providers backend."""
from __future__ import annotations

import os
import re
from typing import Optional

from smolagents import CodeAgent, InferenceClientModel

from tools import (
    analyze_image,
    download_task_file,
    read_table,
    read_webpage,
    transcribe_audio,
    web_search,
    wikipedia_search,
    youtube_transcript,
)

# Qwen2.5-72B is a much stronger tool-using / code-writing model than Llama-3.3
# for GAIA. Override via AGENT_MODEL_ID env var if you want to test others.
DEFAULT_MODEL_ID = "Qwen/Qwen2.5-72B-Instruct"
DEFAULT_PROVIDER = "auto"

# Canonical GAIA system prompt β€” the exact answer-format spec the leaderboard
# uses. Deviating from this format is the #1 cause of "right reasoning, wrong
# score".
SYSTEM_PROMPT = """You are a general AI assistant solving questions from the GAIA benchmark.

I will ask you a question. Think step by step using the tools available. When you
are confident, finish your answer with the following template:

FINAL ANSWER: [YOUR FINAL ANSWER]

YOUR FINAL ANSWER must obey these rules exactly:
- It is a number, OR as few words as possible, OR a comma separated list of
  numbers and/or strings.
- If a number: digits only. No commas inside numbers. No units ($, %, kg, etc.)
  unless the question explicitly asks for them.
- If a string: no articles (a, an, the), no abbreviations (write "Saint Louis"
  not "St. Louis", "New York City" not "NYC"), and digits in plain text unless
  the question asks for numerals.
- If a comma separated list: items separated by ", " (comma + single space),
  in the order requested, each item following the number/string rules above.
- Do NOT include explanations, units, quotes, or trailing punctuation in the
  FINAL ANSWER line itself.

Tool playbook:
- If the question mentions an attached file ("the attached", "this Excel /
  CSV / audio / image / Python file", "the file"), call
  `download_task_file(task_id)` FIRST to get a local path.
- Spreadsheets / CSV: `read_table(path)` then pandas in Python.
- Audio: `transcribe_audio(path)`.
- Image: `analyze_image(path, question)` β€” phrase the question precisely.
- YouTube link: `youtube_transcript(url)`.
- Open web: `web_search(query)` then `read_webpage(url)` on the best hit.
- Encyclopedic facts: `wikipedia_search(topic)`.
- Use Python freely for math, parsing, sorting, set operations, etc.

Self-check before answering:
- Did you answer the literal question asked? (Not a related one.)
- Does the format match the rules above?
- If unsure, do one more verification search.
"""


class GaiaAgent:
    """Stateless wrapper around a smolagents CodeAgent."""

    def __init__(self, model_id: Optional[str] = None, provider: Optional[str] = None):
        token = os.getenv("HF_TOKEN")
        if not token:
            raise RuntimeError(
                "HF_TOKEN is not set. Add it as a Space secret to use HF Inference."
            )
        self.model = InferenceClientModel(
            model_id=model_id or os.getenv("AGENT_MODEL_ID", DEFAULT_MODEL_ID),
            provider=provider or os.getenv("AGENT_PROVIDER", DEFAULT_PROVIDER),
            token=token,
            max_tokens=4096,
        )
        self.tools = [
            web_search,
            read_webpage,
            wikipedia_search,
            youtube_transcript,
            download_task_file,
            read_table,
            transcribe_audio,
            analyze_image,
        ]

    def _build_agent(self) -> CodeAgent:
        # Fresh agent per question β€” keeps memory clean between tasks.
        kwargs = dict(
            tools=self.tools,
            model=self.model,
            max_steps=15,
            additional_authorized_imports=[
                "pandas", "numpy", "json", "re", "math", "statistics",
                "itertools", "collections", "datetime", "csv", "io",
                "pathlib", "string", "base64", "urllib", "unicodedata",
            ],
        )
        # planning_interval was added in smolagents 1.x β€” guard for older installs.
        try:
            return CodeAgent(planning_interval=4, **kwargs)
        except TypeError:
            return CodeAgent(**kwargs)

    def __call__(self, question: str, task_id: Optional[str] = None) -> str:
        agent = self._build_agent()
        prompt = SYSTEM_PROMPT
        if task_id:
            prompt += f"\nThe current task_id is: {task_id}\n"
            # Eager-download the attachment so the agent never misses it.
            try:
                path = download_task_file(task_id)
                if path and not path.startswith("NO_FILE") and not path.startswith(
                    ("Download error", "Download failed")
                ):
                    prompt += (
                        f"An attached file for this task has already been "
                        f"downloaded to: {path}\n"
                        f"Use the right tool on this path (read_table for "
                        f"spreadsheets/CSV, transcribe_audio for audio, "
                        f"analyze_image for images, or open it with Python).\n"
                    )
            except Exception as e:
                print(f"  pre-download skipped: {e}")
        prompt += f"\nQuestion:\n{question}\n"
        try:
            raw = agent.run(prompt)
        except Exception as e:
            return f"AGENT ERROR: {e}"
        return self._normalize(str(raw))

    @staticmethod
    def _normalize(text: str) -> str:
        """Strip every common LLM cruft pattern so exact-match has a chance."""
        if not text:
            return text
        t = str(text).strip()

        # 1) If the model emitted "FINAL ANSWER: X" anywhere, keep only X.
        m = re.search(r"final answer\s*[:\-]\s*(.+)", t, flags=re.IGNORECASE | re.DOTALL)
        if m:
            t = m.group(1).strip()
            # Trim at first newline / closing markdown.
            t = t.split("\n")[0].strip()

        # 2) Strip wrapping markdown bold/italic/code.
        t = re.sub(r"^[*_`]+|[*_`]+$", "", t).strip()

        # 3) Strip wrapping quotes.
        if len(t) >= 2 and t[0] == t[-1] and t[0] in "\"'`β€œβ€β€˜β€™":
            t = t[1:-1].strip()

        # 4) Drop a single trailing period if the answer is short (not a sentence).
        if t.count(" ") < 6:
            t = t.rstrip(".")

        # 5) Collapse internal whitespace.
        t = re.sub(r"[ \t]+", " ", t).strip()

        return t