MananSiingh's picture
Upload 3 files
b0037d9 verified
Raw
History Blame
18.3 kB
"""GAIA Level-1 agent for the HF Agents Course final assignment.
Built on smolagents CodeAgent. Works with ANY of these free LLM backends —
set whichever API key you can get and the agent auto-detects it:
GROQ_API_KEY console.groq.com/keys (free, no card, fast)
CEREBRAS_API_KEY cloud.cerebras.ai (free tier)
OPENROUTER_API_KEY openrouter.ai/keys (has free models)
MISTRAL_API_KEY console.mistral.ai (free tier)
GOOGLE_API_KEY aistudio.google.com/apikey (free, best multimodal)
HF_TOKEN huggingface.co/settings/tokens (needs credits)
Generic escape hatch for any other OpenAI-compatible endpoint:
OPENAI_API_KEY + OPENAI_BASE_URL + AGENT_MODEL
Override the model with AGENT_MODEL if a default model id has been retired.
"""
import base64
import mimetypes
import os
import re
import tempfile
import time
import requests
from smolagents import (
CodeAgent,
DuckDuckGoSearchTool,
VisitWebpageTool,
tool,
)
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
WIKI_UA = "HF-Agents-Course-GAIA-Agent/1.0 (educational use)"
# Provider registry: env var -> (base_url, default model, label)
# Model ids are defaults only; override with AGENT_MODEL if one is retired.
PROVIDERS = [
(
"GROQ_API_KEY",
"https://api.groq.com/openai/v1",
"llama-3.3-70b-versatile",
"Groq",
),
(
"CEREBRAS_API_KEY",
"https://api.cerebras.ai/v1",
"llama-3.3-70b",
"Cerebras",
),
(
"OPENROUTER_API_KEY",
"https://openrouter.ai/api/v1",
"meta-llama/llama-3.3-70b-instruct:free",
"OpenRouter",
),
(
"MISTRAL_API_KEY",
"https://api.mistral.ai/v1",
"mistral-large-latest",
"Mistral",
),
(
"GOOGLE_API_KEY",
"https://generativelanguage.googleapis.com/v1beta/openai/",
"gemini-2.5-flash",
"Gemini",
),
(
"OPENAI_API_KEY",
os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"),
"gpt-4o-mini",
"OpenAI-compatible",
),
]
def _active_provider():
"""Return (api_key, base_url, model_id, label) for the first key found."""
for env_var, base_url, default_model, label in PROVIDERS:
key = os.getenv(env_var)
if key:
return key, base_url, os.getenv("AGENT_MODEL", default_model), label
return None, None, None, None
API_KEY, BASE_URL, MODEL_ID, PROVIDER = _active_provider()
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "")
# --------------------------------------------------------------------------
# Gemini-only helper: native audio / video understanding
# --------------------------------------------------------------------------
def _gemini_generate(parts: list, retries: int = 3) -> str:
model = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
url = (
"https://generativelanguage.googleapis.com/v1beta/models/"
f"{model}:generateContent?key={GOOGLE_API_KEY}"
)
for attempt in range(retries):
resp = requests.post(url, json={"contents": [{"parts": parts}]}, timeout=180)
if resp.status_code == 429 and attempt < retries - 1:
time.sleep(20 * (attempt + 1))
continue
resp.raise_for_status()
return resp.json()["candidates"][0]["content"]["parts"][0]["text"]
return "ERROR: Gemini rate limited."
def _inline_part(file_path: str) -> dict:
mime = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
with open(file_path, "rb") as f:
return {
"inline_data": {
"mime_type": mime,
"data": base64.b64encode(f.read()).decode(),
}
}
# --------------------------------------------------------------------------
# Tools
# --------------------------------------------------------------------------
@tool
def wikipedia_page(title: str) -> str:
"""Fetch the full plain text of an English Wikipedia article. Use this
instead of visit_webpage for Wikipedia — it never gets blocked.
Args:
title: Article title, e.g. "Mercedes Sosa" or "1928 Summer Olympics".
"""
try:
resp = requests.get(
"https://en.wikipedia.org/w/api.php",
params={
"action": "query",
"prop": "extracts",
"explaintext": 1,
"redirects": 1,
"format": "json",
"titles": title,
},
headers={"User-Agent": WIKI_UA},
timeout=45,
)
resp.raise_for_status()
pages = resp.json()["query"]["pages"]
page = list(pages.values())[0]
if "extract" not in page:
return f"No Wikipedia article found for '{title}'. Try wikipedia_search."
return page["extract"][:60000]
except Exception as e:
return f"ERROR fetching Wikipedia page: {e}"
@tool
def wikipedia_search(query: str) -> str:
"""Search English Wikipedia and return matching article titles with snippets.
Use this to find the right title, then call wikipedia_page.
Args:
query: Search terms.
"""
try:
resp = requests.get(
"https://en.wikipedia.org/w/api.php",
params={
"action": "query",
"list": "search",
"srsearch": query,
"srlimit": 10,
"format": "json",
},
headers={"User-Agent": WIKI_UA},
timeout=45,
)
resp.raise_for_status()
hits = resp.json()["query"]["search"]
return "\n".join(
f"- {h['title']}: {re.sub('<[^<]+?>', '', h['snippet'])}" for h in hits
) or "No results."
except Exception as e:
return f"ERROR searching Wikipedia: {e}"
@tool
def fetch_url(url: str) -> str:
"""Fetch a web page as text with a browser-like user agent. Use when
visit_webpage fails with a 403 Forbidden error.
Args:
url: The full URL to fetch.
"""
try:
resp = requests.get(
url,
headers={
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/122.0 Safari/537.36"
)
},
timeout=60,
)
resp.raise_for_status()
try:
from markdownify import markdownify
text = markdownify(resp.text)
except Exception:
text = resp.text
text = re.sub(r"\n{3,}", "\n\n", text)
return text[:50000]
except Exception as e:
return f"ERROR fetching url: {e}"
@tool
def transcribe_audio(file_path: str) -> str:
"""Transcribe a local audio file (mp3/wav/m4a) to English text.
Args:
file_path: Absolute path to the local audio file to transcribe.
"""
# Gemini: native audio understanding
if GOOGLE_API_KEY:
try:
return _gemini_generate(
[{"text": "Transcribe this audio verbatim."}, _inline_part(file_path)]
)
except Exception as e:
return f"ERROR transcribing audio: {e}"
# Groq hosts Whisper on an OpenAI-compatible endpoint
if os.getenv("GROQ_API_KEY"):
try:
with open(file_path, "rb") as f:
resp = requests.post(
"https://api.groq.com/openai/v1/audio/transcriptions",
headers={"Authorization": f"Bearer {os.getenv('GROQ_API_KEY')}"},
files={"file": (os.path.basename(file_path), f)},
data={"model": os.getenv("ASR_MODEL", "whisper-large-v3")},
timeout=180,
)
resp.raise_for_status()
return resp.json()["text"]
except Exception as e:
return f"ERROR transcribing audio: {e}"
# HF Inference fallback
try:
from huggingface_hub import InferenceClient
result = InferenceClient(
token=os.getenv("HF_TOKEN")
).automatic_speech_recognition(
file_path, model=os.getenv("ASR_MODEL", "openai/whisper-large-v3")
)
return result.text if hasattr(result, "text") else str(result)
except Exception as e:
return f"ERROR transcribing audio (no ASR backend available): {e}"
@tool
def analyze_image(file_path: str, question: str) -> str:
"""Answer a question about a local image file using a vision model.
Args:
file_path: Absolute path to the local image file (png/jpg).
question: The question to answer about the image. Be specific; for
chess positions, ask for a full square-by-square board reading
AND the winning move, verified carefully.
"""
if GOOGLE_API_KEY:
try:
return _gemini_generate([{"text": question}, _inline_part(file_path)])
except Exception as e:
return f"ERROR analyzing image: {e}"
if not API_KEY:
return "ERROR: no vision backend configured."
try:
mime = mimetypes.guess_type(file_path)[0] or "image/png"
with open(file_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
vision_model = os.getenv("VISION_MODEL", MODEL_ID)
resp = requests.post(
BASE_URL.rstrip("/") + "/chat/completions",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": vision_model,
"max_tokens": 1500,
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": question},
{
"type": "image_url",
"image_url": {"url": f"data:{mime};base64,{b64}"},
},
],
}
],
},
timeout=180,
)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"]
except Exception as e:
return (
f"ERROR analyzing image: {e}. The model may not support images; "
"set VISION_MODEL to a vision-capable model id."
)
@tool
def analyze_youtube_video(video_url: str, question: str) -> str:
"""Watch a YouTube video and answer a question about its visual and audio
content (counting things on screen, quotes, scenes). Requires a Gemini key.
Args:
video_url: Full YouTube URL, e.g. https://www.youtube.com/watch?v=XXXX
question: The question to answer about the video.
"""
if not GOOGLE_API_KEY:
return (
"ERROR: video analysis needs GOOGLE_API_KEY. Use "
"get_youtube_transcript or web_search for descriptions instead."
)
try:
return _gemini_generate(
[{"text": question}, {"file_data": {"file_uri": video_url}}]
)
except Exception as e:
return f"ERROR analyzing video: {e}"
@tool
def get_youtube_transcript(video_url: str) -> str:
"""Fetch the transcript/captions of a YouTube video as plain text.
Args:
video_url: Full YouTube URL, e.g. https://www.youtube.com/watch?v=XXXX
"""
try:
from youtube_transcript_api import YouTubeTranscriptApi
m = re.search(r"(?:v=|youtu\.be/)([\w-]{11})", video_url)
if not m:
return "ERROR: could not parse video id from URL."
vid = m.group(1)
try:
entries = [s.text for s in YouTubeTranscriptApi().fetch(vid)]
except AttributeError:
entries = [s["text"] for s in YouTubeTranscriptApi.get_transcript(vid)]
return " ".join(entries)[:20000]
except Exception as e:
return f"ERROR fetching transcript: {e}. Try web_search instead."
@tool
def read_file_as_text(file_path: str) -> str:
"""Read a local text-like file (py, txt, csv, json, md) and return its content.
Args:
file_path: Absolute path to the local file.
"""
try:
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
return f.read()[:30000]
except Exception as e:
return f"ERROR reading file: {e}"
# --------------------------------------------------------------------------
# Answer-format guidance (GAIA is scored by EXACT MATCH)
# --------------------------------------------------------------------------
GAIA_INSTRUCTIONS = """You are a general AI assistant answering a benchmark
question scored by EXACT string match. Work step by step with your tools,
then call final_answer() with ONLY the answer itself.
Formatting rules for the final answer (critical):
- Do NOT write "FINAL ANSWER" or any prefix/suffix, explanation, or period
at the end. Output the bare answer only.
- Numbers: plain digits, no thousands separators, no units ($, %, kg) unless
the question explicitly asks for them, no trailing ".0".
- Strings: no articles ("the", "a"), no abbreviations unless asked.
- Comma-separated lists: apply the rules above to each element, use ", "
(comma + space) between elements, and respect any ordering the question
asks for (e.g. alphabetical, ascending).
- If asked for a first name / last name / city / country code only, return
exactly that and nothing more.
Tool strategy:
- Wikipedia questions: use wikipedia_search then wikipedia_page. Do NOT use
visit_webpage on wikipedia.org — it returns 403. The article text often
contains a discography or results table; read it carefully and count.
- If visit_webpage returns 403 Forbidden, retry that URL with fetch_url.
- Attached files: a local path is given; use read_file_as_text,
transcribe_audio, analyze_image, or pandas (pd.read_excel) for .xlsx.
- For .xlsx, inspect the columns first, then compute. Format money like
89706.00 only when the question asks for two decimal places.
- Python-code questions: read the code and reason through it carefully.
- YouTube: try analyze_youtube_video, then get_youtube_transcript, then
web_search for third-party descriptions of the video.
- Some questions are pure reasoning (reversed text, a group-theory table).
Solve those directly in python without searching.
Reliability rules:
- Never give up and guess a number you did not verify. If one source is
blocked, try another tool or another source.
- Re-read the question's exact wording before answering (e.g. "included",
"as of July 2023", "IOC country code", "without abbreviations",
"first name only").
"""
class GAIAAgent:
"""Wraps a smolagents CodeAgent with GAIA-specific tooling and prompting."""
def __init__(self):
if not API_KEY:
raise RuntimeError(
"No LLM API key found. Set one of: GROQ_API_KEY, "
"CEREBRAS_API_KEY, OPENROUTER_API_KEY, MISTRAL_API_KEY, "
"GOOGLE_API_KEY, or OPENAI_API_KEY (+OPENAI_BASE_URL)."
)
from smolagents import OpenAIServerModel
model = OpenAIServerModel(
model_id=MODEL_ID, api_base=BASE_URL, api_key=API_KEY
)
self.agent = CodeAgent(
model=model,
tools=[
DuckDuckGoSearchTool(),
VisitWebpageTool(),
fetch_url,
wikipedia_search,
wikipedia_page,
transcribe_audio,
analyze_image,
analyze_youtube_video,
get_youtube_transcript,
read_file_as_text,
],
additional_authorized_imports=[
"pandas",
"numpy",
"openpyxl",
"json",
"csv",
"re",
"math",
"statistics",
"itertools",
"collections",
"datetime",
],
max_steps=15,
)
print(f"GAIAAgent initialized (backend={PROVIDER}, model={MODEL_ID}).")
@staticmethod
def download_task_file(task_id: str, file_name: str) -> str | None:
"""Download the file attached to a task; returns a local path or None."""
if not file_name:
return None
url = f"{DEFAULT_API_URL}/files/{task_id}"
for attempt in range(4):
try:
resp = requests.get(url, timeout=30)
resp.raise_for_status()
fd, path = tempfile.mkstemp(suffix=os.path.splitext(file_name)[1] or "")
with os.fdopen(fd, "wb") as f:
f.write(resp.content)
return path
except Exception as e:
print(f"File download attempt {attempt + 1} failed for {task_id}: {e}")
time.sleep(3 * (attempt + 1))
return None
@staticmethod
def _clean(answer: str) -> str:
"""Strip wrappers the model sometimes adds despite instructions."""
a = str(answer).strip()
a = re.sub(r"^(final answer\s*:?\s*)", "", a, flags=re.IGNORECASE)
a = a.strip().strip('"').strip("'").strip()
if a.endswith("."):
a = a[:-1]
return a
def __call__(self, question: str, task_id: str = "", file_name: str = "") -> str:
prompt = GAIA_INSTRUCTIONS + "\n\nQuestion: " + question
file_path = self.download_task_file(task_id, file_name)
if file_path:
prompt += (
f"\n\nAn attached file for this question was downloaded to the "
f"local path: {file_path} (original name: {file_name})."
)
elif file_name:
prompt += (
f"\n\nNOTE: this question references an attached file "
f"({file_name}) that could not be downloaded. Answer from "
f"other sources if possible."
)
try:
return self._clean(self.agent.run(prompt))
except Exception as e:
print(f"Agent error on task {task_id}: {e}")
return f"AGENT ERROR: {e}"