Python: Simplify Python Poe tasks and unify package selectors (#4722)

* updated automation tasks and commands, with alias for the time being

* Restore aggregate test exclusions

Preserve the legacy all-tests scope for test --all by excluding lab and devui from the default aggregate sweep, while still allowing explicit package selection. Also ignore hidden/generated test directories such as .mypy_cache during aggregate discovery.

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

* updated versions in pre-commit

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-03-18 19:39:11 +01:00
committed by GitHub
Unverified
parent d3d0100822
commit f48c4512d3
60 changed files with 1704 additions and 527 deletions
+4 -6
View File
@@ -75,7 +75,7 @@ jobs:
os: ${{ runner.os }}
env:
UV_CACHE_DIR: /tmp/.uv-cache
- name: Run fmt, lint, pyright in parallel across packages
- name: Run syntax and pyright across packages
run: uv run poe check-packages
samples-markdown:
@@ -104,10 +104,8 @@ jobs:
os: ${{ runner.os }}
env:
UV_CACHE_DIR: /tmp/.uv-cache
- name: Run samples lint
run: uv run poe samples-lint
- name: Run samples syntax check
run: uv run poe samples-syntax
- name: Run samples checks
run: uv run poe check -S
- name: Run markdown code lint
run: uv run poe markdown-code-lint
@@ -140,4 +138,4 @@ jobs:
- name: Run Mypy
env:
GITHUB_BASE_REF: ${{ github.event.pull_request.base.ref || github.base_ref || 'main' }}
run: uv run poe ci-mypy
run: uv run python scripts/workspace_poe_tasks.py ci-mypy
@@ -38,7 +38,7 @@ jobs:
id: validate_ranges
# Keep workflow running so we can still publish diagnostics from this run.
continue-on-error: true
run: uv run poe validate-dependency-bounds-project --mode upper --project "*"
run: uv run poe validate-dependency-bounds-project --mode upper --package "*"
working-directory: ./python
- name: Upload dependency range report
@@ -203,7 +203,7 @@ jobs:
cat > "${PR_BODY_FILE}" <<'EOF'
This PR was generated by the dependency range validation workflow.
- Ran `uv run poe validate-dependency-bounds-project --mode upper --project "*"`
- Ran `uv run poe validate-dependency-bounds-project --mode upper --package "*"`
- Updated package dependency bounds
- Refreshed `python/uv.lock` with `uv lock --upgrade`
EOF
@@ -48,9 +48,8 @@ jobs:
os: ${{ runner.os }}
- name: Test with pytest (unit tests only)
run: >
uv run poe all-tests
uv run poe test -A
-m "not integration"
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
+1 -2
View File
@@ -100,9 +100,8 @@ jobs:
os: ${{ runner.os }}
- name: Test with pytest (unit tests only)
run: >
uv run poe all-tests
uv run poe test -A
-m "not integration"
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
working-directory: ./python
+2 -2
View File
@@ -32,13 +32,13 @@ jobs:
id: python-setup
uses: ./.github/actions/python-setup
with:
python-version: ${{ matrix.python-version }}
python-version: ${{ env.UV_PYTHON }}
os: ${{ runner.os }}
env:
# Configure a constant location for the uv cache
UV_CACHE_DIR: /tmp/.uv-cache
- name: Run all tests with coverage report
run: uv run poe all-tests-cov --cov-report=xml:python-coverage.xml -q --junitxml=pytest.xml
run: uv run poe test -A -C --cov-report=xml:python-coverage.xml -q --junitxml=pytest.xml
- name: Check coverage threshold
run: python ${{ github.workspace }}/.github/workflows/python-check-coverage.py python-coverage.xml ${{ env.COVERAGE_THRESHOLD }}
- name: Upload coverage report
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
UV_CACHE_DIR: /tmp/.uv-cache
# Unit tests
- name: Run all tests
run: uv run poe all-tests ${{ matrix.python-version == '3.10' && '--ignore-glob=packages/github_copilot/**' || '' }}
run: uv run poe test -A
working-directory: ./python
# Surface failing tests
+26 -16
View File
@@ -13,26 +13,34 @@ description: >
All commands run from the `python/` directory:
```bash
# Format code (ruff format, parallel across packages)
uv run poe fmt
# Lint and auto-fix (ruff check, parallel across packages)
uv run poe lint
# Syntax formatting + checks (parallel across packages by default)
uv run poe syntax
uv run poe syntax -P core
uv run poe syntax -F # Format only
uv run poe syntax -C # Check only
uv run poe syntax -S # Samples only
# Type checking
uv run poe pyright # Pyright (parallel across packages)
uv run poe mypy # MyPy (parallel across packages)
uv run poe pyright # Pyright fan-out across packages
uv run poe pyright -P core
uv run poe pyright -A
uv run poe mypy # MyPy fan-out across packages
uv run poe mypy -P core
uv run poe mypy -A
uv run poe typing # Both pyright and mypy
uv run poe typing -P core
uv run poe typing -A
# All package-level checks in parallel (fmt + lint + pyright + mypy)
# All package-level checks in parallel (syntax + pyright)
uv run poe check-packages
# Full check (packages + samples + tests + markdown)
uv run poe check
uv run poe check -P core
# Samples only
uv run poe samples-lint # Ruff lint on samples/
uv run poe samples-syntax # Pyright syntax check on samples/
uv run poe check -S
uv run poe pyright -S
# Markdown code blocks
uv run poe markdown-code-lint
@@ -40,8 +48,8 @@ uv run poe markdown-code-lint
## Pre-commit Hooks (prek)
Prek hooks run automatically on commit. They check only changed files and run
package-level checks in parallel for affected packages only.
Prek hooks run automatically on commit. They stay lightweight and only check
changed files.
```bash
# Install hooks
@@ -54,8 +62,10 @@ uv run prek run -a
uv run prek run --last-commit
```
When core package changes, type-checking (mypy, pyright) runs across all packages
since type changes propagate. Format and lint only run in changed packages.
They run changed-package syntax formatting/checking, markdown code lint only
when markdown files change, and sample syntax lint/pyright only when files
under `samples/` change.
They intentionally do not run workspace `pyright` or `mypy` by default.
## Ruff Configuration
@@ -80,6 +90,6 @@ in-process with streaming output.
CI splits into 4 parallel jobs:
1. **Pre-commit hooks** — lightweight hooks (SKIP=poe-check)
2. **Package checks**fmt/lint/pyright via check-packages
3. **Samples & markdown**samples-lint, samples-syntax, markdown-code-lint
2. **Package checks**syntax/pyright via check-packages
3. **Samples & markdown**`check -S` plus `markdown-code-lint`
4. **Mypy** — change-detected mypy checks
+9 -9
View File
@@ -47,17 +47,17 @@ 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>
# Defaults to --package "*"; pass a package to scope test mode
uv run poe validate-dependency-bounds-test --package core
# 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>"
uv run poe validate-dependency-bounds-project --mode both --package core --dependency "<dependency-name>"
# Repo-wide automation can reuse the same task
uv run poe validate-dependency-bounds-project --mode upper --project "*"
uv run poe validate-dependency-bounds-project --mode upper --package "*"
# 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>"
uv run poe add-dependency-and-validate-bounds --package core --dependency "<dependency-spec>"
```
### Dependency Bound Notes
@@ -66,7 +66,7 @@ uv run poe add-dependency-and-validate-bounds --project <workspace-package-name>
- 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`.
- 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 `--package "*"` 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`.
@@ -108,12 +108,12 @@ def __getattr__(name: str) -> Any:
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>"`
`uv run poe add-dependency-to-project --package core --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>"`
`uv run poe validate-dependency-bounds-project --mode both --package core --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>"`
`uv run poe add-dependency-and-validate-bounds --package core --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
+7 -4
View File
@@ -41,11 +41,14 @@ Do **not** add sample-only dependencies to the root `pyproject.toml` dev group.
## Syntax Checking
```bash
# Check samples for syntax errors and missing imports
uv run poe samples-syntax
# Format + lint samples
uv run poe syntax -S
# Lint samples
uv run poe samples-lint
# Check samples for syntax errors and missing imports
uv run poe pyright -S
# Lint samples only
uv run poe syntax -S -C
```
## Documentation
+15 -8
View File
@@ -17,20 +17,27 @@ We run tests in two stages, for a PR each commit is tested with unit tests only
# Run tests for all packages in parallel
uv run poe test
# Run tests for a specific package
uv run --directory packages/core poe test
# Run tests for a specific workspace package
uv run poe test -P core
# Run all tests in a single pytest invocation (faster, uses pytest-xdist)
uv run poe all-tests
# Run all selected tests in a single pytest invocation
uv run poe test -A
# With coverage
uv run poe all-tests-cov
uv run poe test -A -C
uv run poe test -P core -C
# Run only unit tests (exclude integration tests)
uv run poe all-tests -m "not integration"
uv run poe test -A -m "not integration"
# Run only integration tests
uv run poe all-tests -m integration
uv run poe test -A -m integration
```
Direct package execution still works when you need it:
```bash
uv run --directory packages/core poe test
```
## Test Configuration
@@ -38,7 +45,7 @@ uv run poe all-tests -m integration
- **Async mode**: `asyncio_mode = "auto"` is enabled — do NOT use `@pytest.mark.asyncio`, but do mark tests with `async def` and use `await` for async calls
- **Timeout**: Default 60 seconds per test
- **Import mode**: `importlib` for cross-package isolation
- **Parallelization**: Large packages (core, ag-ui, orchestrations, anthropic) use `pytest-xdist` (`-n auto --dist worksteal`) in their `poe test` task. The `all-tests` task also uses xdist across all packages.
- **Parallelization**: Large packages (core, ag-ui, orchestrations, anthropic) use `pytest-xdist` (`-n auto --dist worksteal`) in their `poe test` task. The aggregate `uv run poe test -A` sweep also uses xdist across the selected packages.
## Test Directory Structure
+3 -3
View File
@@ -52,10 +52,10 @@ repos:
hooks:
- id: poe-check
name: Run checks through Poe
entry: uv run poe prek-check
entry: uv run python scripts/workspace_poe_tasks.py prek-check
language: system
- repo: https://github.com/PyCQA/bandit
rev: 1.9.3
rev: 1.9.4
hooks:
- id: bandit
name: Bandit Security Checks
@@ -63,7 +63,7 @@ repos:
additional_dependencies: ["bandit[toml]"]
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
rev: 0.10.0
rev: 0.10.10
hooks:
# Update the uv lockfile
- id: uv-lock
+49 -12
View File
@@ -9,9 +9,8 @@
"command": "uv",
"args": [
"run",
"prek",
"run",
"-a"
"poe",
"check"
],
"problemMatcher": {
"owner": "python",
@@ -32,13 +31,13 @@
}
},
{
"label": "Format",
"label": "Syntax",
"type": "shell",
"command": "uv",
"args": [
"run",
"poe",
"fmt",
"syntax",
],
"problemMatcher": {
"owner": "python",
@@ -59,13 +58,42 @@
}
},
{
"label": "Lint",
"label": "Syntax (format only)",
"type": "shell",
"command": "uv",
"args": [
"run",
"poe",
"lint",
"syntax",
"-F",
],
"problemMatcher": {
"owner": "python",
"fileLocation": [
"relative",
"${workspaceFolder}"
],
"pattern": {
"regexp": "^(.*):(\\d+):(\\d+):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"message": 4
}
},
"presentation": {
"panel": "shared"
}
},
{
"label": "Syntax (check only)",
"type": "shell",
"command": "uv",
"args": [
"run",
"poe",
"syntax",
"-C",
],
"problemMatcher": {
"owner": "python",
@@ -169,7 +197,14 @@
{
"label": "Create Venv",
"type": "shell",
"command": "uv venv PYTHON=${input:py_version}",
"command": "uv",
"args": [
"run",
"poe",
"venv",
"-P",
"${input:py_version}"
],
"presentation": {
"reveal": "always",
"panel": "new"
@@ -184,7 +219,8 @@
"run",
"poe",
"setup",
"--python=${input:py_version}"
"-P",
"${input:py_version}"
],
"presentation": {
"reveal": "always",
@@ -200,11 +236,12 @@
"3.10",
"3.11",
"3.12",
"3.13"
"3.13",
"3.14"
],
"id": "py_version",
"description": "Python version",
"default": "3.10"
"default": "3.13"
}
]
}
}
+1 -1
View File
@@ -403,7 +403,7 @@ So we use bounded ranges for external package dependencies in `pyproject.toml`:
- 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.
- 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 --package <workspace-package-name> --dependency "<dependency-name>"` to expand package-scoped bounds.
### Installation Options
+129 -86
View File
@@ -123,28 +123,39 @@ client = OpenAIChatClient(env_file_path="openai.env")
All the tests are located in the `tests` folder of each package. Tests marked with `@pytest.mark.integration` and `@skip_if_..._integration_tests_disabled` are integration tests that require external services (e.g., OpenAI, Azure OpenAI). They are automatically skipped when the required API keys or service endpoints are not configured in your environment or `.env` file.
You can select or exclude integration tests using pytest markers:
The root `test` command now supports both project-scoped fan-out and a single aggregate sweep:
```bash
# Run only unit tests (exclude integration tests)
uv run poe all-tests -m "not integration"
# Run package-local tests across all workspace packages
uv run poe test
# Run only integration tests
uv run poe all-tests -m integration
# Run tests for one workspace package
uv run poe test -P core
# Run an aggregate pytest sweep across the selected packages
uv run poe test -A
# Run only unit tests in aggregate mode
uv run poe test -A -m "not integration"
# Run only integration tests in aggregate mode
uv run poe test -A -m integration
# Run tests with coverage for one package or an aggregate sweep
uv run poe test -P core -C
uv run poe test -A -C
```
Alternatively, you can run them using VSCode Tasks. Open the command palette
(`Ctrl+Shift+P`) and type `Tasks: Run Task`. Select `Test` from the list.
If you want to run the tests for a single package, you can use the `uv run poe test` command with the package name as an argument. For example, to run the tests for the `agent_framework` package, you can use:
Direct package execution still works when you need it:
```bash
uv run poe --directory packages/core test
```
Large packages (core, ag-ui, orchestrations, anthropic) use `pytest-xdist` for parallel test execution within the package. The `all-tests` task also uses xdist across all packages.
These commands also output the coverage report.
Large packages (core, ag-ui, orchestrations, anthropic) use `pytest-xdist` for parallel test execution within the package. The aggregate `test -A` sweep also uses `pytest-xdist` across the selected packages.
## Code quality checks
@@ -158,10 +169,11 @@ Ideally you should run these checks before committing any changes, when you inst
## Code Coverage
We try to maintain a high code coverage for the project. To run the code coverage on the unit tests, you can use the following command:
We try to maintain a high code coverage for the project. To review coverage locally, use either a package-scoped run or the aggregate sweep:
```bash
uv run poe test
uv run poe test -P core -C
uv run poe test -A -C
```
This will show you which files are not covered by the tests, including the specific lines not covered. Make sure to consider the untested lines from the code you are working on, but feel free to add other tests as well, that is always welcome!
@@ -213,7 +225,7 @@ Set up the development environment with a virtual environment, install dependenc
```bash
uv run poe setup
# or with specific Python version
uv run poe setup --python 3.12
uv run poe setup -P 3.12
```
#### `install`
@@ -230,7 +242,7 @@ Create a virtual environment with specified Python version or switch python vers
```bash
uv run poe venv
# or with specific Python version
uv run poe venv --python 3.12
uv run poe venv -P 3.12
```
#### `prek-install`
@@ -239,41 +251,89 @@ Install prek hooks:
uv run poe prek-install
```
### Code Quality and Formatting
### Project-scoped command families
Each of the following tasks run against both the main `agent-framework` package and the extension packages in parallel, ensuring consistent code quality across the project.
These commands default to `--package "*"`, so they run across all workspace packages unless you narrow them with `-P/--package`:
#### `fmt` (format)
Format code using ruff (runs in parallel across all packages):
#### `syntax`
Run Ruff formatting plus Ruff lint checks by default:
```bash
uv run poe fmt
uv run poe syntax
uv run poe syntax -P core
uv run poe syntax -F # format only
uv run poe syntax -C # lint/check only
```
#### `lint`
Run linting checks and fix issues (runs in parallel across all packages):
#### `build`
Build workspace packages and the root meta package:
```bash
uv run poe lint
uv run poe build
uv run poe build -P core
```
#### `clean-dist`
Clean generated dist artifacts:
```bash
uv run poe clean-dist
uv run poe clean-dist -P core
```
### Dual-mode validation and test commands
These command families share the same selector model:
```bash
uv run poe <command> # project fan-out over --package "*"
uv run poe <command> -P core # one-project fan-out
uv run poe <command> -A # aggregate sweep where supported
```
#### `pyright`
Run Pyright type checking (runs in parallel across all packages):
Run Pyright type checking:
```bash
uv run poe pyright
uv run poe pyright -P core
uv run poe pyright -A
```
#### `mypy`
Run MyPy type checking (runs in parallel across all packages):
Run MyPy type checking:
```bash
uv run poe mypy
uv run poe mypy -P core
uv run poe mypy -A
```
#### `typing`
Run both Pyright and MyPy type checking:
Run both Pyright and MyPy:
```bash
uv run poe typing
uv run poe typing -P core
uv run poe typing -A
```
### Code Validation
#### `test`
Run package-local tests in fan-out mode, or switch to one aggregate pytest sweep with `-A`:
```bash
uv run poe test
uv run poe test -P core
uv run poe test -P core -C
uv run poe test -A
uv run poe test -A -C
```
### Sample-target variants
Use `-S/--samples` for sample-only validation instead of separate top-level commands:
```bash
uv run poe syntax -S
uv run poe syntax -S -C
uv run poe pyright -S
uv run poe check -S
```
### Workspace validation and dependency commands
#### `markdown-code-lint`
Lint markdown code blocks:
@@ -281,26 +341,41 @@ Lint markdown code blocks:
uv run poe markdown-code-lint
```
#### `check-packages`
Run the package-level syntax sweep (`syntax`) plus `pyright` across the selected projects:
```bash
uv run poe check-packages
uv run poe check-packages -P core
```
#### `check`
Run package syntax, pyright, and tests for the selected project set. Without `-P/--package`, it also includes sample checks and markdown lint:
```bash
uv run poe check
uv run poe check -P core
uv run poe check -S
```
#### `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>
# Defaults to --package "*"; pass a package to scope test mode
uv run poe validate-dependency-bounds-test -P core
```
#### `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>"
uv run poe validate-dependency-bounds-project -M both -P core -D "<dependency-name>"
```
`--project` defaults to `*`, and `--dependency` is optional. Automation can use `--mode upper --project "*"` to run the upper-bound pass across the workspace.
`--package` defaults to `*`, and `--dependency` is optional. Automation can use `--mode upper --package "*"` 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>"
uv run poe add-dependency-and-validate-bounds -P core -D "<dependency-spec>"
```
#### `upgrade-dev-dependencies`
@@ -310,72 +385,40 @@ 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`
Run all package-level quality checks (format, lint, pyright, mypy) in parallel across all packages. This runs the full cross-product of (package × check) concurrently:
```bash
uv run poe check-packages
```
#### `check`
Run all quality checks including package checks, samples, tests and markdown lint:
```bash
uv run poe check
```
### Testing
#### `test`
Run unit tests with coverage by invoking the `test` task in each package in parallel:
```bash
uv run poe test
```
To run tests for a specific package only, use the `--directory` flag:
```bash
# Run tests for the core package
uv run --directory packages/core poe test
# Run tests for the azure-ai package
uv run --directory packages/azure-ai poe test
```
#### `all-tests`
Run all tests in a single pytest invocation across all packages in parallel (excluding lab and devui). This is faster than `test` as it uses pytest's parallel execution:
```bash
uv run poe all-tests
```
#### `all-tests-cov`
Same as `all-tests` but with coverage reporting enabled:
```bash
uv run poe all-tests-cov
```
### Building and Publishing
#### `build`
Build all packages:
```bash
uv run poe build
```
#### `clean-dist`
Clean the dist directories:
```bash
uv run poe clean-dist
```
#### `publish`
Publish packages to PyPI:
```bash
uv run poe publish
```
### Compatibility aliases
These legacy commands still work during the transition, but prefer the newer forms above:
```bash
uv run poe fmt # prefer: uv run poe syntax -F
uv run poe format # prefer: uv run poe syntax -F
uv run poe lint # prefer: uv run poe syntax -C
uv run poe all-tests # prefer: uv run poe test -A
uv run poe all-tests-cov # prefer: uv run poe test -A -C
uv run poe samples-lint # prefer: uv run poe syntax -S -C
uv run poe samples-syntax # prefer: uv run poe pyright -S
```
## Prek Hooks
Prek hooks run automatically on commit and execute a subset of the checks on changed files only. Package-level checks (fmt, lint, pyright) run in parallel but only for packages with changed files. Markdown and sample checks are skipped when no relevant files were changed. If the `core` package is changed, all packages are checked. You can also run all checks using prek directly:
Prek hooks run automatically on commit and stay intentionally lightweight:
- changed-package syntax formatting
- changed-package syntax lint/check
- markdown code lint only when markdown files change
- sample lint + sample pyright only when files under `samples/` change
They do **not** run workspace `pyright` or `mypy` by default. Use `uv run poe pyright`, `uv run poe mypy`, `uv run poe typing`, `uv run poe check-packages`, or `uv run poe check` when you want deeper validation.
You can run the installed hooks directly with:
```bash
uv run prek run -a
+7 -3
View File
@@ -85,9 +85,13 @@ exclude_dirs = ["tests"]
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_a2a"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = 'pytest -m "not integration" --cov=agent_framework_a2a --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+7 -3
View File
@@ -72,6 +72,10 @@ typeCheckingMode = "basic"
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_ag_ui"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = 'pytest -m "not integration" --cov=agent_framework_ag_ui --cov-report=term-missing:skip-covered -n auto --dist worksteal tests/ag_ui'
+7 -3
View File
@@ -85,9 +85,13 @@ exclude_dirs = ["tests"]
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_anthropic"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = '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"]
@@ -87,9 +87,13 @@ exclude_dirs = ["tests"]
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_ai_search"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = '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"]
+8 -3
View File
@@ -85,11 +85,16 @@ exclude_dirs = ["tests"]
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_ai"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = 'pytest -m "not integration" --cov=agent_framework_azure_ai --cov-report=term-missing:skip-covered tests'
[tool.poe.tasks.integration-tests]
help = "Run the package integration test suite."
cmd = """
pytest --import-mode=importlib
-n logical --dist worksteal
+11 -4
View File
@@ -84,10 +84,17 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_cosmos"
test = "pytest -m \"not integration\" --cov=agent_framework_azure_cosmos --cov-report=term-missing:skip-covered tests"
integration-tests = "pytest tests/test_cosmos_history_provider.py -m integration"
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_cosmos"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = "pytest -m \"not integration\" --cov=agent_framework_azure_cosmos --cov-report=term-missing:skip-covered tests"
[tool.poe.tasks.integration-tests]
help = "Run the package integration test suite."
cmd = "pytest tests/test_cosmos_history_provider.py -m integration"
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
@@ -91,9 +91,13 @@ exclude_dirs = ["tests"]
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azurefunctions"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = 'pytest -m "not integration" --cov=agent_framework_azurefunctions --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+7 -3
View File
@@ -84,9 +84,13 @@ exclude_dirs = ["tests"]
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_bedrock"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = 'pytest -m "not integration" --cov=agent_framework_bedrock --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["hatchling"]
+7 -3
View File
@@ -86,9 +86,13 @@ exclude_dirs = ["tests"]
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_chatkit"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = 'pytest -m "not integration" --cov=agent_framework_chatkit --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+7 -3
View File
@@ -86,9 +86,13 @@ exclude_dirs = ["tests"]
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_claude"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = 'pytest -m "not integration" --cov=agent_framework_claude --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+7 -3
View File
@@ -85,9 +85,13 @@ exclude_dirs = ["tests"]
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_copilotstudio"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = 'pytest -m "not integration" --cov=agent_framework_copilotstudio --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+7 -3
View File
@@ -121,9 +121,13 @@ exclude_dirs = ["tests"]
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = 'pytest -m "not integration" --cov=agent_framework --cov-report=term-missing:skip-covered -n auto --dist worksteal tests'
[tool.flit.module]
name = "agent_framework"
+7 -3
View File
@@ -92,9 +92,13 @@ exclude_dirs = ["tests"]
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_declarative"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = 'pytest -m "not integration" --cov=agent_framework_declarative --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+7 -3
View File
@@ -98,9 +98,13 @@ exclude_dirs = ["tests"]
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_devui"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = 'pytest -m "not integration" --cov=agent_framework_devui --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+7 -3
View File
@@ -97,9 +97,13 @@ exclude_dirs = ["tests"]
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_durabletask"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = 'pytest -m "not integration" --cov=agent_framework_durabletask --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+7 -3
View File
@@ -84,9 +84,13 @@ exclude_dirs = ["tests"]
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_foundry_local"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = '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"]
@@ -85,14 +85,17 @@ exclude_dirs = ["tests"]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
test = "pytest -m \"not integration\" --cov=agent_framework_github_copilot --cov-report=term-missing:skip-covered tests"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = "pytest -m \"not integration\" --cov=agent_framework_github_copilot --cov-report=term-missing:skip-covered tests"
[tool.poe.tasks.pyright]
help = "Run Pyright for this package, skipping automatically on unsupported Python versions."
shell = "python -c \"import sys; exit(0 if sys.version_info < (3,11) else 1)\" || pyright"
interpreter = "posix"
[tool.poe.tasks.mypy]
help = "Run MyPy for this package, skipping automatically on unsupported Python versions."
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"
+2 -2
View File
@@ -71,10 +71,10 @@ 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:
When you need to run lab tests from the repository root, scope the root task to the lab package:
```bash
uv run poe test --seq
uv run poe test -P lab
```
Lightning observability tests intentionally exercise heavier tracing paths and are marked as `resource_intensive`:
+39 -11
View File
@@ -146,17 +146,45 @@ exclude_dirs = ["gaia/tests", "lightning/tests", "tau2/tests"]
[tool.poe]
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy-gaia = "mypy --config-file $POE_ROOT/pyproject.toml gaia/agent_framework_lab_gaia"
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 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'"
[tool.poe.tasks.mypy-gaia]
help = "Run MyPy for the lab GAIA package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml gaia/agent_framework_lab_gaia"
[tool.poe.tasks.mypy-lightning]
help = "Run MyPy for the lab Lightning package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml lightning/agent_framework_lab_lightning"
[tool.poe.tasks.mypy-tau2]
help = "Run MyPy for the lab Tau2 package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml tau2/agent_framework_lab_tau2"
[tool.poe.tasks.mypy]
help = "Run MyPy across all lab subpackages."
sequence = ["mypy-gaia", "mypy-lightning", "mypy-tau2"]
[tool.poe.tasks.test]
help = "Run the default lab unit test suite."
cmd = 'pytest -m "not integration and not resource_intensive" --cov-report=term-missing:skip-covered --junitxml=test-results.xml'
[tool.poe.tasks.test-gaia]
help = "Run the GAIA lab test suite."
cmd = "pytest gaia/tests --cov=agent_framework_lab_gaia --cov-report=term-missing:skip-covered"
[tool.poe.tasks.test-lightning]
help = "Run the Lightning lab test suite."
cmd = "pytest lightning/tests --cov=agent_framework_lab_lightning --cov-report=term-missing:skip-covered"
[tool.poe.tasks.test-tau2]
help = "Run the Tau2 lab test suite."
cmd = "pytest tau2/tests --cov=agent_framework_lab_tau2 --cov-report=term-missing:skip-covered"
[tool.poe.tasks.build]
help = "Skip build for the lab package."
cmd = "echo 'Skipping build'"
[tool.poe.tasks.publish]
help = "Skip publish for the lab package."
cmd = "echo 'Skipping publish'"
[tool.pytest.ini_options]
pythonpath = ["."]
+7 -3
View File
@@ -85,9 +85,13 @@ exclude_dirs = ["tests"]
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_mem0"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = 'pytest -m "not integration" --cov=agent_framework_mem0 --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+7 -3
View File
@@ -88,9 +88,13 @@ exclude_dirs = ["tests"]
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_ollama"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = 'pytest -m "not integration" --cov=agent_framework_ollama --cov-report=term-missing:skip-covered tests'
[tool.uv.build-backend]
module-name = "agent_framework_ollama"
@@ -83,9 +83,13 @@ exclude_dirs = ["tests"]
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_orchestrations"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = '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"]
+7 -3
View File
@@ -84,9 +84,13 @@ exclude_dirs = ["tests"]
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_purview"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = 'pytest -m "not integration" --cov=agent_framework_purview --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.9,<4.0"]
+7 -3
View File
@@ -87,9 +87,13 @@ exclude_dirs = ["tests"]
executor.type = "uv"
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'
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_redis"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = 'pytest -m "not integration" --cov=agent_framework_redis --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
+130 -147
View File
@@ -225,94 +225,137 @@ exclude_dirs = ["tests", "scripts", "samples"]
[tool.poe]
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 --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"
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"
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"
clean-dist = ["clean-dist-packages", "clean-dist-meta"]
# build and publish
build-packages = "python scripts/run_tasks_in_packages_if_exists.py build"
build-meta = "python -m flit build"
build = ["build-packages", "build-meta"]
publish = "uv publish"
# combined checks
check-packages = "python scripts/run_tasks_in_packages_if_exists.py fmt lint pyright"
check = ["check-packages", "samples-lint", "samples-syntax", "test", "markdown-code-lint"]
[tool.poe.tasks.all-tests-cov]
cmd = """
pytest --import-mode=importlib
-m "not integration"
--cov=agent_framework
--cov=agent_framework_core
--cov=agent_framework_a2a
--cov=agent_framework_ag_ui
--cov=agent_framework_anthropic
--cov=agent_framework_azure_ai
--cov=agent_framework_azure_ai_search
--cov=agent_framework_azurefunctions
--cov=agent_framework_chatkit
--cov=agent_framework_copilotstudio
--cov=agent_framework_mem0
--cov=agent_framework_purview
--cov=agent_framework_redis
--cov=agent_framework_orchestrations
--cov=agent_framework_declarative
--cov-config=pyproject.toml
--cov-report=term-missing:skip-covered
--ignore-glob=packages/lab/**
--ignore-glob=packages/devui/**
-rs
-n logical --dist worksteal
packages/**/tests
"""
[tool.poe.tasks.all-tests]
cmd = """
pytest --import-mode=importlib
-m "not integration"
--ignore-glob=packages/lab/**
--ignore-glob=packages/devui/**
-rs
-n logical --dist worksteal
packages/**/tests
"""
[tool.poe.tasks.venv]
cmd = "uv venv --clear --python $python"
args = [{ name = "python", default = "3.13", options = ['-p', '--python'] }]
# Workspace setup
[tool.poe.tasks.install]
help = "Install all workspace packages, extras, and dev dependencies from the lockfile."
cmd = "uv sync --all-packages --all-extras --dev --frozen --prerelease=if-necessary-or-explicit"
[tool.poe.tasks.setup]
help = "Create the workspace virtual environment for -P/--python, install dependencies, and install prek hooks."
sequence = [
{ ref = "venv --python $python"},
{ ref = "install" },
{ ref = "prek-install" }
]
args = [{ name = "python", default = "3.13", options = ['-p', '--python'] }]
args = [{ name = "python", default = "3.13", options = ['-P', '-p', '--python'] }]
[tool.poe.tasks.venv]
help = "Create or recreate the workspace virtual environment for -P/--python."
cmd = "uv venv --clear --python $python"
args = [{ name = "python", default = "3.13", options = ['-P', '-p', '--python'] }]
[tool.poe.tasks.prek-install]
help = "Install or refresh the prek git hooks."
cmd = "prek install --overwrite"
# Syntax, typing, and validation
[tool.poe.tasks.syntax]
help = "Run Ruff formatting and Ruff checks for -P/--package packages, or use -S/--samples; add -F/--format or -C/--check to narrow the mode."
cmd = "python scripts/workspace_poe_tasks.py syntax"
[tool.poe.tasks.fmt]
help = "DEPRECATED: Use `syntax --format` instead."
cmd = "python scripts/workspace_poe_tasks.py syntax --format"
[tool.poe.tasks.format]
help = "DEPRECATED: Use `syntax --format` instead."
cmd = "python scripts/workspace_poe_tasks.py syntax --format"
[tool.poe.tasks.lint]
help = "DEPRECATED: Use `syntax --check` instead."
cmd = "python scripts/workspace_poe_tasks.py syntax --check"
[tool.poe.tasks.samples-lint]
help = "DEPRECATED: Use `syntax --samples --check` instead."
cmd = "python scripts/workspace_poe_tasks.py syntax --samples --check"
[tool.poe.tasks.pyright]
help = "Run Pyright for -P/--package packages, use -A/--all for one aggregate sweep, or use -S/--samples for sample checks."
cmd = "python scripts/workspace_poe_tasks.py pyright"
[tool.poe.tasks.mypy]
help = "Run MyPy for -P/--package packages, or use -A/--all for one aggregate sweep."
cmd = "python scripts/workspace_poe_tasks.py mypy"
[tool.poe.tasks.typing]
help = "Run both MyPy and Pyright for -P/--package packages, or use -A/--all for aggregate mode."
cmd = "python scripts/workspace_poe_tasks.py typing"
[tool.poe.tasks.samples-syntax]
help = "DEPRECATED: Use `pyright --samples` instead."
cmd = "python scripts/workspace_poe_tasks.py pyright --samples"
[tool.poe.tasks.check-packages]
help = "Run `syntax` and `pyright` for -P/--package packages."
cmd = "python scripts/workspace_poe_tasks.py check-packages"
[tool.poe.tasks.check]
help = "Run package syntax, pyright, and tests for -P/--package packages; without -P also include sample checks and markdown code lint, or use -S/--samples for sample-only checks."
cmd = "python scripts/workspace_poe_tasks.py check"
[tool.poe.tasks.markdown-code-lint]
help = "Lint Python code blocks embedded in README and sample markdown files."
cmd = "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"
# Testing
[tool.poe.tasks.test]
help = "Run tests for -P/--package packages, or use -A/--all for one aggregate sweep; add -C/--cov for coverage."
cmd = "python scripts/workspace_poe_tasks.py test"
[tool.poe.tasks.all-tests]
help = "DEPRECATED: Use `test --all` instead."
cmd = "python scripts/workspace_poe_tasks.py test --all"
[tool.poe.tasks.all-tests-cov]
help = "DEPRECATED: Use `test --all --cov` instead."
cmd = "python scripts/workspace_poe_tasks.py test --all --cov"
# Build and publishing
[tool.poe.tasks._clean-dist-packages]
cmd = "python scripts/workspace_poe_tasks.py clean-dist"
[tool.poe.tasks._clean-dist-meta]
cmd = "rm -rf dist"
[tool.poe.tasks.clean-dist]
help = "Remove generated dist artifacts for -P/--package packages and the root meta package."
sequence = [
{ ref = "_clean-dist-packages --package ${project}" },
{ ref = "_clean-dist-meta" },
]
args = [{ name = "project", default = "*", options = ["-P", "--package"] }]
[tool.poe.tasks._build-packages]
cmd = "python scripts/workspace_poe_tasks.py build"
[tool.poe.tasks._build-meta]
cmd = "python -m flit build"
[tool.poe.tasks.build]
help = "Build -P/--package packages and the root meta package."
sequence = [
{ ref = "_build-packages --package ${project}" },
{ ref = "_build-meta" },
]
args = [{ name = "project", default = "*", options = ["-P", "--package"] }]
[tool.poe.tasks.publish]
help = "Publish built distributions with uv."
cmd = "uv publish"
# Dependency maintenance
[tool.poe.tasks.upgrade-dev-dependency-pins]
help = "Repin the workspace dev dependency versions used in pyproject.toml."
cmd = "python -m scripts.dependencies.upgrade_dev_dependencies"
[tool.poe.tasks.upgrade-lockfile]
[tool.poe.tasks._upgrade-lockfile]
cmd = "uv lock --upgrade"
[tool.poe.tasks.upgrade-dev-dependencies]
help = "Repin dev dependencies, refresh uv.lock, reinstall, and rerun validation commands."
sequence = [
{ ref = "upgrade-dev-dependency-pins" },
{ ref = "upgrade-lockfile" },
{ ref = "_upgrade-lockfile" },
{ ref = "install" },
{ ref = "check" },
{ ref = "typing" },
@@ -320,17 +363,20 @@ sequence = [
]
[tool.poe.tasks.add-dependency-to-project]
cmd = "uv add --package ${project} ${dependency}"
help = "Add a dependency to a -P/--package workspace package selected by short name such as `core`."
cmd = "python -m scripts.dependencies.add_dependency_to_project --package ${project} --dependency ${dependency}"
args = [
{ name = "project", options = ["-p", "--project"] },
{ name = "dependency", options = ["-d", "--dependency"] },
{ name = "project", options = ["-P", "--package"] },
{ name = "dependency", options = ["-D", "-d", "--dependency"] },
]
[tool.poe.tasks.validate-dependency-bounds-test]
help = "Run workspace dependency-bound validation in test mode, optionally scoped with -P/--package short names such as `core`."
shell = "python -m scripts.dependencies.validate_dependency_bounds --mode test --package \"$project\""
args = [{ name = "project", default = "*", options = ["-p", "--project"] }]
args = [{ name = "project", default = "*", options = ["-P", "--package"] }]
[tool.poe.tasks.validate-dependency-bounds-project]
help = "Validate lower and upper dependency bounds for a -P/--package workspace package, optionally narrowed with -M/--mode and -D/--dependency."
shell = """
command=(python -m scripts.dependencies.validate_dependency_bounds --mode "${mode}" --package "${project}")
if [ -n "${dependency}" ]; then
@@ -340,85 +386,22 @@ fi
"""
interpreter = "bash"
args = [
{ name = "mode", default = "both", options = ["-m", "--mode"] },
{ name = "project", default = "*", options = ["-p", "--project"] },
{ name = "dependency", default = "", options = ["-d", "--dependency"] },
{ name = "mode", default = "both", options = ["-M", "-m", "--mode"] },
{ name = "project", default = "*", options = ["-P", "--package"] },
{ name = "dependency", default = "", options = ["-D", "-d", "--dependency"] },
]
[tool.poe.tasks.add-dependency-and-validate-bounds]
help = "Add a dependency to a -P/--package workspace package selected by short name such as `core`, then validate its dependency bounds with -D/--dependency."
sequence = [
{ ref = "add-dependency-to-project --project ${project} --dependency ${dependency}" },
{ ref = "validate-dependency-bounds-project --mode both --project ${project} --dependency ${dependency}" },
{ ref = "add-dependency-to-project --package ${project} --dependency ${dependency}" },
{ ref = "validate-dependency-bounds-project --mode both --package ${project} --dependency ${dependency}" },
]
args = [
{ name = "project", options = ["-p", "--project"] },
{ name = "dependency", options = ["-d", "--dependency"] },
{ name = "project", options = ["-P", "--package"] },
{ name = "dependency", options = ["-D", "-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 }]
[tool.poe.tasks.prek-check-packages]
cmd = "uv run python scripts/run_tasks_in_changed_packages.py fmt lint pyright --files ${files}"
args = [{ name = "files", default = ".", positional = true, multiple = true }]
[tool.poe.tasks.prek-markdown-code-lint]
cmd = """uv run python scripts/check_md_code_blocks.py ${files} --no-glob
--exclude cookiecutter-agent-framework-lab --exclude tau2
--exclude packages/devui/frontend --exclude context_providers/azure_ai_search"""
args = [{ name = "files", default = ".", positional = true, multiple = true }]
[tool.poe.tasks.prek-samples-check]
shell = """
HAS_SAMPLES=false
for f in ${files}; do
case "$f" in
samples/*) HAS_SAMPLES=true; break ;;
esac
done
if [ "$HAS_SAMPLES" = true ]; then
echo "Sample files changed, running samples checks..."
uv run ruff check samples --fix --exclude samples/autogen-migration,samples/semantic-kernel-migration --ignore E501,ASYNC,B901,TD002
uv run pyright -p pyrightconfig.samples.json --warnings
else
echo "No sample files changed, skipping samples checks"
fi
"""
interpreter = "bash"
args = [{ name = "files", default = ".", positional = true, multiple = true }]
[tool.poe.tasks.ci-mypy]
shell = """
# Try multiple strategies to get changed files
if [ -n "$GITHUB_BASE_REF" ]; then
# In GitHub Actions PR context
git fetch origin $GITHUB_BASE_REF --depth=1 2>/dev/null || true
CHANGED_FILES=$(git diff --name-only origin/$GITHUB_BASE_REF...HEAD -- . 2>/dev/null || \
git diff --name-only FETCH_HEAD...HEAD -- . 2>/dev/null || \
git diff --name-only HEAD^...HEAD -- . 2>/dev/null || \
echo ".")
else
# Local development
CHANGED_FILES=$(git diff --name-only origin/main...HEAD -- . 2>/dev/null || \
git diff --name-only main...HEAD -- . 2>/dev/null || \
git diff --name-only HEAD~1 -- . 2>/dev/null || \
echo ".")
fi
echo "Changed files: $CHANGED_FILES"
uv run python scripts/run_tasks_in_changed_packages.py mypy --files $CHANGED_FILES
"""
interpreter = "bash"
[tool.poe.tasks.prek-check]
sequence = [
{ ref = "prek-check-packages ${files}" },
{ ref = "prek-markdown-code-lint ${files}" },
{ ref = "prek-samples-check ${files}" }
]
args = [{ name = "files", default = ".", positional = true, multiple = true }]
[tool.setuptools.packages.find]
where = ["packages"]
include = ["agent_framework**"]
@@ -29,7 +29,9 @@ async def main() -> None:
client = OpenAIChatClient()
try:
task = asyncio.create_task(client.get_response(messages=[Message(role="user", text="Tell me a fantasy story.")]))
task = asyncio.create_task(
client.get_response(messages=[Message(role="user", text="Tell me a fantasy story.")])
)
await asyncio.sleep(1)
task.cancel()
await task
@@ -94,9 +94,7 @@ class EchoingChatClient(BaseChatClient[OptionsT]):
response_text = f"{response_text} {suffix}"
stream_delay_seconds = float(options.get("stream_delay_seconds", 0.05))
response_message = Message(
role="assistant", contents=[Content.from_text(response_text)]
)
response_message = Message(role="assistant", text=response_text)
response = ChatResponse(
messages=[response_message],
@@ -27,15 +27,9 @@ class KeepLastUserTurnStrategy:
group_annotation = message.additional_properties.get(GROUP_ANNOTATION_KEY)
group_id = group_annotation.get("id") if isinstance(group_annotation, dict) else None
kind = group_annotation.get("kind") if isinstance(group_annotation, dict) else None
if (
isinstance(group_id, str)
and isinstance(kind, str)
and group_id not in group_kinds
):
if isinstance(group_id, str) and isinstance(kind, str) and group_id not in group_kinds:
group_kinds[group_id] = kind
user_group_ids = [
group_id for group_id in group_ids if group_kinds.get(group_id) == "user"
]
user_group_ids = [group_id for group_id in group_ids if group_kinds.get(group_id) == "user"]
if not user_group_ids:
return False
keep_user_group_id = user_group_ids[-1]
@@ -33,9 +33,7 @@ Key components:
class TiktokenTokenizer(TokenizerProtocol):
"""TokenizerProtocol implementation backed by tiktoken's o200k_base (gpt-4.1 and up default) encoding."""
def __init__(
self, *, encoding_name: str = "o200k_base", model_name: str | None = None
) -> None:
def __init__(self, *, encoding_name: str = "o200k_base", model_name: str | None = None) -> None:
if model_name is not None:
self._encoding = tiktoken.encoding_for_model(model_name)
else:
@@ -62,10 +60,7 @@ def _build_messages() -> list[Message]:
),
Message(
role="user",
text=(
"Now provide a detailed checklist with owners, rollback "
"gates, and validation criteria."
),
text=("Now provide a detailed checklist with owners, rollback gates, and validation criteria."),
),
Message(
role="assistant",
@@ -37,9 +37,7 @@ def get_weather(
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
temperature = 53
return (
f"The weather in {location} is {conditions[0]} with a high of {temperature}°C."
)
return f"The weather in {location} is {conditions[0]} with a high of {temperature}°C."
@tool(approval_mode="never_require")
@@ -68,9 +66,7 @@ class AddExclamation(Executor):
"""Add exclamation mark to text."""
@handler
async def add_exclamation(
self, text: str, ctx: WorkflowContext[Never, str]
) -> None:
async def add_exclamation(self, text: str, ctx: WorkflowContext[Never, str]) -> None:
"""Add exclamation and yield as workflow output."""
result = f"{text}!"
await ctx.yield_output(result)
@@ -19,9 +19,7 @@ from copilot.generated.session_events import PermissionRequest
from copilot.types import PermissionRequestResult
def prompt_permission(
request: PermissionRequest, context: dict[str, str]
) -> PermissionRequestResult:
def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult:
"""Permission handler that prompts the user for approval."""
print(f"\n[Permission Request: {request.kind}]")
@@ -75,7 +75,9 @@ unit_converter_skill = Skill(
# ---------------------------------------------------------------------------
# 2. Dynamic Resources — callable function via @skill.resource
# ---------------------------------------------------------------------------
@unit_converter_skill.resource(name="conversion-policy", description="Current conversion formatting and rounding policy")
@unit_converter_skill.resource(
name="conversion-policy", description="Current conversion formatting and rounding policy"
)
def conversion_policy(**kwargs: Any) -> Any:
"""Return the current conversion policy.
@@ -148,8 +150,7 @@ async def main() -> None:
print("Converting units")
print("-" * 60)
response = await agent.run(
"How many kilometers is a marathon (26.2 miles)? "
"And how many pounds is 75 kilograms?",
"How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?",
precision=2,
)
print(f"Agent: {response}\n")
@@ -70,8 +70,7 @@ async def main() -> None:
print("Converting units")
print("-" * 60)
response = await agent.run(
"How many kilometers is a marathon (26.2 miles)? "
"And how many pounds is 75 kilograms?"
"How many kilometers is a marathon (26.2 miles)? And how many pounds is 75 kilograms?"
)
print(f"Agent: {response}\n")
@@ -137,8 +137,7 @@ async def main() -> None:
print("Converting units")
print("-" * 60)
response = await agent.run(
"How many kilometers is a marathon (26.2 miles)? "
"And how many liters is a 5-gallon bucket?"
"How many kilometers is a marathon (26.2 miles)? And how many liters is a 5-gallon bucket?"
)
print(f"Agent: {response}\n")
@@ -135,8 +135,7 @@ async def scenario_max_function_calls():
)
response = await agent.run(
"Search for the weather in Paris, London, Tokyo, "
"New York, and Sydney, and also search for best travel tips."
"Search for the weather in Paris, London, Tokyo, New York, and Sydney, and also search for best travel tips."
)
print(f" Response: {response.text[:200]}...")
print()
@@ -236,8 +235,12 @@ async def scenario_per_agent_tool_limits():
session_b = agent_b.create_session()
await agent_b.run("Look up quantum computing", session=session_b)
print(f" agent_a_lookup.invocation_count = {agent_a_lookup.invocation_count} (limit {agent_a_lookup.max_invocations})")
print(f" agent_b_lookup.invocation_count = {agent_b_lookup.invocation_count} (limit {agent_b_lookup.max_invocations})")
print(
f" agent_a_lookup.invocation_count = {agent_a_lookup.invocation_count} (limit {agent_a_lookup.max_invocations})"
)
print(
f" agent_b_lookup.invocation_count = {agent_b_lookup.invocation_count} (limit {agent_b_lookup.max_invocations})"
)
print(" → Agent A hit its limit; Agent B used 1 of 5.")
print()
@@ -254,8 +257,8 @@ async def scenario_combined():
client = OpenAIResponsesClient()
# 1. Configure the client with both iteration and function call limits.
client.function_invocation_configuration["max_iterations"] = 5 # max 5 LLM roundtrips
client.function_invocation_configuration["max_function_calls"] = 8 # max 8 total tool calls
client.function_invocation_configuration["max_iterations"] = 5 # max 5 LLM roundtrips
client.function_invocation_configuration["max_function_calls"] = 8 # max 8 total tool calls
print(f" max_iterations = {client.function_invocation_configuration['max_iterations']}")
print(f" max_function_calls = {client.function_invocation_configuration['max_function_calls']}")
@@ -47,12 +47,8 @@ async def main() -> None:
session = agent.create_session()
# Run the agent with the session; tools receive it via ctx.session.
print(
f"Agent: {await agent.run('What is the weather in London?', session=session)}"
)
print(
f"Agent: {await agent.run('What is the weather in Amsterdam?', session=session)}"
)
print(f"Agent: {await agent.run('What is the weather in London?', session=session)}")
print(f"Agent: {await agent.run('What is the weather in Amsterdam?', session=session)}")
print(f"Agent: {await agent.run('What cities did I ask about?', session=session)}")
+110 -50
View File
@@ -72,56 +72,116 @@ def _random_date_within_last_two_months() -> datetime:
def _build_invoices() -> list[Invoice]:
"""Build 10 mock invoices."""
return [
Invoice("TICKET-XYZ987", "INV789", "Contoso", _random_date_within_last_two_months(), [
Product("T-Shirts", 150, 10.00),
Product("Hats", 200, 15.00),
Product("Glasses", 300, 5.00),
]),
Invoice("TICKET-XYZ111", "INV111", "XStore", _random_date_within_last_two_months(), [
Product("T-Shirts", 2500, 12.00),
Product("Hats", 1500, 8.00),
Product("Glasses", 200, 20.00),
]),
Invoice("TICKET-XYZ222", "INV222", "Cymbal Direct", _random_date_within_last_two_months(), [
Product("T-Shirts", 1200, 14.00),
Product("Hats", 800, 7.00),
Product("Glasses", 500, 25.00),
]),
Invoice("TICKET-XYZ333", "INV333", "Contoso", _random_date_within_last_two_months(), [
Product("T-Shirts", 400, 11.00),
Product("Hats", 600, 15.00),
Product("Glasses", 700, 5.00),
]),
Invoice("TICKET-XYZ444", "INV444", "XStore", _random_date_within_last_two_months(), [
Product("T-Shirts", 800, 10.00),
Product("Hats", 500, 18.00),
Product("Glasses", 300, 22.00),
]),
Invoice("TICKET-XYZ555", "INV555", "Cymbal Direct", _random_date_within_last_two_months(), [
Product("T-Shirts", 1100, 9.00),
Product("Hats", 900, 12.00),
Product("Glasses", 1200, 15.00),
]),
Invoice("TICKET-XYZ666", "INV666", "Contoso", _random_date_within_last_two_months(), [
Product("T-Shirts", 2500, 8.00),
Product("Hats", 1200, 10.00),
Product("Glasses", 1000, 6.00),
]),
Invoice("TICKET-XYZ777", "INV777", "XStore", _random_date_within_last_two_months(), [
Product("T-Shirts", 1900, 13.00),
Product("Hats", 1300, 16.00),
Product("Glasses", 800, 19.00),
]),
Invoice("TICKET-XYZ888", "INV888", "Cymbal Direct", _random_date_within_last_two_months(), [
Product("T-Shirts", 2200, 11.00),
Product("Hats", 1700, 8.50),
Product("Glasses", 600, 21.00),
]),
Invoice("TICKET-XYZ999", "INV999", "Contoso", _random_date_within_last_two_months(), [
Product("T-Shirts", 1400, 10.50),
Product("Hats", 1100, 9.00),
Product("Glasses", 950, 12.00),
]),
Invoice(
"TICKET-XYZ987",
"INV789",
"Contoso",
_random_date_within_last_two_months(),
[
Product("T-Shirts", 150, 10.00),
Product("Hats", 200, 15.00),
Product("Glasses", 300, 5.00),
],
),
Invoice(
"TICKET-XYZ111",
"INV111",
"XStore",
_random_date_within_last_two_months(),
[
Product("T-Shirts", 2500, 12.00),
Product("Hats", 1500, 8.00),
Product("Glasses", 200, 20.00),
],
),
Invoice(
"TICKET-XYZ222",
"INV222",
"Cymbal Direct",
_random_date_within_last_two_months(),
[
Product("T-Shirts", 1200, 14.00),
Product("Hats", 800, 7.00),
Product("Glasses", 500, 25.00),
],
),
Invoice(
"TICKET-XYZ333",
"INV333",
"Contoso",
_random_date_within_last_two_months(),
[
Product("T-Shirts", 400, 11.00),
Product("Hats", 600, 15.00),
Product("Glasses", 700, 5.00),
],
),
Invoice(
"TICKET-XYZ444",
"INV444",
"XStore",
_random_date_within_last_two_months(),
[
Product("T-Shirts", 800, 10.00),
Product("Hats", 500, 18.00),
Product("Glasses", 300, 22.00),
],
),
Invoice(
"TICKET-XYZ555",
"INV555",
"Cymbal Direct",
_random_date_within_last_two_months(),
[
Product("T-Shirts", 1100, 9.00),
Product("Hats", 900, 12.00),
Product("Glasses", 1200, 15.00),
],
),
Invoice(
"TICKET-XYZ666",
"INV666",
"Contoso",
_random_date_within_last_two_months(),
[
Product("T-Shirts", 2500, 8.00),
Product("Hats", 1200, 10.00),
Product("Glasses", 1000, 6.00),
],
),
Invoice(
"TICKET-XYZ777",
"INV777",
"XStore",
_random_date_within_last_two_months(),
[
Product("T-Shirts", 1900, 13.00),
Product("Hats", 1300, 16.00),
Product("Glasses", 800, 19.00),
],
),
Invoice(
"TICKET-XYZ888",
"INV888",
"Cymbal Direct",
_random_date_within_last_two_months(),
[
Product("T-Shirts", 2200, 11.00),
Product("Hats", 1700, 8.50),
Product("Glasses", 600, 21.00),
],
),
Invoice(
"TICKET-XYZ999",
"INV999",
"Contoso",
_random_date_within_last_two_months(),
[
Product("T-Shirts", 1400, 10.50),
Product("Hats", 1100, 9.00),
Product("Glasses", 950, 12.00),
],
),
]
+1 -1
View File
@@ -62,7 +62,7 @@ Users can create a `.env` file in the `python/` directory based on `.env.example
## Syntax Checking
Run `uv run poe samples-syntax` to check samples for syntax errors and missing imports from `agent_framework`. This uses a relaxed pyright configuration that validates imports without strict type checking.
Run `uv run poe pyright -S` to check samples for syntax errors and missing imports from `agent_framework`. This uses a relaxed pyright configuration that validates imports without strict type checking.
Some samples depend on external packages (e.g., `azure.ai.agentserver.agentframework`, `microsoft_agents`) that are not installed in the dev environment. These are excluded in `pyrightconfig.samples.json`. When adding or modifying these excluded samples, add them to the exclude list and manually verify they have no import errors from `agent_framework` packages by temporarily removing them from the exclude list and running the check.
+8 -8
View File
@@ -46,14 +46,14 @@ These are the normal user-facing entrypoints:
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>"
uv run poe validate-dependency-bounds-test --package core
uv run poe validate-dependency-bounds-project --mode both --package core --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.
- `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 `--package` argument defaults to `*`, and `--dependency` is optional, so automation can also use it for repo-wide upper-bound runs.
### GitHub Actions workflows
@@ -61,7 +61,7 @@ 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 "*"`
- Runs `uv run poe validate-dependency-bounds-project --mode upper --package "*"`
- Uploads `python/scripts/dependencies/dependency-range-results.json`
- Creates issues for failing candidate versions and opens/updates a PR for passing range updates
@@ -76,10 +76,10 @@ 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
python -m scripts.dependencies.validate_dependency_bounds --mode test --package core --dry-run
python -m scripts.dependencies.validate_dependency_bounds --mode both --package core --dependencies openai --dry-run
python -m scripts.dependencies._dependency_bounds_lower_impl --packages core --dependencies openai --dry-run
python -m scripts.dependencies._dependency_bounds_upper_impl --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`.
@@ -1,5 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff: noqa: INP001, S404, S603
# ruff: noqa: S404, S603
"""Lower dependency bounds, validate, and persist the oldest passing set."""
@@ -21,14 +21,15 @@ from urllib import error as urllib_error
from urllib import request as urllib_request
import tomli
from packaging.requirements import InvalidRequirement, Requirement
from packaging.version import InvalidVersion, Version
from rich import print
from scripts.dependencies._dependency_bounds_runtime import (
extend_command_with_runtime_tools,
extend_command_with_task,
)
from packaging.requirements import InvalidRequirement, Requirement
from packaging.version import InvalidVersion, Version
from rich import print
from scripts.task_runner import discover_projects, extract_poe_tasks
from scripts.task_runner import discover_projects, extract_poe_tasks, project_filter_matches
CHECK_TASK_PRIORITY = ("check", "typing", "pyright", "mypy", "lint")
REQ_PATTERN = r"^\s*([A-Za-z0-9_.-]+(?:\[[^\]]+\])?)\s*(.*?)\s*$"
@@ -937,7 +938,7 @@ def main() -> None:
"--packages",
nargs="*",
default=None,
help="Optional package filters by workspace path (e.g., packages/core) or package name.",
help="Optional package filters by short name (for example core), workspace path, or package name.",
)
parser.add_argument(
"--dependencies",
@@ -1001,7 +1002,11 @@ def main() -> None:
project_section = package_config.get("project", {})
optional_dependencies = project_section.get("optional-dependencies", {}) or {}
dependency_groups = package_config.get("dependency-groups", {}) or {}
if package_filters and str(project_path) not in package_filters and package_name not in package_filters:
# Reuse the shared selector matcher so direct optimizer runs accept the
# same short-name package filters as the contributor-facing Poe tasks.
if package_filters and not any(
project_filter_matches(project_path, package_filter, [package_name]) for package_filter in package_filters
):
continue
plans.append(
PackagePlan(
@@ -1,5 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff: noqa: INP001, S404, S603
# ruff: noqa: S404, S603
"""Raise dependency upper bounds, validate, and persist the latest passing set."""
@@ -22,15 +22,16 @@ from urllib import error as urllib_error
from urllib import request as urllib_request
import tomli
from packaging.requirements import InvalidRequirement, Requirement
from packaging.version import InvalidVersion, Version
from rich import print
from scripts.dependencies._dependency_bounds_runtime import (
extend_command_with_runtime_tools,
extend_command_with_task,
next_zero_major_minor_boundary,
)
from packaging.requirements import InvalidRequirement, Requirement
from packaging.version import InvalidVersion, Version
from rich import print
from scripts.task_runner import discover_projects, extract_poe_tasks
from scripts.task_runner import discover_projects, extract_poe_tasks, project_filter_matches
CHECK_TASK_PRIORITY = ("check", "typing", "pyright", "mypy", "lint")
REQ_PATTERN = r"^\s*([A-Za-z0-9_.-]+(?:\[[^\]]+\])?)\s*(.*?)\s*$"
@@ -1088,7 +1089,7 @@ def main() -> None:
"--packages",
nargs="*",
default=None,
help="Optional package filters by workspace path (e.g., packages/core) or package name.",
help="Optional package filters by short name (for example core), workspace path, or package name.",
)
parser.add_argument(
"--dependencies",
@@ -1153,7 +1154,11 @@ def main() -> None:
project_section = package_config.get("project", {})
optional_dependencies = project_section.get("optional-dependencies", {}) or {}
dependency_groups = package_config.get("dependency-groups", {}) or {}
if package_filters and str(project_path) not in package_filters and package_name not in package_filters:
# Reuse the shared selector matcher so direct optimizer runs accept the
# same short-name package filters as the contributor-facing Poe tasks.
if package_filters and not any(
project_filter_matches(project_path, package_filter, [package_name]) for package_filter in package_filters
):
continue
plans.append(
PackagePlan(
@@ -0,0 +1,118 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff: noqa: S603
"""Add a dependency to one workspace package selected by short name or path.
``uv add --package`` expects the published workspace distribution name, while
the root Poe surface intentionally speaks in short repo package names such as
``core``. This wrapper keeps the user-facing selector stable and translates it
just before delegating to uv.
"""
from __future__ import annotations
import argparse
import subprocess
from dataclasses import dataclass
from pathlib import Path
import tomli
from rich import print
from scripts.task_runner import discover_projects, project_filter_matches
@dataclass(frozen=True)
class WorkspacePackage:
"""Workspace package metadata needed for `uv add --package`."""
short_name: str
project_path: Path
distribution_name: str
def _load_distribution_name(pyproject_file: Path) -> str:
with pyproject_file.open("rb") as f:
data = tomli.load(f)
return str(data.get("project", {}).get("name", "")).strip()
def _discover_workspace_packages(workspace_root: Path) -> list[WorkspacePackage]:
workspace_pyproject = workspace_root / "pyproject.toml"
packages: list[WorkspacePackage] = []
for project_path in sorted(discover_projects(workspace_pyproject), key=str):
pyproject_file = workspace_root / project_path / "pyproject.toml"
if not pyproject_file.exists():
continue
distribution_name = _load_distribution_name(pyproject_file)
if not distribution_name:
continue
packages.append(
WorkspacePackage(
short_name=project_path.name,
project_path=project_path,
distribution_name=distribution_name,
)
)
return packages
def _resolve_workspace_package(workspace_root: Path, project_filter: str) -> WorkspacePackage:
"""Resolve one workspace package from a user-facing selector.
The wrapper accepts the same short-name/path/distribution-name vocabulary as
the other root tasks, but errors on ambiguous matches so dependency edits
never hit the wrong package.
"""
matches = [
package
for package in _discover_workspace_packages(workspace_root)
if project_filter_matches(package.project_path, project_filter, [package.short_name, package.distribution_name])
]
if not matches:
raise SystemExit(f"No workspace package matched selector '{project_filter}'.")
if len(matches) > 1:
names = ", ".join(sorted(package.short_name for package in matches))
raise SystemExit(
f"Package selector '{project_filter}' matched multiple workspace packages: {names}. "
"Use a more specific short name or path."
)
return matches[0]
def main() -> None:
"""Resolve a workspace project selector, then delegate to `uv add`."""
parser = argparse.ArgumentParser(
description="Add a dependency to a single workspace package selected by short name, path, or package name."
)
parser.add_argument(
"-P",
"--package",
dest="project",
metavar="PACKAGE",
required=True,
help="Workspace package selector, such as `core`.",
)
# Keep the old long flag as a silent alias while downstream automation
# finishes moving to the user-facing ``--package`` spelling.
parser.add_argument("--project", dest="project", help=argparse.SUPPRESS)
parser.add_argument("-D", "--dependency", required=True, help="Dependency specifier to add.")
args = parser.parse_args()
workspace_root = Path(__file__).resolve().parents[2]
package = _resolve_workspace_package(workspace_root, args.project)
print(
f"[cyan]Adding {args.dependency} to {package.short_name} "
f"({package.distribution_name})[/cyan]"
)
result = subprocess.run(
["uv", "add", "--package", package.distribution_name, args.dependency],
cwd=workspace_root,
check=False,
)
if result.returncode:
raise SystemExit(result.returncode)
if __name__ == "__main__":
main()
@@ -1,5 +1,5 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff: noqa: INP001, S404, S603
# ruff: noqa: S404, S603
"""Unified dependency-bound validation entrypoint.
@@ -8,6 +8,10 @@ Modes:
- 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.
Package filters intentionally reuse the root task selector semantics so the
same short package names (for example ``core``) work in both contributor
commands and direct debugging entrypoints.
"""
from __future__ import annotations
@@ -23,6 +27,7 @@ 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,
@@ -33,7 +38,7 @@ from scripts.dependencies._dependency_bounds_upper_impl import (
_load_package_name,
_resolve_internal_editables,
)
from scripts.task_runner import discover_projects, extract_poe_tasks
from scripts.task_runner import discover_projects, extract_poe_tasks, project_filter_matches
_LOWER_IMPL_MODULE = "scripts.dependencies._dependency_bounds_lower_impl"
_UPPER_IMPL_MODULE = "scripts.dependencies._dependency_bounds_upper_impl"
@@ -76,10 +81,10 @@ def _coerce_subprocess_output(output: str | bytes | None) -> str:
def _build_test_plans(workspace_root: Path, package_filter: str | None) -> list[PackageTestPlan]:
"""Build per-package test plans for the requested workspace selector."""
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] = []
@@ -89,7 +94,14 @@ def _build_test_plans(workspace_root: Path, package_filter: str | None) -> list[
continue
package_name = _load_package_name(pyproject_file)
if normalized_filter and str(project_path) != normalized_filter and package_name != normalized_filter:
# Reuse the shared matcher so dependency-bound test mode accepts the
# same short names and legacy path-style selectors as the root Poe
# commands.
if (
package_filter
and package_filter != "*"
and not project_filter_matches(project_path, package_filter, [package_name])
):
continue
available_tasks = extract_poe_tasks(pyproject_file)
@@ -366,7 +378,10 @@ def main() -> None:
parser.add_argument(
"--package",
default=None,
help="Optional workspace package path/name filter for all modes. Use '*' or omit it for the whole workspace.",
help=(
"Optional workspace package selector for all modes, such as `core`. "
"Use '*' or omit it for the whole workspace."
),
)
parser.add_argument(
"--dependencies",
+80 -12
View File
@@ -1,6 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
"""Shared utilities for running poe tasks across workspace packages in parallel."""
"""Shared utilities for running Poe tasks across workspace packages.
These helpers centralize workspace discovery, selector matching, and execution
mode so the root task dispatcher and dependency tooling interpret package
filters the same way.
"""
import concurrent.futures
import glob
@@ -8,6 +13,8 @@ import os
import subprocess
import sys
import time
from collections.abc import Sequence
from fnmatch import fnmatch
from pathlib import Path
import tomli
@@ -70,12 +77,67 @@ def build_work_items(projects: list[Path], task_names: list[str]) -> list[tuple[
return work_items
def _run_task_subprocess(project: Path, task: str, workspace_root: Path) -> tuple[Path, str, int, str, str, float]:
def normalize_project_filter(value: str) -> str:
"""Normalize a user-supplied workspace selector.
Strip presentation differences so short names, relative paths, and globs can
be compared with one matcher.
"""
normalized = value.strip().strip("/").replace("\\", "/")
return normalized or "."
def build_project_filter_candidates(project: Path | str, aliases: Sequence[str] = ()) -> set[str]:
"""Return accepted selector values for one workspace project.
We accept the workspace path, short package name, and any supplied aliases
so user-facing ``--package core`` stays stable even when underlying tools
still need paths or distribution names.
"""
normalized_path = normalize_project_filter(str(project))
candidates = {normalized_path}
if normalized_path == ".":
candidates.update({"./", "root"})
else:
# Accept bare short names like ``core`` alongside ``packages/core`` and
# ``./packages/core`` so callers do not have to care which form a
# downstream script prefers.
path = Path(normalized_path)
candidates.add(path.name)
candidates.add(f"./{normalized_path}")
for alias in aliases:
normalized_alias = normalize_project_filter(alias)
if normalized_alias and normalized_alias != ".":
candidates.add(normalized_alias)
return {candidate.lower() for candidate in candidates}
def project_filter_matches(project: Path | str, pattern: str, aliases: Sequence[str] = ()) -> bool:
"""Return whether a project matches a user-supplied selector or glob.
Matching happens against the normalized candidate set so CLI callers can use
the same selector vocabulary everywhere.
"""
normalized_pattern = normalize_project_filter(pattern).lower()
return any(
fnmatch(candidate, normalized_pattern)
for candidate in build_project_filter_candidates(project, aliases)
)
def _run_task_subprocess(
project: Path,
task: str,
workspace_root: Path,
task_args: Sequence[str] = (),
) -> tuple[Path, str, int, str, str, float]:
"""Run a single poe task in a project directory via subprocess."""
start = time.monotonic()
cwd = workspace_root / project
result = subprocess.run(
["uv", "run", "poe", task],
["uv", "run", "poe", task, *task_args],
cwd=cwd,
capture_output=True,
text=True,
@@ -84,20 +146,20 @@ def _run_task_subprocess(project: Path, task: str, workspace_root: Path) -> tupl
return (project, task, result.returncode, result.stdout, result.stderr, elapsed)
def _run_sequential(work_items: list[tuple[Path, str]]) -> None:
def _run_sequential(work_items: list[tuple[Path, str]], task_args: Sequence[str] = ()) -> None:
"""Run tasks sequentially using in-process PoeThePoet (streaming output)."""
from poethepoet.app import PoeThePoet
for project, task in work_items:
print(f"Running task {task} in {project}")
app = PoeThePoet(cwd=project)
result = app(cli_args=[task])
result = app(cli_args=[task, *task_args])
if result:
sys.exit(result)
def _run_parallel(work_items: list[tuple[Path, str]], workspace_root: Path) -> None:
"""Run all (package × task) combinations in parallel via subprocesses."""
def _run_parallel(work_items: list[tuple[Path, str]], workspace_root: Path, task_args: Sequence[str] = ()) -> None:
"""Run all (package x task) combinations in parallel via subprocesses."""
max_workers = min(len(work_items), os.cpu_count() or 4)
failures: list[tuple[Path, str, str, str]] = []
completed = 0
@@ -107,7 +169,7 @@ def _run_parallel(work_items: list[tuple[Path, str]], workspace_root: Path) -> N
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {
executor.submit(_run_task_subprocess, project, task, workspace_root): (project, task)
executor.submit(_run_task_subprocess, project, task, workspace_root, task_args): (project, task)
for project, task in work_items
}
for future in concurrent.futures.as_completed(futures):
@@ -123,7 +185,7 @@ def _run_parallel(work_items: list[tuple[Path, str]], workspace_root: Path) -> N
if failures:
print(f"\n[red]{len(failures)} task(s) failed:[/red]")
for project, task, stdout, stderr in failures:
print(f"\n[red]{'='*60}[/red]")
print(f"\n[red]{'=' * 60}[/red]")
print(f"[red]FAILED: {task} in {project}[/red]")
if stdout.strip():
print(stdout)
@@ -134,7 +196,13 @@ def _run_parallel(work_items: list[tuple[Path, str]], workspace_root: Path) -> N
print(f"\n[green]All {total} task(s) passed ✓[/green]")
def run_tasks(work_items: list[tuple[Path, str]], workspace_root: Path, *, sequential: bool = False) -> None:
def run_tasks(
work_items: list[tuple[Path, str]],
workspace_root: Path,
*,
sequential: bool = False,
task_args: Sequence[str] = (),
) -> None:
"""Run work items either in parallel or sequentially.
Single items use in-process PoeThePoet for streaming output.
@@ -145,6 +213,6 @@ def run_tasks(work_items: list[tuple[Path, str]], workspace_root: Path, *, seque
return
if sequential or len(work_items) == 1:
_run_sequential(work_items)
_run_sequential(work_items, task_args)
else:
_run_parallel(work_items, workspace_root)
_run_parallel(work_items, workspace_root, task_args)
+698
View File
@@ -0,0 +1,698 @@
# Copyright (c) Microsoft. All rights reserved.
"""Dispatch contributor-facing workspace tasks with consistent scope flags.
This script is the single root-task entrypoint used by ``python/pyproject.toml``.
It keeps selector semantics, aggregate-vs-fan-out behaviour, and compatibility
aliases in one place so docs and automation can share the same command surface.
"""
from __future__ import annotations
import argparse
import os
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
import tomli
from packaging.specifiers import SpecifierSet
from packaging.version import Version
from rich import print
from task_runner import build_work_items, discover_projects, project_filter_matches, run_tasks
WORKSPACE_ROOT = Path(__file__).resolve().parent.parent
WORKSPACE_PYPROJECT = WORKSPACE_ROOT / "pyproject.toml"
CURRENT_PYTHON = Version(f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}")
SAMPLE_EXCLUDES = "samples/autogen-migration,samples/semantic-kernel-migration"
SAMPLE_RUFF_IGNORE = "E501,ASYNC,B901,TD002"
MARKDOWN_EXCLUDES = [
"cookiecutter-agent-framework-lab",
"tau2",
"packages/devui/frontend",
"context_providers/azure_ai_search",
]
DEFAULT_AGGREGATE_TEST_EXCLUDES = {"devui", "lab"}
@dataclass(frozen=True)
class WorkspaceProject:
"""Metadata about a workspace package."""
path: Path
name: str
distribution_name: str
requires_python: str | None
def parse_args(argv: list[str]) -> tuple[argparse.Namespace, list[str]]:
"""Parse the workspace command and return any pass-through arguments."""
parser = argparse.ArgumentParser(description="Dispatch workspace Poe tasks with consistent scope flags.")
subparsers = parser.add_subparsers(dest="command", required=True)
def add_project_option(command: argparse.ArgumentParser) -> None:
command.add_argument(
"-P",
"--package",
dest="project",
default="*",
metavar="PACKAGE",
help="Workspace package selector or glob pattern, such as `core`.",
)
# Keep a hidden compatibility alias while old automation and local
# muscle memory migrate from ``--project`` to ``--package``.
command.add_argument("--project", dest="project", help=argparse.SUPPRESS)
def add_syntax_mode_options(command: argparse.ArgumentParser) -> None:
command.add_argument("-F", "--format", action="store_true", help="Run formatting only.")
command.add_argument("-C", "--check", action="store_true", help="Run lint checks only.")
def add_all_option(command: argparse.ArgumentParser) -> None:
command.add_argument("-A", "--all", action="store_true", help="Run a single aggregate workspace sweep.")
def add_samples_option(command: argparse.ArgumentParser) -> None:
command.add_argument("-S", "--samples", action="store_true", help="Target samples/ instead of packages.")
def add_cov_option(command: argparse.ArgumentParser) -> None:
command.add_argument("-C", "--cov", action="store_true", help="Enable coverage output.")
syntax = subparsers.add_parser("syntax")
add_project_option(syntax)
add_samples_option(syntax)
add_syntax_mode_options(syntax)
for command_name in ("fmt", "build", "clean-dist", "check-packages"):
command = subparsers.add_parser(command_name)
add_project_option(command)
lint = subparsers.add_parser("lint")
add_project_option(lint)
add_samples_option(lint)
pyright = subparsers.add_parser("pyright")
add_project_option(pyright)
add_all_option(pyright)
add_samples_option(pyright)
mypy = subparsers.add_parser("mypy")
add_project_option(mypy)
add_all_option(mypy)
typing = subparsers.add_parser("typing")
add_project_option(typing)
add_all_option(typing)
test = subparsers.add_parser("test")
add_project_option(test)
add_all_option(test)
add_cov_option(test)
check = subparsers.add_parser("check")
add_project_option(check)
add_samples_option(check)
prek_check = subparsers.add_parser("prek-check")
prek_check.add_argument("files", nargs="*", default=["."], help="Files reported by pre-commit.")
subparsers.add_parser("ci-mypy")
return parser.parse_known_args(argv)
def load_toml(file_path: Path) -> dict:
"""Load a TOML file."""
with file_path.open("rb") as file:
return tomli.load(file)
def discover_workspace_projects() -> list[WorkspaceProject]:
"""Return workspace packages together with their Python-version metadata."""
projects: list[WorkspaceProject] = []
for project_path in discover_projects(WORKSPACE_PYPROJECT):
pyproject = load_toml(WORKSPACE_ROOT / project_path / "pyproject.toml")
requires_python = pyproject.get("project", {}).get("requires-python")
distribution_name = str(pyproject.get("project", {}).get("name", "")).strip()
projects.append(
WorkspaceProject(
path=project_path,
name=project_path.name,
distribution_name=distribution_name,
requires_python=requires_python,
)
)
return projects
def supports_current_python(project: WorkspaceProject) -> bool:
"""Return whether the current interpreter satisfies the project's Python requirement."""
if not project.requires_python:
return True
return SpecifierSet(project.requires_python).contains(CURRENT_PYTHON, prereleases=True)
def select_projects(pattern: str) -> list[WorkspaceProject]:
"""Select supported workspace projects that match the supplied pattern.
The shared matcher accepts short names such as ``core``, legacy path-style
values, and distribution names so every root task family speaks the same
selector dialect.
"""
matched_projects = [
project
for project in discover_workspace_projects()
if project_filter_matches(project.path, pattern, aliases=[project.name, project.distribution_name])
]
if not matched_projects:
print(f"[red]No workspace projects matched pattern '{pattern}'.[/red]")
raise SystemExit(2)
supported_projects = [project for project in matched_projects if supports_current_python(project)]
unsupported_projects = [project.name for project in matched_projects if not supports_current_python(project)]
if unsupported_projects:
version = f"{sys.version_info.major}.{sys.version_info.minor}"
print(
"[yellow]Skipping packages not supported by "
f"Python {version}: {', '.join(sorted(unsupported_projects))}[/yellow]"
)
return supported_projects
def relative_path(path: Path) -> str:
"""Convert a workspace path to a stable relative string."""
return path.relative_to(WORKSPACE_ROOT).as_posix()
def collect_source_dirs(projects: list[WorkspaceProject]) -> list[Path]:
"""Collect top-level import package directories for the selected projects."""
source_dirs: set[Path] = set()
for project in projects:
project_root = WORKSPACE_ROOT / project.path
for init_file in project_root.rglob("__init__.py"):
package_dir = init_file.parent
if package_dir.name.startswith("agent_framework"):
source_dirs.add(package_dir)
return sorted(source_dirs)
def collect_test_dirs(projects: list[WorkspaceProject]) -> list[Path]:
"""Collect test directories for the selected projects."""
test_dirs: set[Path] = set()
for project in projects:
project_root = WORKSPACE_ROOT / project.path
for directory_name in ("tests", "ag_ui_tests"):
for test_dir in project_root.rglob(directory_name):
relative_test_dir = test_dir.relative_to(project_root)
# Ignore hidden/generated trees such as ``.mypy_cache`` so the
# aggregate sweep only targets real repository test directories.
if test_dir.is_dir() and not any(part.startswith(".") for part in relative_test_dir.parts):
test_dirs.add(test_dir)
return sorted(test_dirs)
def run_command(command: list[str]) -> None:
"""Run a subprocess from the workspace root and stream its output."""
result = subprocess.run(command, cwd=WORKSPACE_ROOT, check=False)
if result.returncode:
raise SystemExit(result.returncode)
def run_fan_out(task_names: list[str], project_pattern: str, task_args: list[str]) -> None:
"""Run package-local Poe tasks across the selected projects."""
selected_projects = select_projects(project_pattern)
if not selected_projects:
print("[yellow]No selected projects support the current Python version, skipping.[/yellow]")
return
work_items = build_work_items([project.path for project in selected_projects], task_names)
run_tasks(work_items, WORKSPACE_ROOT, task_args=task_args)
def sample_pyright_config() -> str:
"""Return the sample Pyright configuration for the current interpreter."""
if sys.version_info < (3, 11):
return "pyrightconfig.samples.py310.json"
return "pyrightconfig.samples.json"
def run_sample_lint(extra_args: list[str]) -> None:
"""Run linting against samples/."""
command = [
"uv",
"run",
"ruff",
"check",
"samples",
"--fix",
"--exclude",
SAMPLE_EXCLUDES,
"--ignore",
SAMPLE_RUFF_IGNORE,
*extra_args,
]
run_command(command)
def run_sample_format(extra_args: list[str]) -> None:
"""Run formatting against samples/."""
command = [
"uv",
"run",
"ruff",
"format",
"samples",
"--exclude",
SAMPLE_EXCLUDES,
*extra_args,
]
run_command(command)
def run_sample_pyright(extra_args: list[str]) -> None:
"""Run sample syntax/import validation."""
command = ["uv", "run", "pyright", "-p", sample_pyright_config(), "--warnings", *extra_args]
run_command(command)
def run_markdown_code_lint(files: list[str] | None = None) -> None:
"""Run markdown code-block linting globally or for the changed markdown files only."""
command = [
"uv",
"run",
"python",
"scripts/check_md_code_blocks.py",
]
if files is None:
command.extend([
"README.md",
"./packages/**/README.md",
"./samples/**/*.md",
])
else:
if not files:
print("[yellow]No markdown files changed, skipping markdown code lint.[/yellow]")
return
command.extend(files)
command.append("--no-glob")
for excluded_path in MARKDOWN_EXCLUDES:
command.extend(["--exclude", excluded_path])
run_command(command)
def run_aggregate_pyright(project_pattern: str, extra_args: list[str]) -> None:
"""Run a single Pyright sweep across the selected project roots."""
projects = select_projects(project_pattern)
if not projects:
print("[yellow]No selected projects support the current Python version, skipping.[/yellow]")
return
project_paths = [relative_path(WORKSPACE_ROOT / project.path) for project in projects]
run_command(["uv", "run", "pyright", *extra_args, *project_paths])
def run_aggregate_mypy(project_pattern: str, extra_args: list[str]) -> None:
"""Run a single MyPy sweep across the selected project import roots."""
projects = select_projects(project_pattern)
if not projects:
print("[yellow]No selected projects support the current Python version, skipping.[/yellow]")
return
source_dirs = [relative_path(path) for path in collect_source_dirs(projects)]
if not source_dirs:
print("[yellow]No import roots found for the selected projects, skipping MyPy.[/yellow]")
return
run_command(["uv", "run", "mypy", "--config-file", "pyproject.toml", *extra_args, *source_dirs])
def run_aggregate_test(project_pattern: str, cov: bool, extra_args: list[str]) -> None:
"""Run a single pytest sweep across the selected project test directories."""
projects = select_projects(project_pattern)
if not projects:
print("[yellow]No selected projects support the current Python version, skipping.[/yellow]")
return
if project_pattern == "*":
# Preserve the legacy ``all-tests`` contract when ``test --all`` runs with
# the default selector: experimental packages stay opt-in instead of
# suddenly joining every PR unit-test sweep.
projects = [project for project in projects if project.name not in DEFAULT_AGGREGATE_TEST_EXCLUDES]
if not projects:
print("[yellow]No aggregate-test projects remain after applying default exclusions.[/yellow]")
return
test_dirs = [relative_path(path) for path in collect_test_dirs(projects)]
if not test_dirs:
print("[yellow]No test directories found for the selected projects, skipping pytest.[/yellow]")
return
command = [
"uv",
"run",
"pytest",
"--import-mode=importlib",
"-m",
"not integration",
"-rs",
"-n",
"logical",
"--dist",
"worksteal",
]
if cov:
for source_dir in collect_source_dirs(projects):
command.append(f"--cov={source_dir.name}")
command.extend(["--cov-config=pyproject.toml", "--cov-report=term-missing:skip-covered"])
command.extend(extra_args)
command.extend(test_dirs)
run_command(command)
def normalize_changed_file(file_path: str) -> str:
"""Normalize changed-file paths passed from git or pre-commit."""
normalized = file_path.replace("\\", "/")
if normalized.startswith("python/"):
return normalized[7:]
return normalized
def has_changed_sample_files(files: list[str]) -> bool:
"""Return whether any changed file lives under samples/."""
return any(normalize_changed_file(file_path).startswith("samples/") for file_path in files)
def changed_markdown_files(files: list[str]) -> list[str]:
"""Return markdown files from the provided change list."""
markdown_files = [normalize_changed_file(file_path) for file_path in files]
return sorted({file_path for file_path in markdown_files if file_path.endswith(".md")})
def run_changed_package_tasks(task_names: list[str], files: list[str]) -> None:
"""Run package-local tasks only in packages affected by the provided file list."""
command = [
"uv",
"run",
"python",
"scripts/run_tasks_in_changed_packages.py",
*task_names,
"--files",
*files,
]
run_command(command)
def run_prek_check(files: list[str]) -> None:
"""Run the lightweight pre-commit task surface."""
normalized_files = [normalize_changed_file(file_path) for file_path in files] or ["."]
run_changed_package_tasks(["fmt", "lint"], normalized_files)
run_markdown_code_lint(changed_markdown_files(normalized_files))
if has_changed_sample_files(normalized_files):
print("[cyan]Sample files changed, running sample checks.[/cyan]")
run_sample_lint([])
run_sample_pyright([])
else:
print("[yellow]No sample files changed, skipping sample checks.[/yellow]")
def git_diff_name_only(*revisions: str) -> list[str] | None:
"""Try a git diff strategy and return changed files if it succeeds."""
result = subprocess.run(
["git", "diff", "--name-only", *revisions, "--", "."],
cwd=WORKSPACE_ROOT,
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
return None
return [line for line in result.stdout.splitlines() if line]
def detect_ci_changed_files() -> list[str]:
"""Detect changed files for change-based mypy runs."""
base_ref = os.environ.get("GITHUB_BASE_REF")
if base_ref:
subprocess.run(
["git", "fetch", "origin", base_ref, "--depth=1"],
cwd=WORKSPACE_ROOT,
capture_output=True,
text=True,
check=False,
)
strategies = [
(f"origin/{base_ref}...HEAD",),
("FETCH_HEAD...HEAD",),
("HEAD^...HEAD",),
]
else:
strategies = [
("origin/main...HEAD",),
("main...HEAD",),
("HEAD~1",),
]
for strategy in strategies:
changed_files = git_diff_name_only(*strategy)
if changed_files is not None:
return changed_files or ["."]
return ["."]
def run_ci_mypy() -> None:
"""Run MyPy only where changes require it, mirroring CI behaviour."""
changed_files = detect_ci_changed_files()
print("[cyan]Changed files for CI mypy:[/cyan]")
for file_path in changed_files:
print(f" {file_path}")
run_changed_package_tasks(["mypy"], changed_files)
def ensure_no_extra_args(command_name: str, extra_args: list[str]) -> None:
"""Reject unsupported pass-through arguments for commands that do not forward them."""
if extra_args:
joined_args = " ".join(extra_args)
print(f"[red]Command '{command_name}' does not accept extra arguments: {joined_args}[/red]")
raise SystemExit(2)
def resolve_syntax_modes(*, format_selected: bool, check_selected: bool) -> tuple[bool, bool]:
"""Resolve which syntax steps to run."""
if not format_selected and not check_selected:
return True, True
return format_selected, check_selected
def run_syntax(
*,
project_pattern: str,
samples: bool,
format_selected: bool,
check_selected: bool,
extra_args: list[str],
) -> None:
"""Run formatting and/or lint checking for packages or samples.
Combined package mode deliberately dispatches ``fmt`` and ``lint`` together
so the shared task runner can start both legs in parallel.
"""
run_format, run_check = resolve_syntax_modes(
format_selected=format_selected,
check_selected=check_selected,
)
if run_format and run_check and extra_args:
joined_args = " ".join(extra_args)
print(
"[red]Extra arguments are only supported when syntax runs a single mode; "
f"use either --format or --check with: {joined_args}[/red]"
)
raise SystemExit(2)
if samples and project_pattern != "*":
print("[red]--samples cannot be combined with --package.[/red]")
raise SystemExit(2)
format_args = extra_args if run_format and not run_check else []
check_args = extra_args if run_check and not run_format else []
if samples:
if run_format:
run_sample_format(format_args)
if run_check:
run_sample_lint(check_args)
return
if run_format and run_check:
# Fan out both legs in one call so task_runner can parallelize format
# and lint work across the same selected package set.
run_fan_out(["fmt", "lint"], project_pattern, [])
return
if run_format:
run_fan_out(["fmt"], project_pattern, format_args)
if run_check:
run_fan_out(["lint"], project_pattern, check_args)
def main() -> None:
"""Dispatch the requested workspace task."""
args, extra_args = parse_args(sys.argv[1:])
if args.command == "syntax":
run_syntax(
project_pattern=args.project,
samples=args.samples,
format_selected=args.format,
check_selected=args.check,
extra_args=extra_args,
)
return
if args.command == "fmt":
run_syntax(
project_pattern=args.project,
samples=False,
format_selected=True,
check_selected=False,
extra_args=extra_args,
)
return
if args.command == "lint":
if args.samples:
run_syntax(
project_pattern=args.project,
samples=True,
format_selected=False,
check_selected=True,
extra_args=extra_args,
)
return
run_syntax(
project_pattern=args.project,
samples=False,
format_selected=False,
check_selected=True,
extra_args=extra_args,
)
return
if args.command == "pyright":
if args.samples:
if args.all or args.project != "*":
print("[red]--samples cannot be combined with --all or --package.[/red]")
raise SystemExit(2)
run_sample_pyright(extra_args)
return
if args.all:
run_aggregate_pyright(args.project, extra_args)
return
run_fan_out(["pyright"], args.project, extra_args)
return
if args.command == "mypy":
if args.all:
run_aggregate_mypy(args.project, extra_args)
return
run_fan_out(["mypy"], args.project, extra_args)
return
if args.command == "typing":
ensure_no_extra_args(args.command, extra_args)
if args.all:
# Start MyPy first so combined typing runs follow the requested
# ordering even though completion still depends on runtime duration.
run_aggregate_mypy(args.project, [])
run_aggregate_pyright(args.project, [])
return
# Preserve the same "MyPy first" ordering for the per-package fan-out
# path as well.
run_fan_out(["mypy", "pyright"], args.project, [])
return
if args.command == "test":
if args.all:
run_aggregate_test(args.project, args.cov, extra_args)
return
run_fan_out(["test"], args.project, extra_args)
return
if args.command == "build":
ensure_no_extra_args(args.command, extra_args)
run_fan_out(["build"], args.project, [])
return
if args.command == "clean-dist":
ensure_no_extra_args(args.command, extra_args)
run_fan_out(["clean-dist"], args.project, [])
return
if args.command == "check-packages":
ensure_no_extra_args(args.command, extra_args)
run_syntax(
project_pattern=args.project,
samples=False,
format_selected=False,
check_selected=False,
extra_args=[],
)
run_fan_out(["pyright"], args.project, [])
return
if args.command == "check":
ensure_no_extra_args(args.command, extra_args)
if args.samples:
if args.project != "*":
print("[red]--samples cannot be combined with --package.[/red]")
raise SystemExit(2)
run_syntax(
project_pattern="*",
samples=True,
format_selected=False,
check_selected=False,
extra_args=[],
)
run_sample_pyright([])
return
run_syntax(
project_pattern=args.project,
samples=False,
format_selected=False,
check_selected=False,
extra_args=[],
)
run_fan_out(["pyright"], args.project, [])
run_fan_out(["test"], args.project, [])
# Sample validation and markdown lint are intentionally workspace-wide;
# a package-scoped check should stay focused on the selected package set.
if args.project == "*":
run_syntax(
project_pattern="*",
samples=True,
format_selected=False,
check_selected=False,
extra_args=[],
)
run_sample_pyright([])
run_markdown_code_lint()
return
if args.command == "prek-check":
ensure_no_extra_args(args.command, extra_args)
run_prek_check(args.files)
return
if args.command == "ci-mypy":
ensure_no_extra_args(args.command, extra_args)
run_ci_mypy()
return
print(f"[red]Unsupported command: {args.command}[/red]")
raise SystemExit(2)
if __name__ == "__main__":
main()
+39 -10
View File
@@ -1,10 +1,39 @@
[tool.poe.tasks]
fmt = "ruff format"
format.ref = "fmt"
lint = "ruff check"
pyright = "pyright"
publish = "uv publish"
clean-dist = "rm -rf dist"
build-package = "uv build"
move-dist = "sh -c 'mkdir -p ../../dist && mv dist/* ../../dist/ 2>/dev/null || true'"
build = ["build-package", "move-dist"]
[tool.poe.tasks.syntax]
help = "Run Ruff formatting and Ruff checks for this package."
sequence = ["fmt", "lint"]
[tool.poe.tasks.fmt]
help = "DEPRECATED: Use `syntax --format` instead."
cmd = "ruff format"
[tool.poe.tasks.format]
help = "DEPRECATED: Use `syntax --format` instead."
ref = "fmt"
[tool.poe.tasks.lint]
help = "DEPRECATED: Use `syntax --check` instead."
cmd = "ruff check"
[tool.poe.tasks.pyright]
help = "Run Pyright for this package."
cmd = "pyright"
[tool.poe.tasks.publish]
help = "Publish this package with uv."
cmd = "uv publish"
[tool.poe.tasks.clean-dist]
help = "Remove generated dist artifacts for this package."
cmd = "rm -rf dist"
[tool.poe.tasks.build-package]
help = "Build distribution artifacts for this package."
cmd = "uv build"
[tool.poe.tasks.move-dist]
help = "Move built package artifacts into the workspace dist directory."
cmd = "sh -c 'mkdir -p ../../dist && mv dist/* ../../dist/ 2>/dev/null || true'"
[tool.poe.tasks.build]
help = "Build this package and move its artifacts into the workspace dist directory."
sequence = ["build-package", "move-dist"]