| """ |
| 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 |
|
|
| |
|
|
| 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", |
| } |
|
|
| |
|
|
| 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() |
| |
| 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: |
| |
| 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) |
|
|
| |
|
|
| 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 |
|
|
| |
| 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") |
|
|
| |
| 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") |
|
|
| |
| 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 |
|
|
| |
| 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() |
|
|