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
+4 -4
View File
@@ -69,7 +69,7 @@ def equal(arg1: str, arg2: str) -> bool:
```python
# Core
from agent_framework import ChatAgent, Message, tool
from agent_framework import Agent, Message, tool
# Components
from agent_framework.observability import enable_instrumentation
@@ -82,16 +82,16 @@ from agent_framework.azure import AzureOpenAIChatClient
## Public API and Exports
In `__init__.py` files that define package-level public APIs, use direct re-export imports plus an explicit
`__all__`. Avoid identity aliases like `from ._agents import ChatAgent as ChatAgent`, and avoid
`__all__`. Avoid identity aliases like `from ._agents import Agent as Agent`, and avoid
`from module import *`.
Do not define `__all__` in internal non-`__init__.py` modules. Exception: modules intentionally exposed as a
public import surface (for example, `agent_framework.observability`) should define `__all__`.
```python
__all__ = ["ChatAgent", "Message", "ChatResponse"]
__all__ = ["Agent", "Message", "ChatResponse"]
from ._agents import ChatAgent
from ._agents import Agent
from ._types import Message, ChatResponse
```
+43 -1
View File
@@ -33,13 +33,44 @@ Uses [uv](https://github.com/astral-sh/uv) for dependency management and
# Full setup (venv + install + prek hooks)
uv run poe setup
# Install/update all dependencies
# Install dependencies from lockfile (frozen resolution with prerelease policy)
uv run poe install
# Create venv with specific Python version
uv run poe venv --python 3.12
# Intentionally upgrade a specific dependency to reduce lockfile conflicts
uv lock --upgrade-package <dependency-name> && uv run poe install
# Refresh all dev dependency pins, lockfile, and validation in one run
uv run poe upgrade-dev-dependencies
# First, run workspace-wide lower/upper compatibility gates
uv run poe validate-dependency-bounds-test
# Defaults to --project "*"; pass a package to scope test mode
uv run poe validate-dependency-bounds-test --project <workspace-package-name>
# Then expand bounds for one dependency in the target package
uv run poe validate-dependency-bounds-project --mode both --project <workspace-package-name> --dependency "<dependency-name>"
# Repo-wide automation can reuse the same task
uv run poe validate-dependency-bounds-project --mode upper --project "*"
# Add a dependency to one project and run both validators for that project/dependency
uv run poe add-dependency-and-validate-bounds --project <workspace-package-name> --dependency "<dependency-spec>"
```
### Dependency Bound Notes
- Stable dependencies (`>=1.0`) should typically be bounded as `>=<known-good>,<next-major>`.
- Prerelease (`dev`/`a`/`b`/`rc`) and `<1.0` dependencies should use hard bounds with an explicit upper cap (avoid open-ended ranges).
- For `<1.0` dependencies, prefer the broadest validated range the package can really support. That may be a patch line, a minor line, or multiple minor lines when checks/tests show the broader lane is compatible.
- Prefer supporting multiple majors when practical; if APIs diverge across supported majors, use version-conditional imports/paths.
- For dependency changes, run workspace-wide bound gates first, then `validate-dependency-bounds-project --mode both` for the target package/dependency to keep minimum and maximum constraints current. The same task can also drive repo-wide upper-bound automation by using `--project "*"` and omitting `--dependency`.
- Prefer targeted lock updates with `uv lock --upgrade-package <dependency-name>` to reduce `uv.lock` merge conflicts.
- Use `add-dependency-and-validate-bounds` for package-scoped dependency additions plus bound validation in one command.
- Use `upgrade-dev-dependencies` for repo-wide dev tooling refreshes; it repins dev dependencies, refreshes `uv.lock`, and reruns `check`, `typing`, and `test`.
## Lazy Loading Pattern
Provider folders in core use `__getattr__` to lazy load from connector packages:
@@ -74,6 +105,17 @@ def __getattr__(name: str) -> Any:
4. Do **NOT** add to `[all]` extra in `packages/core/pyproject.toml`
5. Do **NOT** create lazy loading in core yet
Recommended dependency workflow during connector implementation:
1. Add the dependency to the target package:
`uv run poe add-dependency-to-project --project <workspace-package-name> --dependency "<dependency-spec>"`
2. Implement connector code and tests.
3. Validate dependency bounds for that package/dependency:
`uv run poe validate-dependency-bounds-project --mode both --project <workspace-package-name> --dependency "<dependency-name>"`
4. If the package has meaningful tests/checks that validate dependency compatibility, you can use the add + validation flow in one command:
`uv run poe add-dependency-and-validate-bounds --project <workspace-package-name> --dependency "<dependency-spec>"`
If compatibility checks are not in place yet, add the dependency first, then implement tests before running bound validation.
### Promotion to Stable
1. Move samples to root `samples/` folder
+20 -3
View File
@@ -165,10 +165,14 @@ user_msg = Message("user", ["Hello, world!"])
asst_msg = Message("assistant", ["Hello, world!"])
# ❌ Not preferred - unnecessary inheritance
from agent_framework import UserMessage, AssistantMessage
class UserMessage(Message):
pass
user_msg = UserMessage(content="Hello, world!")
asst_msg = AssistantMessage(content="Hello, world!")
class AssistantMessage(Message):
pass
user_msg = UserMessage("user", ["Hello, world!"])
asst_msg = AssistantMessage("assistant", ["Hello, world!"])
```
### Import Structure
@@ -388,6 +392,19 @@ All non-core packages declare a lower bound on `agent-framework-core` (e.g., `"a
- **Core version changes**: When `agent-framework-core` is updated with breaking or significant changes and its version is bumped, update the `agent-framework-core>=...` lower bound in every other package's `pyproject.toml` to match the new core version.
- **Non-core version changes**: Non-core packages (connectors, extensions) can have their own versions incremented independently while keeping the existing core lower bound pinned. Only raise the core lower bound if the non-core package actually depends on new core APIs.
### External Dependency Version Bounds
The guiding principle for external dependencies is to make the range of allowed versions as broad as possible, even if that means we have to do some conditional imports, and other tricks to allow small changes in versions.
So we use bounded ranges for external package dependencies in `pyproject.toml`:
- For stable dependencies (`>=1.0.0`), use a lower bound at a known-good version and an explicit upper bound that reflects the maximum major version we currently support (for example: `openai>=1.99.0,<3`).
- For prerelease (`dev`/`a`/`b`/`rc`) dependencies, use a known-good lower bound with a hard upper boundary in the same prerelease line (for example: `azure-ai-projects>=2.0.0b3,<2.0.0b4`).
- For `<1.0.0` dependencies, use a known-good bounded range with an explicit upper cap. Prefer the broadest validated range the package can actually support: that may be a patch line, a minor line, or multiple minor lines (for example: `a2a-sdk>=0.3.5,<0.4.0`, `fastapi>=0.115.0,<0.136.0`, `uvicorn>=0.30.0,<0.39.0`).
- For prerelease (`dev`/`a`/`b`/`rc`) dependencies, use a known-good bounded range with a hard upper cap and keep the range only as broad as the package's validation coverage justifies.
- Prefer keeping support for multiple major versions when practical. This may mean that the upper bound spans multiple major versions when the dependency maintains backward compatibility; if APIs differ between supported majors, version-conditional imports/branches are acceptable to preserve compatibility.
- When adding or changing an external dependency, first run `uv run poe validate-dependency-bounds-test` to validate workspace-wide lower/upper compatibility, then run `uv run poe validate-dependency-bounds-project --mode both --project <workspace-package-name> --dependency "<dependency-name>"` to expand package-scoped bounds.
### Installation Options
Connectors are distributed as separate packages and are not imported by default in the core package. Users install the specific connectors they need:
+33 -1
View File
@@ -217,10 +217,13 @@ uv run poe setup --python 3.12
```
#### `install`
Install all dependencies including extras and dev dependencies, including updates:
Install all dependencies (including extras and dev dependencies) from the lockfile using frozen resolution:
```bash
uv run poe install
```
For intentional dependency upgrades, run `uv lock --upgrade-package <dependency-name>` and then run `uv run poe install`.
For repo-wide dev tooling refreshes, run `uv run poe upgrade-dev-dependencies` to repin dev dependencies, refresh `uv.lock`, and rerun validation, typing, and tests.
#### `venv`
Create a virtual environment with specified Python version or switch python version:
@@ -278,6 +281,35 @@ Lint markdown code blocks:
uv run poe markdown-code-lint
```
#### `validate-dependency-bounds-test`
Run workspace-wide dependency compatibility gates at lower and upper resolutions. This runs test + pyright across all packages and stops on first failure:
```bash
uv run poe validate-dependency-bounds-test
# Defaults to --project "*"; pass a package to scope test mode
uv run poe validate-dependency-bounds-test --project <workspace-package-name>
```
#### `validate-dependency-bounds-project`
Validate and extend dependency bounds for a single dependency in a single package. Use `--mode lower`, `--mode upper`, or the default `--mode both`:
```bash
uv run poe validate-dependency-bounds-project --mode both --project <workspace-package-name> --dependency "<dependency-name>"
```
`--project` defaults to `*`, and `--dependency` is optional. Automation can use `--mode upper --project "*"` to run the upper-bound pass across the workspace.
For `<1.0` dependencies, prefer the broadest validated range the package can really support. That may still be a single patch or minor line, but multi-minor ranges are fine when the package's checks/tests prove they work.
#### `add-dependency-and-validate-bounds`
Add an external dependency to a workspace project and run both validators for that same project/dependency:
```bash
uv run poe add-dependency-and-validate-bounds --project <workspace-package-name> --dependency "<dependency-spec>"
```
#### `upgrade-dev-dependencies`
Refresh exact dev dependency pins across the workspace, run `uv lock --upgrade`, reinstall from the frozen lockfile, then rerun validation, typing, and tests:
```bash
uv run poe upgrade-dev-dependencies
```
Use this for repo-wide dev tooling refreshes. For targeted runtime dependency upgrades, prefer `uv lock --upgrade-package <dependency-name>` plus the package-scoped bound validation tasks above.
### Comprehensive Checks
#### `check-packages`
+2 -2
View File
@@ -24,7 +24,7 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.0.0rc4",
"a2a-sdk>=0.3.5",
"a2a-sdk>=0.3.5,<0.3.24",
]
[tool.uv]
@@ -87,7 +87,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_a2a"
test = "pytest -m \"not integration\" --cov=agent_framework_a2a --cov-report=term-missing:skip-covered tests"
test = 'pytest -m "not integration" --cov=agent_framework_a2a --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
@@ -6,13 +6,12 @@ from __future__ import annotations
import logging
import os
from typing import cast
from typing import Any, cast
import uvicorn
from agent_framework import ChatOptions
from agent_framework._clients import SupportsChatGetResponse
from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint
from agent_framework.anthropic import AnthropicClient
from agent_framework.azure import AzureOpenAIChatClient
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
@@ -26,6 +25,15 @@ from ..agents.task_steps_agent import task_steps_agent_wrapped
from ..agents.ui_generator_agent import ui_generator_agent
from ..agents.weather_agent import weather_agent
AnthropicClient: type[Any] | None
try:
import agent_framework.anthropic as _anthropic_namespace
except ImportError:
# If the Anthropic client isn't installed, we can still run the server with Azure OpenAI as the default chat client
AnthropicClient = None
else:
AnthropicClient = cast(type[Any] | None, getattr(_anthropic_namespace, "AnthropicClient", None))
# Configure logging to file and console (disabled by default - set ENABLE_DEBUG_LOGGING=1 to enable)
if os.getenv("ENABLE_DEBUG_LOGGING"):
log_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "ag_ui_server.log")
@@ -70,7 +78,9 @@ app.add_middleware(
# Set CHAT_CLIENT=anthropic to use Anthropic, defaults to Azure OpenAI
client: SupportsChatGetResponse[ChatOptions] = cast(
SupportsChatGetResponse[ChatOptions],
AnthropicClient() if os.getenv("CHAT_CLIENT", "").lower() == "anthropic" else AzureOpenAIChatClient(),
AnthropicClient()
if AnthropicClient is not None and os.getenv("CHAT_CLIENT", "").lower() == "anthropic"
else AzureOpenAIChatClient(),
)
# Agentic Chat - basic chat agent
+6 -6
View File
@@ -23,15 +23,15 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.0.0rc4",
"ag-ui-protocol>=0.1.9",
"fastapi>=0.115.0",
"uvicorn>=0.30.0"
"ag-ui-protocol==0.1.13",
"fastapi>=0.115.0,<0.133.1",
"uvicorn[standard]>=0.30.0,<0.42.0"
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"httpx>=0.27.0",
"pytest==9.0.2",
"httpx==0.28.1",
]
[build-system]
@@ -74,4 +74,4 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_ag_ui"
test = "pytest -m \"not integration\" --cov=agent_framework_ag_ui --cov-report=term-missing:skip-covered -n auto --dist worksteal tests/ag_ui"
test = 'pytest -m "not integration" --cov=agent_framework_ag_ui --cov-report=term-missing:skip-covered -n auto --dist worksteal tests/ag_ui'
+2 -2
View File
@@ -24,7 +24,7 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.0.0rc4",
"anthropic>=0.70.0,<1",
"anthropic>=0.80.0,<0.80.1",
]
[tool.uv]
@@ -87,7 +87,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_anthropic"
test = "pytest -m \"not integration\" --cov=agent_framework_anthropic --cov-report=term-missing:skip-covered -n auto --dist worksteal tests"
test = 'pytest -m "not integration" --cov=agent_framework_anthropic --cov-report=term-missing:skip-covered -n auto --dist worksteal tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
@@ -24,7 +24,7 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.0.0rc4",
"azure-search-documents==11.7.0b2",
"azure-search-documents>=11.7.0b2,<11.7.0b3",
]
[tool.uv]
@@ -89,7 +89,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_ai_search"
test = "pytest -m \"not integration\" --cov=agent_framework_azure_ai_search --cov-report=term-missing:skip-covered tests"
test = 'pytest -m "not integration" --cov=agent_framework_azure_ai_search --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+4 -4
View File
@@ -24,9 +24,9 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.0.0rc4",
"azure-ai-agents == 1.2.0b5",
"azure-ai-inference>=1.0.0b9",
"aiohttp",
"azure-ai-agents>=1.2.0b5,<1.2.0b6",
"azure-ai-inference>=1.0.0b9,<1.0.0b10",
"aiohttp>=3.7.0,<4",
]
[tool.uv]
@@ -87,7 +87,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_ai"
test = "pytest -m \"not integration\" --cov=agent_framework_azure_ai --cov-report=term-missing:skip-covered tests"
test = 'pytest -m "not integration" --cov=agent_framework_azure_ai --cov-report=term-missing:skip-covered tests'
[tool.poe.tasks.integration-tests]
cmd = """
+1 -1
View File
@@ -24,7 +24,7 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.0.0rc4",
"azure-cosmos>=4.9.0",
"azure-cosmos>=4.3.0,<5",
]
[tool.uv]
@@ -24,8 +24,8 @@ classifiers = [
dependencies = [
"agent-framework-core>=1.0.0rc4",
"agent-framework-durabletask",
"azure-functions",
"azure-functions-durable",
"azure-functions>=1.24.0,<2",
"azure-functions-durable>=1.3.1,<2",
]
[dependency-groups]
@@ -93,7 +93,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azurefunctions"
test = "pytest -m \"not integration\" --cov=agent_framework_azurefunctions --cov-report=term-missing:skip-covered tests"
test = 'pytest -m "not integration" --cov=agent_framework_azurefunctions --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+1 -1
View File
@@ -86,7 +86,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_bedrock"
test = "pytest -m \"not integration\" --cov=agent_framework_bedrock --cov-report=term-missing:skip-covered tests"
test = 'pytest -m "not integration" --cov=agent_framework_bedrock --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["hatchling"]
+2 -2
View File
@@ -23,7 +23,7 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.0.0rc4",
"openai-chatkit>=1.4.0,<2.0.0",
"openai-chatkit>=1.4.1,<2.0.0",
]
[tool.uv]
@@ -88,7 +88,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_chatkit"
test = "pytest -m \"not integration\" --cov=agent_framework_chatkit --cov-report=term-missing:skip-covered tests"
test = 'pytest -m "not integration" --cov=agent_framework_chatkit --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+2 -2
View File
@@ -24,7 +24,7 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.0.0rc4",
"claude-agent-sdk>=0.1.25",
"claude-agent-sdk>=0.1.36,<0.1.49",
]
[tool.uv]
@@ -88,7 +88,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_claude"
test = "pytest -m \"not integration\" --cov=agent_framework_claude --cov-report=term-missing:skip-covered tests"
test = 'pytest -m "not integration" --cov=agent_framework_claude --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+2 -2
View File
@@ -24,7 +24,7 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.0.0rc4",
"microsoft-agents-copilotstudio-client>=0.3.1",
"microsoft-agents-copilotstudio-client>=0.3.1,<0.3.2",
]
[tool.uv]
@@ -87,7 +87,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_copilotstudio"
test = "pytest -m \"not integration\" --cov=agent_framework_copilotstudio --cov-report=term-missing:skip-covered tests"
test = 'pytest -m "not integration" --cov=agent_framework_copilotstudio --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
@@ -1,5 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff: noqa: RUF070, RUF100
from __future__ import annotations
import asyncio
@@ -665,7 +665,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
if output_format:
tool["output_format"] = output_format
if model:
tool["model"] = model
tool["model"] = model # type: ignore
if quality:
tool["quality"] = quality
if partial_images is not None:
+8 -16
View File
@@ -24,19 +24,19 @@ classifiers = [
]
dependencies = [
# utilities
"typing-extensions",
"typing-extensions>=4.15.0,<5",
"pydantic>=2,<3",
"python-dotenv>=1,<2",
# telemetry
"opentelemetry-api>=1.39.0",
"opentelemetry-sdk>=1.39.0",
"opentelemetry-semantic-conventions-ai>=0.4.13",
"opentelemetry-api>=1.39.0,<2",
"opentelemetry-sdk>=1.39.0,<2",
"opentelemetry-semantic-conventions-ai>=0.4.13,<0.4.14",
# connectors and functions
"openai>=1.99.0",
"openai>=1.99.0,<3",
"azure-identity>=1,<2",
"azure-ai-projects>=2.0.0,<3.0",
"mcp[ws]>=1.24.0,<2",
"packaging>=24.1",
"packaging>=24.1,<25",
]
[project.optional-dependencies]
@@ -76,15 +76,7 @@ environments = [
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = [
'tests',
'packages/core/tests',
'packages/a2a/tests',
'packages/azure-ai/tests',
'packages/copilotstudio/tests',
'packages/mem0/tests',
'packages/runtime/tests'
]
testpaths = ['tests']
addopts = "-ra -q -r fEX"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
@@ -131,7 +123,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework"
test = "pytest -m \"not integration\" --cov=agent_framework --cov-report=term-missing:skip-covered -n auto --dist worksteal tests"
test = 'pytest -m "not integration" --cov=agent_framework --cov-report=term-missing:skip-covered -n auto --dist worksteal tests'
[tool.flit.module]
name = "agent_framework"
@@ -0,0 +1,159 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import os
from unittest.mock import AsyncMock, MagicMock
import pytest
from openai.types import CreateEmbeddingResponse
from openai.types import Embedding as OpenAIEmbedding
from openai.types.create_embedding_response import Usage
from agent_framework.azure import AzureOpenAIEmbeddingClient
from agent_framework.openai import OpenAIEmbeddingOptions
def _make_openai_response(
embeddings: list[list[float]],
model: str = "text-embedding-3-small",
prompt_tokens: int = 5,
total_tokens: int = 5,
) -> CreateEmbeddingResponse:
"""Helper to create a mock OpenAI embeddings response."""
data = [OpenAIEmbedding(embedding=emb, index=i, object="embedding") for i, emb in enumerate(embeddings)]
return CreateEmbeddingResponse(
data=data,
model=model,
object="list",
usage=Usage(prompt_tokens=prompt_tokens, total_tokens=total_tokens),
)
@pytest.fixture
def azure_embedding_unit_test_env(monkeypatch: pytest.MonkeyPatch) -> None:
"""Clear ambient Azure OpenAI embedding env vars for deterministic unit tests."""
for key in (
"AZURE_OPENAI_ENDPOINT",
"AZURE_OPENAI_API_KEY",
"AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME",
"AZURE_OPENAI_BASE_URL",
"AZURE_OPENAI_TOKEN_ENDPOINT",
):
monkeypatch.delenv(key, raising=False)
def test_azure_construction_with_deployment_name(azure_embedding_unit_test_env: None) -> None:
client = AzureOpenAIEmbeddingClient(
deployment_name="text-embedding-3-small",
api_key="test-key",
endpoint="https://test.openai.azure.com/",
)
assert client.model_id == "text-embedding-3-small"
def test_azure_construction_with_existing_client(azure_embedding_unit_test_env: None) -> None:
mock_client = MagicMock()
client = AzureOpenAIEmbeddingClient(
deployment_name="my-deployment",
async_client=mock_client,
)
assert client.model_id == "my-deployment"
assert client.client is mock_client
def test_azure_construction_missing_deployment_name_raises(azure_embedding_unit_test_env: None) -> None:
with pytest.raises(ValueError, match="deployment name is required"):
AzureOpenAIEmbeddingClient(
api_key="test-key",
endpoint="https://test.openai.azure.com/",
)
def test_azure_construction_missing_credentials_raises(azure_embedding_unit_test_env: None) -> None:
with pytest.raises(ValueError, match="api_key, credential, or a client"):
AzureOpenAIEmbeddingClient(
deployment_name="test",
endpoint="https://test.openai.azure.com/",
)
async def test_azure_get_embeddings(azure_embedding_unit_test_env: None) -> None:
mock_response = _make_openai_response(
embeddings=[[0.1, 0.2]],
)
mock_async_client = MagicMock()
mock_async_client.embeddings = MagicMock()
mock_async_client.embeddings.create = AsyncMock(return_value=mock_response)
client = AzureOpenAIEmbeddingClient(
deployment_name="text-embedding-3-small",
async_client=mock_async_client,
)
result = await client.get_embeddings(["hello"])
assert len(result) == 1
assert result[0].vector == [0.1, 0.2]
def test_azure_otel_provider_name(azure_embedding_unit_test_env: None) -> None:
mock_client = MagicMock()
client = AzureOpenAIEmbeddingClient(
deployment_name="test",
async_client=mock_client,
)
assert client.OTEL_PROVIDER_NAME == "azure.ai.openai"
skip_if_azure_openai_integration_tests_disabled = pytest.mark.skipif(
not os.getenv("AZURE_OPENAI_ENDPOINT")
or (not os.getenv("AZURE_OPENAI_API_KEY") and not os.getenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME")),
reason="No Azure OpenAI credentials provided; skipping integration tests.",
)
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
async def test_integration_azure_openai_get_embeddings() -> None:
"""End-to-end test of Azure OpenAI embedding generation."""
client = AzureOpenAIEmbeddingClient()
result = await client.get_embeddings(["hello world"])
assert len(result) == 1
assert isinstance(result[0].vector, list)
assert len(result[0].vector) > 0
assert all(isinstance(v, float) for v in result[0].vector)
assert result[0].model_id is not None
assert result.usage is not None
assert result.usage["input_token_count"] > 0
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
async def test_integration_azure_openai_get_embeddings_multiple() -> None:
"""Test Azure OpenAI embedding generation for multiple inputs."""
client = AzureOpenAIEmbeddingClient()
result = await client.get_embeddings(["hello", "world", "test"])
assert len(result) == 3
dims = [len(e.vector) for e in result]
assert all(d == dims[0] for d in dims)
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
async def test_integration_azure_openai_get_embeddings_with_dimensions() -> None:
"""Test Azure OpenAI embedding generation with custom dimensions."""
client = AzureOpenAIEmbeddingClient()
options: OpenAIEmbeddingOptions = {"dimensions": 256}
result = await client.get_embeddings(["hello world"], options=options)
assert len(result) == 1
assert len(result[0].vector) == 256
@@ -10,7 +10,6 @@ from openai.types import CreateEmbeddingResponse
from openai.types import Embedding as OpenAIEmbedding
from openai.types.create_embedding_response import Usage
from agent_framework.azure import AzureOpenAIEmbeddingClient
from agent_framework.openai import (
OpenAIEmbeddingClient,
OpenAIEmbeddingOptions,
@@ -190,73 +189,6 @@ async def test_openai_empty_values_returns_empty(openai_unit_test_env: None) ->
client.client.embeddings.create.assert_not_called()
# --- Azure OpenAI unit tests ---
def test_azure_construction_with_deployment_name() -> None:
client = AzureOpenAIEmbeddingClient(
deployment_name="text-embedding-3-small",
api_key="test-key",
endpoint="https://test.openai.azure.com/",
)
assert client.model_id == "text-embedding-3-small"
def test_azure_construction_with_existing_client() -> None:
mock_client = MagicMock()
client = AzureOpenAIEmbeddingClient(
deployment_name="my-deployment",
async_client=mock_client,
)
assert client.model_id == "my-deployment"
assert client.client is mock_client
def test_azure_construction_missing_deployment_name_raises(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME", raising=False)
with pytest.raises(ValueError, match="deployment name is required"):
AzureOpenAIEmbeddingClient(
api_key="test-key",
endpoint="https://test.openai.azure.com/",
)
def test_azure_construction_missing_credentials_raises() -> None:
with pytest.raises(ValueError, match="api_key, credential, or a client"):
AzureOpenAIEmbeddingClient(
deployment_name="test",
endpoint="https://test.openai.azure.com/",
)
async def test_azure_get_embeddings() -> None:
mock_response = _make_openai_response(
embeddings=[[0.1, 0.2]],
)
mock_async_client = MagicMock()
mock_async_client.embeddings = MagicMock()
mock_async_client.embeddings.create = AsyncMock(return_value=mock_response)
client = AzureOpenAIEmbeddingClient(
deployment_name="text-embedding-3-small",
async_client=mock_async_client,
)
result = await client.get_embeddings(["hello"])
assert len(result) == 1
assert result[0].vector == [0.1, 0.2]
def test_azure_otel_provider_name() -> None:
mock_client = MagicMock()
client = AzureOpenAIEmbeddingClient(
deployment_name="test",
async_client=mock_client,
)
assert client.OTEL_PROVIDER_NAME == "azure.ai.openai"
# --- Integration tests ---
skip_if_openai_integration_tests_disabled = pytest.mark.skipif(
@@ -264,12 +196,6 @@ skip_if_openai_integration_tests_disabled = pytest.mark.skipif(
reason="No real OPENAI_API_KEY provided; skipping integration tests.",
)
skip_if_azure_openai_integration_tests_disabled = pytest.mark.skipif(
not os.getenv("AZURE_OPENAI_ENDPOINT")
or (not os.getenv("AZURE_OPENAI_API_KEY") and not os.getenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME")),
reason="No Azure OpenAI credentials provided; skipping integration tests.",
)
@skip_if_openai_integration_tests_disabled
@pytest.mark.flaky
@@ -315,49 +241,3 @@ async def test_integration_openai_get_embeddings_with_dimensions() -> None:
assert len(result) == 1
assert len(result[0].vector) == 256
@skip_if_azure_openai_integration_tests_disabled
@pytest.mark.flaky
@pytest.mark.integration
async def test_integration_azure_openai_get_embeddings() -> None:
"""End-to-end test of Azure OpenAI embedding generation."""
client = AzureOpenAIEmbeddingClient()
result = await client.get_embeddings(["hello world"])
assert len(result) == 1
assert isinstance(result[0].vector, list)
assert len(result[0].vector) > 0
assert all(isinstance(v, float) for v in result[0].vector)
assert result[0].model_id is not None
assert result.usage is not None
assert result.usage["input_token_count"] > 0
@skip_if_azure_openai_integration_tests_disabled
@pytest.mark.flaky
@pytest.mark.integration
async def test_integration_azure_openai_get_embeddings_multiple() -> None:
"""Test Azure OpenAI embedding generation for multiple inputs."""
client = AzureOpenAIEmbeddingClient()
result = await client.get_embeddings(["hello", "world", "test"])
assert len(result) == 3
dims = [len(e.vector) for e in result]
assert all(d == dims[0] for d in dims)
@skip_if_azure_openai_integration_tests_disabled
@pytest.mark.flaky
@pytest.mark.integration
async def test_integration_azure_openai_get_embeddings_with_dimensions() -> None:
"""Test Azure OpenAI embedding generation with custom dimensions."""
client = AzureOpenAIEmbeddingClient()
options: OpenAIEmbeddingOptions = {"dimensions": 256}
result = await client.get_embeddings(["hello world"], options=options)
assert len(result) == 1
assert len(result[0].vector) == 256
@@ -1876,6 +1876,19 @@ def test_prepare_tools_for_openai_with_image_generation_options() -> None:
assert image_tool["quality"] == "high"
def test_prepare_tools_for_openai_with_custom_image_generation_model() -> None:
"""Test image generation tool conversion with a custom model string."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
tool = OpenAIResponsesClient.get_image_generation_tool(model="custom-image-model")
resp_tools = client._prepare_tools_for_openai([tool])
assert len(resp_tools) == 1
image_tool = resp_tools[0]
assert image_tool["type"] == "image_generation"
assert image_tool["model"] == "custom-image-model"
def test_parse_chunk_from_openai_with_mcp_approval_request() -> None:
"""Test that a streaming mcp_approval_request event is parsed into FunctionApprovalRequestContent."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
+3 -3
View File
@@ -23,12 +23,12 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.0.0rc4",
"powerfx>=0.0.31; python_version < '3.14'",
"powerfx>=0.0.32,<0.0.35; python_version < '3.14'",
"pyyaml>=6.0,<7.0",
]
[dependency-groups]
dev = [
"types-PyYaml"
"types-PyYaml==6.0.12.20250915"
]
[tool.uv]
@@ -94,7 +94,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_declarative"
test = "pytest -m \"not integration\" --cov=agent_framework_declarative --cov-report=term-missing:skip-covered tests"
test = 'pytest -m "not integration" --cov=agent_framework_declarative --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+12 -6
View File
@@ -24,14 +24,20 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.0.0rc4",
"fastapi>=0.104.0",
"uvicorn[standard]>=0.24.0",
"python-dotenv>=1.0.0",
"fastapi>=0.115.0,<0.133.1",
"uvicorn[standard]>=0.30.0,<0.42.0"
]
[project.optional-dependencies]
dev = ["pytest>=7.0.0", "watchdog>=3.0.0", "agent-framework-orchestrations"]
all = ["pytest>=7.0.0", "watchdog>=3.0.0"]
dev = [
"pytest==9.0.2",
"watchdog==6.0.0",
"agent-framework-orchestrations==1.0.0b260304",
]
all = [
"pytest==9.0.2",
"watchdog==6.0.0",
]
[project.scripts]
devui = "agent_framework_devui:main"
@@ -94,7 +100,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_devui"
test = "pytest -m \"not integration\" --cov=agent_framework_devui --cov-report=term-missing:skip-covered tests"
test = 'pytest -m "not integration" --cov=agent_framework_devui --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+5 -9
View File
@@ -29,18 +29,16 @@ Durable execution support for long-running agent workflows using Azure Durable F
## Usage
```python
from durabletask.client import TaskHubGrpcClient
from durabletask.worker import TaskHubGrpcWorker
from agent_framework import ChatAgent
from agent_framework import Agent
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_durabletask import DurableAIAgentClient, DurableAIAgentWorker
from durabletask.client import TaskHubGrpcClient
from durabletask.worker import TaskHubGrpcWorker
# Client side
dt_client = TaskHubGrpcClient(host_address="localhost:4001")
agent_client = DurableAIAgentClient(dt_client)
agent = agent_client.get_agent("assistant")
response = agent.run("Hello, how are you?")
print(response.text)
durable_agent = agent_client.get_agent("assistant")
# Worker side
dt_worker = TaskHubGrpcWorker(host_address="localhost:4001")
@@ -48,10 +46,8 @@ agent_worker = DurableAIAgentWorker(dt_worker)
# Create a chat client for the agent
chat_client = AzureOpenAIChatClient()
my_agent = ChatAgent(chat_client=chat_client, name="assistant")
my_agent = Agent(client=chat_client, name="assistant")
agent_worker.add_agent(my_agent)
dt_worker.start()
```
## Import Path
+8 -7
View File
@@ -15,17 +15,18 @@ The durable task integration lets you host Microsoft Agent Framework agents usin
### Basic Usage Example
```python
from agent_framework import Agent
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_durabletask import DurableAIAgentWorker
from durabletask.worker import TaskHubGrpcWorker
from agent_framework.azure import DurableAIAgentWorker
# Create the worker
with TaskHubGrpcWorker(...) as worker:
worker = TaskHubGrpcWorker(host_address="localhost:4001")
agent_worker = DurableAIAgentWorker(worker)
# Register the agent worker wrapper
agent_worker = DurableAIAgentWorker(worker)
# Register the agent
agent_worker.add_agent(my_agent)
chat_client = AzureOpenAIChatClient()
my_agent = Agent(client=chat_client, name="assistant")
agent_worker.add_agent(my_agent)
```
For more details, review the Python [README](https://github.com/microsoft/agent-framework/tree/main/python/README.md) and the samples directory.
@@ -29,9 +29,10 @@ class DurableAIAgentWorker:
Example:
```python
from durabletask import TaskHubGrpcWorker
from durabletask.worker import TaskHubGrpcWorker
from agent_framework import Agent
from agent_framework.azure import DurableAIAgentWorker
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_durabletask import DurableAIAgentWorker
# Create the underlying worker
worker = TaskHubGrpcWorker(host_address="localhost:4001")
@@ -40,6 +41,7 @@ class DurableAIAgentWorker:
agent_worker = DurableAIAgentWorker(worker)
# Register agents
client = AzureOpenAIChatClient()
my_agent = Agent(client=client, name="assistant")
agent_worker.add_agent(my_agent)
+5 -5
View File
@@ -23,14 +23,14 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.0.0rc4",
"durabletask>=1.3.0",
"durabletask-azuremanaged>=1.3.0",
"python-dateutil>=2.8.0",
"durabletask>=1.3.0,<2",
"durabletask-azuremanaged>=1.3.0,<2",
"python-dateutil>=2.8.0,<3",
]
[dependency-groups]
dev = [
"types-python-dateutil>=2.9.0",
"types-python-dateutil==2.9.0.20260305",
]
[tool.uv]
@@ -99,7 +99,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_durabletask"
test = "pytest -m \"not integration\" --cov=agent_framework_durabletask --cov-report=term-missing:skip-covered tests"
test = 'pytest -m "not integration" --cov=agent_framework_durabletask --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+2 -2
View File
@@ -24,7 +24,7 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.0.0rc4",
"foundry-local-sdk>=0.5.1,<1",
"foundry-local-sdk>=0.5.1,<0.5.2",
]
[tool.uv]
@@ -86,7 +86,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_foundry_local"
test = "pytest -m \"not integration\" --cov=agent_framework_foundry_local --cov-report=term-missing:skip-covered tests"
test = 'pytest -m "not integration" --cov=agent_framework_foundry_local --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
@@ -25,20 +25,27 @@ from agent_framework._settings import load_settings
from agent_framework._tools import FunctionTool, ToolTypes
from agent_framework._types import AgentRunInputs, normalize_tools
from agent_framework.exceptions import AgentException
from copilot import CopilotClient, CopilotSession
from copilot.generated.session_events import PermissionRequest, SessionEvent, SessionEventType
from copilot.types import (
CopilotClientOptions,
MCPServerConfig,
MessageOptions,
PermissionRequestResult,
ResumeSessionConfig,
SessionConfig,
SystemMessageConfig,
ToolInvocation,
ToolResult,
)
from copilot.types import Tool as CopilotTool
try:
from copilot import CopilotClient, CopilotSession
from copilot.generated.session_events import PermissionRequest, SessionEvent, SessionEventType
from copilot.types import (
CopilotClientOptions,
MCPServerConfig,
MessageOptions,
PermissionRequestResult,
ResumeSessionConfig,
SessionConfig,
SystemMessageConfig,
ToolInvocation,
ToolResult,
)
from copilot.types import Tool as CopilotTool
except ImportError as _copilot_import_error:
raise ImportError(
"GitHubCopilotAgent requires the 'github-copilot-sdk' package, which is only available on Python 3.11+. "
"Please use Python 3.11 or later."
) from _copilot_import_error
if sys.version_info >= (3, 13):
from typing import TypeVar
+11 -3
View File
@@ -3,7 +3,7 @@ name = "agent-framework-github-copilot"
description = "GitHub Copilot integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.11"
requires-python = ">=3.10"
version = "1.0.0b260311"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
@@ -15,6 +15,7 @@ classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
@@ -23,7 +24,7 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.0.0rc4",
"github-copilot-sdk>=0.1.32",
"github-copilot-sdk>=0.1.31,<0.1.33; python_version >= '3.11'",
]
[tool.uv]
@@ -85,9 +86,16 @@ executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_github_copilot"
test = "pytest -m \"not integration\" --cov=agent_framework_github_copilot --cov-report=term-missing:skip-covered tests"
[tool.poe.tasks.pyright]
shell = "python -c \"import sys; exit(0 if sys.version_info < (3,11) else 1)\" || pyright"
interpreter = "posix"
[tool.poe.tasks.mypy]
shell = "python -c \"import sys; exit(0 if sys.version_info < (3,11) else 1)\" || mypy --config-file $POE_ROOT/pyproject.toml agent_framework_github_copilot"
interpreter = "posix"
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
build-backend = "flit_core.buildapi"
@@ -1,5 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff: noqa: E402
import unittest.mock
from datetime import datetime, timezone
from typing import Any
@@ -7,6 +9,9 @@ from unittest.mock import AsyncMock, MagicMock, patch
from uuid import uuid4
import pytest
copilot = pytest.importorskip("copilot")
from agent_framework import (
AgentResponse,
AgentResponseUpdate,
+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()
+8 -6
View File
@@ -4,23 +4,25 @@ Integration with Mem0 for agent memory management.
## Main Classes
- **`Mem0Provider`** - Context provider that integrates Mem0 memory into agents
- **`Mem0ContextProvider`** - Context provider that integrates Mem0 memory into agents
## Usage
```python
from agent_framework.mem0 import Mem0Provider
from agent_framework.mem0 import Mem0ContextProvider
provider = Mem0Provider(api_key="your-key")
agent = Agent(..., context_provider=provider)
provider = Mem0ContextProvider(
api_key="your-key",
user_id="user-id",
)
```
## Import Path
```python
from agent_framework.mem0 import Mem0Provider
from agent_framework.mem0 import Mem0ContextProvider
# or directly:
from agent_framework_mem0 import Mem0Provider
from agent_framework_mem0 import Mem0ContextProvider
```
## Notes
+2 -2
View File
@@ -24,7 +24,7 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.0.0rc4",
"mem0ai>=1.0.0",
"mem0ai>=1.0.0,<2",
]
[tool.uv]
@@ -87,7 +87,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_mem0"
test = "pytest -m \"not integration\" --cov=agent_framework_mem0 --cov-report=term-missing:skip-covered tests"
test = 'pytest -m "not integration" --cov=agent_framework_mem0 --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+2 -2
View File
@@ -24,7 +24,7 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.0.0rc4",
"ollama >= 0.5.3",
"ollama>=0.5.3,<0.5.4",
]
[tool.uv]
@@ -90,7 +90,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_ollama"
test = "pytest -m \"not integration\" --cov=agent_framework_ollama --cov-report=term-missing:skip-covered tests"
test = 'pytest -m "not integration" --cov=agent_framework_ollama --cov-report=term-missing:skip-covered tests'
[tool.uv.build-backend]
module-name = "agent_framework_ollama"
@@ -85,7 +85,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_orchestrations"
test = "pytest -m \"not integration\" --cov=agent_framework_orchestrations --cov-report=term-missing:skip-covered -n auto --dist worksteal tests"
test = 'pytest -m "not integration" --cov=agent_framework_orchestrations --cov-report=term-missing:skip-covered -n auto --dist worksteal tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+3 -3
View File
@@ -25,8 +25,8 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.0.0rc4",
"azure-core>=1.30.0",
"httpx>=0.27.0",
"azure-core>=1.30.0,<2",
"httpx>=0.27.0,<0.29",
]
[tool.uv]
@@ -86,7 +86,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_purview"
test = "pytest -m \"not integration\" --cov=agent_framework_purview --cov-report=term-missing:skip-covered tests"
test = 'pytest -m "not integration" --cov=agent_framework_purview --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.9,<4.0"]
+7 -7
View File
@@ -4,22 +4,22 @@ Redis-based storage for agent threads and context.
## Main Classes
- **`RedisChatMessageStore`** - Persistent message store using Redis
- **`RedisProvider`** - Context provider with Redis backing
- **`RedisHistoryProvider`** - Persistent chat history provider using Redis
- **`RedisContextProvider`** - Context provider with Redis-backed retrieval
## Usage
```python
from agent_framework.redis import RedisChatMessageStore
from agent_framework.redis import RedisContextProvider, RedisHistoryProvider
store = RedisChatMessageStore(redis_url="redis://localhost:6379")
agent = Agent(..., chat_message_store_factory=lambda: store)
context_provider = RedisContextProvider(redis_url="redis://localhost:6379")
history_provider = RedisHistoryProvider(redis_url="redis://localhost:6379")
```
## Import Path
```python
from agent_framework.redis import RedisChatMessageStore, RedisProvider
from agent_framework.redis import RedisContextProvider, RedisHistoryProvider
# or directly:
from agent_framework_redis import RedisChatMessageStore
from agent_framework_redis import RedisContextProvider, RedisHistoryProvider
```
+3 -3
View File
@@ -10,15 +10,15 @@ pip install agent-framework-redis --pre
### Memory Context Provider
The `RedisProvider` enables persistent context & memory capabilities for your agents, allowing them to remember user preferences and conversation context across sessions and threads.
The `RedisContextProvider` enables persistent context and memory capabilities for your agents, allowing them to remember user preferences and conversation context across sessions and threads.
#### Basic Usage Examples
Review the set of [getting started examples](../../samples/02-agents/context_providers/redis/README.md) for using the Redis context provider.
### Redis Chat Message Store
### Redis History Provider
The `RedisChatMessageStore` provides persistent conversation storage using Redis Lists, enabling chat history to survive application restarts and support distributed applications.
The `RedisHistoryProvider` provides persistent conversation storage using Redis Lists, enabling chat history to survive application restarts and support distributed applications.
#### Key Features
+4 -4
View File
@@ -24,9 +24,9 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.0.0rc4",
"redis>=6.4.0",
"redisvl>=0.8.2",
"numpy>=2.2.6"
"redis>=6.4.0,<7.2.1",
"redisvl>=0.11.0,<0.16",
"numpy>=2.2.6,<3"
]
[tool.uv]
@@ -89,7 +89,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_redis"
test = "pytest -m \"not integration\" --cov=agent_framework_redis --cov-report=term-missing:skip-covered tests"
test = 'pytest -m "not integration" --cov=agent_framework_redis --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+70 -31
View File
@@ -28,22 +28,22 @@ dependencies = [
[dependency-groups]
dev = [
"uv>=0.9,<1.0.0",
"flit>=3.12.0",
"ruff>=0.11.8",
"pytest>=8.4.1",
"pytest-asyncio>=1.0.0",
"pytest-cov>=6.2.1",
"pytest-xdist[psutil]>=3.8.0",
"pytest-timeout>=2.3.1",
"pytest-retry>=1",
"mypy>=1.16.1",
"pyright>=1.1.402",
"uv==0.10.9",
"flit==3.12.0",
"ruff==0.15.5",
"pytest==9.0.2",
"pytest-asyncio==1.3.0",
"pytest-cov==7.0.0",
"pytest-xdist[psutil]==3.8.0",
"pytest-timeout==2.4.0",
"pytest-retry==1.7.0",
"mypy==1.19.1",
"pyright==1.1.408",
#tasks
"poethepoet>=0.36.0",
"rich",
"tomli",
"prek>=0.3.2",
"poethepoet==0.42.1",
"rich==13.7.1",
"tomli==2.4.0",
"prek==0.3.4",
]
[tool.uv]
@@ -54,18 +54,6 @@ environments = [
"sys_platform == 'linux'",
"sys_platform == 'win32'"
]
override-dependencies = [
# A conflict between the dependency of litellm[proxy] < 0.30.0, which is a dependency of agent-lightning
# and uvicorn >= 0.34.0, which is a dependency of tau2
"uvicorn==0.38.0",
# Similar problem with websockets, which is a dependency conflict between litellm[proxy] and mcp
"websockets==15.0.1",
# grpcio 1.67.x has no Python 3.14 wheels; grpcio 1.76.0+ supports Python 3.14
# litellm constrains grpcio<1.68.0 due to resource exhaustion bug (https://github.com/grpc/grpc/issues/38290)
# Use version-specific overrides to satisfy both constraints
"grpcio>=1.76.0; python_version >= '3.14'",
"grpcio>=1.62.3,<1.68.0; python_version < '3.14'",
]
[tool.uv.workspace]
members = [ "packages/*" ]
@@ -149,8 +137,6 @@ ignore = [
"**/tests/**" = ["D", "INP", "TD", "ERA001", "RUF", "S"]
"samples/**" = ["D", "INP", "ERA001", "RUF", "S", "T201", "CPY"]
"*.ipynb" = ["CPY", "E501"]
# RUF070: Assignment before yield is intentional - context manager must exit before yielding
"**/agent_framework/_workflows/_workflow.py" = ["RUF070"]
[tool.ruff.format]
docstring-code-format = true
@@ -213,7 +199,7 @@ executor.type = "uv"
[tool.poe.tasks]
markdown-code-lint = "uv run python scripts/check_md_code_blocks.py 'README.md' './packages/**/README.md' './samples/**/*.md' --exclude cookiecutter-agent-framework-lab --exclude tau2 --exclude 'packages/devui/frontend' --exclude context_providers/azure_ai_search"
prek-install = "prek install --overwrite"
install = "uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit"
install = "uv sync --all-packages --all-extras --dev --frozen --prerelease=if-necessary-or-explicit"
test = "python scripts/run_tasks_in_packages_if_exists.py test"
fmt = "python scripts/run_tasks_in_packages_if_exists.py fmt"
format.ref = "fmt"
@@ -221,8 +207,9 @@ lint = "python scripts/run_tasks_in_packages_if_exists.py lint"
samples-lint = "ruff check samples --fix --exclude samples/autogen-migration,samples/semantic-kernel-migration --ignore E501,ASYNC,B901,TD002"
pyright = "python scripts/run_tasks_in_packages_if_exists.py pyright"
mypy = "python scripts/run_tasks_in_packages_if_exists.py mypy"
samples-syntax = "pyright -p pyrightconfig.samples.json --warnings"
typing = "python scripts/run_tasks_in_packages_if_exists.py mypy pyright"
samples-syntax.shell = "pyright -p $(python -c \"import sys; print('pyrightconfig.samples.py310.json' if sys.version_info < (3,11) else 'pyrightconfig.samples.json')\") --warnings"
samples-syntax.interpreter = "posix"
# cleaning
clean-dist-packages = "python scripts/run_tasks_in_packages_if_exists.py clean-dist"
clean-dist-meta = "rm -rf dist"
@@ -287,6 +274,58 @@ sequence = [
]
args = [{ name = "python", default = "3.13", options = ['-p', '--python'] }]
[tool.poe.tasks.upgrade-dev-dependency-pins]
cmd = "python -m scripts.dependencies.upgrade_dev_dependencies"
[tool.poe.tasks.upgrade-lockfile]
cmd = "uv lock --upgrade"
[tool.poe.tasks.upgrade-dev-dependencies]
sequence = [
{ ref = "upgrade-dev-dependency-pins" },
{ ref = "upgrade-lockfile" },
{ ref = "install" },
{ ref = "check" },
{ ref = "typing" },
{ ref = "test" },
]
[tool.poe.tasks.add-dependency-to-project]
cmd = "uv add --package ${project} ${dependency}"
args = [
{ name = "project", options = ["-p", "--project"] },
{ name = "dependency", options = ["-d", "--dependency"] },
]
[tool.poe.tasks.validate-dependency-bounds-test]
shell = "python -m scripts.dependencies.validate_dependency_bounds --mode test --package \"$project\""
args = [{ name = "project", default = "*", options = ["-p", "--project"] }]
[tool.poe.tasks.validate-dependency-bounds-project]
shell = """
command=(python -m scripts.dependencies.validate_dependency_bounds --mode "${mode}" --package "${project}")
if [ -n "${dependency}" ]; then
command+=(--dependencies "${dependency}")
fi
"${command[@]}"
"""
interpreter = "bash"
args = [
{ name = "mode", default = "both", options = ["-m", "--mode"] },
{ name = "project", default = "*", options = ["-p", "--project"] },
{ name = "dependency", default = "", options = ["-d", "--dependency"] },
]
[tool.poe.tasks.add-dependency-and-validate-bounds]
sequence = [
{ ref = "add-dependency-to-project --project ${project} --dependency ${dependency}" },
{ ref = "validate-dependency-bounds-project --mode both --project ${project} --dependency ${dependency}" },
]
args = [
{ name = "project", options = ["-p", "--project"] },
{ name = "dependency", options = ["-d", "--dependency"] },
]
[tool.poe.tasks.prek-pyright]
cmd = "uv run python scripts/run_tasks_in_changed_packages.py pyright --files ${files}"
args = [{ name = "files", default = ".", positional = true, multiple = true }]
+17
View File
@@ -0,0 +1,17 @@
{
"include": ["samples"],
"exclude": [
"**/autogen/**",
"**/autogen-migration/**",
"**/semantic-kernel-migration/**",
"**/demos/**",
"**/_to_delete/**",
"**/05-end-to-end/**",
"**/agent_with_foundry_tracing.py",
"**/azure_responses_client_with_foundry.py",
"**/github_copilot/**"
],
"typeCheckingMode": "off",
"reportMissingImports": "error",
"reportAttributeAccessIssue": "error"
}
+5
View File
@@ -0,0 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
"""Shared Python workspace scripts."""
+95
View File
@@ -0,0 +1,95 @@
# Dependency Scripts
This folder contains the Python workspace tooling for dependency maintenance:
- validating runtime dependency lower and upper bounds
- refreshing exact dev dependency pins
- writing dependency validation reports for local runs and workflows
Run the commands below from the `python/` directory.
## Files in this folder
- `validate_dependency_bounds.py`
- Main entrypoint for dependency-bound workflows.
- Supports `test`, `lower`, `upper`, and `both` modes.
- `test` runs workspace-wide smoke validation at the lower and upper ends of the currently allowed ranges.
- `lower`, `upper`, and `both` dispatch to the lower/upper optimizer implementations for one package.
- `upgrade_dev_dependencies.py`
- Refreshes exact dev dependency pins across the root `pyproject.toml` and package `pyproject.toml` files.
- Reuses the same version-selection logic as the upper-bound tooling so direct dev-tooling refreshes and dependency-range expansion stay consistent.
- `_dependency_bounds_lower_impl.py`
- Package-scoped lower-bound optimizer.
- Tries older dependency versions within the currently allowed line and keeps the oldest passing lower bound.
- Writes `dependency-lower-bound-results.json` in this folder by default.
- `_dependency_bounds_upper_impl.py`
- Package-scoped upper-bound optimizer.
- Tries newer dependency versions within candidate lines and keeps the newest passing upper bound.
- Also contains shared parsing/rewrite helpers reused by `upgrade_dev_dependencies.py`.
- Writes `dependency-range-results.json` in this folder by default.
- `_dependency_bounds_runtime.py`
- Shared helper used by the validators to build isolated `uv run` commands.
- Reattaches the repo-wide toolchain (`ruff`, `pyright`, `pytest`, `poethepoet`, and related helpers) inside temporary environments so package tasks behave the same way they do in the workspace.
## Common entrypoints
### Poe tasks
These are the normal user-facing entrypoints:
```bash
uv run poe upgrade-dev-dependency-pins
uv run poe upgrade-dev-dependencies
uv run poe validate-dependency-bounds-test
uv run poe validate-dependency-bounds-test --project <workspace-package-name>
uv run poe validate-dependency-bounds-project --mode both --project <workspace-package-name> --dependency "<dependency-name>"
```
- `upgrade-dev-dependency-pins` only refreshes exact dev pins in `pyproject.toml` files.
- `upgrade-dev-dependencies` refreshes dev pins (using task above), runs `uv lock --upgrade`, reinstalls from the frozen lockfile, then runs `check`, `typing`, and `test`.
- `validate-dependency-bounds-test` runs the repo-wide lower/upper smoke gate.
- `validate-dependency-bounds-project` is the single package-scoped task; use `--mode lower`, `--mode upper`, or `--mode both` for the target package/dependency pair. Its `--project` argument defaults to `*`, and `--dependency` is optional, so automation can also use it for repo-wide upper-bound runs.
### GitHub Actions workflows
These workflows call the Poe tasks:
- `.github/workflows/python-dependency-range-validation.yml`
- Trigger: `workflow_dispatch`
- Runs `uv run poe validate-dependency-bounds-project --mode upper --project "*"`
- Uploads `python/scripts/dependencies/dependency-range-results.json`
- Creates issues for failing candidate versions and opens/updates a PR for passing range updates
- `.github/workflows/python-dev-dependency-upgrade.yml`
- Trigger: `workflow_dispatch`
- Runs `uv run poe upgrade-dev-dependencies`
- Commits any resulting `pyproject.toml` / `uv.lock` changes and opens/updates a PR
### Direct module execution
These are useful for debugging or targeted manual runs:
```bash
python -m scripts.dependencies.upgrade_dev_dependencies --dry-run --version-source lock
python -m scripts.dependencies.validate_dependency_bounds --mode test --package packages/core --dry-run
python -m scripts.dependencies.validate_dependency_bounds --mode both --package packages/core --dependencies openai --dry-run
python -m scripts.dependencies._dependency_bounds_lower_impl --packages packages/core --dependencies openai --dry-run
python -m scripts.dependencies._dependency_bounds_upper_impl --packages packages/core --dependencies openai --dry-run
```
Use the direct lower/upper implementation modules mainly for debugging or development of the optimizers themselves. For normal usage, prefer the Poe tasks or `validate_dependency_bounds.py`.
## Generated report files
The validators write JSON reports into this folder:
- `dependency-bounds-test-results.json`
- `dependency-lower-bound-results.json`
- `dependency-range-results.json`
These report files are ignored by git.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,86 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff: noqa: INP001
"""Shared runtime helpers for dependency-bound validation commands."""
from __future__ import annotations
from functools import lru_cache
from pathlib import Path
import tomli
from packaging.requirements import InvalidRequirement, Requirement
_TOOL_REQUIREMENT_NAMES = {
"mypy",
"poethepoet",
"pyright",
"pytest",
"pytest-asyncio",
"pytest-cov",
"pytest-retry",
"pytest-timeout",
"pytest-xdist",
"ruff",
}
_ADDITIONAL_RUNTIME_REQUIREMENTS = (
"graphviz",
"opentelemetry-exporter-otlp-proto-grpc",
"opentelemetry-exporter-otlp-proto-http",
)
# Run pyright through the current interpreter so its import resolution matches the uv-created environment.
_PYRIGHT_COMMAND = (
"import subprocess, sys; "
"raise SystemExit(subprocess.call([sys.executable, '-m', 'pyright', '--pythonpath', sys.executable]))"
)
@lru_cache(maxsize=8)
def load_runtime_tool_requirements(workspace_root: str) -> list[str]:
"""Load shared tool requirements used by package test and typing tasks."""
workspace_path = Path(workspace_root)
pyproject_path = workspace_path / "pyproject.toml"
data = tomli.loads(pyproject_path.read_text())
dev_requirements = data.get("dependency-groups", {}).get("dev", []) or []
# `uv run --isolated` starts from a clean environment, so the validator has to re-attach the
# shared tooling that package-level poe tasks expect to find.
runtime_requirements: list[str] = []
for requirement in dev_requirements:
if not isinstance(requirement, str):
continue
try:
parsed = Requirement(requirement)
except InvalidRequirement:
continue
if parsed.name.lower() in _TOOL_REQUIREMENT_NAMES:
runtime_requirements.append(requirement)
return runtime_requirements
def extend_command_with_runtime_tools(command: list[str], workspace_root: Path) -> None:
"""Append shared tooling requirements to a uv run command."""
# Mirror the repo-wide test/lint toolchain inside the temporary environment before adding the task.
for requirement in load_runtime_tool_requirements(str(workspace_root.resolve())):
command.extend(["--with", requirement])
for requirement in _ADDITIONAL_RUNTIME_REQUIREMENTS:
command.extend(["--with", requirement])
def extend_command_with_task(command: list[str], task_name: str) -> None:
"""Append the command needed to execute one validation task."""
if task_name == "pyright":
command.extend(["python", "-c", _PYRIGHT_COMMAND])
return
command.extend(["python", "-m", "poethepoet", task_name])
def next_zero_major_minor_boundary(version_text: str) -> str:
"""Return the exclusive upper bound for the next 0.x minor after the given version."""
from packaging.version import Version
version = Version(version_text)
return f"0.{version.minor + 1}.0"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,180 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff: noqa: INP001
"""Refresh dev dependency pins across the Python workspace."""
from __future__ import annotations
import argparse
from dataclasses import dataclass
from pathlib import Path
import tomli
from rich import print
from scripts.dependencies._dependency_bounds_upper_impl import (
VersionCatalog,
_apply_package_replacements,
_collect_dev_pin_replacements,
_load_lock_versions,
)
from scripts.task_runner import discover_projects
@dataclass(frozen=True)
class WorkspaceProject:
"""Workspace project metadata used for dev dependency pin refresh."""
name: str
project_path: str
pyproject_path: str
pyproject_file: Path
def _read_project_name(pyproject_file: Path) -> str:
"""Return the normalized project name declared in a pyproject file."""
with pyproject_file.open("rb") as f:
data = tomli.load(f)
project = data.get("project", {}) or {}
project_name = str(project.get("name", "")).strip()
return project_name or pyproject_file.parent.name
def _discover_workspace_projects(workspace_root: Path) -> list[WorkspaceProject]:
"""Return the root project plus all package projects in the workspace."""
workspace_pyproject = workspace_root / "pyproject.toml"
projects = [
WorkspaceProject(
name=_read_project_name(workspace_pyproject),
project_path=".",
pyproject_path="pyproject.toml",
pyproject_file=workspace_pyproject,
)
]
# The root project carries the repo-wide dev toolchain pins, while package pyprojects may
# carry package-specific dev extras/groups. Refresh both surfaces in one pass so the
# workspace stays internally consistent after a tooling bump.
# Reuse the shared workspace discovery logic so this script stays aligned with the rest
# of the repo-level task runners when packages are added or moved.
for project in sorted(discover_projects(workspace_pyproject), key=lambda value: str(value)):
pyproject_file = workspace_root / project / "pyproject.toml"
if not pyproject_file.exists():
continue
projects.append(
WorkspaceProject(
name=_read_project_name(pyproject_file),
project_path=str(project),
pyproject_path=str(project / "pyproject.toml"),
pyproject_file=pyproject_file,
)
)
return projects
def _normalize_filter(value: str) -> str:
"""Normalize a package filter for matching project names and paths."""
normalized = value.strip().strip("/").lower()
return normalized or "."
def _select_projects(projects: list[WorkspaceProject], package_filters: list[str] | None) -> list[WorkspaceProject]:
"""Filter workspace projects by package name or workspace path if requested."""
if not package_filters:
return projects
normalized_filters = {_normalize_filter(value) for value in package_filters if value.strip()}
selected: list[WorkspaceProject] = []
for project in projects:
normalized_path = _normalize_filter(project.project_path)
candidates = {project.name.lower(), normalized_path}
if normalized_path != ".":
candidates.add(f"./{normalized_path}")
if candidates & normalized_filters:
selected.append(project)
return selected
def main() -> None:
"""Refresh exact dev dependency pins in workspace pyproject files."""
parser = argparse.ArgumentParser(
description=(
"Refresh dev dependency pins across the workspace pyproject.toml files. "
"By default, resolves versions from PyPI and falls back to uv.lock when network access is unavailable."
)
)
parser.add_argument(
"--packages",
nargs="*",
default=None,
help="Optional project filters by workspace path (for example packages/core) or package name.",
)
parser.add_argument(
"--version-source",
choices=["pypi", "lock"],
default="pypi",
help="Version source for selecting the newest dev pin.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print planned replacements without updating files.",
)
args = parser.parse_args()
workspace_root = Path(__file__).resolve().parents[2]
lock_versions = _load_lock_versions(workspace_root)
# Reuse the same version catalog as the bound-expansion tooling so dev pin refreshes choose
# versions with the same PyPI-vs-lock fallback behavior as the dependency validators.
catalog = VersionCatalog(lock_versions=lock_versions, source=args.version_source)
selected_projects = _select_projects(
_discover_workspace_projects(workspace_root),
package_filters=args.packages,
)
if not selected_projects:
filters = ", ".join(args.packages or [])
raise SystemExit(f"No matching workspace projects found for: {filters}")
updated_projects = 0
updated_requirements = 0
for project in selected_projects:
# Keep the replacement logic centralized in the upper-bound helper so exact dev pins are
# formatted consistently regardless of whether we update them directly here or while
# widening runtime dependency bounds.
replacements = _collect_dev_pin_replacements(project.pyproject_file, catalog=catalog)
if not replacements:
continue
updated_projects += 1
updated_requirements += len(replacements)
if args.dry_run:
print(f"[yellow]Planned updates for {project.pyproject_path}[/yellow]")
for original, replacement in replacements.items():
print(f" - {original} -> {replacement}")
continue
_apply_package_replacements(project.pyproject_file, replacements)
print(
f"[green]Updated {project.pyproject_path}[/green] "
f"({project.name}) with {len(replacements)} dev dependency pin refresh(es)."
)
if updated_projects == 0:
print("[green]No dev dependency pin updates were needed.[/green]")
return
action = "Would update" if args.dry_run else "Updated"
print(
f"[green]{action} {updated_requirements} dev dependency pin(s) "
f"across {updated_projects} workspace project(s).[/green]"
)
if __name__ == "__main__":
main()
@@ -0,0 +1,490 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff: noqa: INP001, S404, S603
"""Unified dependency-bound validation entrypoint.
Modes:
- test: run workspace-wide compatibility gates at lower and upper resolutions.
- lower: run lower-bound expansion for one package.
- upper: run upper-bound expansion for one package.
- both: run lower then upper expansion for one package.
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
import tomli
from rich import print
from scripts.dependencies._dependency_bounds_runtime import (
extend_command_with_runtime_tools,
extend_command_with_task,
)
from scripts.dependencies._dependency_bounds_upper_impl import (
_build_internal_graph,
_build_workspace_package_map,
_load_package_name,
_resolve_internal_editables,
)
from scripts.task_runner import discover_projects, extract_poe_tasks
_LOWER_IMPL_MODULE = "scripts.dependencies._dependency_bounds_lower_impl"
_UPPER_IMPL_MODULE = "scripts.dependencies._dependency_bounds_upper_impl"
@dataclass
class PackageTestPlan:
"""Workspace package settings needed for global test-mode validation."""
project_path: Path
package_name: str
include_dev_group: bool
include_dev_extra: bool
optional_extras: list[str]
internal_editables: list[Path]
def _utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
def _truncate_error(stdout: str, stderr: str, *, max_chars: int = 2000) -> str:
combined = "\n".join(part for part in [stderr.strip(), stdout.strip()] if part)
if len(combined) <= max_chars:
return combined
return f"...\n{combined[-max_chars:]}"
def _write_json(path: Path, payload: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(payload, indent=2, sort_keys=False))
def _coerce_subprocess_output(output: str | bytes | None) -> str:
if output is None:
return ""
if isinstance(output, bytes):
return output.decode(errors="replace")
return output
def _build_test_plans(workspace_root: Path, package_filter: str | None) -> list[PackageTestPlan]:
workspace_pyproject = workspace_root / "pyproject.toml"
package_map = _build_workspace_package_map(workspace_root)
internal_graph = _build_internal_graph(workspace_root, package_map)
normalized_filter = None if package_filter in {None, "", "*"} else package_filter
plans: list[PackageTestPlan] = []
missing_tasks: list[str] = []
for project_path in sorted(set(discover_projects(workspace_pyproject))):
pyproject_file = workspace_root / project_path / "pyproject.toml"
if not pyproject_file.exists():
continue
package_name = _load_package_name(pyproject_file)
if normalized_filter and str(project_path) != normalized_filter and package_name != normalized_filter:
continue
available_tasks = extract_poe_tasks(pyproject_file)
required_tasks = {"test", "pyright"}
if not required_tasks.issubset(available_tasks):
missing = sorted(required_tasks - available_tasks)
missing_tasks.append(f"{project_path}: missing {', '.join(missing)}")
continue
with pyproject_file.open("rb") as f:
package_config = tomli.load(f)
project_section = package_config.get("project", {})
optional_dependencies = project_section.get("optional-dependencies", {}) or {}
dependency_groups = package_config.get("dependency-groups", {}) or {}
plans.append(
PackageTestPlan(
project_path=project_path,
package_name=package_name,
include_dev_group="dev" in dependency_groups,
include_dev_extra="dev" in optional_dependencies,
optional_extras=sorted(name for name in optional_dependencies if name not in {"all", "dev"}),
internal_editables=_resolve_internal_editables(package_name, package_map, internal_graph),
)
)
if missing_tasks:
details = "\n".join(missing_tasks)
raise RuntimeError(f"Test mode requires test+pyright in every package.\n{details}")
return plans
def _run_package_tasks(
workspace_root: Path,
plan: PackageTestPlan,
*,
resolution: str,
timeout_seconds: int,
dry_run: bool,
) -> tuple[bool, str | None]:
# Test mode intentionally uses the same isolated uv execution model as the optimizer scripts
# so the smoke gate matches the environment that lower/upper probes will run in.
env = dict(os.environ)
env["UV_PRERELEASE"] = "allow"
# Avoid letting nested uv commands target the caller's active environment; validation should
# stay inside uv's isolated throwaway environment instead of mutating `.venv`.
env.pop("VIRTUAL_ENV", None)
for task_name in ("test", "pyright"):
command = [
"uv",
"--no-progress",
"--directory",
str(workspace_root / plan.project_path),
"run",
"--isolated",
"--resolution",
resolution,
"--prerelease",
"allow",
"--quiet",
]
extend_command_with_runtime_tools(command, workspace_root)
if plan.include_dev_group:
command.extend(["--group", "dev"])
if plan.include_dev_extra:
command.extend(["--extra", "dev"])
for extra_name in plan.optional_extras:
command.extend(["--extra", extra_name])
for editable_path in plan.internal_editables:
command.extend(["--with-editable", str(editable_path)])
extend_command_with_task(command, task_name)
if dry_run:
print(f"[cyan]DRY RUN[/cyan] {' '.join(command)}")
continue
try:
result = subprocess.run(
command,
capture_output=True,
text=True,
timeout=timeout_seconds,
check=False,
env=env,
)
except subprocess.TimeoutExpired as exc:
error_message = _truncate_error(
_coerce_subprocess_output(exc.stdout),
_coerce_subprocess_output(exc.stderr),
)
if not error_message:
error_message = "Process timed out without additional output."
return (
False,
(
f"Task '{task_name}' timed out for {plan.project_path} at resolution '{resolution}' "
f"after {timeout_seconds} seconds.\n{error_message}"
),
)
if result.returncode != 0:
error_message = _truncate_error(result.stdout, result.stderr)
return (
False,
f"Task '{task_name}' failed for {plan.project_path} at resolution '{resolution}'.\n{error_message}",
)
return True, None
def _run_test_mode(
*,
workspace_root: Path,
package_filter: str | None,
timeout_seconds: int,
dry_run: bool,
output_json: Path,
) -> int:
plans = _build_test_plans(workspace_root, package_filter)
if not plans:
print("[yellow]No workspace packages found for test mode.[/yellow]")
return 0
report: dict = {
"started_at": _utc_now(),
"mode": "test",
"workspace_root": str(workspace_root),
"dry_run": dry_run,
"scenarios": [],
"summary": {
"packages_total": len(plans),
"scenarios_passed": 0,
"scenarios_failed": 0,
},
}
_write_json(output_json, report)
print(f"[cyan]Writing dependency-bounds test report to {output_json}[/cyan]")
# Smoke both ends of the allowed range: `lowest-direct` approximates lower-bound resolution,
# while `highest` exercises the newest versions currently permitted by each package's specifiers.
scenario_specs = [("lower", "lowest-direct"), ("upper", "highest")]
for scenario_name, resolution in scenario_specs:
print(f"[bold]Running {scenario_name} scenario ({resolution})[/bold]")
scenario_result: dict = {
"name": scenario_name,
"resolution": resolution,
"status": "passed",
"packages": [],
}
for plan in plans:
success, error = _run_package_tasks(
workspace_root,
plan,
resolution=resolution,
timeout_seconds=timeout_seconds,
dry_run=dry_run,
)
scenario_result["packages"].append(
{
"project_path": str(plan.project_path),
"package_name": plan.package_name,
"status": "passed" if success else "failed",
"error": error,
}
)
if success:
print(f"[green]{plan.project_path}: {scenario_name} passed[/green]")
continue
scenario_result["status"] = "failed"
report["scenarios"].append(scenario_result)
report["summary"]["scenarios_failed"] += 1
report["updated_at"] = _utc_now()
_write_json(output_json, report)
print(f"[red]{plan.project_path}: {scenario_name} failed[/red]")
print(f"[red]{error}[/red]")
return 1
report["scenarios"].append(scenario_result)
report["summary"]["scenarios_passed"] += 1
report["updated_at"] = _utc_now()
_write_json(output_json, report)
print("[bold green]Test mode completed successfully.[/bold green]")
return 0
def _build_optimizer_command(
*,
workspace_root: Path,
module_name: str,
package: str | None,
dependencies: list[str] | None,
parallelism: int,
max_candidates: int,
version_source: str,
timeout_seconds: int,
dry_run: bool,
output_json: str | None,
) -> list[str]:
command = [
sys.executable,
"-m",
module_name,
"--parallelism",
str(parallelism),
"--max-candidates",
str(max_candidates),
"--version-source",
version_source,
"--timeout-seconds",
str(timeout_seconds),
]
if package:
command.extend(["--packages", package])
if dependencies:
command.extend(["--dependencies", *dependencies])
if output_json:
command.extend(["--output-json", output_json])
if dry_run:
command.append("--dry-run")
return command
def _run_optimizer_mode(
*,
workspace_root: Path,
module_name: str,
package: str | None,
dependencies: list[str] | None,
parallelism: int,
max_candidates: int,
version_source: str,
timeout_seconds: int,
dry_run: bool,
output_json: str | None,
) -> int:
command = _build_optimizer_command(
workspace_root=workspace_root,
module_name=module_name,
package=package,
dependencies=dependencies,
parallelism=parallelism,
max_candidates=max_candidates,
version_source=version_source,
timeout_seconds=timeout_seconds,
dry_run=dry_run,
output_json=output_json,
)
print(f"[cyan]Running:[/cyan] {' '.join(command)}")
result = subprocess.run(command, cwd=workspace_root, check=False)
return result.returncode
def _with_suffix(path: str | None, suffix: str) -> str | None:
if path is None:
return None
value = Path(path)
return str(value.with_name(f"{value.stem}-{suffix}{value.suffix}"))
def main() -> None:
"""Parse arguments and run the requested dependency-bound mode."""
parser = argparse.ArgumentParser(
description=(
"Unified dependency-bound workflow. Use mode=test for workspace-wide lower+upper gates, "
"or lower/upper/both for package-scoped or workspace-wide bound expansion."
)
)
parser.add_argument(
"--mode",
required=True,
choices=("test", "lower", "upper", "both"),
help="Execution mode: test (global) or lower/upper/both (package-scoped).",
)
parser.add_argument(
"--package",
default=None,
help="Optional workspace package path/name filter for all modes. Use '*' or omit it for the whole workspace.",
)
parser.add_argument(
"--dependencies",
nargs="*",
default=None,
help="Optional dependency-name filters for lower/upper/both. Omit to process all matching dependencies.",
)
parser.add_argument(
"--parallelism",
type=int,
default=max(1, min(os.cpu_count() or 4, 8)),
help="Parallelism forwarded to lower/upper optimizer scripts.",
)
parser.add_argument(
"--max-candidates",
type=int,
default=0,
help="Maximum candidate bounds per dependency for lower/upper optimizer scripts (0 = no limit).",
)
parser.add_argument(
"--version-source",
choices=("pypi", "lock"),
default="pypi",
help="Version source for candidate bounds.",
)
parser.add_argument(
"--timeout-seconds",
type=int,
default=1200,
help="Timeout per task command execution.",
)
parser.add_argument("--dry-run", action="store_true", help="Do not execute mutating actions.")
parser.add_argument(
"--output-json",
default=None,
help="Optional output report path for lower/upper modes (both mode appends -lower/-upper).",
)
parser.add_argument(
"--test-output-json",
default="scripts/dependencies/dependency-bounds-test-results.json",
help="Output report path for test mode.",
)
args = parser.parse_args()
workspace_root = Path(__file__).resolve().parents[2]
normalized_package = None if args.package in {None, "", "*"} else args.package
if args.mode == "test":
exit_code = _run_test_mode(
workspace_root=workspace_root,
package_filter=normalized_package,
timeout_seconds=args.timeout_seconds,
dry_run=args.dry_run,
output_json=(workspace_root / args.test_output_json).resolve(),
)
raise SystemExit(exit_code)
if args.mode == "lower":
exit_code = _run_optimizer_mode(
workspace_root=workspace_root,
module_name=_LOWER_IMPL_MODULE,
package=normalized_package,
dependencies=args.dependencies,
parallelism=args.parallelism,
max_candidates=args.max_candidates,
version_source=args.version_source,
timeout_seconds=args.timeout_seconds,
dry_run=args.dry_run,
output_json=args.output_json,
)
raise SystemExit(exit_code)
if args.mode == "upper":
exit_code = _run_optimizer_mode(
workspace_root=workspace_root,
module_name=_UPPER_IMPL_MODULE,
package=normalized_package,
dependencies=args.dependencies,
parallelism=args.parallelism,
max_candidates=args.max_candidates,
version_source=args.version_source,
timeout_seconds=args.timeout_seconds,
dry_run=args.dry_run,
output_json=args.output_json,
)
raise SystemExit(exit_code)
# Lower runs first so the subsequent upper pass starts from the widest lower bound that has
# already been validated; when `--output-json` is supplied, each pass gets its own suffixed report.
lower_exit = _run_optimizer_mode(
workspace_root=workspace_root,
module_name=_LOWER_IMPL_MODULE,
package=normalized_package,
dependencies=args.dependencies,
parallelism=args.parallelism,
max_candidates=args.max_candidates,
version_source=args.version_source,
timeout_seconds=args.timeout_seconds,
dry_run=args.dry_run,
output_json=_with_suffix(args.output_json, "lower"),
)
if lower_exit != 0:
raise SystemExit(lower_exit)
upper_exit = _run_optimizer_mode(
workspace_root=workspace_root,
module_name=_UPPER_IMPL_MODULE,
package=normalized_package,
dependencies=args.dependencies,
parallelism=args.parallelism,
max_candidates=args.max_candidates,
version_source=args.version_source,
timeout_seconds=args.timeout_seconds,
dry_run=args.dry_run,
output_json=_with_suffix(args.output_json, "upper"),
)
raise SystemExit(upper_exit)
if __name__ == "__main__":
main()
+1328 -396
View File
File diff suppressed because it is too large Load Diff