First_agent_template / tools /attachment_reader.py
Realistvik0's picture
Update tools/attachment_reader.py
e915a10 verified
Raw History Blame Contribute Delete
7.09 kB
"""
tools/attachment_reader.py
Tool that lets the agent read a task's attached file from a local path,
plus a helper that downloads the attachment from the scoring API.
Optional dependencies (only needed for the matching file types):
pip install openpyxl # .xlsx
pip install pypdf # .pdf
pip install python-docx # .docx
"""
import os
import re
import mimetypes
import requests
import pandas as pd
from smolagents import Tool
ATTACHMENTS_DIR = "attachments"
MAX_CHARS = 20000
TEXT_EXTENSIONS = {".txt", ".md", ".json", ".py", ".html", ".xml", ".yaml", ".yml", ".log", ".jsonl"}
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"}
AUDIO_EXTENSIONS = {".mp3", ".wav", ".m4a", ".flac", ".ogg"}
# =========================================================
# Download helper
# =========================================================
def _filename_from_response(response: requests.Response, task_id: str) -> str:
"""Work out a sensible filename from the response headers."""
disposition = response.headers.get("Content-Disposition", "")
match = re.search(r'filename\*?=(?:UTF-8\'\')?"?([^";]+)"?', disposition)
if match:
return os.path.basename(match.group(1).strip())
content_type = response.headers.get("Content-Type", "").split(";")[0].strip()
extension = mimetypes.guess_extension(content_type) or ""
return f"{task_id}{extension}"
def _save_bytes(task_id: str, filename: str, data: bytes) -> str:
task_dir = os.path.join(ATTACHMENTS_DIR, str(task_id))
os.makedirs(task_dir, exist_ok=True)
path = os.path.join(task_dir, filename)
with open(path, "wb") as f:
f.write(data)
return path
def _download_from_scoring_api(api_url: str, task_id: str, timeout: int = 30) -> str | None:
"""Try the scoring API's /files/{task_id} endpoint. Returns a path or None."""
url = f"{api_url}/files/{task_id}"
try:
response = requests.get(url, timeout=timeout)
if response.status_code != 200 or not response.content:
print(
f"[attachment] {url} -> HTTP {response.status_code} "
f"{response.text[:150]!r}"
)
return None
path = _save_bytes(task_id, _filename_from_response(response, task_id), response.content)
print(f"[attachment] downloaded from scoring API: {path}")
return path
except Exception as e:
print(f"[attachment] scoring API download failed for {task_id}: {e}")
return None
def _download_from_gaia_dataset(task_id: str, file_name: str) -> str | None:
"""
Fallback: fetch the file straight from the gated GAIA dataset on the Hub.
Needs HF_TOKEN in the environment (a Space secret) and that your account
has accepted the terms at https://huggingface.co/datasets/gaia-benchmark/GAIA
"""
token = os.environ.get("HF_TOKEN")
if not token:
print("[attachment] HF_TOKEN not set; cannot use the GAIA dataset fallback.")
return None
try:
from huggingface_hub import hf_hub_download
cached = hf_hub_download(
repo_id="gaia-benchmark/GAIA",
filename=f"2023/validation/{file_name}",
repo_type="dataset",
token=token,
)
with open(cached, "rb") as f:
path = _save_bytes(task_id, os.path.basename(file_name), f.read())
print(f"[attachment] downloaded from GAIA dataset: {path}")
return path
except Exception as e:
print(f"[attachment] GAIA dataset fallback failed for {file_name}: {e}")
return None
def download_attachment(api_url: str, task_id: str, file_name: str | None = None,
timeout: int = 30) -> str | None:
"""
Gets the attachment for a task. Tries the scoring API first and, if that
fails and the task's file_name is known, falls back to the GAIA dataset.
Returns the local file path, or None. Never raises.
"""
path = _download_from_scoring_api(api_url, task_id, timeout)
if path is None and file_name:
path = _download_from_gaia_dataset(task_id, file_name)
return path
# =========================================================
# Reader tool
# =========================================================
def _truncate(text: str) -> str:
if len(text) <= MAX_CHARS:
return text
return text[:MAX_CHARS] + f"\n\n[... truncated, {len(text) - MAX_CHARS} more characters ...]"
def _read_spreadsheet(path: str, extension: str) -> str:
if extension == ".csv":
sheets = {"csv": pd.read_csv(path)}
else:
sheets = pd.read_excel(path, sheet_name=None)
parts = []
for name, df in sheets.items():
parts.append(f"=== Sheet: {name} ({df.shape[0]} rows x {df.shape[1]} columns) ===")
parts.append(df.to_csv(index=False))
return "\n".join(parts)
def _read_pdf(path: str) -> str:
from pypdf import PdfReader
reader = PdfReader(path)
return "\n\n".join(
f"--- Page {i + 1} ---\n{page.extract_text() or ''}"
for i, page in enumerate(reader.pages)
)
def _read_docx(path: str) -> str:
import docx
document = docx.Document(path)
return "\n".join(p.text for p in document.paragraphs)
class AttachmentReaderTool(Tool):
name = "read_attachment"
description = (
"Reads a file attached to the current task from a local path and returns "
"its contents as text. Supports spreadsheets (.xlsx, .xls, .csv), PDF, "
"Word (.docx) and plain text files. Use this when the question mentions "
"an attached file and a local path is provided."
)
inputs = {
"file_path": {
"type": "string",
"description": "Local path of the attached file, as given in the question.",
}
}
output_type = "string"
def forward(self, file_path: str) -> str:
if not os.path.exists(file_path):
return f"ERROR: file not found: {file_path}"
extension = os.path.splitext(file_path)[1].lower()
try:
if extension in {".xlsx", ".xlsm", ".xls", ".csv"}:
return _truncate(_read_spreadsheet(file_path, extension))
if extension == ".pdf":
return _truncate(_read_pdf(file_path))
if extension == ".docx":
return _truncate(_read_docx(file_path))
if extension in TEXT_EXTENSIONS:
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
return _truncate(f.read())
if extension in IMAGE_EXTENSIONS:
return "ERROR: image files cannot be read as text by this tool."
if extension in AUDIO_EXTENSIONS:
return "ERROR: audio files cannot be read as text by this tool."
# Unknown extension: try as text.
with open(file_path, "r", encoding="utf-8") as f:
return _truncate(f.read())
except Exception as e:
return f"ERROR: could not read {file_path}: {e}"