AI-Unit2 / gaia_agent.py
eigbney's picture
Upload gaia_agent.py
cbf3b4c verified
Raw
History Blame Contribute Delete
11.6 kB
"""
GAIA Benchmark Agent β€” Claude + web search
Produces a JSONL file ready to submit to:
https://huggingface.co/spaces/gaia-benchmark/leaderboard
Requirements:
pip install anthropic datasets huggingface_hub
Usage:
export ANTHROPIC_API_KEY="sk-ant-..."
huggingface-cli login # needed to access the gated GAIA dataset
python gaia_agent.py
Optional flags:
--split test | validation (default: test)
--level 1 | 2 | 3 | all (default: all)
--concurrency number of parallel calls (default: 3)
--no-search disable web search tool
--output path to output JSONL (default: submission.jsonl)
--limit max questions to run (default: all)
"""
import argparse
import json
import os
import re
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
import anthropic
from datasets import load_dataset
from huggingface_hub import snapshot_download
# ── Configuration ────────────────────────────────────────────────────────────
MODEL = "claude-sonnet-4-20250514"
MAX_TOKENS = 2048
SYSTEM_PROMPT = (
"You are a general AI assistant. I will ask you a question. "
"Report your thoughts, and finish your answer with the following template: "
"FINAL ANSWER: [YOUR FINAL ANSWER]. "
"YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma "
"separated list of numbers and/or strings. "
"If you are asked for a number, don't use comma to write your number neither use "
"units such as $ or percent sign unless specified otherwise. "
"If you are asked for a string, don't use articles, neither abbreviations "
"(e.g. for cities), and write the digits in plain text unless specified otherwise. "
"If you are asked for a comma separated list, apply the above rules depending of "
"whether the element to be put in the list is a number or a string."
)
WEB_SEARCH_TOOL = {
"type": "web_search_20250305",
"name": "web_search",
}
# ── Helpers ──────────────────────────────────────────────────────────────────
def extract_final_answer(text: str) -> str:
"""Pull the text after 'FINAL ANSWER:' from the model response."""
match = re.search(r"FINAL ANSWER:\s*(.+)", text, re.IGNORECASE)
if match:
return match.group(1).strip()
# Fallback: last non-empty line
lines = [l.strip() for l in text.strip().splitlines() if l.strip()]
return lines[-1] if lines else text.strip()
def build_question_content(example: dict, data_dir: str) -> list:
"""
Build the message content for a question.
Attaches any associated file (PDF or image) as a base64 document/image block.
"""
content = []
file_path = example.get("file_path", "")
if file_path:
full_path = Path(data_dir) / file_path
if full_path.exists():
suffix = full_path.suffix.lower()
try:
with open(full_path, "rb") as f:
import base64
b64 = base64.standard_b64encode(f.read()).decode()
if suffix == ".pdf":
content.append({
"type": "document",
"source": {
"type": "base64",
"media_type": "application/pdf",
"data": b64,
},
})
elif suffix in {".png", ".jpg", ".jpeg", ".gif", ".webp"}:
media_map = {
".png": "image/png", ".jpg": "image/jpeg",
".jpeg": "image/jpeg", ".gif": "image/gif",
".webp": "image/webp",
}
content.append({
"type": "image",
"source": {
"type": "base64",
"media_type": media_map[suffix],
"data": b64,
},
})
else:
# Plain text / CSV / other β€” embed as a document
text_data = full_path.read_text(errors="replace")
content.append({
"type": "text",
"text": f"[Attached file: {full_path.name}]\n{text_data}",
})
except Exception as e:
print(f" Warning: could not read attachment {full_path}: {e}")
content.append({"type": "text", "text": example["Question"]})
return content
def run_single(
client: anthropic.Anthropic,
example: dict,
data_dir: str,
use_search: bool,
retries: int = 3,
) -> dict:
"""Call the Claude API for one GAIA question and return a result dict."""
task_id = example["task_id"]
content = build_question_content(example, data_dir)
kwargs = dict(
model=MODEL,
max_tokens=MAX_TOKENS,
system=SYSTEM_PROMPT,
messages=[{"role": "user", "content": content}],
)
if use_search:
kwargs["tools"] = [WEB_SEARCH_TOOL]
for attempt in range(1, retries + 1):
try:
response = client.messages.create(**kwargs)
full_text = "".join(
block.text for block in response.content if hasattr(block, "text")
)
answer = extract_final_answer(full_text)
return {
"task_id": task_id,
"model_answer": answer,
"reasoning_trace": full_text,
}
except anthropic.RateLimitError:
wait = 2 ** attempt
print(f" Rate limit on {task_id}, waiting {wait}s…")
time.sleep(wait)
except Exception as e:
if attempt == retries:
print(f" Failed {task_id} after {retries} attempts: {e}")
return {
"task_id": task_id,
"model_answer": "",
"reasoning_trace": f"ERROR: {e}",
}
time.sleep(1)
# ── Main ─────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="GAIA benchmark agent using Claude")
parser.add_argument("--split", default="test",
choices=["test", "validation"])
parser.add_argument("--level", default="all",
choices=["1", "2", "3", "all"])
parser.add_argument("--concurrency", default=3, type=int)
parser.add_argument("--no-search", action="store_true")
parser.add_argument("--output", default="submission.jsonl")
parser.add_argument("--limit", default=None, type=int,
help="Run only the first N questions (useful for testing)")
args = parser.parse_args()
api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
sys.exit("Error: ANTHROPIC_API_KEY environment variable not set.")
client = anthropic.Anthropic(api_key=api_key)
use_search = not args.no_search
# ── Download GAIA dataset ────────────────────────────────────────────────
print("Downloading GAIA dataset from Hugging Face…")
print("(Make sure you have run 'huggingface-cli login' and accepted dataset terms)")
try:
data_dir = snapshot_download(
repo_id="gaia-benchmark/GAIA",
repo_type="dataset",
)
except Exception as e:
sys.exit(f"Dataset download failed: {e}\n"
"Run 'huggingface-cli login' and accept the dataset terms at "
"https://huggingface.co/datasets/gaia-benchmark/GAIA")
# ── Load split(s) ────────────────────────────────────────────────────────
config_map = {
"all": ["2023_level1", "2023_level2", "2023_level3"],
"1": ["2023_level1"],
"2": ["2023_level2"],
"3": ["2023_level3"],
}
configs = config_map[args.level]
examples = []
for cfg in configs:
try:
ds = load_dataset(data_dir, cfg, split=args.split)
examples.extend(list(ds))
print(f" Loaded {len(ds)} questions from {cfg}/{args.split}")
except Exception as e:
print(f" Warning: could not load {cfg}/{args.split}: {e}")
if not examples:
sys.exit("No questions loaded. Exiting.")
if args.limit:
examples = examples[: args.limit]
print(f"\nRunning {len(examples)} questions | "
f"model={MODEL} | concurrency={args.concurrency} | "
f"web_search={use_search}\n")
# ── Resume from existing output ──────────────────────────────────────────
done_ids = set()
output_path = Path(args.output)
if output_path.exists():
with open(output_path) as f:
for line in f:
try:
done_ids.add(json.loads(line)["task_id"])
except Exception:
pass
print(f"Resuming β€” {len(done_ids)} questions already answered.\n")
pending = [ex for ex in examples if ex["task_id"] not in done_ids]
if not pending:
print("All questions already answered. Nothing to do.")
return
# ── Run agent ────────────────────────────────────────────────────────────
total = len(pending)
completed = 0
errors = 0
with open(output_path, "a") as out_f:
with ThreadPoolExecutor(max_workers=args.concurrency) as executor:
futures = {
executor.submit(run_single, client, ex, data_dir, use_search): ex
for ex in pending
}
for future in as_completed(futures):
ex = futures[future]
completed += 1
try:
result = future.result()
if result["model_answer"]:
status = "βœ“"
else:
status = "βœ—"
errors += 1
print(
f"[{completed}/{total}] {status} {result['task_id']} "
f"β†’ {result['model_answer'][:60]}"
)
out_f.write(json.dumps(result) + "\n")
out_f.flush()
except Exception as e:
errors += 1
print(f"[{completed}/{total}] βœ— {ex['task_id']} β€” unexpected error: {e}")
print(f"\nDone. {completed - errors}/{total} answered successfully.")
print(f"Submission file: {output_path.resolve()}")
print("\nNext step: upload to https://huggingface.co/spaces/gaia-benchmark/leaderboard")
if __name__ == "__main__":
main()