File size: 9,024 Bytes
fbfb168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
"""Tools for the GAIA Level-1 evaluation agent."""

from __future__ import annotations

import os
import re
import subprocess
import tempfile
from pathlib import Path

import requests
from langchain_core.tools import tool

API_URL = os.getenv("SCORING_API_URL", "https://agents-course-unit4-scoring.hf.space")
GAIA_REPO = "gaia-benchmark/GAIA"
FILES_DIR = Path(tempfile.gettempdir()) / "gaia_task_files"
FILES_DIR.mkdir(parents=True, exist_ok=True)

_GAIA_FILES: list[str] | None = None


def _truncate(text: str, limit: int = 1800) -> str:
    text = text.strip()
    if len(text) <= limit:
        return text
    return text[:limit] + "\n...[truncated]"


@tool
def wikipedia_search(query: str) -> str:
    """Search English Wikipedia and return a short page summary."""
    try:
        import wikipedia

        wikipedia.set_lang("en")
        results = wikipedia.search(query, results=3)
        if not results:
            return f"No Wikipedia results for: {query}"
        title = results[0]
        page = wikipedia.page(title, auto_suggest=False)
        return _truncate(f"Title: {page.title}\nURL: {page.url}\n\n{page.summary}")
    except Exception as e:  # noqa: BLE001
        return f"Wikipedia error: {e}"


@tool
def web_search(query: str) -> str:
    """Search the public web and return top result snippets."""
    try:
        try:
            from ddgs import DDGS
        except ImportError:
            from duckduckgo_search import DDGS

        rows = []
        with DDGS() as ddgs:
            for i, item in enumerate(ddgs.text(query, max_results=5), start=1):
                rows.append(
                    f"{i}. {item.get('title')}\n"
                    f"URL: {item.get('href')}\n"
                    f"{item.get('body')}"
                )
        return _truncate("\n\n".join(rows) if rows else f"No web results for: {query}")
    except Exception as e:  # noqa: BLE001
        return f"Web search error: {e}"


@tool
def youtube_transcript(url: str) -> str:
    """Fetch the transcript/captions text for a YouTube video URL."""
    try:
        from youtube_transcript_api import YouTubeTranscriptApi

        match = re.search(r"(?:v=|youtu\.be/)([A-Za-z0-9_-]{6,})", url)
        if not match:
            return "Could not parse YouTube video id from URL."
        video_id = match.group(1)
        api = YouTubeTranscriptApi()
        parts = api.fetch(video_id)
        text = " ".join(getattr(p, "text", str(p)) for p in parts)
        return _truncate(text, 3000)
    except Exception as e:  # noqa: BLE001
        return f"YouTube transcript error: {e}"


def _fetch_from_api(task_id: str) -> Path | None:
    resp = requests.get(f"{API_URL}/files/{task_id}", timeout=60)
    if resp.status_code != 200:
        return None
    filename = task_id
    match = re.search(r'filename="?([^";]+)"?', resp.headers.get("content-disposition", ""))
    if match:
        filename = match.group(1)
    path = FILES_DIR / filename
    path.write_bytes(resp.content)
    return path


def _fetch_from_gaia(task_id: str) -> Path | None:
    """The scoring API often has no file path; GAIA stores attachments as <task_id>.<ext>."""
    global _GAIA_FILES
    from huggingface_hub import hf_hub_download, list_repo_files

    token = os.getenv("HF_TOKEN")
    if _GAIA_FILES is None:
        _GAIA_FILES = list_repo_files(GAIA_REPO, repo_type="dataset", token=token)
    remote = next((f for f in _GAIA_FILES if Path(f).stem == task_id), None)
    if not remote:
        return None
    return Path(hf_hub_download(GAIA_REPO, remote, repo_type="dataset", token=token))


def _preview(path: Path) -> str:
    suffix = path.suffix.lower()
    if suffix in {".txt", ".py", ".csv", ".md", ".json", ".jsonld"}:
        return path.read_text(errors="ignore")[:1500]
    if suffix in {".xlsx", ".xls"}:
        return "Excel file saved. Use analyze_excel to compute values."
    if suffix in {".mp3", ".wav", ".m4a"}:
        return "Audio file saved. Use transcribe_audio to listen."
    if suffix in {".png", ".jpg", ".jpeg", ".webp"}:
        return "Image file saved. Use analyze_image to inspect it."
    if suffix == ".pdf":
        return "PDF file saved."
    return f"Binary file saved ({path.stat().st_size} bytes)."


@tool
def download_task_file(task_id: str) -> str:
    """Download the file attached to a GAIA task_id.

    Tries the scoring API first, then the GAIA dataset on the Hugging Face Hub.
    Returns the saved path plus a short content preview.
    """
    try:
        path = _fetch_from_api(task_id)
        source = "scoring API"
        if path is None:
            path = _fetch_from_gaia(task_id)
            source = "GAIA dataset"
        if path is None:
            return f"No file found for task_id {task_id}."
        return f"Saved to: {path} (via {source})\nPreview:\n{_preview(path)}"
    except Exception as e:  # noqa: BLE001
        if "gated" in str(e).lower() or "403" in str(e):
            return (
                f"The file for {task_id} lives in the gated GAIA dataset. Accept the terms "
                f"at https://huggingface.co/datasets/{GAIA_REPO} to enable downloads."
            )
        return f"download_task_file error: {e}"


@tool
def run_python_file(path: str) -> str:
    """Execute a local Python file and return stdout/stderr (for attached .py tasks)."""
    try:
        proc = subprocess.run(
            ["python", path],
            capture_output=True,
            text=True,
            timeout=30,
            cwd=str(Path(path).parent),
        )
        out = (proc.stdout or "") + (("\n" + proc.stderr) if proc.stderr else "")
        return _truncate(out.strip() or f"(no output, exit={proc.returncode})")
    except Exception as e:  # noqa: BLE001
        return f"run_python_file error: {e}"


@tool
def analyze_excel(path: str, question: str) -> str:
    """Read an Excel file and return sheet names plus a compact table preview to answer sales questions."""
    try:
        import pandas as pd

        xls = pd.ExcelFile(path)
        chunks = [f"Sheets: {xls.sheet_names}"]
        for sheet in xls.sheet_names:
            df = pd.read_excel(xls, sheet_name=sheet)
            chunks.append(f"\nSheet={sheet} columns={list(df.columns)}")
            chunks.append(df.head(30).to_csv(index=False))
            # helpful totals if numeric columns exist
            num = df.select_dtypes(include="number")
            if not num.empty:
                chunks.append("Numeric column sums:\n" + num.sum().to_string())
        chunks.append(f"\nQuestion reminder: {question}")
        return _truncate("\n".join(chunks), 3000)
    except Exception as e:  # noqa: BLE001
        return f"analyze_excel error: {e}"


@tool
def transcribe_audio(path: str) -> str:
    """Transcribe an audio file (mp3/wav) using Groq Whisper."""
    try:
        from groq import Groq

        client = Groq(api_key=os.getenv("GROQ_API_KEY"))
        with open(path, "rb") as f:
            result = client.audio.transcriptions.create(
                file=f,
                model="whisper-large-v3",
            )
        text = getattr(result, "text", None) or str(result)
        return _truncate(text, 3000)
    except Exception as e:  # noqa: BLE001
        return f"transcribe_audio error: {e}"


@tool
def analyze_image(path: str, question: str) -> str:
    """Answer a question about a local image file (chess positions, charts, photos).

    Needs GROQ_VISION_MODEL set to a vision-capable Groq model.
    """
    model = os.getenv("GROQ_VISION_MODEL")
    if not model:
        return (
            "No vision model is configured, so the image cannot be read. "
            "Answer from the question text alone."
        )
    try:
        import base64

        from groq import Groq

        image = Path(path)
        mime = "image/png" if image.suffix.lower() == ".png" else "image/jpeg"
        encoded = base64.b64encode(image.read_bytes()).decode()
        client = Groq(api_key=os.getenv("GROQ_API_KEY"))
        resp = client.chat.completions.create(
            model=model,
            temperature=0,
            messages=[
                {
                    "role": "user",
                    "content": [
                        {"type": "text", "text": question},
                        {
                            "type": "image_url",
                            "image_url": {"url": f"data:{mime};base64,{encoded}"},
                        },
                    ],
                }
            ],
        )
        return _truncate(resp.choices[0].message.content or "")
    except Exception as e:  # noqa: BLE001
        return f"analyze_image error: {e}"


@tool
def reverse_text(text: str) -> str:
    """Reverse a string. Useful when a question is written backwards."""
    return text[::-1]


TOOLS = [
    wikipedia_search,
    web_search,
    youtube_transcript,
    download_task_file,
    run_python_file,
    analyze_excel,
    transcribe_audio,
    analyze_image,
    reverse_text,
]