mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
4a043c6c66
commit
55ddd841b7
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user