Spaces:
Runtime error
Runtime error
File size: 8,612 Bytes
9b55593 | 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 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 | """
solution_smolagents.py
======================
Unit 4 hands-on solution built on the **smolagents** framework (Unit 2.1).
Strategy
--------
A single `CodeAgent` equipped with research / multimodal / file tools. The agent
writes its own Python (native smolagents capability) so it can compute the
algebra / set-theory / Excel / attached-Python questions itself, while the tools
cover web research, page reading, audio transcription and image understanding.
The GAIA system prompt (paper Figure 2) is injected so that the model obeys the
strict final-answer format required for exact-match scoring.
Usage
-----
from solution_smolagents import GAIAAgent
agent = GAIAAgent()
print(agent.answer_question("8e867cd7-...", "How many studio albums ..."))
"""
from __future__ import annotations
import importlib.resources
import yaml
from smolagents import CodeAgent, Tool
import gaia_common as gc
def _default_prompt_templates() -> dict:
"""Load smolagents' default CodeAgent prompt templates so we can override
only the system prompt without losing the rest."""
return yaml.safe_load(
importlib.resources.files("smolagents.prompts").joinpath("code_agent.yaml").read_text()
)
# ----------------------------------------------------------------------------
# Tools (smolagents `Tool` subclasses - robust, no source introspection needed)
# ----------------------------------------------------------------------------
class WebSearchTool(Tool):
name = "web_search"
description = (
"Search the web (DuckDuckGo with fallbacks) and return the top results as "
"numbered text with titles, URLs and snippets. Use for fact-finding and to "
"discover pages to open with fetch_page."
)
inputs = {
"query": {"type": "string", "description": "The search query."},
"max_results": {"type": "integer", "description": "Number of results to return (default 5).", "nullable": True},
}
output_type = "string"
def forward(self, query: str, max_results: int = 5) -> str:
return gc.web_search(query, max_results=max_results)
class FetchPageTool(Tool):
name = "fetch_page"
description = (
"Fetch a URL (HTML or PDF) and return its human-readable text content. "
"Use after web_search to read the actual source of an answer."
)
inputs = {
"url": {"type": "string", "description": "The full URL to fetch."},
}
output_type = "string"
def forward(self, url: str) -> str:
return gc.fetch_page(url)
class DownloadTaskFileTool(Tool):
name = "download_task_file"
description = (
"Download the file attached to a GAIA question (image, audio, spreadsheet, "
"Python file, PDF, ...). Returns the local path to the downloaded file, or "
"'NO FILE' if the question has no attachment."
)
inputs = {
"task_id": {"type": "string", "description": "The GAIA task id of the question."},
}
output_type = "string"
def forward(self, task_id: str) -> str:
path = gc.download_task_file(task_id)
return path if path else "NO FILE"
class ReadFileTool(Tool):
name = "read_file"
description = (
"Read a local file and return its content as text. Dispatches automatically on the "
"extension: spreadsheets -> markdown table, PDF -> extracted text, images -> vision "
"description, audio -> transcription, text/Python -> raw text."
)
inputs = {
"path": {"type": "string", "description": "Local path to the file."},
"question": {"type": "string", "description": "Optional: the original question, used to guide image analysis.", "nullable": True},
}
output_type = "string"
def forward(self, path: str, question: str = "") -> str:
return gc.read_any_file(path, question)
class TranscribeAudioTool(Tool):
name = "transcribe_audio"
description = (
"Transcribe an audio file (local path) or a YouTube video (URL) to text using "
"Whisper. Use for questions that reference .mp3 attachments or YouTube videos."
)
inputs = {
"target": {"type": "string", "description": "Local audio file path OR a YouTube URL."},
}
output_type = "string"
def forward(self, target: str) -> str:
if target.startswith("http"):
return gc.youtube_transcript(target)
return gc.transcribe_audio(target)
class AnalyzeImageTool(Tool):
name = "analyze_image"
description = (
"Analyze an image file with a vision-language model and return a detailed text "
"description. Use for images (e.g. chess positions, figures, screenshots)."
)
inputs = {
"path": {"type": "string", "description": "Local path to the image file."},
"question": {"type": "string", "description": "The question or specific instruction for the image."},
}
output_type = "string"
def forward(self, path: str, question: str) -> str:
return gc.analyze_image(path, question)
class ExecutePythonTool(Tool):
name = "execute_python"
description = (
"Run a self-contained Python program in a fresh subprocess and return its stdout. "
"Use for exact arithmetic, data munging, set/group theory checks, and for running "
"an attached .py file (read it first with read_file, then execute its code)."
)
inputs = {
"code": {"type": "string", "description": "The complete Python code to run. It must be self-contained (imports inside)."},
}
output_type = "string"
def forward(self, code: str) -> str:
return gc.execute_python(code)
# ----------------------------------------------------------------------------
# The agent
# ----------------------------------------------------------------------------
DEFAULT_TOOLS = [
WebSearchTool(),
FetchPageTool(),
DownloadTaskFileTool(),
ReadFileTool(),
TranscribeAudioTool(),
AnalyzeImageTool(),
ExecutePythonTool(),
]
EXTRA_AUTHORIZED_IMPORTS = [
"pandas", "numpy", "math", "statistics", "json", "re", "datetime",
"collections", "itertools", "fractions", "urllib", "requests", "csv", "html",
]
class GAIAAgent:
"""smolagents CodeAgent specialised for the GAIA level-1 leaderboard."""
def __init__(
self,
model=None,
tools: list[Tool] | None = None,
max_steps: int = 14,
additional_authorized_imports: list[str] | None = None,
):
self.model = model or gc.make_smolagents_model()
templates = _default_prompt_templates()
templates["system_prompt"] = gc.GAIA_SYSTEM_PROMPT
self.agent = CodeAgent(
tools=tools or DEFAULT_TOOLS,
model=self.model,
max_steps=max_steps,
additional_authorized_imports=additional_authorized_imports or EXTRA_AUTHORIZED_IMPORTS,
prompt_templates=templates,
verbosity_level=1,
)
# -- public API ---------------------------------------------------------
def answer_question(self, task_id: str, question: str) -> str:
"""Full pipeline for one question: download attachment, run the agent,
extract and normalize the final answer."""
file_path = gc.download_task_file(task_id)
prompt = self._build_prompt(question, file_path)
try:
output = self.agent.run(prompt)
finally:
self.agent.memory.reset() # fresh memory per question
answer = gc.extract_final_answer(str(output))
return gc.normalize_final_answer(answer)
def __call__(self, task_id: str, question: str) -> str:
return self.answer_question(task_id, question)
# -- helpers ------------------------------------------------------------
@staticmethod
def _build_prompt(question: str, file_path: str | None) -> str:
file_hint = ""
if file_path:
file_hint = (
f"\n\nAn attachment for this question has already been downloaded to: {file_path}\n"
f"Use the read_file tool on it if you need it. The question asks: {question}"
)
return f"{question}{file_hint}{gc.ANSWER_ONLY_PROMPT}"
def run_demo(subset: int | None = 3):
"""Quick smoke test on the first `subset` questions."""
agent = GAIAAgent()
for item in gc.fetch_questions()[:subset]:
print("\n---", item["task_id"], "---")
print(item["question"][:150])
print("ANSWER:", agent.answer_question(item["task_id"], item["question"]))
if __name__ == "__main__":
run_demo()
|