Spaces:
Runtime error
Runtime error
| """ | |
| 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 ------------------------------------------------------------ | |
| 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() | |