mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: chore(python): improve dependency range automation (#4343)
* chore(python): improve dependency range automation - tighten dependency bounds and coding standards guidance\n- add dependency range validation workflow, reporting, and issue automation\n- update related tests and dependency pins for compatibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * updated text and pyarrow * new lock * fixed workflow * updated deps * fix tiktoken * chore(python): refine dependency validation workflows Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(python): add high-level dependency validation comments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * WIP * added additional comments and excludes * added dev dependency handling and workflow and updates to package ranges * added readme and simplified commands * fix markers * chore(python): address dependency review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Tighten dependency bounds, remove stale overrides, restore Python 3.10 support - Apply dependency bound policy across all packages: stable >=1.0 deps use >=floor,<next_major; pre-1.0/prerelease deps use validated hard-bounded ranges - Remove stale root tool.uv.override-dependencies (uvicorn, websockets, grpcio) - Lower github_copilot requires-python to >=3.10 with github-copilot-sdk gated behind python_version >= 3.11 marker; import raises ImportError on 3.10 - Skip github_copilot pyright/mypy/test tasks on Python <3.11 - Use version-conditional pyrightconfig for samples on Python 3.10 - Add compatibility fix in core responses client for older openai typed dicts - Normalize uv.lock prerelease mode and refresh dev dependencies - Update CODING_STANDARD.md, DEV_SETUP.md, and package management skill docs Closes #902 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * small tweaks * add note in workflow * fix workflows and several versions * fix duplicate --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
67b0282813
commit
50fdcbaf57
+4
-4
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 = """
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -62,6 +62,27 @@ For example, to use the GAIA module:
|
||||
from agent_framework.lab.gaia import GAIA
|
||||
```
|
||||
|
||||
## Running Tests Locally
|
||||
|
||||
For machine-safe local runs, prefer package-scoped commands first:
|
||||
|
||||
```bash
|
||||
uv run --directory packages/lab poe test
|
||||
uv run --directory packages/lab pytest -q -m "not integration"
|
||||
```
|
||||
|
||||
When you need to run package tasks from the repository root, use sequential mode to avoid launching all package tests in parallel:
|
||||
|
||||
```bash
|
||||
uv run poe test --seq
|
||||
```
|
||||
|
||||
Lightning observability tests intentionally exercise heavier tracing paths and are marked as `resource_intensive`:
|
||||
|
||||
```bash
|
||||
uv run --directory packages/lab pytest lightning/tests/test_lightning.py -m "resource_intensive" -q
|
||||
```
|
||||
|
||||
## Should I consume Lab Modules?
|
||||
|
||||
If you are looking for stable and production-ready features, you should not use lab modules. Stick to the core framework.
|
||||
|
||||
@@ -10,10 +10,11 @@ import re
|
||||
import string
|
||||
import tempfile
|
||||
import time
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Callable, Iterable
|
||||
from datetime import datetime
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
from typing import Any, Protocol, cast
|
||||
|
||||
from opentelemetry.trace import NoOpTracer, SpanKind, get_tracer
|
||||
from tqdm import tqdm
|
||||
@@ -23,6 +24,33 @@ from ._types import Evaluation, Evaluator, Prediction, Task, TaskResult, TaskRun
|
||||
__all__ = ["GAIA", "GAIATelemetryConfig", "gaia_scorer"]
|
||||
|
||||
|
||||
class _OrjsonModule(Protocol):
|
||||
def dumps(self, obj: object, /, default: Callable[[Any], object] | None = None) -> bytes: ...
|
||||
|
||||
def loads(self, obj: str | bytes | bytearray, /) -> object: ...
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _get_orjson() -> _OrjsonModule | None:
|
||||
try:
|
||||
import orjson as runtime_orjson # pyright: ignore[reportMissingImports]
|
||||
except ImportError:
|
||||
return None
|
||||
return cast(_OrjsonModule, runtime_orjson)
|
||||
|
||||
|
||||
def _dump_json_line(value: object) -> str:
|
||||
if (runtime_orjson := _get_orjson()) is not None:
|
||||
return runtime_orjson.dumps(value, default=str).decode("utf-8")
|
||||
return json.dumps(value, default=str)
|
||||
|
||||
|
||||
def _load_json_value(value: str | bytes) -> object:
|
||||
if (runtime_orjson := _get_orjson()) is not None:
|
||||
return runtime_orjson.loads(value)
|
||||
return json.loads(value)
|
||||
|
||||
|
||||
class GAIATelemetryConfig:
|
||||
"""Configuration for GAIA telemetry and tracing."""
|
||||
|
||||
@@ -226,13 +254,7 @@ def _read_jsonl(path: Path) -> Iterable[dict[str, Any]]:
|
||||
for line in f:
|
||||
if not line.strip():
|
||||
continue
|
||||
parsed: object
|
||||
try:
|
||||
import orjson
|
||||
|
||||
parsed = orjson.loads(line)
|
||||
except Exception:
|
||||
parsed = json.loads(line)
|
||||
parsed = _load_json_value(line)
|
||||
|
||||
record = _coerce_record(parsed)
|
||||
if record is not None:
|
||||
@@ -620,12 +642,7 @@ class GAIA:
|
||||
"prediction_metadata": result.prediction.metadata,
|
||||
"evaluation_details": result.evaluation.details,
|
||||
}
|
||||
try:
|
||||
import orjson
|
||||
|
||||
f.write(orjson.dumps(record, default=str).decode("utf-8") + "\n")
|
||||
except ImportError:
|
||||
f.write(json.dumps(record, default=str) + "\n")
|
||||
f.write(_dump_json_line(record) + "\n")
|
||||
|
||||
|
||||
def viewer_main() -> None:
|
||||
@@ -646,13 +663,7 @@ def viewer_main() -> None:
|
||||
with open(args.results_file, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
if line.strip():
|
||||
try:
|
||||
import orjson
|
||||
|
||||
parsed: object = orjson.loads(line)
|
||||
except ImportError:
|
||||
parsed = json.loads(line)
|
||||
|
||||
parsed = _load_json_value(line)
|
||||
record = _coerce_record(parsed)
|
||||
if record is not None:
|
||||
results.append(record)
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
|
||||
"""RL Module for Microsoft Agent Framework."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
from agent_framework.observability import enable_instrumentation
|
||||
from agentlightning import AgentOpsTracer # type: ignore
|
||||
from agentlightning.tracer import (
|
||||
AgentOpsTracer, # pyright: ignore[reportMissingImports] # type: ignore[import-not-found]
|
||||
)
|
||||
|
||||
try:
|
||||
__version__ = importlib.metadata.version(__name__)
|
||||
@@ -23,11 +27,11 @@ class AgentFrameworkTracer(AgentOpsTracer): # type: ignore
|
||||
def init(self) -> None:
|
||||
"""Initialize the agent-framework-lab-lightning for training."""
|
||||
enable_instrumentation()
|
||||
super().init()
|
||||
super().init() # pyright: ignore[reportUnknownMemberType]
|
||||
|
||||
def teardown(self) -> None:
|
||||
"""Teardown the agent-framework-lab-lightning for training."""
|
||||
super().teardown()
|
||||
super().teardown() # pyright: ignore[reportUnknownMemberType]
|
||||
|
||||
|
||||
__all__: list[str] = ["AgentFrameworkTracer"]
|
||||
|
||||
@@ -7,12 +7,8 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
agentlightning = pytest.importorskip("agentlightning")
|
||||
|
||||
from agent_framework import AgentExecutor, AgentResponse, Agent, WorkflowBuilder, Workflow
|
||||
from agent_framework_lab_lightning import AgentFrameworkTracer
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agentlightning import TracerTraceToTriplet
|
||||
from openai.types.chat import ChatCompletion, ChatCompletionMessage
|
||||
from openai.types.chat.chat_completion import Choice
|
||||
|
||||
@@ -118,6 +114,7 @@ async def test_openai_workflow_two_agents(workflow_two_agents: Workflow):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.resource_intensive
|
||||
async def test_observability(workflow_two_agents: Workflow):
|
||||
r"""Expected trace tree:
|
||||
|
||||
@@ -129,6 +126,10 @@ async def test_observability(workflow_two_agents: Workflow):
|
||||
| |
|
||||
[chat gpt-4o] [chat gpt-4o]
|
||||
"""
|
||||
pytest.importorskip("agentlightning")
|
||||
from agent_framework_lab_lightning import AgentFrameworkTracer
|
||||
from agentlightning.adapter import TracerTraceToTriplet
|
||||
|
||||
tracer = AgentFrameworkTracer()
|
||||
try:
|
||||
tracer.init()
|
||||
|
||||
@@ -32,8 +32,8 @@ gaia = [
|
||||
"opentelemetry-api>=1.39.0",
|
||||
"tqdm>=4.60.0",
|
||||
"huggingface-hub>=0.20.0",
|
||||
"orjson>=3.8.0",
|
||||
"pyarrow>=10.0.0", # For reading parquet files
|
||||
"orjson>=3.10.7,<4",
|
||||
"pyarrow>=18.0.0", # For reading parquet files
|
||||
]
|
||||
|
||||
# Lightning RL training module dependencies
|
||||
@@ -56,19 +56,19 @@ math = [
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"uv",
|
||||
"ruff>=0.11.8",
|
||||
"pytest>=8.4.1",
|
||||
"mypy>=1.16.1",
|
||||
"pyright>=1.1.402",
|
||||
"uv==0.10.9",
|
||||
"ruff==0.15.5",
|
||||
"pytest==9.0.2",
|
||||
"mypy==1.19.1",
|
||||
"pyright==1.1.408",
|
||||
#tasks
|
||||
"poethepoet>=0.36.0",
|
||||
"rich",
|
||||
"tomli",
|
||||
"tomli-w",
|
||||
"poethepoet==0.42.1",
|
||||
"rich==13.7.1",
|
||||
"tomli==2.4.0",
|
||||
"tomli-w==1.2.0",
|
||||
# tau2 from source (not available on PyPI)
|
||||
"tau2@ git+https://github.com/sierra-research/tau2-bench@5ba9e3e56db57c5e4114bf7f901291f09b2c5619",
|
||||
"prek>=0.3.2",
|
||||
"prek==0.3.4",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
@@ -144,7 +144,6 @@ targets = ["agent_framework_lab_gaia", "agent_framework_lab_lightning", "agent_f
|
||||
exclude_dirs = ["gaia/tests", "lightning/tests", "tau2/tests"]
|
||||
|
||||
[tool.poe]
|
||||
executor.type = "uv"
|
||||
include = "../../shared_tasks.toml"
|
||||
|
||||
[tool.poe.tasks]
|
||||
@@ -152,10 +151,10 @@ mypy-gaia = "mypy --config-file $POE_ROOT/pyproject.toml gaia/agent_framework_la
|
||||
mypy-lightning = "mypy --config-file $POE_ROOT/pyproject.toml lightning/agent_framework_lab_lightning"
|
||||
mypy-tau2 = "mypy --config-file $POE_ROOT/pyproject.toml tau2/agent_framework_lab_tau2"
|
||||
mypy = ["mypy-gaia", "mypy-lightning", "mypy-tau2"]
|
||||
test = "pytest -m \"not integration\" --cov-report=term-missing:skip-covered --junitxml=test-results.xml"
|
||||
test-gaia = "pytest -m \"not integration\" gaia/tests --cov=agent_framework_lab_gaia --cov-report=term-missing:skip-covered"
|
||||
test-lightning = "pytest -m \"not integration\" lightning/tests --cov=agent_framework_lab_lightning --cov-report=term-missing:skip-covered"
|
||||
test-tau2 = "pytest -m \"not integration\" tau2/tests --cov=agent_framework_lab_tau2 --cov-report=term-missing:skip-covered"
|
||||
test = 'pytest -m "not integration and not resource_intensive" --cov-report=term-missing:skip-covered --junitxml=test-results.xml'
|
||||
test-gaia = "pytest gaia/tests --cov=agent_framework_lab_gaia --cov-report=term-missing:skip-covered"
|
||||
test-lightning = "pytest lightning/tests --cov=agent_framework_lab_lightning --cov-report=term-missing:skip-covered"
|
||||
test-tau2 = "pytest tau2/tests --cov=agent_framework_lab_tau2 --cov-report=term-missing:skip-covered"
|
||||
build = "echo 'Skipping build'"
|
||||
publish = "echo 'Skipping publish'"
|
||||
|
||||
@@ -167,4 +166,5 @@ asyncio_default_fixture_loop_scope = "function"
|
||||
markers = [
|
||||
"unit: marks tests as unit tests",
|
||||
"integration: marks tests as integration tests",
|
||||
"resource_intensive: marks tests that are expensive and excluded from default package test runs",
|
||||
]
|
||||
|
||||
@@ -2,62 +2,39 @@
|
||||
|
||||
"""Tests for tau2 utils module."""
|
||||
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from agent_framework import Content, FunctionTool, Message
|
||||
from agent_framework_lab_tau2._tau2_utils import (
|
||||
convert_agent_framework_messages_to_tau2_messages,
|
||||
convert_tau2_tool_to_function_tool,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
from tau2.data_model.message import AssistantMessage, SystemMessage, ToolCall, ToolMessage, UserMessage
|
||||
from tau2.domains.airline.data_model import FlightDB
|
||||
from tau2.domains.airline.tools import AirlineTools
|
||||
from tau2.environment.environment import Environment
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tau2_airline_environment() -> Environment:
|
||||
airline_db_remote_path = "https://raw.githubusercontent.com/sierra-research/tau2-bench/5ba9e3e56db57c5e4114bf7f901291f09b2c5619/data/tau2/domains/airline/db.json"
|
||||
airline_policy_remote_path = "https://raw.githubusercontent.com/sierra-research/tau2-bench/5ba9e3e56db57c5e4114bf7f901291f09b2c5619/data/tau2/domains/airline/policy.md"
|
||||
|
||||
# Create cache directory
|
||||
cache_dir = Path(__file__).parent / "data"
|
||||
cache_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Define cache file paths
|
||||
db_cache_path = cache_dir / "airline_db.json"
|
||||
policy_cache_path = cache_dir / "airline_policy.md"
|
||||
|
||||
# Download files only if they don't exist in cache
|
||||
if not db_cache_path.exists():
|
||||
urllib.request.urlretrieve(airline_db_remote_path, db_cache_path)
|
||||
|
||||
if not policy_cache_path.exists():
|
||||
urllib.request.urlretrieve(airline_policy_remote_path, policy_cache_path)
|
||||
|
||||
# Load data from cached files
|
||||
db = FlightDB.load(str(db_cache_path))
|
||||
tools = AirlineTools(db)
|
||||
with open(policy_cache_path) as fp:
|
||||
policy = fp.read()
|
||||
|
||||
yield Environment(
|
||||
domain_name="airline",
|
||||
policy=policy,
|
||||
tools=tools,
|
||||
)
|
||||
class _DummyToolInput(BaseModel):
|
||||
param: str
|
||||
|
||||
|
||||
def test_convert_tau2_tool_to_function_tool_basic(tau2_airline_environment):
|
||||
class _DummyToolResult(BaseModel):
|
||||
output: str
|
||||
|
||||
|
||||
class _DummyTau2Tool:
|
||||
def __init__(self, name: str, description: str) -> None:
|
||||
self.name = name
|
||||
self._description = description
|
||||
self.params = _DummyToolInput
|
||||
|
||||
def _get_description(self) -> str:
|
||||
return self._description
|
||||
|
||||
def __call__(self, **kwargs: str) -> _DummyToolResult:
|
||||
return _DummyToolResult(output=kwargs["param"])
|
||||
|
||||
|
||||
def test_convert_tau2_tool_to_function_tool_basic():
|
||||
"""Test basic conversion from tau2 tool to FunctionTool."""
|
||||
# Get real tools from tau2 environment
|
||||
tools = tau2_airline_environment.get_tools()
|
||||
|
||||
# Use the first available tool for testing
|
||||
assert len(tools) > 0, "No tools available in environment"
|
||||
tau2_tool = tools[0]
|
||||
tau2_tool = _DummyTau2Tool(name="lookup_booking", description="Lookup booking by id.")
|
||||
|
||||
# Convert the tool
|
||||
tool = convert_tau2_tool_to_function_tool(tau2_tool)
|
||||
@@ -68,20 +45,25 @@ def test_convert_tau2_tool_to_function_tool_basic(tau2_airline_environment):
|
||||
assert tool.description == tau2_tool._get_description()
|
||||
assert tool.input_model == tau2_tool.params
|
||||
|
||||
# Test that the function is callable (we won't call it with real params to avoid side effects)
|
||||
result = tool.func(param="ABC123")
|
||||
assert isinstance(result, _DummyToolResult)
|
||||
assert result.output == "ABC123"
|
||||
assert callable(tool.func)
|
||||
|
||||
|
||||
def test_convert_tau2_tool_to_function_tool_multiple_tools(tau2_airline_environment):
|
||||
def test_convert_tau2_tool_to_function_tool_multiple_tools():
|
||||
"""Test conversion with multiple tau2 tools."""
|
||||
# Get real tools from tau2 environment
|
||||
tools = tau2_airline_environment.get_tools()
|
||||
tools = [
|
||||
_DummyTau2Tool(name="lookup_booking", description="Lookup booking by id."),
|
||||
_DummyTau2Tool(name="cancel_booking", description="Cancel an existing booking."),
|
||||
_DummyTau2Tool(name="check_policy", description="Get policy details."),
|
||||
]
|
||||
|
||||
# Convert multiple tools
|
||||
function_tools = [convert_tau2_tool_to_function_tool(tool) for tool in tools[:3]] # Test first 3 tools
|
||||
function_tools = [convert_tau2_tool_to_function_tool(tool) for tool in tools]
|
||||
|
||||
# Verify all conversions
|
||||
for tool, tau2_tool in zip(function_tools, tools[:3], strict=False):
|
||||
for tool, tau2_tool in zip(function_tools, tools, strict=False):
|
||||
assert isinstance(tool, FunctionTool)
|
||||
assert tool.name == tau2_tool.name
|
||||
assert tool.description == tau2_tool._get_description()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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 }]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
"""Shared Python workspace scripts."""
|
||||
@@ -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()
|
||||
Generated
+1328
-396
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user