File size: 5,777 Bytes
3894cc5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""GAIA assignment agent — Wikipedia, web, files, audio, vision, YouTube."""

from __future__ import annotations

import os
import re
from pathlib import Path

from smolagents import CodeAgent, InferenceClientModel, VisitWebpageTool, WebSearchTool, WikipediaSearchTool

from files import file_context_block
from tools import build_custom_tools

ANSWER_INSTRUCTIONS = """
You are a precise GAIA benchmark agent. Solve the question using tools when needed.

Available capabilities:
- wikipedia_search: English Wikipedia articles
- web_search: DuckDuckGo web search
- visit_webpage: Read a specific URL as markdown
- read_spreadsheet: Parse Excel/CSV attachments
- execute_python_file: Run an attached .py file
- transcribe_audio: Speech-to-text for .mp3 attachments
- analyze_image: Vision Q&A for image attachments
- get_youtube_transcript: Captions from YouTube URLs in the question

Strategy:
1. Read the question carefully — note required OUTPUT FORMAT (number, list, names only, etc.).
2. If an attached file path is provided, use the matching file tool first.
3. For YouTube links in the question, use get_youtube_transcript.
4. Prefer wikipedia_search for Wikipedia-specific questions; otherwise web_search + visit_webpage.
5. For logic/reversal/math in the question text, reason directly — no tool needed.
6. Call final_answer(...) as soon as you are confident.

final_answer rules (CRITICAL — exact match grading):
- Return ONLY the answer string — no explanation, no preamble.
- Do NOT write "FINAL ANSWER" or "The answer is".
- Follow the question's format exactly (comma-separated, two decimals, last names only, etc.).
- Use the minimal text the question asks for.
"""


def maybe_unreverse(question: str) -> str:
    """Un-reverse GAIA-style reversed English prompts."""
    stripped = question.strip()
    if not stripped:
        return question
    reversed_q = stripped[::-1]
    starters = ("if you", "what ", "how ", "who ", "when ", "where ", "which ", "give ")
    if reversed_q.lower().startswith(starters):
        return reversed_q
    return question


def normalize_answer(text) -> str:
    """Strip common LLM formatting so exact-match grading has a better chance."""
    if text is None:
        return ""

    answer = str(text).strip()
    if answer.lower() in ("none", "null", "n/a", "unknown"):
        return ""

    # CodeAgent sometimes returns final_answer(...) as text instead of executing it.
    final_call = re.search(
        r"final_answer\s*\(\s*(['\"])(.*?)\1\s*\)",
        answer,
        flags=re.IGNORECASE | re.DOTALL,
    )
    if final_call:
        answer = final_call.group(2).strip()
    else:
        final_call = re.search(
            r"final_answer\s*\(\s*([^)]+)\s*\)",
            answer,
            flags=re.IGNORECASE,
        )
        if final_call:
            answer = final_call.group(1).strip().strip("'\"")

    # Drop leading "Thought:" blocks if a cleaner tail exists.
    if "Thought:" in answer and "\n" in answer:
        lines = [line.strip() for line in answer.splitlines() if line.strip()]
        if lines:
            answer = lines[-1]

    answer = re.sub(r"^```(?:\w+)?\s*|\s*```$", "", answer, flags=re.MULTILINE).strip()

    for prefix in (
        "FINAL ANSWER:",
        "Final answer:",
        "Final_answer",
        "FINAL_ANSWER",
        "Answer:",
        "The answer is:",
        "The answer is",
    ):
        if answer.lower().startswith(prefix.lower()):
            answer = answer[len(prefix) :].strip(" :")
            break

    if len(answer) >= 2 and answer[0] == answer[-1] and answer[0] in "\"'":
        answer = answer[1:-1].strip()

    # Reject obvious failure messages.
    lowered = answer.lower()
    if lowered.startswith("unfortunately") or lowered.startswith("i was unable"):
        return ""

    return answer


class BasicAgent:
    """Full GAIA agent with search, web, and multimodal file tools."""

    def __init__(self):
        model_id = os.getenv("HF_MODEL_ID", "Qwen/Qwen2.5-7B-Instruct")
        token = os.getenv("HF_TOKEN")

        model = InferenceClientModel(model_id=model_id, token=token)
        wiki_tool = WikipediaSearchTool(
            user_agent="HF-Agents-Course-Student (https://huggingface.co/agents-course)",
            language="en",
            content_type="text",
        )

        toolset = [
            wiki_tool,
            WebSearchTool(max_results=8),
            VisitWebpageTool(max_output_length=30000),
            *build_custom_tools(),
        ]

        self.agent = CodeAgent(
            tools=toolset,
            model=model,
            max_steps=int(os.getenv("AGENT_MAX_STEPS", "25")),
            verbosity_level=int(os.getenv("AGENT_VERBOSITY", "0")),
            additional_authorized_imports=["re", "json", "math"],
        )
        tool_names = [getattr(t, "name", str(t)) for t in toolset]
        print(f"BasicAgent initialized (model={model_id}, tools={tool_names}).")

    def __call__(
        self,
        question: str,
        task_id: str | None = None,
        file_path: str | Path | None = None,
    ) -> str:
        print(f"Agent received question (first 50 chars): {question[:50]}...")
        if file_path:
            print(f"Attached file: {file_path}")

        question = maybe_unreverse(question)
        path = Path(file_path) if file_path else None
        task = f"{ANSWER_INSTRUCTIONS.strip()}\n\nQuestion:\n{question}"
        task += file_context_block(path)

        raw = self.agent.run(task)
        answer = normalize_answer(raw)
        if not answer:
            print("Agent did not produce a final answer (max steps or empty result).")
            answer = "UNKNOWN"
        print(f"Agent returning answer: {answer[:120]}...")
        return answer