Python: Fix Python pyright package scoping and typing remediation (#4426)

* Fix Python pyright package scoping and typing remediation

Implements issue #4407 by removing the root pyright include, adding package-level pyright includes, and resolving pyright/mypy typing issues across Python packages. Also cleans unnecessary casts and applies line-level, rule-specific ignores where external libraries are too dynamic.

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

* Reduce pyright cost in handoff cloning

Simplify cloned_options construction in HandoffAgentExecutor to avoid expensive TypedDict narrowing/inference in _handoff.py, which was causing pyright to spend a long time in orchestrations.

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

* fix types

* Fix lint and type-check regressions

Resolve current Python package check failures across lint, pyright, and mypy after recent code changes, including purview/declarative pyright issues and multiple ruff simplification findings.

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

* fixed hooks

* Stabilize package tests and test tasks

Resolve cross-package non-integration test failures, simplify streaming type flow, harden locale/culture handling, and standardize package test poe tasks to exclude integration tests where applicable.

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

* lots of small fixes

* Fix current Python test regressions

Address current failing unit tests in azure-ai, bedrock, and azure-cosmos while keeping Bedrock parsing logic inline (no new static helper methods).

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

* small fixes

* small fixes

* removed pydantic from json

* final updates

* fix core

* fix tests

* fix obser

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-03-05 16:32:24 +01:00
committed by GitHub
Unverified
parent 4a043c6c66
commit 55ddd841b7
122 changed files with 2328 additions and 2407 deletions
@@ -274,10 +274,14 @@ class AgentFunctionApp(DFAppBase):
"""
from agent_framework._workflows._state import State
data = json.loads(inputData)
message_data = data["message"]
data_obj = json.loads(inputData)
if not isinstance(data_obj, dict):
raise ValueError("Activity inputData must decode to a JSON object")
data = cast(dict[str, Any], data_obj)
message_data = data.get("message")
shared_state_snapshot = data.get("shared_state_snapshot", {})
source_executor_ids = data.get("source_executor_ids", [SOURCE_ORCHESTRATOR])
source_executor_ids = cast(list[str], data.get("source_executor_ids", [SOURCE_ORCHESTRATOR]))
if not self.workflow:
raise RuntimeError("Workflow not initialized in AgentFunctionApp")
@@ -299,15 +303,20 @@ class AgentFunctionApp(DFAppBase):
shared_state = State()
# Deserialize shared state values to reconstruct dataclasses/Pydantic models
deserialized_state = {k: deserialize_value(v) for k, v in (shared_state_snapshot or {}).items()}
original_snapshot = dict(deserialized_state)
deserialized_state: dict[str, Any] = {
str(k): deserialize_value(v) for k, v in shared_state_snapshot.items()
}
original_snapshot: dict[str, Any] = dict(deserialized_state)
shared_state.import_state(deserialized_state)
if is_hitl_response:
# Handle HITL response by calling the executor's @response_handler
if not isinstance(message_data, dict):
raise ValueError("HITL message payload must be a JSON object")
await execute_hitl_response_handler(
executor=executor,
hitl_message=message_data,
hitl_message=cast(dict[str, Any], message_data),
shared_state=shared_state,
runner_context=runner_context,
)
@@ -323,11 +332,11 @@ class AgentFunctionApp(DFAppBase):
# Commit pending state changes and export
shared_state.commit()
current_state = shared_state.export_state()
original_keys = set(original_snapshot.keys())
current_keys = set(current_state.keys())
original_keys: set[str] = set(original_snapshot.keys())
current_keys: set[str] = set(current_state.keys())
# Deleted = was in original, not in current
deletes = original_keys - current_keys
deletes: set[str] = original_keys - current_keys
# Updates = keys in current that are new or have different values
updates = {
@@ -348,7 +357,7 @@ class AgentFunctionApp(DFAppBase):
pending_request_info_events = await runner_context.get_pending_request_info_events()
# Serialize pending request info events for orchestrator
serialized_pending_requests = []
serialized_pending_requests: list[dict[str, Any]] = []
for _request_id, event in pending_request_info_events.items():
serialized_pending_requests.append({
"request_id": event.request_id,
@@ -361,7 +370,7 @@ class AgentFunctionApp(DFAppBase):
})
# Serialize messages for JSON compatibility
serialized_sent_messages = []
serialized_sent_messages: list[dict[str, Any]] = []
for _source_id, msg_list in sent_messages.items():
for msg in msg_list:
serialized_sent_messages.append({
@@ -441,6 +450,9 @@ class AgentFunctionApp(DFAppBase):
) -> func.HttpResponse:
"""HTTP endpoint to get workflow status."""
instance_id = req.route_params.get("instanceId")
if not instance_id:
return self._build_error_response("Instance ID is required", status_code=400)
status = await client.get_status(instance_id)
if not status:
@@ -457,17 +469,23 @@ class AgentFunctionApp(DFAppBase):
}
# Add pending HITL requests info if available
custom_status = status.custom_status or {}
if isinstance(custom_status, dict) and custom_status.get("pending_requests"):
if (
(custom_status := status.custom_status)
and isinstance(custom_status, dict)
and (pending_requests_dict := custom_status.get("pending_requests")) # type: ignore
and isinstance(pending_requests_dict, dict)
):
base_url = self._build_base_url(req.url)
pending_requests = []
for req_id, req_data in custom_status["pending_requests"].items():
pending_requests: list[dict[str, Any]] = []
for req_id, req_data in pending_requests_dict.items(): # type: ignore
if not isinstance(req_data, dict):
continue
pending_requests.append({
"requestId": req_id,
"sourceExecutor": req_data.get("source_executor_id"),
"requestData": req_data.get("data"),
"requestType": req_data.get("request_type"),
"responseType": req_data.get("response_type"),
"sourceExecutor": req_data.get("source_executor_id"), # type: ignore[reportUnknownMemberType]
"requestData": req_data.get("data"), # type: ignore[reportUnknownMemberType]
"requestType": req_data.get("request_type"), # type: ignore[reportUnknownMemberType]
"responseType": req_data.get("response_type"), # type: ignore[reportUnknownMemberType]
"respondUrl": f"{base_url}/api/workflow/respond/{instance_id}/{req_id}",
})
response["pendingHumanInputRequests"] = pending_requests
@@ -515,6 +533,11 @@ class AgentFunctionApp(DFAppBase):
mimetype="application/json",
)
# Ensure route handlers are registered (prevents unused function warnings)
_ = start_workflow_orchestration
_ = get_workflow_status
_ = send_hitl_response
def _build_status_url(self, request_url: str, instance_id: str) -> str:
"""Build the status URL for a workflow instance."""
base_url = self._build_base_url(request_url)
@@ -13,22 +13,24 @@ This module adds:
- serialize_value / deserialize_value: convenience aliases for encode/decode
- reconstruct_to_type: for HITL responses where external data (without type markers)
needs to be reconstructed to a known type
- _resolve_type: resolves 'module:class' type keys to Python types
- resolve_type: resolves 'module:class' type keys to Python types
"""
from __future__ import annotations
import importlib
import logging
from contextlib import suppress
from dataclasses import is_dataclass
from typing import Any
from agent_framework._workflows._checkpoint_encoding import decode_checkpoint_value, encode_checkpoint_value
from pydantic import BaseModel
logger = logging.getLogger(__name__)
def _resolve_type(type_key: str) -> type | None:
def resolve_type(type_key: str) -> type | None:
"""Resolve a 'module:class' type key to its Python type.
Args:
@@ -108,11 +110,9 @@ def reconstruct_to_type(value: Any, target_type: type) -> Any:
if value is None:
return None
try:
with suppress(TypeError):
if isinstance(value, target_type):
return value
except TypeError:
pass
if not isinstance(value, dict):
return value
@@ -123,17 +123,18 @@ def reconstruct_to_type(value: Any, target_type: type) -> Any:
return decoded
# Try Pydantic model validation (for unmarked dicts, e.g., external HITL data)
if hasattr(target_type, "model_validate"):
if issubclass(target_type, BaseModel):
try:
return target_type.model_validate(value)
except Exception:
logger.debug("Could not validate Pydantic model %s", target_type)
return value # type: ignore[return-value]
# Try dataclass construction (for unmarked dicts, e.g., external HITL data)
if is_dataclass(target_type) and isinstance(target_type, type):
if is_dataclass(target_type) and isinstance(target_type, type): # type: ignore
try:
return target_type(**value)
except Exception:
logger.debug("Could not construct dataclass %s", target_type)
return value
return value # type: ignore[return-value]
@@ -44,12 +44,13 @@ from agent_framework._workflows._edge import (
SingleEdgeGroup,
SwitchCaseEdgeGroup,
)
from agent_framework._workflows._state import State
from agent_framework_durabletask import AgentSessionId, DurableAgentSession, DurableAIAgent
from azure.durable_functions import DurableOrchestrationContext
from ._context import CapturingRunnerContext
from ._orchestration import AzureFunctionsAgentExecutor
from ._serialization import _resolve_type, deserialize_value, reconstruct_to_type, serialize_value
from ._serialization import deserialize_value, reconstruct_to_type, resolve_type, serialize_value
logger = logging.getLogger(__name__)
@@ -148,7 +149,7 @@ def _evaluate_edge_condition_sync(edge: Edge, message: Any) -> bool:
True if the edge should be traversed, False otherwise
"""
# Access the internal condition directly since should_route is async
condition = edge._condition
condition = edge._condition # pyright: ignore[reportPrivateUsage]
if condition is None:
return True
result = condition(message)
@@ -322,7 +323,8 @@ def _prepare_activity_task(
activity_input_json = json.dumps(activity_input)
# Use the prefixed activity name that matches the registered function
activity_name = f"dafx-{executor_id}"
return context.call_activity(activity_name, activity_input_json)
orchestration_context: Any = context
return orchestration_context.call_activity(activity_name, activity_input_json)
# ============================================================================
@@ -346,13 +348,16 @@ def _process_agent_response(
ExecutorResult containing the processed response
"""
response_text = agent_response.text if agent_response else None
structured_response = None
structured_response: dict[str, Any] | None = None
if agent_response and agent_response.value is not None:
if hasattr(agent_response.value, "model_dump"):
structured_response = agent_response.value.model_dump()
model_dump = getattr(agent_response.value, "model_dump", None)
if callable(model_dump):
dumped = model_dump()
if isinstance(dumped, dict):
structured_response = dumped # type: ignore[assignment]
elif isinstance(agent_response.value, dict):
structured_response = agent_response.value
structured_response = agent_response.value # type: ignore[assignment]
output_message = build_agent_executor_response(
executor_id=executor_id,
@@ -726,7 +731,7 @@ def run_workflow_orchestrator(
if winner == approval_task:
# Cancel the timeout
timeout_task.cancel()
timeout_task.cancel() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
# Get the response
raw_response = approval_task.result
@@ -756,7 +761,7 @@ def run_workflow_orchestrator(
)
else:
# Timeout occurred — cancel the dangling external event listener
approval_task.cancel()
approval_task.cancel() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
logger.warning("HITL request %s timed out after %s hours", request_id, hitl_timeout_hours)
raise TimeoutError(
f"Human-in-the-loop request '{request_id}' timed out after {hitl_timeout_hours} hours."
@@ -864,7 +869,8 @@ def _extract_message_content(message: Any) -> str:
# Extract text from the last message in the request
message_content = message.messages[-1].text or ""
elif isinstance(message, dict):
logger.warning("Unexpected dict message in _extract_message_content. Keys: %s", list(message.keys()))
key_names = list(message.keys()) # type: ignore[union-attr]
logger.warning("Unexpected dict message in _extract_message_content. Keys: %s", key_names) # type: ignore
elif isinstance(message, str):
message_content = message
@@ -879,7 +885,7 @@ def _extract_message_content(message: Any) -> str:
async def execute_hitl_response_handler(
executor: Any,
hitl_message: dict[str, Any],
shared_state: Any,
shared_state: State,
runner_context: CapturingRunnerContext,
) -> None:
"""Execute a HITL response handler on an executor.
@@ -910,7 +916,7 @@ async def execute_hitl_response_handler(
response = _deserialize_hitl_response(response_data, response_type_str)
# Find the matching response handler
handler = executor._find_response_handler(original_request, response)
handler = executor._find_response_handler(original_request, response) # pyright: ignore[reportPrivateUsage]
if handler is None:
logger.warning(
@@ -965,7 +971,7 @@ def _deserialize_hitl_response(response_data: Any, response_type_str: str | None
# Try to deserialize using the type hint
if response_type_str:
response_type = _resolve_type(response_type_str)
response_type = resolve_type(response_type_str)
if response_type:
logger.debug("Found response type %s, attempting reconstruction", response_type)
result = reconstruct_to_type(response_data, response_type)
@@ -67,6 +67,7 @@ omit = [
[tool.pyright]
extends = "../../pyproject.toml"
include = ["agent_framework_azurefunctions"]
[tool.mypy]
plugins = ['pydantic.mypy']
@@ -92,7 +93,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azurefunctions"
test = "pytest --cov=agent_framework_azurefunctions --cov-report=term-missing:skip-covered tests"
test = "pytest -m \"not integration\" --cov=agent_framework_azurefunctions --cov-report=term-missing:skip-covered tests"
[build-system]
requires = ["flit-core >= 3.11,<4.0"]