chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
# Agent Framework Lab - GAIA
The GAIA benchmark can be used for evaluating agents and workflows built using the Agent Framework.
It includes built-in benchmarks as well as utilities for running custom evaluations.
> **Note**: This module is part of the consolidated `agent-framework-lab` package. Install the package with the `gaia` extra to use this module.
## Setup
Install the `agent-framework-lab` package with GAIA dependencies:
```bash
pip install "agent-framework-lab[gaia]"
```
Set up Hugging Face token:
```bash
export HF_TOKEN="hf\*..." # must have access to gaia-benchmark/GAIA
```
## Create an evaluation script
Create a Python script (e.g., `run_gaia.py`) with the following content:
```python
from agent_framework.lab.gaia import GAIA, Task, Prediction, GAIATelemetryConfig
async def run_task(task: Task) -> Prediction:
return Prediction(prediction="answer here", messages=[])
async def main() -> None:
# Optional: Enable telemetry for detailed tracing
telemetry_config = GAIATelemetryConfig(
enable_tracing=True,
trace_to_file=True,
file_path="gaia_traces.jsonl"
)
runner = GAIA(telemetry_config=telemetry_config)
await runner.run(run_task, level=1, max_n=5, parallel=2)
```
See the [gaia_sample.py](./samples/gaia_sample.py) for more detail.
## View results
We provide a console viewer for reading GAIA results:
```bash
uv run gaia_viewer "gaia_results_<timestamp>.jsonl" --detailed
```
@@ -0,0 +1,26 @@
# Copyright (c) Microsoft. All rights reserved.
"""GAIA benchmark module for Agent Framework."""
import importlib.metadata
from ._types import Evaluation, Evaluator, Prediction, Task, TaskResult, TaskRunner
from .gaia import GAIA, GAIATelemetryConfig, gaia_scorer, viewer_main
try:
__version__ = importlib.metadata.version(__name__)
except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0" # Fallback for development mode
__all__ = [
"GAIA",
"Evaluation",
"Evaluator",
"GAIATelemetryConfig",
"Prediction",
"Task",
"TaskResult",
"TaskRunner",
"gaia_scorer",
"viewer_main",
]
@@ -0,0 +1,79 @@
# Copyright (c) Microsoft. All rights reserved.
"""Common types for agent evaluation."""
from dataclasses import dataclass
from typing import Any, Protocol, runtime_checkable
__all__ = [
"Evaluation",
"Evaluator",
"Prediction",
"Task",
"TaskResult",
"TaskRunner",
]
@dataclass
class Task:
"""Represents a task to be evaluated."""
task_id: str
question: str
answer: str | None = None
level: int | None = None
file_name: str | None = None
metadata: dict[str, Any] | None = None
@dataclass
class Prediction:
"""Represents a prediction made by an agent for a task."""
prediction: str
messages: list[Any] | None = None
metadata: dict[str, Any] | None = None
def __post_init__(self) -> None:
if self.messages is None:
self.messages = []
@dataclass
class Evaluation:
"""Represents the evaluation result of a prediction."""
is_correct: bool
score: float
details: dict[str, Any] | None = None
@dataclass
class TaskResult:
"""Complete result for a single task evaluation."""
task_id: str
task: Task
prediction: Prediction
evaluation: Evaluation
runtime_seconds: float | None = None
error: str | None = None
@runtime_checkable
class TaskRunner(Protocol):
"""Protocol for running tasks."""
async def __call__(self, task: Task) -> Prediction:
"""Run a single task and return the prediction."""
...
@runtime_checkable
class Evaluator(Protocol):
"""Protocol for evaluating predictions."""
async def __call__(self, task: Task, prediction: Prediction) -> Evaluation:
"""Evaluate a prediction for a given task."""
...
@@ -0,0 +1,712 @@
# Copyright (c) Microsoft. All rights reserved.
"""GAIA benchmark implementation for Agent Framework."""
import asyncio
import json
import os
import random
import re
import string
import tempfile
import time
from collections.abc import Callable, Iterable
from datetime import datetime
from functools import lru_cache
from pathlib import Path
from typing import Any, Protocol, cast
from opentelemetry.trace import NoOpTracer, SpanKind, get_tracer
from tqdm import tqdm
from ._types import Evaluation, Evaluator, Prediction, Task, TaskResult, TaskRunner
__all__ = ["GAIA", "GAIATelemetryConfig", "gaia_scorer"]
class _OrjsonModule(Protocol):
def dumps(self, obj: object, /, default: Callable[[Any], object] | None = None) -> bytes: ...
def loads(self, obj: str | bytes | bytearray, /) -> object: ...
@lru_cache(maxsize=1)
def _get_orjson() -> _OrjsonModule | None:
try:
import orjson as runtime_orjson
except ImportError:
return None
return cast(_OrjsonModule, runtime_orjson)
def _dump_json_line(value: object) -> str:
if (runtime_orjson := _get_orjson()) is not None:
return runtime_orjson.dumps(value, default=str).decode("utf-8")
return json.dumps(value, default=str)
def _load_json_value(value: str | bytes) -> object:
if (runtime_orjson := _get_orjson()) is not None:
return runtime_orjson.loads(value)
return json.loads(value)
class GAIATelemetryConfig:
"""Configuration for GAIA telemetry and tracing."""
def __init__(
self,
enable_tracing: bool = False,
otlp_endpoint: str | None = None,
trace_to_file: bool = False,
file_path: str | None = None,
):
"""Initialize telemetry configuration.
Args:
enable_tracing: Whether to enable OpenTelemetry tracing
otlp_endpoint: OTLP endpoint for trace export
trace_to_file: Whether to export traces to local file
file_path: Path for local file export (defaults to gaia_traces.json)
Note:
For Azure Monitor integration, configure using environment variables
(OTEL_EXPORTER_OTLP_ENDPOINT, etc.) or call ``configure_azure_monitor()``
before creating the GAIA instance.
"""
self.enable_tracing = enable_tracing
self.otlp_endpoint = otlp_endpoint
self.trace_to_file = trace_to_file
self.file_path = file_path or "gaia_traces.json"
def configure_otel_providers(self) -> None:
"""Set up OpenTelemetry based on configuration."""
if not self.enable_tracing:
return
# If only file tracing is requested (no OTLP),
# skip the default configure_otel_providers which adds console exporter
if self.trace_to_file and not self.otlp_endpoint:
# Set up minimal tracing with only file export
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.trace import set_tracer_provider
tracer_provider = TracerProvider()
set_tracer_provider(tracer_provider)
self._setup_file_export()
else:
# Use full observability setup for OTLP
from agent_framework.observability import configure_otel_providers
# Set OTLP endpoint env var if provided
if self.otlp_endpoint:
import os
os.environ.setdefault("OTEL_EXPORTER_OTLP_ENDPOINT", self.otlp_endpoint)
configure_otel_providers(
enable_sensitive_data=True, # Enable for detailed task traces
)
# Set up local file export if requested
if self.trace_to_file:
self._setup_file_export()
def _setup_file_export(self) -> None:
"""Set up local file export for traces."""
try:
import json
import os
from collections.abc import Sequence
from opentelemetry.sdk.trace import ReadableSpan, TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter, SpanExportResult
from opentelemetry.trace import get_tracer_provider
class FileSpanExporter(SpanExporter):
def __init__(self, file_path: str):
self.file_path = file_path
# Ensure directory exists
os.makedirs(os.path.dirname(os.path.abspath(file_path)), exist_ok=True)
def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
try:
with open(self.file_path, "a", encoding="utf-8") as f:
for span in spans:
span_data = {
"trace_id": format(span.context.trace_id, "032x") if span.context else "unknown",
"span_id": format(span.context.span_id, "016x") if span.context else "unknown",
"name": span.name,
"start_time": span.start_time,
"end_time": span.end_time,
"duration_ns": (span.end_time - span.start_time)
if (span.end_time and span.start_time)
else None,
"attributes": dict(span.attributes) if span.attributes else {},
"status": {
"status_code": span.status.status_code.name if span.status else "UNSET",
"description": span.status.description if span.status else None,
},
}
f.write(json.dumps(span_data, default=str) + "\n")
return SpanExportResult.SUCCESS
except Exception:
return SpanExportResult.FAILURE
def shutdown(self) -> None:
pass
tracer_provider = get_tracer_provider()
if isinstance(tracer_provider, TracerProvider):
file_exporter = FileSpanExporter(self.file_path)
tracer_provider.add_span_processor(BatchSpanProcessor(file_exporter))
except ImportError:
print("Warning: Could not set up file export for traces. Missing dependencies.")
def _normalize_number_str(number_str: str) -> float:
"""Normalize a number string for comparison."""
for ch in ["$", "%", ","]:
number_str = number_str.replace(ch, "")
try:
return float(number_str)
except ValueError:
return float("inf")
def _split_string(s: str, chars: list[str] | None = None) -> list[str]:
"""Split string by multiple delimiters."""
if chars is None:
chars = [",", ";"]
return re.split(f"[{''.join(chars)}]", s)
def _normalize_str(s: str, remove_punct: bool = True) -> str:
"""Normalize string for comparison."""
no_spaces = re.sub(r"\s", "", s or "")
if remove_punct:
table = str.maketrans("", "", string.punctuation)
return no_spaces.lower().translate(table)
return no_spaces.lower()
def gaia_scorer(model_answer: str | None, ground_truth: str) -> bool:
"""Official GAIA scoring function.
Args:
model_answer: The model's answer
ground_truth: The ground truth answer
Returns:
True if the answer is correct, False otherwise
"""
def is_float(x: Any) -> bool:
try:
float(x)
return True
except Exception:
return False
if model_answer is None:
model_answer = "None"
if is_float(ground_truth):
# numeric exact match after normalization
return abs(_normalize_number_str(model_answer) - float(ground_truth)) < 1e-6
if any(ch in ground_truth for ch in [",", ";"]):
# list with per-element compare (number or string)
gt_elems = _split_string(ground_truth)
ma_elems = _split_string(model_answer)
if len(gt_elems) != len(ma_elems):
return False
comparisons: list[bool] = []
for ma, gt in zip(ma_elems, gt_elems, strict=False):
if is_float(gt):
comparisons.append(abs(_normalize_number_str(ma) - float(gt)) < 1e-6)
else:
comparisons.append(_normalize_str(ma, remove_punct=False) == _normalize_str(gt, remove_punct=False))
return all(comparisons)
# string normalize + exact
return _normalize_str(model_answer) == _normalize_str(ground_truth)
def _coerce_record(raw: object) -> dict[str, Any] | None:
if isinstance(raw, dict):
raw_dict = cast(dict[object, Any], raw)
if all(isinstance(key, str) for key in raw_dict):
return cast(dict[str, Any], raw_dict)
return None
def _parse_level(level: object) -> int | None:
if isinstance(level, int):
return level
if isinstance(level, str) and level.isdigit():
return int(level)
return None
def _read_jsonl(path: Path) -> Iterable[dict[str, Any]]:
"""Read JSONL file and yield parsed records."""
with path.open("rb") as f:
for line in f:
if not line.strip():
continue
parsed = _load_json_value(line)
record = _coerce_record(parsed)
if record is not None:
yield record
def _load_gaia_local(repo_dir: Path, wanted_levels: list[int] | None = None, max_n: int | None = None) -> list[Task]:
"""Load GAIA tasks from local repository directory."""
tasks: list[Task] = []
# First try to load from parquet files (new format)
# Prioritize validation split over test split (validation has answers)
parquet_files = sorted(
repo_dir.rglob("metadata*.parquet"), key=lambda p: (0 if "validation" in str(p) else 1, str(p))
)
for p in parquet_files:
try:
import pyarrow.parquet as pq
pq_any = cast(Any, pq)
table: Any = pq_any.read_table(p)
rows = cast(list[object], table.to_pylist())
for row in rows:
record = _coerce_record(row)
if record is None:
continue
# Robustly extract fields used across variants
q_obj = record.get("Question") or record.get("question") or record.get("query") or record.get("prompt")
ans = record.get("Final answer") or record.get("answer") or record.get("final_answer")
if not isinstance(q_obj, str):
continue
q = q_obj
qid = str(
record.get("task_id")
or record.get("question_id")
or record.get("id")
or record.get("uuid")
or f"{p.stem}:{len(tasks)}"
)
lvl = _parse_level(record.get("Level") or record.get("level"))
fname_obj = record.get("file_name") or record.get("filename")
fname = fname_obj if isinstance(fname_obj, str) else None
# Only evaluate examples with public answers (dev/validation split)
# Skip if no question, no answer, or answer is placeholder like "?"
if ans is None or str(ans).strip() in ["?", ""]:
continue
if wanted_levels and (lvl not in wanted_levels):
continue
tasks.append(
Task(task_id=qid, question=q, answer=str(ans), level=lvl, file_name=fname, metadata=record)
)
except ImportError:
print("Warning: pyarrow not installed. Install with: pip install pyarrow")
continue
except Exception as e:
print(f"Warning: Could not load parquet file {p}: {e}")
continue
# Fall back to jsonl files (old format) if no parquet files found
if not tasks:
for p in repo_dir.rglob("metadata.jsonl"):
for rec in _read_jsonl(p):
# Robustly extract fields used across variants
q_obj = rec.get("Question") or rec.get("question") or rec.get("query") or rec.get("prompt")
ans = rec.get("Final answer") or rec.get("answer") or rec.get("final_answer")
if not isinstance(q_obj, str):
continue
q = q_obj
qid = str(
rec.get("task_id")
or rec.get("question_id")
or rec.get("id")
or rec.get("uuid")
or f"{p.stem}:{len(tasks)}"
)
lvl = _parse_level(rec.get("Level") or rec.get("level"))
fname_obj = rec.get("file_name") or rec.get("filename")
fname = fname_obj if isinstance(fname_obj, str) else None
# Only evaluate examples with public answers (dev/validation split)
# Skip if no question, no answer, or answer is placeholder like "?"
if ans is None or str(ans).strip() in ["?", ""]:
continue
if wanted_levels and (lvl not in wanted_levels):
continue
tasks.append(Task(task_id=qid, question=q, answer=str(ans), level=lvl, file_name=fname, metadata=rec))
# Shuffle to help with rate-limits and fairness if max_n is provided
random.shuffle(tasks)
if max_n:
tasks = tasks[:max_n]
return tasks
class GAIA:
"""GAIA benchmark runner for Agent Framework.
GAIA (General AI Assistant) is a benchmark for general-purpose AI assistants.
This class provides utilities to run the benchmark with custom agents.
"""
def __init__(
self,
evaluator: Evaluator | None = None,
data_dir: str | None = None,
hf_token: str | None = None,
telemetry_config: GAIATelemetryConfig | None = None,
):
"""Initialize GAIA benchmark runner.
Args:
evaluator: Custom evaluator function. If None, uses default GAIA scorer.
data_dir: Directory to cache GAIA data. Defaults to a temporary directory.
hf_token: Hugging Face token for accessing the GAIA dataset.
telemetry_config: Configuration for telemetry and tracing. If None, no tracing is performed.
"""
self.evaluator = evaluator or self._default_evaluator
self.data_dir = Path(data_dir or Path(tempfile.gettempdir()) / "data_gaia_hub")
self.hf_token = hf_token
self.telemetry_config = telemetry_config or GAIATelemetryConfig()
# Set up telemetry
self.telemetry_config.configure_otel_providers()
# Initialize tracer
if self.telemetry_config.enable_tracing:
self.tracer = get_tracer("gaia_benchmark", "1.0.0")
else:
self.tracer = NoOpTracer()
async def _default_evaluator(self, task: Task, prediction: Prediction) -> Evaluation:
"""Default evaluator using GAIA official scoring."""
is_correct = gaia_scorer(prediction.prediction, task.answer or "")
return Evaluation(is_correct=is_correct, score=1.0 if is_correct else 0.0)
def _ensure_data(self) -> Path:
"""Ensure GAIA data is available locally."""
if self.data_dir.exists() and any(self.data_dir.rglob("metadata.jsonl")):
return self.data_dir
# Download data if not available
token = self.hf_token or os.environ.get("HF_TOKEN")
if not token:
raise RuntimeError(
"HF_TOKEN environment variable or hf_token parameter is required "
"to access the GAIA dataset. Please set your Hugging Face token "
"with access to gaia-benchmark/GAIA."
)
import huggingface_hub
hf_hub = cast(Any, huggingface_hub)
local_dir = hf_hub.snapshot_download(
repo_id="gaia-benchmark/GAIA",
repo_type="dataset",
revision="682dd723ee1e1697e00360edccf2366dc8418dd9",
token=token,
local_dir=str(self.data_dir),
force_download=False,
)
if not isinstance(local_dir, str):
raise TypeError("snapshot_download returned unexpected non-string path")
return Path(local_dir)
async def _run_single_task(
self, task: Task, task_runner: TaskRunner, semaphore: asyncio.Semaphore, timeout: int | None = None
) -> TaskResult:
"""Run a single task with error handling and timing."""
async with semaphore:
with self.tracer.start_as_current_span(
"gaia.task.run",
kind=SpanKind.INTERNAL,
attributes={
"gaia.task.id": task.task_id,
"gaia.task.level": task.level or 0,
"gaia.task.has_file": task.file_name is not None,
"gaia.task.timeout": timeout or 0,
},
) as span:
start_time = time.time()
try:
# Add task execution span
with self.tracer.start_as_current_span(
"gaia.task.execute",
kind=SpanKind.INTERNAL,
attributes={
"gaia.task.question_length": len(task.question or ""),
"gaia.task.file_name": task.file_name or "",
},
):
if timeout:
prediction = await asyncio.wait_for(task_runner(task), timeout=timeout)
else:
prediction = await task_runner(task)
# Add evaluation span
with self.tracer.start_as_current_span("gaia.task.evaluate", kind=SpanKind.INTERNAL):
evaluation = await self.evaluator(task, prediction)
runtime_seconds = time.time() - start_time
# Add results to span
if span:
span.set_attributes({
"gaia.task.runtime_seconds": runtime_seconds,
"gaia.task.is_correct": evaluation.is_correct,
"gaia.task.score": evaluation.score,
"gaia.task.prediction_length": len(prediction.prediction or ""),
})
return TaskResult(
task_id=task.task_id,
task=task,
prediction=prediction,
evaluation=evaluation,
runtime_seconds=runtime_seconds,
)
except Exception as e:
runtime_seconds = time.time() - start_time
# Record error in span
if span:
span.set_attributes({
"gaia.task.runtime_seconds": runtime_seconds,
"gaia.task.error": str(e),
"gaia.task.is_correct": False,
"gaia.task.score": 0.0,
})
span.record_exception(e)
return TaskResult(
task_id=task.task_id,
task=task,
prediction=Prediction(prediction="", messages=[]),
evaluation=Evaluation(is_correct=False, score=0.0),
runtime_seconds=runtime_seconds,
error=str(e),
)
async def run(
self,
task_runner: TaskRunner,
level: int | list[int] = 1,
max_n: int | None = None,
parallel: int = 1,
timeout: int | None = None,
out: str | None = None,
) -> list[TaskResult]:
"""Run the GAIA benchmark.
Args:
task_runner: Function that takes a Task and returns a Prediction
level: GAIA level(s) to run (1, 2, 3, or list of levels)
max_n: Maximum number of tasks to run per level
parallel: Number of parallel tasks to run
timeout: Timeout per task in seconds
out: Output file to save results including detailed traces (optional)
Returns:
List of TaskResult objects
"""
with self.tracer.start_as_current_span(
"gaia.benchmark.run",
kind=SpanKind.INTERNAL,
attributes={
"gaia.benchmark.levels": str(level),
"gaia.benchmark.max_n": max_n or 0,
"gaia.benchmark.parallel": parallel,
"gaia.benchmark.timeout": timeout or 0,
},
) as benchmark_span:
# Ensure data is available
with self.tracer.start_as_current_span("gaia.data.ensure", kind=SpanKind.INTERNAL):
data_path = self._ensure_data()
# Parse level parameter
levels = [level] if isinstance(level, int) else level
# Load tasks
with self.tracer.start_as_current_span(
"gaia.tasks.load",
kind=SpanKind.INTERNAL,
attributes={
"gaia.tasks.levels": str(levels),
"gaia.tasks.max_n": max_n or 0,
},
) as load_span:
tasks = _load_gaia_local(data_path, wanted_levels=levels, max_n=max_n)
if load_span:
load_span.set_attributes({
"gaia.tasks.loaded_count": len(tasks),
})
if not tasks:
raise RuntimeError(
f"No GAIA tasks found for levels {levels}. "
"Make sure you have dataset access and selected valid levels."
)
# Update benchmark span with task info
if benchmark_span:
benchmark_span.set_attributes({
"gaia.benchmark.total_tasks": len(tasks),
})
# Run tasks
semaphore = asyncio.Semaphore(parallel)
results: list[TaskResult] = []
tasks_coroutines = [self._run_single_task(task, task_runner, semaphore, timeout) for task in tasks]
with self.tracer.start_as_current_span("gaia.tasks.execute_all", kind=SpanKind.INTERNAL):
for coro in tqdm(
asyncio.as_completed(tasks_coroutines), total=len(tasks_coroutines), desc="Evaluating tasks"
):
result = await coro
results.append(result)
# Calculate summary statistics
correct = sum(1 for r in results if r.evaluation.is_correct)
accuracy = correct / len(results) if results else 0.0
avg_runtime = sum(r.runtime_seconds or 0 for r in results) / len(results) if results else 0.0
# Update benchmark span with final results
if benchmark_span:
benchmark_span.set_attributes({
"gaia.benchmark.accuracy": accuracy,
"gaia.benchmark.correct_count": correct,
"gaia.benchmark.total_count": len(results),
"gaia.benchmark.avg_runtime_seconds": avg_runtime,
})
# Save results if requested
if out:
with self.tracer.start_as_current_span(
"gaia.results.save", kind=SpanKind.INTERNAL, attributes={"gaia.results.output_file": out}
):
self._save_results(results, out)
return results
def _save_results(self, results: list[TaskResult], output_path: str) -> None:
"""Save results with detailed trace information to JSONL file."""
with open(output_path, "w", encoding="utf-8") as f:
for result in results:
# Convert messages to serializable format
serializable_messages: list[dict[str, Any] | str] = []
if result.prediction.messages:
for msg in result.prediction.messages:
if hasattr(msg, "model_dump"):
# Pydantic model
serializable_messages.append(msg.model_dump())
elif hasattr(msg, "__dict__"):
# Regular object with attributes
serializable_messages.append(cast(dict[str, Any], getattr(msg, "__dict__", {})))
else:
# Fallback to string representation
serializable_messages.append(str(msg))
record = {
"task_id": result.task_id,
"level": result.task.level,
"question": result.task.question,
"answer": result.task.answer,
"prediction": result.prediction.prediction,
"is_correct": result.evaluation.is_correct,
"score": result.evaluation.score,
"runtime_seconds": result.runtime_seconds,
"error": result.error,
"timestamp": datetime.now().isoformat(),
# Include detailed trace information
"task_metadata": result.task.metadata,
"file_name": result.task.file_name,
"messages": serializable_messages,
"prediction_metadata": result.prediction.metadata,
"evaluation_details": result.evaluation.details,
}
f.write(_dump_json_line(record) + "\n")
def viewer_main() -> None:
"""Main function for the gaia_viewer script."""
import argparse
parser = argparse.ArgumentParser(description="View GAIA benchmark results")
parser.add_argument("results_file", help="Path to results JSONL file")
parser.add_argument("--detailed", action="store_true", help="Show detailed view")
parser.add_argument("--level", type=int, help="Filter by level")
parser.add_argument("--correct-only", action="store_true", help="Show only correct answers")
parser.add_argument("--incorrect-only", action="store_true", help="Show only incorrect answers")
args = parser.parse_args()
# Load results
results: list[dict[str, Any]] = []
with open(args.results_file, encoding="utf-8") as f:
for line in f:
if line.strip():
parsed = _load_json_value(line)
record = _coerce_record(parsed)
if record is not None:
results.append(record)
# Apply filters
if args.level is not None:
results = [r for r in results if r.get("level") == args.level]
if args.correct_only:
results = [r for r in results if r.get("is_correct")]
elif args.incorrect_only:
results = [r for r in results if not r.get("is_correct")]
# Display results
if not results:
print("No results match the filters.")
return
total = len(results)
correct = sum(1 for r in results if r.get("is_correct"))
accuracy = correct / total if total > 0 else 0.0
print("GAIA Results Summary:")
print(f"Total: {total}, Correct: {correct}, Accuracy: {accuracy:.3f}")
print("-" * 80)
for i, result in enumerate(results, 1):
status = "" if result.get("is_correct") else ""
level = result.get("level", "?")
task_id = result.get("task_id", "unknown")
print(f"[{i}/{total}] {status} Level {level} - {task_id}")
if args.detailed:
print(f"Question: {result.get('question', 'N/A')[:100]}...")
print(f"Answer: {result.get('answer', 'N/A')}")
print(f"Prediction: {result.get('prediction', 'N/A')}")
if result.get("error"):
print(f"Error: {result.get('error')}")
if result.get("runtime_seconds"):
print(f"Runtime: {result.get('runtime_seconds'):.2f}s")
print("-" * 40)
if __name__ == "__main__":
viewer_main()
@@ -0,0 +1 @@
py.typed
@@ -0,0 +1,67 @@
# Copyright (c) Microsoft. All rights reserved.
"""Azure AI Agent factory for GAIA benchmark.
This module provides a factory function to create an Azure AI agent
configured for GAIA benchmark tasks.
Required Environment Variables:
FOUNDRY_PROJECT_ENDPOINT: Azure AI project endpoint URL
FOUNDRY_MODEL: Name of the model deployment to use
Optional Environment Variables:
BING_CONNECTION_ID: ID of the Bing connection for web search
Authentication:
Uses Azure CLI credentials via AzureCliCredential.
Run `az login` before executing to authenticate.
Example:
export FOUNDRY_PROJECT_ENDPOINT="https://your-project.azure.com"
export FOUNDRY_MODEL="gpt-4o"
export BING_CONNECTION_ID="connection-id"
az login
"""
import os
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from azure.identity.aio import AzureCliCredential
@asynccontextmanager
async def create_gaia_agent() -> AsyncIterator[Agent]:
"""Create an Azure AI agent configured for GAIA benchmark tasks.
The agent is configured with:
- Bing Search tool for web information retrieval
- Code Interpreter tool for calculations and data analysis
Yields:
Agent: A configured agent ready to run GAIA tasks.
Example:
async with create_gaia_agent() as agent:
result = await agent.run("What is the capital of France?")
print(result.text)
"""
async with (
AzureCliCredential() as credential,
FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=credential,
).as_agent(
name="GaiaAgent",
instructions="Solve tasks to your best ability. Use Bing Search to find "
"information and Code Interpreter to perform calculations and data analysis.",
tools=[
FoundryChatClient.get_web_search_tool(),
FoundryChatClient.get_code_interpreter_tool(),
],
) as agent,
):
yield agent
@@ -0,0 +1,295 @@
# Copyright (c) Microsoft. All rights reserved.
"""GAIA Benchmark Sample.
Run the GAIA (General AI Assistant) benchmark with configurable agent providers,
telemetry options, and benchmark parameters.
Agent Providers:
- Azure AI (default): See azure_ai_agent.py for required environment variables
- OpenAI: See openai_agent.py for required environment variables
Prerequisites:
1. Set HF_TOKEN environment variable with your Hugging Face token:
- Get token: https://huggingface.co/settings/tokens
- Request dataset access: https://huggingface.co/datasets/gaia-benchmark/GAIA
- Set: export HF_TOKEN="your-huggingface-token"
2. Configure your chosen agent provider (see agent module files for details)
Telemetry:
When using --otlp-endpoint or --trace-file, OpenTelemetry will export trace data
in JSON format to the console in addition to the configured endpoints. This is
expected behavior from the OpenTelemetry SDK and provides visibility into the
telemetry being captured. The traces are also exported to:
- OTLP endpoint (e.g., Aspire Dashboard) if --otlp-endpoint is specified
- Local file if --trace-file is specified
To suppress console output, redirect stderr: `python gaia_sample.py 2>/dev/null`
Usage:
# Run with default settings (Azure AI agent)
uv run python gaia_sample.py
# Run with OpenAI agent
uv run python gaia_sample.py --agent-provider openai
# Run with telemetry export to Aspire Dashboard
uv run python gaia_sample.py --otlp-endpoint http://localhost:4318
# See all options
uv run python gaia_sample.py --help
"""
import argparse
from agent_framework.lab.gaia import GAIA, Evaluation, GAIATelemetryConfig, Prediction, Task
async def evaluate_task(task: Task, prediction: Prediction) -> Evaluation:
"""Evaluate the prediction for a given task."""
# Simple evaluation: check if the prediction contains the answer
is_correct = (task.answer or "").lower() in prediction.prediction.lower()
return Evaluation(is_correct=is_correct, score=1 if is_correct else 0)
async def main(
otlp_endpoint: str | None = None,
trace_file: str | None = None,
result_file: str | None = None,
data_dir: str | None = None,
agent_provider: str = "azure-ai",
level: int | list[int] = 1,
max_n: int = 2,
parallel: int = 1,
timeout: int = 120,
) -> None:
"""Run GAIA benchmark with telemetry configuration.
Args:
otlp_endpoint: Optional OTLP endpoint URL for exporting traces (e.g., http://localhost:4318)
trace_file: Optional file path to export traces to. If None, traces won't be saved to file.
result_file: Optional file path to save benchmark results. If None, results won't be saved to file.
data_dir: Directory to cache GAIA dataset. If None, uses temp directory.
agent_provider: Agent provider to use: 'azure-ai' or 'openai' (default: 'azure-ai')
level: GAIA level(s) to run (1, 2, or 3)
max_n: Maximum number of tasks to run per level
parallel: Number of parallel tasks to run
timeout: Timeout per task in seconds
"""
# Check for required Hugging Face token
import logging
import os
# Suppress console logging for traces and verbose SDK output
logging.getLogger("opentelemetry").setLevel(logging.ERROR)
logging.getLogger("azure").setLevel(logging.WARNING)
logging.getLogger("agent_framework").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
# Suppress OpenTelemetry exporters console output
import os as _os
_os.environ.setdefault("OTEL_PYTHON_LOG_LEVEL", "error")
# Print trace export configuration
print("\n=== Telemetry Configuration ===")
if trace_file:
print(f"📁 Trace file: {os.path.abspath(trace_file)}")
else:
print("📁 Trace file: disabled")
if otlp_endpoint:
print(f"🌐 OTLP endpoint: {otlp_endpoint}")
else:
print("🌐 OTLP endpoint: disabled")
if result_file:
print(f"📊 Results file: {os.path.abspath(result_file)}")
else:
print("📊 Results file: disabled")
print("\n=== Run Configuration ===")
print(f"🤖 Agent provider: {agent_provider}")
if data_dir:
print(f"📂 Data directory: {os.path.abspath(data_dir)}")
else:
import tempfile
from pathlib import Path
default_data_dir = Path(tempfile.gettempdir()) / "data_gaia_hub"
print(f"📂 Data directory: {default_data_dir} (default)")
print(f"🎯 Level: {level}")
print(f"🔢 Max tasks: {max_n}")
print(f"⚡ Parallel: {parallel}")
print(f"⏱️ Timeout: {timeout}s")
print()
# Import the appropriate agent factory based on provider
if agent_provider == "azure-ai":
from azure_ai_agent import create_gaia_agent
elif agent_provider == "openai":
from openai_agent import create_gaia_agent
else:
raise ValueError(f"Unknown agent provider: {agent_provider}. Use 'azure-ai' or 'openai'.")
# Configure telemetry for tracing
telemetry_config = GAIATelemetryConfig(
enable_tracing=True, # Enable OpenTelemetry tracing
trace_to_file=trace_file is not None, # Export traces to local file only if path provided
file_path=trace_file, # Custom file path for traces (can be None)
otlp_endpoint=otlp_endpoint, # Optional OTLP endpoint for Aspire Dashboard or other collectors
)
# Create a single agent once and reuse it for all tasks
async with create_gaia_agent() as agent:
async def run_task(task: Task) -> Prediction:
"""Run a single GAIA task and return the prediction using the shared agent."""
input_message = f"Task: {task.question}"
if task.file_name:
input_message += f"\nFile: {task.file_name}"
result = await agent.run(input_message)
return Prediction(prediction=result.text, messages=result.messages)
# Create the GAIA benchmark runner with telemetry configuration
runner = GAIA(
evaluator=evaluate_task,
telemetry_config=telemetry_config,
data_dir=data_dir,
)
# Run the benchmark with the task runner.
# By default, this will check for locally cached benchmark data and checkout
# the latest version from HuggingFace if not found.
# Note: The GAIA dataset has been updated to use Parquet format.
# If you encounter issues, try using validation split which has labeled data.
results = await runner.run(
run_task,
level=level,
max_n=max_n,
parallel=parallel,
timeout=timeout,
out=result_file, # Output file to save results including detailed traces (optional, None = no file output)
)
# Print summary similar to the viewer in gaia.py
total = len(results)
correct = sum(1 for r in results if r.evaluation.is_correct)
accuracy = correct / total if total > 0 else 0.0
avg_runtime = sum(r.runtime_seconds or 0 for r in results) / total if total > 0 else 0.0
print("\n=== GAIA Benchmark Summary ===")
print(f"📝 Total: {total}, ✅ Correct: {correct}, 🎯 Accuracy: {accuracy:.3f}")
print(f"⏱️ Average runtime: {avg_runtime:.2f}s")
if result_file:
print(f"💾 Detailed results saved to: {result_file}")
if __name__ == "__main__":
import asyncio
# Parse command line arguments
parser = argparse.ArgumentParser(
description="Run GAIA benchmark with optional telemetry export to OTLP endpoint and/or file",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Run with default settings
python gaia_sample.py
# Run with custom data directory
python gaia_sample.py --data-dir ./gaia_data
# Run with OpenAI agent provider
python gaia_sample.py --agent-provider openai
# Run with trace file export
python gaia_sample.py --trace-file gaia_benchmark_traces.jsonl
# Run level 2 tasks with 5 maximum tasks
python gaia_sample.py --level 2 --max-n 5
# Run with OTLP export to Aspire Dashboard and custom settings
python gaia_sample.py --otlp-endpoint http://localhost:4318 --level 1 --max-n 10 --parallel 2
# Run with all options configured
python gaia_sample.py --agent-provider openai \
--trace-file traces.jsonl \
--result-file results.jsonl \
--otlp-endpoint http://localhost:4318 --level 1 --max-n 5 --parallel 2 --timeout 180
""",
)
parser.add_argument(
"--otlp-endpoint",
type=str,
default=None,
help="OTLP endpoint URL for exporting traces (e.g., http://localhost:4318 for Aspire Dashboard)",
)
parser.add_argument(
"--trace-file",
type=str,
default=None,
help="File path to export traces to (e.g., gaia_benchmark_traces.jsonl). "
"If not set, traces won't be saved to file.",
)
parser.add_argument(
"--result-file",
type=str,
default="gaia_results_level1.jsonl",
help="File path to save benchmark results (default: gaia_results_level1.jsonl)",
)
parser.add_argument(
"--data-dir",
type=str,
default=None,
help="Directory to cache GAIA dataset. If not set, uses system temp directory.",
)
parser.add_argument(
"--agent-provider",
type=str,
default="azure-ai",
choices=["azure-ai", "openai"],
help="Agent provider to use: 'azure-ai' or 'openai' (default: 'azure-ai')",
)
parser.add_argument(
"--level",
type=int,
default=1,
choices=[1, 2, 3],
help="GAIA benchmark level to run: 1, 2, or 3 (default: 1)",
)
parser.add_argument(
"--max-n",
type=int,
default=2,
help="Maximum number of tasks to run per level (default: 2)",
)
parser.add_argument(
"--parallel",
type=int,
default=1,
help="Number of parallel tasks to run (default: 1)",
)
parser.add_argument(
"--timeout",
type=int,
default=120,
help="Timeout per task in seconds (default: 120)",
)
args = parser.parse_args()
asyncio.run(
main(
otlp_endpoint=args.otlp_endpoint,
trace_file=args.trace_file,
result_file=args.result_file,
data_dir=args.data_dir,
agent_provider=args.agent_provider,
level=args.level,
max_n=args.max_n,
parallel=args.parallel,
timeout=args.timeout,
)
)
@@ -0,0 +1,61 @@
# Copyright (c) Microsoft. All rights reserved.
"""OpenAI Agent factory for GAIA benchmark.
This module provides a factory function to create an OpenAI agent
configured for GAIA benchmark tasks using the OpenAI Responses API.
Required Environment Variables:
OPENAI_API_KEY: Your OpenAI API key
OPENAI_CHAT_MODEL: Model to use with Responses API (e.g., gpt-4o, gpt-4o-mini)
Optional Environment Variables:
OPENAI_BASE_URL: Custom API base URL if using a proxy or compatible service
OPENAI_ORG_ID: Organization ID for OpenAI API (if applicable)
Authentication:
Uses OPENAI_API_KEY environment variable.
Get your API key from: https://platform.openai.com/api-keys
Example:
export OPENAI_API_KEY="sk-..."
export OPENAI_CHAT_MODEL="gpt-4o"
"""
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from agent_framework import Agent
from agent_framework.openai import OpenAIChatClient
@asynccontextmanager
async def create_gaia_agent() -> AsyncIterator[Agent]:
"""Create an OpenAI agent configured for GAIA benchmark tasks.
Uses OpenAI Responses API for enhanced capabilities.
The agent is configured with:
- Web Search tool for information retrieval
- Code Interpreter tool for calculations and data analysis
Yields:
Agent: A configured agent ready to run GAIA tasks.
Example:
async with create_gaia_agent() as agent:
result = await agent.run("What is the capital of France?")
print(result.text)
"""
client = OpenAIChatClient()
async with client.as_agent(
name="GaiaAgent",
instructions="Solve tasks to your best ability. Use Web Search to find "
"information and Code Interpreter to perform calculations and data analysis.",
tools=[
OpenAIChatClient.get_web_search_tool(),
OpenAIChatClient.get_code_interpreter_tool(),
],
) as agent:
yield agent
@@ -0,0 +1,36 @@
# Copyright (c) Microsoft. All rights reserved.
"""Tests for GAIA benchmark implementation."""
from agent_framework_lab_gaia import gaia_scorer
class TestGAIAScorer:
"""Test the GAIA scoring function."""
def test_numeric_exact_match(self):
"""Test numeric exact matching."""
assert gaia_scorer("42", "42") is True
assert gaia_scorer("42.0", "42") is True
assert gaia_scorer("42", "42.0") is True
assert gaia_scorer("42", "43") is False
def test_string_normalization(self):
"""Test string normalization and matching."""
assert gaia_scorer("Hello World", "hello world") is True
assert gaia_scorer("Hello, World!", "helloworld") is True
assert gaia_scorer("test", "TEST") is True
assert gaia_scorer("test", "different") is False
def test_list_matching(self):
"""Test list matching with comma/semicolon separation."""
assert gaia_scorer("1,2,3", "1,2,3") is True
assert gaia_scorer("1; 2; 3", "1,2,3") is True
assert gaia_scorer("apple,banana", "apple,banana") is True
assert gaia_scorer("1,2,3", "1,2,4") is False
assert gaia_scorer("1,2", "1,2,3") is False
def test_none_handling(self):
"""Test handling of None values."""
assert gaia_scorer("None", "test") is False
assert gaia_scorer("", "test") is False