mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
67b0282813
commit
50fdcbaf57
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user