Skip to content

Commit 1543370

Browse files
authored
Python: Lab: Updates to GAIA module (microsoft#1763)
* Lab: Updates to GAIA module * update * emoj! * fix lint * update lab test workflow to only trigger for python changes * lint * lint * Fix broken OpenAI agents JS documentation link
1 parent 7431b46 commit 1543370

9 files changed

Lines changed: 575 additions & 89 deletions

File tree

.github/workflows/python-lab-tests.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,34 @@ env:
1616
UV_CACHE_DIR: /tmp/.uv-cache
1717

1818
jobs:
19+
paths-filter:
20+
runs-on: ubuntu-latest
21+
permissions:
22+
contents: read
23+
pull-requests: read
24+
outputs:
25+
pythonChanges: ${{ steps.filter.outputs.python}}
26+
steps:
27+
- uses: actions/checkout@v5
28+
- uses: dorny/paths-filter@v3
29+
id: filter
30+
with:
31+
filters: |
32+
python:
33+
- 'python/**'
34+
# run only if 'python' files were changed
35+
- name: python tests
36+
if: steps.filter.outputs.python == 'true'
37+
run: echo "Python file"
38+
# run only if not 'python' files were changed
39+
- name: not python tests
40+
if: steps.filter.outputs.python != 'true'
41+
run: echo "NOT python file"
42+
1943
python-lab-tests:
2044
name: Python Lab Tests
45+
needs: paths-filter
46+
if: needs.paths-filter.outputs.pythonChanges == 'true'
2147
runs-on: ${{ matrix.os }}
2248
strategy:
2349
fail-fast: true

docs/decisions/0006-userapproval.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ This document aims to provide options and capture the decision on how to model t
2222
See various features that would need to be supported via this type of mechanism, plus how various other frameworks support this:
2323

2424
- Also see [dotnet issue 6492](https://github.com/dotnet/extensions/issues/6492), which discusses the need for a similar pattern in the context of MCP approvals.
25-
- Also see [the openai RunToolApprovalItem](https://openai.github.io/openai-agents-js/openai/agents/classes/runtoolapprovalitem/).
2625
- Also see [the openai human-in-the-loop guide](https://openai.github.io/openai-agents-js/guides/human-in-the-loop/#approval-requests).
2726
- Also see [the openai MCP guide](https://openai.github.io/openai-agents-js/guides/mcp/#optional-approval-flow).
2827
- Also see [MCP Approval Requests from OpenAI](https://platform.openai.com/docs/guides/tools-remote-mcp#approvals).

python/packages/lab/gaia/README.md

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,6 @@ async def main() -> None:
4343

4444
See the [gaia_sample.py](./samples/gaia_sample.py) for more detail.
4545

46-
### Run the evaluation
47-
48-
Run the evaluation script using `uv`:
49-
50-
```bash
51-
uv run python run_gaia.py
52-
```
53-
54-
By default, the script will first look for cached GAIA data in the `data_gaia_hub` directory,
55-
and download it if not found.
56-
The result will be saved to `gaia_results_<timestamp>.jsonl`.
57-
58-
**Don't run the script inside this directory because it will confuse the local `agent_framework` namespace
59-
package with the real one.**
60-
6146
## View results
6247

6348
We provide a console viewer for reading GAIA results:

python/packages/lab/gaia/agent_framework_lab_gaia/gaia.py

Lines changed: 99 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,29 @@ def setup_observability(self) -> None:
5454
if not self.enable_tracing:
5555
return
5656

57-
from agent_framework.observability import setup_observability
57+
# If only file tracing is requested (no OTLP or Application Insights),
58+
# skip the default setup_observability which adds console exporter
59+
if self.trace_to_file and not self.otlp_endpoint and not self.applicationinsights_connection_string:
60+
# Set up minimal tracing with only file export
61+
from opentelemetry.sdk.trace import TracerProvider
62+
from opentelemetry.trace import set_tracer_provider
63+
64+
tracer_provider = TracerProvider()
65+
set_tracer_provider(tracer_provider)
66+
self._setup_file_export()
67+
else:
68+
# Use full observability setup for OTLP/AppInsights
69+
from agent_framework.observability import setup_observability
5870

59-
setup_observability(
60-
enable_sensitive_data=True, # Enable for detailed task traces
61-
otlp_endpoint=self.otlp_endpoint,
62-
applicationinsights_connection_string=self.applicationinsights_connection_string,
63-
)
71+
setup_observability(
72+
enable_sensitive_data=True, # Enable for detailed task traces
73+
otlp_endpoint=self.otlp_endpoint,
74+
applicationinsights_connection_string=self.applicationinsights_connection_string,
75+
)
6476

65-
# Set up local file export if requested
66-
if self.trace_to_file:
67-
self._setup_file_export()
77+
# Set up local file export if requested
78+
if self.trace_to_file:
79+
self._setup_file_export()
6880

6981
def _setup_file_export(self) -> None:
7082
"""Set up local file export for traces."""
@@ -204,29 +216,87 @@ def _load_gaia_local(repo_dir: Path, wanted_levels: list[int] | None = None, max
204216
"""Load GAIA tasks from local repository directory."""
205217
tasks: list[Task] = []
206218

207-
for p in repo_dir.rglob("metadata.jsonl"):
208-
for rec in _read_jsonl(p):
209-
# Robustly extract fields used across variants
210-
q = rec.get("Question") or rec.get("question") or rec.get("query") or rec.get("prompt")
211-
ans = rec.get("Final answer") or rec.get("answer") or rec.get("final_answer")
212-
qid = str(
213-
rec.get("task_id")
214-
or rec.get("question_id")
215-
or rec.get("id")
216-
or rec.get("uuid")
217-
or f"{p.stem}:{len(tasks)}"
218-
)
219-
lvl = rec.get("Level") or rec.get("level")
220-
fname = rec.get("file_name") or rec.get("filename") or None
219+
# First try to load from parquet files (new format)
220+
# Prioritize validation split over test split (validation has answers)
221+
parquet_files = sorted(
222+
repo_dir.rglob("metadata*.parquet"), key=lambda p: (0 if "validation" in str(p) else 1, str(p))
223+
)
221224

222-
# Only evaluate examples with public answers (dev/validation split)
223-
if not q or ans is None:
224-
continue
225+
for p in parquet_files:
226+
try:
227+
import pyarrow.parquet as pq
228+
229+
table = pq.read_table(p)
230+
for row in table.to_pylist():
231+
# Robustly extract fields used across variants
232+
q = row.get("Question") or row.get("question") or row.get("query") or row.get("prompt")
233+
ans = row.get("Final answer") or row.get("answer") or row.get("final_answer")
234+
qid = str(
235+
row.get("task_id")
236+
or row.get("question_id")
237+
or row.get("id")
238+
or row.get("uuid")
239+
or f"{p.stem}:{len(tasks)}"
240+
)
241+
lvl = row.get("Level") or row.get("level")
225242

226-
if wanted_levels and (lvl not in wanted_levels):
227-
continue
243+
# Convert level to int if it's a string
244+
def _parse_level(lvl: Any) -> int | None:
245+
"""Parse level value to integer if possible."""
246+
if isinstance(lvl, int):
247+
return lvl
248+
if isinstance(lvl, str) and lvl.isdigit():
249+
return int(lvl)
250+
return None
228251

229-
tasks.append(Task(task_id=qid, question=q, answer=str(ans), level=lvl, file_name=fname, metadata=rec))
252+
lvl = _parse_level(lvl)
253+
fname = row.get("file_name") or row.get("filename") or None
254+
255+
# Only evaluate examples with public answers (dev/validation split)
256+
# Skip if no question, no answer, or answer is placeholder like "?"
257+
if not q or ans is None or str(ans).strip() in ["?", ""]:
258+
continue
259+
260+
if wanted_levels and (lvl not in wanted_levels):
261+
continue
262+
263+
tasks.append(Task(task_id=qid, question=q, answer=str(ans), level=lvl, file_name=fname, metadata=row))
264+
except ImportError:
265+
print("Warning: pyarrow not installed. Install with: pip install pyarrow")
266+
continue
267+
except Exception as e:
268+
print(f"Warning: Could not load parquet file {p}: {e}")
269+
continue
270+
271+
# Fall back to jsonl files (old format) if no parquet files found
272+
if not tasks:
273+
for p in repo_dir.rglob("metadata.jsonl"):
274+
for rec in _read_jsonl(p):
275+
# Robustly extract fields used across variants
276+
q = rec.get("Question") or rec.get("question") or rec.get("query") or rec.get("prompt")
277+
ans = rec.get("Final answer") or rec.get("answer") or rec.get("final_answer")
278+
qid = str(
279+
rec.get("task_id")
280+
or rec.get("question_id")
281+
or rec.get("id")
282+
or rec.get("uuid")
283+
or f"{p.stem}:{len(tasks)}"
284+
)
285+
lvl = rec.get("Level") or rec.get("level")
286+
# Convert level to int if it's a string
287+
if isinstance(lvl, str) and lvl.isdigit():
288+
lvl = int(lvl)
289+
fname = rec.get("file_name") or rec.get("filename") or None
290+
291+
# Only evaluate examples with public answers (dev/validation split)
292+
# Skip if no question, no answer, or answer is placeholder like "?"
293+
if not q or ans is None or str(ans).strip() in ["?", ""]:
294+
continue
295+
296+
if wanted_levels and (lvl not in wanted_levels):
297+
continue
298+
299+
tasks.append(Task(task_id=qid, question=q, answer=str(ans), level=lvl, file_name=fname, metadata=rec))
230300

231301
# Shuffle to help with rate-limits and fairness if max_n is provided
232302
random.shuffle(tasks)
@@ -290,7 +360,6 @@ def _ensure_data(self) -> Path:
290360
"with access to gaia-benchmark/GAIA."
291361
)
292362

293-
print(f"Downloading GAIA dataset to {self.data_dir}...")
294363
from huggingface_hub import snapshot_download
295364

296365
local_dir = snapshot_download( # type: ignore
@@ -438,8 +507,6 @@ async def run(
438507
"Make sure you have dataset access and selected valid levels."
439508
)
440509

441-
print(f"Running {len(tasks)} GAIA tasks (levels={levels}) with {parallel} parallel workers...")
442-
443510
# Update benchmark span with task info
444511
if benchmark_span:
445512
benchmark_span.set_attributes({
@@ -473,17 +540,12 @@ async def run(
473540
"gaia.benchmark.avg_runtime_seconds": avg_runtime,
474541
})
475542

476-
print("\nGAIA Benchmark Results:")
477-
print(f"Accuracy: {accuracy:.3f} ({correct}/{len(results)})")
478-
print(f"Average runtime: {avg_runtime:.2f}s")
479-
480543
# Save results if requested
481544
if out:
482545
with self.tracer.start_as_current_span(
483546
"gaia.results.save", kind=SpanKind.INTERNAL, attributes={"gaia.results.output_file": out}
484547
):
485548
self._save_results(results, out)
486-
print(f"Results saved to {out}")
487549

488550
return results
489551

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
"""Azure AI Agent factory for GAIA benchmark.
4+
5+
This module provides a factory function to create an Azure AI agent
6+
configured for GAIA benchmark tasks.
7+
8+
Required Environment Variables:
9+
AZURE_AI_PROJECT_ENDPOINT: Azure AI project endpoint URL
10+
AZURE_AI_MODEL_DEPLOYMENT_NAME: Name of the model deployment to use
11+
12+
Optional Environment Variables:
13+
BING_CONNECTION_NAME: Name of the Bing connection for web search
14+
OR
15+
BING_CONNECTION_ID: ID of the Bing connection for web search
16+
17+
Authentication:
18+
Uses Azure CLI credentials via AzureCliCredential.
19+
Run `az login` before executing to authenticate.
20+
21+
Example:
22+
export AZURE_AI_PROJECT_ENDPOINT="https://your-project.azure.com"
23+
export AZURE_AI_MODEL_DEPLOYMENT_NAME="gpt-4o"
24+
export BING_CONNECTION_NAME="bing-grounding-connection"
25+
az login
26+
"""
27+
28+
from collections.abc import AsyncIterator
29+
from contextlib import asynccontextmanager
30+
31+
from agent_framework import ChatAgent, HostedCodeInterpreterTool, HostedWebSearchTool
32+
from agent_framework.azure import AzureAIAgentClient
33+
from azure.identity.aio import AzureCliCredential
34+
35+
36+
@asynccontextmanager
37+
async def create_gaia_agent() -> AsyncIterator[ChatAgent]:
38+
"""Create an Azure AI agent configured for GAIA benchmark tasks.
39+
40+
The agent is configured with:
41+
- Bing Search tool for web information retrieval
42+
- Code Interpreter tool for calculations and data analysis
43+
44+
Yields:
45+
ChatAgent: A configured agent ready to run GAIA tasks.
46+
47+
Example:
48+
async with create_gaia_agent() as agent:
49+
result = await agent.run("What is the capital of France?")
50+
print(result.text)
51+
"""
52+
async with (
53+
AzureCliCredential() as credential,
54+
AzureAIAgentClient(async_credential=credential).create_agent(
55+
name="GaiaAgent",
56+
instructions="Solve tasks to your best ability. Use Bing Search to find "
57+
"information and Code Interpreter to perform calculations and data analysis.",
58+
tools=[
59+
HostedWebSearchTool(
60+
name="Bing Grounding Search",
61+
description="Search the web for current information using Bing",
62+
),
63+
HostedCodeInterpreterTool(),
64+
],
65+
) as agent,
66+
):
67+
yield agent

0 commit comments

Comments
 (0)