Python: replace pre-commit with prek, add PEP 723 script deps, clean up dev dependencies (#3748)

* python: replace pre-commit with prek, add PEP 723 script deps, clean up dev dependencies

- Replace pre-commit with prek (Rust-native, faster pre-commit alternative)
- Move supported hooks to repo: builtin for zero-clone speed
- Add new builtin hooks: trailing-whitespace, check-merge-conflict, detect-private-key, check-added-large-files
- Update all hook versions to latest (pre-commit-hooks v6, pyupgrade v3.21.2, bandit 1.9.3, uv-pre-commit 0.10.0)
- Add PEP 723 inline script metadata to 34 samples with external deps
- Remove autogen-agentchat/autogen-ext from dev deps (now declared per-sample)
- Remove unused dev deps: pytest-env, tomli-w
- Add agent-framework-core>=1.0.0b260130 lower bound to all 21 packages
- Update CI workflow to use j178/prek-action
- Update docs: DEV_SETUP.md, AGENTS.md, CODING_STANDARD.md, SAMPLE_GUIDELINES.md

* updated lock

* python: fix prek config paths for local execution and CI workflow

Remove global 'files: ^python/' filter and strip python/ prefix from all path patterns in .pre-commit-config.yaml so prek finds files when run from the python/ directory. Update CI workflow to use --cd python instead of --config path. Include trailing whitespace fixes and dev dependency cleanup.

* python: move helper scripts to scripts/ folder and exclude from checks

* python: exclude AGENTS.md from prek markdown code lint

* python: exclude AGENTS.md and azure_ai_search sample from markdown lint

* fix m365 sample

* python: ignore CPY rule for samples with PEP 723 headers

* fix in dev_setup

* python: replace aiofiles with regular open in samples

* python: suppress reportUnusedImport in markdown code block checker

* python: use samples pyright config for markdown code block checker

Write a temp pyrightconfig.json matching pyrightconfig.samples.json rules (typeCheckingMode=off, only reportMissingImports and reportAttributeAccessIssue). Filter output to only fail on these rules since syntax-level errors (top-level await, undefined vars) are expected in README documentation snippets.

* python: use markdown-code-lint with fixed globs instead of prek file list

The prek-markdown-code-lint task received all changed files including non-README markdown and files with pre-existing broken imports. Replace with the standard markdown-code-lint task which uses the correct glob patterns (README.md, packages/**/README.md, samples/**/*.md).

* python: exclude READMEs with pre-existing broken imports from markdown lint

* python: fix broken README code snippets instead of excluding them

- ag-ui: replace TextContent (removed) with content.type == 'text'
- durabletask: fix import path to durabletask.worker.TaskHubGrpcWorker
- orchestrations: use constructor params instead of .participants() method
- observability: mark deprecated code blocks as plain text, filter
  reportMissingImports to agent_framework modules only
- remove README excludes from markdown-code-lint task

* add revision to gaia download

* feat(python): parallelize checks across packages

Run (package × task) cross-product in parallel using ThreadPoolExecutor
and subprocesses. Key changes:

- Add scripts/task_runner.py with shared parallel execution engine
- Update run_tasks_in_packages_if_exists.py to accept multiple tasks
- Update run_tasks_in_changed_packages.py with --files flag and parallel support
- Add check-packages poe task (fmt+lint+pyright+mypy in parallel)
- Add prek-markdown-code-lint and prek-samples-check with change detection
- Split CI code quality workflow into parallel prek and mypy jobs
- Update DEV_SETUP.md to document new parallel behavior

Core package changes still trigger checks on all packages.

* feat(ci): split code quality into 4 parallel jobs

Split the single prek job into parallel jobs:
- pre-commit-hooks: lightweight hooks (SKIP=poe-check)
- package-checks: fmt/lint/pyright/mypy via check-packages
- samples-markdown: samples-lint, samples-syntax, markdown-code-lint
- mypy: change-detected mypy checks

All 4 jobs run concurrently (×2 Python versions = 8 runners).

* feat(ci): use only Python 3.10 for code quality checks

* refactor(python): add future annotations and remove quoted types

Add `from __future__ import annotations` to 93 package files that
used quoted string annotations, then run pyupgrade --py310-plus to
remove the now-unnecessary quotes.

Fixes https://github.com/microsoft/agent-framework/issues/3578
This commit is contained in:
Eduard van Valkenburg
2026-02-09 18:51:01 +01:00
committed by GitHub
Unverified
parent ad0dac3c86
commit 977c3adfb2
177 changed files with 1373 additions and 1010 deletions
@@ -1,5 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import sys
from collections.abc import Callable, Mapping
from pathlib import Path
@@ -1,4 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import os
from collections.abc import MutableMapping
from contextvars import ContextVar
@@ -101,7 +103,7 @@ class Property(SerializationMixin):
@classmethod
def from_dict(
cls, value: MutableMapping[str, Any], /, *, dependencies: MutableMapping[str, Any] | None = None
) -> "Property":
) -> Property:
"""Create a Property instance from a dictionary, dispatching to the appropriate subclass."""
# Only dispatch if we're being called on the base Property class
if cls is not Property:
@@ -211,7 +213,7 @@ class PropertySchema(SerializationMixin):
@classmethod
def from_dict(
cls, value: MutableMapping[str, Any], /, *, dependencies: MutableMapping[str, Any] | None = None
) -> "PropertySchema":
) -> PropertySchema:
"""Create a PropertySchema instance from a dictionary, filtering out 'kind' field."""
# Filter out 'kind', 'type', 'name', and 'description' fields that may appear in YAML
# but aren't PropertySchema params
@@ -491,7 +493,7 @@ class AgentDefinition(SerializationMixin):
@classmethod
def from_dict(
cls, value: MutableMapping[str, Any], /, *, dependencies: MutableMapping[str, Any] | None = None
) -> "AgentDefinition":
) -> AgentDefinition:
"""Create an AgentDefinition instance from a dictionary, dispatching to the appropriate subclass."""
# Only dispatch if we're being called on the base AgentDefinition class
if cls is not AgentDefinition:
@@ -537,7 +539,7 @@ class Tool(SerializationMixin):
@classmethod
def from_dict(
cls: type[TTool], value: MutableMapping[str, Any], /, *, dependencies: MutableMapping[str, Any] | None = None
) -> "TTool":
) -> TTool:
"""Create a Tool instance from a dictionary, dispatching to the appropriate subclass."""
# Only dispatch if we're being called on the base Tool class
if cls is not Tool:
@@ -867,7 +869,7 @@ class Resource(SerializationMixin):
@classmethod
def from_dict(
cls, value: MutableMapping[str, Any], /, *, dependencies: MutableMapping[str, Any] | None = None
) -> "Resource":
) -> Resource:
"""Create a Resource instance from a dictionary, dispatching to the appropriate subclass."""
# Only dispatch if we're being called on the base Resource class
if cls is not Resource:
@@ -7,6 +7,8 @@ This module implements handlers for:
- InvokePromptAgent: Invoke a local prompt-based agent
"""
from __future__ import annotations
import json
from collections.abc import AsyncGenerator
from typing import Any, cast
@@ -185,7 +187,7 @@ def _build_messages_from_state(ctx: ActionContext) -> list[ChatMessage]:
@action_handler("InvokeAzureAgent")
async def handle_invoke_azure_agent(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]:
async def handle_invoke_azure_agent(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]:
"""Invoke a hosted Azure AI agent.
Supports both Python-style and .NET-style YAML schemas:
@@ -523,7 +525,7 @@ def _normalize_variable_path(variable: str) -> str:
@action_handler("InvokePromptAgent")
async def handle_invoke_prompt_agent(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]:
async def handle_invoke_prompt_agent(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]:
"""Invoke a local prompt-based agent (similar to InvokeAzureAgent but for local agents).
Action schema:
@@ -16,6 +16,8 @@ network calls. The `return; yield` pattern makes a function an async generator w
actually yielding any events.
"""
from __future__ import annotations
from collections.abc import AsyncGenerator
from typing import TYPE_CHECKING, Any, cast
@@ -37,7 +39,7 @@ logger = get_logger("agent_framework.declarative.workflows.actions")
@action_handler("SetValue")
async def handle_set_value(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]: # noqa: RUF029
async def handle_set_value(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]: # noqa: RUF029
"""Set a value in the workflow state.
Action schema:
@@ -63,7 +65,7 @@ async def handle_set_value(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent,
@action_handler("SetVariable")
async def handle_set_variable(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]: # noqa: RUF029
async def handle_set_variable(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]: # noqa: RUF029
"""Set a variable in the workflow state (.NET workflow format).
This is an alias for SetValue with 'variable' instead of 'path'.
@@ -113,7 +115,7 @@ def _normalize_variable_path(variable: str) -> str:
@action_handler("AppendValue")
async def handle_append_value(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]: # noqa: RUF029
async def handle_append_value(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]: # noqa: RUF029
"""Append a value to a list in the workflow state.
Action schema:
@@ -139,7 +141,7 @@ async def handle_append_value(ctx: ActionContext) -> AsyncGenerator[WorkflowEven
@action_handler("SendActivity")
async def handle_send_activity(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]: # noqa: RUF029
async def handle_send_activity(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]: # noqa: RUF029
"""Send text or attachments to the user.
Action schema (object form):
@@ -189,7 +191,7 @@ async def handle_send_activity(ctx: ActionContext) -> AsyncGenerator[WorkflowEve
@action_handler("EmitEvent")
async def handle_emit_event(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]: # noqa: RUF029
async def handle_emit_event(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]: # noqa: RUF029
"""Emit a custom workflow event.
Action schema:
@@ -213,7 +215,7 @@ async def handle_emit_event(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent,
yield CustomEvent(name=name, data=evaluated_data)
def _evaluate_dict_values(d: dict[str, Any], state: "WorkflowState") -> dict[str, Any]:
def _evaluate_dict_values(d: dict[str, Any], state: WorkflowState) -> dict[str, Any]:
"""Recursively evaluate PowerFx expressions in a dictionary.
Args:
@@ -245,7 +247,7 @@ def _evaluate_dict_values(d: dict[str, Any], state: "WorkflowState") -> dict[str
@action_handler("SetTextVariable")
async def handle_set_text_variable(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]: # noqa: RUF029
async def handle_set_text_variable(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]: # noqa: RUF029
"""Set a text variable with string interpolation support.
This is similar to SetVariable but supports multi-line text with
@@ -281,7 +283,7 @@ async def handle_set_text_variable(ctx: ActionContext) -> AsyncGenerator[Workflo
@action_handler("SetMultipleVariables")
async def handle_set_multiple_variables(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]: # noqa: RUF029
async def handle_set_multiple_variables(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]: # noqa: RUF029
"""Set multiple variables at once.
Action schema:
@@ -313,7 +315,7 @@ async def handle_set_multiple_variables(ctx: ActionContext) -> AsyncGenerator[Wo
@action_handler("ResetVariable")
async def handle_reset_variable(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]: # noqa: RUF029
async def handle_reset_variable(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]: # noqa: RUF029
"""Reset a variable to its default/blank state.
Action schema:
@@ -336,7 +338,7 @@ async def handle_reset_variable(ctx: ActionContext) -> AsyncGenerator[WorkflowEv
@action_handler("ClearAllVariables")
async def handle_clear_all_variables(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]: # noqa: RUF029
async def handle_clear_all_variables(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]: # noqa: RUF029
"""Clear all turn-scoped variables.
Action schema:
@@ -350,7 +352,7 @@ async def handle_clear_all_variables(ctx: ActionContext) -> AsyncGenerator[Workf
@action_handler("CreateConversation")
async def handle_create_conversation(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]: # noqa: RUF029
async def handle_create_conversation(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]: # noqa: RUF029
"""Create a new conversation context.
Action schema (.NET style):
@@ -399,7 +401,7 @@ async def handle_create_conversation(ctx: ActionContext) -> AsyncGenerator[Workf
@action_handler("AddConversationMessage")
async def handle_add_conversation_message(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]: # noqa: RUF029
async def handle_add_conversation_message(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]: # noqa: RUF029
"""Add a message to a conversation.
Action schema:
@@ -451,7 +453,7 @@ async def handle_add_conversation_message(ctx: ActionContext) -> AsyncGenerator[
@action_handler("CopyConversationMessages")
async def handle_copy_conversation_messages(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]: # noqa: RUF029
async def handle_copy_conversation_messages(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]: # noqa: RUF029
"""Copy messages from one conversation to another.
Action schema:
@@ -506,7 +508,7 @@ async def handle_copy_conversation_messages(ctx: ActionContext) -> AsyncGenerato
@action_handler("RetrieveConversationMessages")
async def handle_retrieve_conversation_messages(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]: # noqa: RUF029
async def handle_retrieve_conversation_messages(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]: # noqa: RUF029
"""Retrieve messages from a conversation and store in a variable.
Action schema:
@@ -547,7 +549,7 @@ async def handle_retrieve_conversation_messages(ctx: ActionContext) -> AsyncGene
yield # Make it a generator
def _interpolate_string(text: str, state: "WorkflowState") -> str:
def _interpolate_string(text: str, state: WorkflowState) -> str:
"""Interpolate {Variable.Path} references in a string.
Args:
@@ -7,6 +7,8 @@ This module implements handlers for:
- TryCatch: Try-catch-finally error handling
"""
from __future__ import annotations
from collections.abc import AsyncGenerator
from dataclasses import dataclass
@@ -44,7 +46,7 @@ class ErrorEvent(WorkflowEvent):
@action_handler("ThrowException")
async def handle_throw_exception(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]: # noqa: RUF029
async def handle_throw_exception(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]: # noqa: RUF029
"""Raise an exception that can be caught by TryCatch.
Action schema:
@@ -67,7 +69,7 @@ async def handle_throw_exception(ctx: ActionContext) -> AsyncGenerator[WorkflowE
@action_handler("TryCatch")
async def handle_try_catch(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]:
async def handle_try_catch(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]:
"""Try-catch-finally error handling.
Action schema:
@@ -23,6 +23,8 @@ the full RecalcEngine API. We work around this by:
See: dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/
"""
from __future__ import annotations
import logging
import sys
from collections.abc import Mapping
@@ -148,7 +150,7 @@ class DeclarativeWorkflowState:
"""
self._state = state
def initialize(self, inputs: "Mapping[str, Any] | None" = None) -> None:
def initialize(self, inputs: Mapping[str, Any] | None = None) -> None:
"""Initialize the declarative state with inputs.
Args:
@@ -814,7 +816,7 @@ class DeclarativeActionExecutor(Executor):
async def _ensure_state_initialized(
self,
ctx: "WorkflowContext[Any, Any]",
ctx: WorkflowContext[Any, Any],
trigger: Any,
) -> DeclarativeWorkflowState:
"""Ensure declarative state is initialized.
@@ -11,6 +11,8 @@ action definitions and creates a proper workflow graph with:
- Loop edges for foreach
"""
from __future__ import annotations
from typing import Any
from agent_framework._workflows import (
@@ -10,6 +10,8 @@ Each YAML action becomes a real Executor node in the workflow graph,
enabling checkpointing, visualization, and pause/resume capabilities.
"""
from __future__ import annotations
from collections.abc import Mapping
from pathlib import Path
from typing import Any, cast
@@ -506,7 +508,7 @@ class WorkflowFactory:
f"Invalid agent definition. Expected 'file', 'kind', or 'connection': {agent_def}"
)
def register_agent(self, name: str, agent: SupportsAgentRun | AgentExecutor) -> "WorkflowFactory":
def register_agent(self, name: str, agent: SupportsAgentRun | AgentExecutor) -> WorkflowFactory:
"""Register an agent instance with the factory for use in workflows.
Registered agents are available to InvokeAzureAgent actions by name.
@@ -552,7 +554,7 @@ class WorkflowFactory:
self._agents[name] = agent
return self
def register_binding(self, name: str, func: Any) -> "WorkflowFactory":
def register_binding(self, name: str, func: Any) -> WorkflowFactory:
"""Register a function binding with the factory for use in workflow actions.
Bindings allow workflow actions to invoke Python functions by name.
@@ -7,6 +7,8 @@ workflow actions defined in YAML. Each action type (InvokeAzureAgent, Foreach, e
has a corresponding handler registered via the @action_handler decorator.
"""
from __future__ import annotations
from collections.abc import AsyncGenerator, Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
@@ -27,13 +29,13 @@ class ActionContext:
for executing nested actions (for control flow constructs like Foreach).
"""
state: "WorkflowState"
state: WorkflowState
"""The current workflow state with variables and agent results."""
action: dict[str, Any]
"""The action definition from the YAML."""
execute_actions: "ExecuteActionsFn"
execute_actions: ExecuteActionsFn
"""Function to execute a list of nested actions (for Foreach, If, etc.)."""
agents: dict[str, Any]
@@ -150,7 +152,7 @@ class ActionHandler(Protocol):
def __call__(
self,
ctx: ActionContext,
) -> AsyncGenerator[WorkflowEvent, None]:
) -> AsyncGenerator[WorkflowEvent]:
"""Execute the action and yield events.
Args:
@@ -8,6 +8,8 @@ This module implements handlers for human input patterns:
- ExternalLoop processing: Loop while waiting for external input
"""
from __future__ import annotations
from collections.abc import AsyncGenerator
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, cast
@@ -75,7 +77,7 @@ class ExternalLoopEvent(WorkflowEvent):
@action_handler("Question")
async def handle_question(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]: # noqa: RUF029
async def handle_question(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]: # noqa: RUF029
"""Handle Question action - request human input with optional validation.
Action schema:
@@ -140,7 +142,7 @@ async def handle_question(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, N
@action_handler("RequestExternalInput")
async def handle_request_external_input(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]: # noqa: RUF029
async def handle_request_external_input(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]: # noqa: RUF029
"""Handle RequestExternalInput action - request input from external system.
Action schema:
@@ -197,7 +199,7 @@ async def handle_request_external_input(ctx: ActionContext) -> AsyncGenerator[Wo
@action_handler("WaitForInput")
async def handle_wait_for_input(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent, None]: # noqa: RUF029
async def handle_wait_for_input(ctx: ActionContext) -> AsyncGenerator[WorkflowEvent]: # noqa: RUF029
"""Handle WaitForInput action - pause and wait for external input.
Action schema:
@@ -231,7 +233,7 @@ async def handle_wait_for_input(ctx: ActionContext) -> AsyncGenerator[WorkflowEv
def process_external_loop(
input_config: dict[str, Any],
state: "WorkflowState",
state: WorkflowState,
) -> tuple[bool, str | None]:
"""Process the externalLoop.when pattern from action input.
@@ -10,6 +10,8 @@ These functions can be used as fallbacks when PowerFx is not available,
or registered with the PowerFx engine when it is available.
"""
from __future__ import annotations
from typing import Any, cast
@@ -9,6 +9,8 @@ This module provides state management for declarative workflows, handling:
- Agent results and context
"""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any, cast
@@ -624,7 +626,7 @@ class WorkflowState:
"""Reset the agent result for a new agent invocation."""
self._agent.clear()
def clone(self) -> "WorkflowState":
def clone(self) -> WorkflowState:
"""Create a shallow copy of the state.
Returns: