Python: chore(python): improve dependency range automation (#4343)

* chore(python): improve dependency range automation

- tighten dependency bounds and coding standards guidance\n- add dependency range validation workflow, reporting, and issue automation\n- update related tests and dependency pins for compatibility

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* updated text and pyarrow

* new lock

* fixed workflow

* updated deps

* fix tiktoken

* chore(python): refine dependency validation workflows

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(python): add high-level dependency validation comments

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* WIP

* added additional comments and excludes

* added dev dependency handling and workflow and updates to package ranges

* added readme and simplified commands

* fix markers

* chore(python): address dependency review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Tighten dependency bounds, remove stale overrides, restore Python 3.10 support

- Apply dependency bound policy across all packages: stable >=1.0 deps use
  >=floor,<next_major; pre-1.0/prerelease deps use validated hard-bounded ranges
- Remove stale root tool.uv.override-dependencies (uvicorn, websockets, grpcio)
- Lower github_copilot requires-python to >=3.10 with github-copilot-sdk gated
  behind python_version >= 3.11 marker; import raises ImportError on 3.10
- Skip github_copilot pyright/mypy/test tasks on Python <3.11
- Use version-conditional pyrightconfig for samples on Python 3.10
- Add compatibility fix in core responses client for older openai typed dicts
- Normalize uv.lock prerelease mode and refresh dev dependencies
- Update CODING_STANDARD.md, DEV_SETUP.md, and package management skill docs

Closes #902

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* small tweaks

* add note in workflow

* fix workflows and several versions

* fix duplicate

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-03-13 13:32:37 +01:00
committed by GitHub
Unverified
parent 67b0282813
commit 50fdcbaf57
61 changed files with 5500 additions and 779 deletions
+21
View File
@@ -62,6 +62,27 @@ For example, to use the GAIA module:
from agent_framework.lab.gaia import GAIA
```
## Running Tests Locally
For machine-safe local runs, prefer package-scoped commands first:
```bash
uv run --directory packages/lab poe test
uv run --directory packages/lab pytest -q -m "not integration"
```
When you need to run package tasks from the repository root, use sequential mode to avoid launching all package tests in parallel:
```bash
uv run poe test --seq
```
Lightning observability tests intentionally exercise heavier tracing paths and are marked as `resource_intensive`:
```bash
uv run --directory packages/lab pytest lightning/tests/test_lightning.py -m "resource_intensive" -q
```
## Should I consume Lab Modules?
If you are looking for stable and production-ready features, you should not use lab modules. Stick to the core framework.
@@ -10,10 +10,11 @@ import re
import string
import tempfile
import time
from collections.abc import Iterable
from collections.abc import Callable, Iterable
from datetime import datetime
from functools import lru_cache
from pathlib import Path
from typing import Any, cast
from typing import Any, Protocol, cast
from opentelemetry.trace import NoOpTracer, SpanKind, get_tracer
from tqdm import tqdm
@@ -23,6 +24,33 @@ from ._types import Evaluation, Evaluator, Prediction, Task, TaskResult, TaskRun
__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 # pyright: ignore[reportMissingImports]
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."""
@@ -226,13 +254,7 @@ def _read_jsonl(path: Path) -> Iterable[dict[str, Any]]:
for line in f:
if not line.strip():
continue
parsed: object
try:
import orjson
parsed = orjson.loads(line)
except Exception:
parsed = json.loads(line)
parsed = _load_json_value(line)
record = _coerce_record(parsed)
if record is not None:
@@ -620,12 +642,7 @@ class GAIA:
"prediction_metadata": result.prediction.metadata,
"evaluation_details": result.evaluation.details,
}
try:
import orjson
f.write(orjson.dumps(record, default=str).decode("utf-8") + "\n")
except ImportError:
f.write(json.dumps(record, default=str) + "\n")
f.write(_dump_json_line(record) + "\n")
def viewer_main() -> None:
@@ -646,13 +663,7 @@ def viewer_main() -> None:
with open(args.results_file, encoding="utf-8") as f:
for line in f:
if line.strip():
try:
import orjson
parsed: object = orjson.loads(line)
except ImportError:
parsed = json.loads(line)
parsed = _load_json_value(line)
record = _coerce_record(parsed)
if record is not None:
results.append(record)
@@ -2,10 +2,14 @@
"""RL Module for Microsoft Agent Framework."""
from __future__ import annotations
import importlib.metadata
from agent_framework.observability import enable_instrumentation
from agentlightning import AgentOpsTracer # type: ignore
from agentlightning.tracer import (
AgentOpsTracer, # pyright: ignore[reportMissingImports] # type: ignore[import-not-found]
)
try:
__version__ = importlib.metadata.version(__name__)
@@ -23,11 +27,11 @@ class AgentFrameworkTracer(AgentOpsTracer): # type: ignore
def init(self) -> None:
"""Initialize the agent-framework-lab-lightning for training."""
enable_instrumentation()
super().init()
super().init() # pyright: ignore[reportUnknownMemberType]
def teardown(self) -> None:
"""Teardown the agent-framework-lab-lightning for training."""
super().teardown()
super().teardown() # pyright: ignore[reportUnknownMemberType]
__all__: list[str] = ["AgentFrameworkTracer"]
@@ -7,12 +7,8 @@ from unittest.mock import AsyncMock, patch
import pytest
agentlightning = pytest.importorskip("agentlightning")
from agent_framework import AgentExecutor, AgentResponse, Agent, WorkflowBuilder, Workflow
from agent_framework_lab_lightning import AgentFrameworkTracer
from agent_framework.openai import OpenAIChatClient
from agentlightning import TracerTraceToTriplet
from openai.types.chat import ChatCompletion, ChatCompletionMessage
from openai.types.chat.chat_completion import Choice
@@ -118,6 +114,7 @@ async def test_openai_workflow_two_agents(workflow_two_agents: Workflow):
)
@pytest.mark.resource_intensive
async def test_observability(workflow_two_agents: Workflow):
r"""Expected trace tree:
@@ -129,6 +126,10 @@ async def test_observability(workflow_two_agents: Workflow):
| |
[chat gpt-4o] [chat gpt-4o]
"""
pytest.importorskip("agentlightning")
from agent_framework_lab_lightning import AgentFrameworkTracer
from agentlightning.adapter import TracerTraceToTriplet
tracer = AgentFrameworkTracer()
try:
tracer.init()
+17 -17
View File
@@ -32,8 +32,8 @@ gaia = [
"opentelemetry-api>=1.39.0",
"tqdm>=4.60.0",
"huggingface-hub>=0.20.0",
"orjson>=3.8.0",
"pyarrow>=10.0.0", # For reading parquet files
"orjson>=3.10.7,<4",
"pyarrow>=18.0.0", # For reading parquet files
]
# Lightning RL training module dependencies
@@ -56,19 +56,19 @@ math = [
[dependency-groups]
dev = [
"uv",
"ruff>=0.11.8",
"pytest>=8.4.1",
"mypy>=1.16.1",
"pyright>=1.1.402",
"uv==0.10.9",
"ruff==0.15.5",
"pytest==9.0.2",
"mypy==1.19.1",
"pyright==1.1.408",
#tasks
"poethepoet>=0.36.0",
"rich",
"tomli",
"tomli-w",
"poethepoet==0.42.1",
"rich==13.7.1",
"tomli==2.4.0",
"tomli-w==1.2.0",
# tau2 from source (not available on PyPI)
"tau2@ git+https://github.com/sierra-research/tau2-bench@5ba9e3e56db57c5e4114bf7f901291f09b2c5619",
"prek>=0.3.2",
"prek==0.3.4",
]
[project.scripts]
@@ -144,7 +144,6 @@ targets = ["agent_framework_lab_gaia", "agent_framework_lab_lightning", "agent_f
exclude_dirs = ["gaia/tests", "lightning/tests", "tau2/tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
@@ -152,10 +151,10 @@ mypy-gaia = "mypy --config-file $POE_ROOT/pyproject.toml gaia/agent_framework_la
mypy-lightning = "mypy --config-file $POE_ROOT/pyproject.toml lightning/agent_framework_lab_lightning"
mypy-tau2 = "mypy --config-file $POE_ROOT/pyproject.toml tau2/agent_framework_lab_tau2"
mypy = ["mypy-gaia", "mypy-lightning", "mypy-tau2"]
test = "pytest -m \"not integration\" --cov-report=term-missing:skip-covered --junitxml=test-results.xml"
test-gaia = "pytest -m \"not integration\" gaia/tests --cov=agent_framework_lab_gaia --cov-report=term-missing:skip-covered"
test-lightning = "pytest -m \"not integration\" lightning/tests --cov=agent_framework_lab_lightning --cov-report=term-missing:skip-covered"
test-tau2 = "pytest -m \"not integration\" tau2/tests --cov=agent_framework_lab_tau2 --cov-report=term-missing:skip-covered"
test = 'pytest -m "not integration and not resource_intensive" --cov-report=term-missing:skip-covered --junitxml=test-results.xml'
test-gaia = "pytest gaia/tests --cov=agent_framework_lab_gaia --cov-report=term-missing:skip-covered"
test-lightning = "pytest lightning/tests --cov=agent_framework_lab_lightning --cov-report=term-missing:skip-covered"
test-tau2 = "pytest tau2/tests --cov=agent_framework_lab_tau2 --cov-report=term-missing:skip-covered"
build = "echo 'Skipping build'"
publish = "echo 'Skipping publish'"
@@ -167,4 +166,5 @@ asyncio_default_fixture_loop_scope = "function"
markers = [
"unit: marks tests as unit tests",
"integration: marks tests as integration tests",
"resource_intensive: marks tests that are expensive and excluded from default package test runs",
]
@@ -2,62 +2,39 @@
"""Tests for tau2 utils module."""
import urllib.request
from pathlib import Path
import pytest
from agent_framework import Content, FunctionTool, Message
from agent_framework_lab_tau2._tau2_utils import (
convert_agent_framework_messages_to_tau2_messages,
convert_tau2_tool_to_function_tool,
)
from pydantic import BaseModel
from tau2.data_model.message import AssistantMessage, SystemMessage, ToolCall, ToolMessage, UserMessage
from tau2.domains.airline.data_model import FlightDB
from tau2.domains.airline.tools import AirlineTools
from tau2.environment.environment import Environment
@pytest.fixture(scope="session")
def tau2_airline_environment() -> Environment:
airline_db_remote_path = "https://raw.githubusercontent.com/sierra-research/tau2-bench/5ba9e3e56db57c5e4114bf7f901291f09b2c5619/data/tau2/domains/airline/db.json"
airline_policy_remote_path = "https://raw.githubusercontent.com/sierra-research/tau2-bench/5ba9e3e56db57c5e4114bf7f901291f09b2c5619/data/tau2/domains/airline/policy.md"
# Create cache directory
cache_dir = Path(__file__).parent / "data"
cache_dir.mkdir(exist_ok=True)
# Define cache file paths
db_cache_path = cache_dir / "airline_db.json"
policy_cache_path = cache_dir / "airline_policy.md"
# Download files only if they don't exist in cache
if not db_cache_path.exists():
urllib.request.urlretrieve(airline_db_remote_path, db_cache_path)
if not policy_cache_path.exists():
urllib.request.urlretrieve(airline_policy_remote_path, policy_cache_path)
# Load data from cached files
db = FlightDB.load(str(db_cache_path))
tools = AirlineTools(db)
with open(policy_cache_path) as fp:
policy = fp.read()
yield Environment(
domain_name="airline",
policy=policy,
tools=tools,
)
class _DummyToolInput(BaseModel):
param: str
def test_convert_tau2_tool_to_function_tool_basic(tau2_airline_environment):
class _DummyToolResult(BaseModel):
output: str
class _DummyTau2Tool:
def __init__(self, name: str, description: str) -> None:
self.name = name
self._description = description
self.params = _DummyToolInput
def _get_description(self) -> str:
return self._description
def __call__(self, **kwargs: str) -> _DummyToolResult:
return _DummyToolResult(output=kwargs["param"])
def test_convert_tau2_tool_to_function_tool_basic():
"""Test basic conversion from tau2 tool to FunctionTool."""
# Get real tools from tau2 environment
tools = tau2_airline_environment.get_tools()
# Use the first available tool for testing
assert len(tools) > 0, "No tools available in environment"
tau2_tool = tools[0]
tau2_tool = _DummyTau2Tool(name="lookup_booking", description="Lookup booking by id.")
# Convert the tool
tool = convert_tau2_tool_to_function_tool(tau2_tool)
@@ -68,20 +45,25 @@ def test_convert_tau2_tool_to_function_tool_basic(tau2_airline_environment):
assert tool.description == tau2_tool._get_description()
assert tool.input_model == tau2_tool.params
# Test that the function is callable (we won't call it with real params to avoid side effects)
result = tool.func(param="ABC123")
assert isinstance(result, _DummyToolResult)
assert result.output == "ABC123"
assert callable(tool.func)
def test_convert_tau2_tool_to_function_tool_multiple_tools(tau2_airline_environment):
def test_convert_tau2_tool_to_function_tool_multiple_tools():
"""Test conversion with multiple tau2 tools."""
# Get real tools from tau2 environment
tools = tau2_airline_environment.get_tools()
tools = [
_DummyTau2Tool(name="lookup_booking", description="Lookup booking by id."),
_DummyTau2Tool(name="cancel_booking", description="Cancel an existing booking."),
_DummyTau2Tool(name="check_policy", description="Get policy details."),
]
# Convert multiple tools
function_tools = [convert_tau2_tool_to_function_tool(tool) for tool in tools[:3]] # Test first 3 tools
function_tools = [convert_tau2_tool_to_function_tool(tool) for tool in tools]
# Verify all conversions
for tool, tau2_tool in zip(function_tools, tools[:3], strict=False):
for tool, tau2_tool in zip(function_tools, tools, strict=False):
assert isinstance(tool, FunctionTool)
assert tool.name == tau2_tool.name
assert tool.description == tau2_tool._get_description()