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
@@ -9,7 +9,7 @@ import os
import re
import sys
from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, MutableMapping, Sequence
from typing import Any, ClassVar, Generic, TypedDict
from typing import Any, ClassVar, Generic, TypedDict, cast
from agent_framework import (
AGENT_FRAMEWORK_USER_AGENT,
@@ -77,9 +77,9 @@ from azure.ai.agents.models import (
RunStatus,
RunStep,
RunStepDeltaChunk,
RunStepDeltaCodeInterpreterDetailItemObject,
RunStepDeltaCodeInterpreterImageOutput,
RunStepDeltaCodeInterpreterLogOutput,
RunStepDeltaToolCall,
SubmitToolApprovalAction,
SubmitToolOutputsAction,
ThreadMessageOptions,
@@ -704,7 +704,7 @@ class AzureAIAgentClient(
args["tool_approvals"] = tool_approvals
await self.agents_client.runs.submit_tool_outputs_stream(**args) # type: ignore[reportUnknownMemberType]
# Pass the handler to the stream to continue processing
stream = handler # type: ignore
stream = handler
final_thread_id = thread_run.thread_id
else:
# Handle thread creation or cancellation
@@ -881,7 +881,7 @@ class AzureAIAgentClient(
azure_search_tool_calls: list[dict[str, Any]] = []
response_stream = await stream.__aenter__() if isinstance(stream, AsyncAgentRunStream) else stream # type: ignore[no-untyped-call]
try:
async for event_type, event_data, _ in response_stream: # type: ignore
async for event_type, event_data, _ in response_stream:
match event_data:
case MessageDeltaChunk():
# only one event_type: AgentStreamEvent.THREAD_MESSAGE_DELTA
@@ -997,21 +997,16 @@ class AzureAIAgentClient(
role="assistant",
)
case RunStepDeltaChunk(): # type: ignore
if (
event_data.delta.step_details is not None
and event_data.delta.step_details.type == "tool_calls"
and event_data.delta.step_details.tool_calls is not None # type: ignore[attr-defined]
):
for tool_call in event_data.delta.step_details.tool_calls: # type: ignore[attr-defined]
if tool_call.type == "code_interpreter" and isinstance(
tool_call.code_interpreter,
RunStepDeltaCodeInterpreterDetailItemObject,
):
step_details = event_data.delta.step_details
if step_details is not None and step_details.type == "tool_calls":
tool_calls = cast(list[RunStepDeltaToolCall], step_details.tool_calls) # type: ignore
for tool_call in tool_calls:
if tool_call.type == "code_interpreter" and tool_call.code_interpreter is not None: # type: ignore[attr-defined, reportUnknownMemberType]
code_contents: list[Content] = []
if tool_call.code_interpreter.input is not None:
logger.debug(f"Code Interpreter Input: {tool_call.code_interpreter.input}")
if tool_call.code_interpreter.outputs is not None:
for output in tool_call.code_interpreter.outputs:
if tool_call.code_interpreter.input is not None: # type: ignore[attr-defined, reportUnknownMemberType]
logger.debug(f"Code Interpreter Input: {tool_call.code_interpreter.input}") # type: ignore[attr-defined, reportUnknownMemberType]
if tool_call.code_interpreter.outputs is not None: # type: ignore[attr-defined, reportUnknownMemberType]
for output in tool_call.code_interpreter.outputs: # type: ignore[attr-defined, reportUnknownMemberType]
if isinstance(output, RunStepDeltaCodeInterpreterLogOutput) and output.logs:
code_contents.append(Content.from_text(text=output.logs))
if (
@@ -1027,7 +1022,7 @@ class AzureAIAgentClient(
contents=code_contents,
conversation_id=thread_id,
message_id=response_id,
raw_representation=tool_call.code_interpreter,
raw_representation=tool_call.code_interpreter, # type: ignore[attr-defined, reportUnknownMemberType]
response_id=response_id,
)
case _: # ThreadMessage or string
@@ -1056,17 +1051,15 @@ class AzureAIAgentClient(
) -> None:
"""Capture Azure AI Search tool call data from completed steps."""
try:
if (
hasattr(step_data, "step_details")
and hasattr(step_data.step_details, "tool_calls")
and step_data.step_details.tool_calls
):
for tool_call in step_data.step_details.tool_calls:
if hasattr(tool_call, "type") and tool_call.type == "azure_ai_search":
step_details = getattr(step_data, "step_details", None)
tool_calls = getattr(step_details, "tool_calls", None) if step_details is not None else None
if isinstance(tool_calls, list):
for tool_call in cast(list[object], tool_calls):
if getattr(tool_call, "type", None) == "azure_ai_search":
# Store the complete tool call as a dictionary
tool_call_dict = {
"id": getattr(tool_call, "id", None),
"type": tool_call.type,
"type": getattr(tool_call, "type", None),
"azure_ai_search": getattr(tool_call, "azure_ai_search", None),
}
azure_search_tool_calls.append(tool_call_dict)
@@ -1219,19 +1212,18 @@ class AzureAIAgentClient(
self, options: Mapping[str, Any]
) -> AgentsToolChoiceOptionMode | AgentsNamedToolChoice | None:
"""Prepare the tool choice mode for Azure AI Agents API."""
tool_choice = options.get("tool_choice")
tool_choice = cast(str | dict[str, str] | None, options.get("tool_choice"))
if tool_choice is None:
return None
if tool_choice == "none":
return AgentsToolChoiceOptionMode.NONE
if tool_choice == "auto":
return AgentsToolChoiceOptionMode.AUTO
if isinstance(tool_choice, Mapping) and tool_choice.get("mode") == "required":
if isinstance(tool_choice, str) and tool_choice in {"none", "auto"}:
return AgentsToolChoiceOptionMode(tool_choice)
if isinstance(tool_choice, dict):
mode = tool_choice.get("mode")
req_fn = tool_choice.get("required_function_name")
if req_fn:
if mode == "required" and req_fn is not None:
return AgentsNamedToolChoice(
type=AgentsNamedToolChoiceType.FUNCTION,
function=FunctionName(name=str(req_fn)),
function=FunctionName(name=req_fn),
)
return None
@@ -1369,14 +1361,9 @@ class AzureAIAgentClient(
# SDK Tool wrappers (McpTool, FileSearchTool, BingGroundingTool, etc.)
tool_definitions.extend(tool.definitions)
# Handle tool resources (MCP resources handled separately by _prepare_mcp_resources)
if (
run_options is not None
and hasattr(tool, "resources")
and tool.resources
and "mcp" not in tool.resources
):
if "tool_resources" not in run_options:
run_options["tool_resources"] = {}
resources = getattr(tool, "resources", None)
if run_options is not None and resources and isinstance(resources, Mapping) and "mcp" not in resources:
run_options.setdefault("tool_resources", {})
run_options["tool_resources"].update(tool.resources)
else:
# Pass through ToolDefinition, dict, and other types unchanged
@@ -6,7 +6,7 @@ import json
import logging
import re
import sys
from collections.abc import Awaitable, Callable, Mapping, Sequence
from collections.abc import Awaitable, Callable, Mapping, MutableMapping, Sequence
from contextlib import suppress
from typing import Any, ClassVar, Generic, Literal, TypedDict, TypeVar, cast
@@ -304,7 +304,7 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
# Import Azure Monitor with proper error handling
try:
from azure.monitor.opentelemetry import configure_azure_monitor
from azure.monitor.opentelemetry import configure_azure_monitor # type: ignore[import]
except ImportError as exc:
raise ImportError(
"azure-monitor-opentelemetry is required for Azure Monitor integration. "
@@ -433,31 +433,36 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
"""Extract comparable tool names from runtime tool payloads."""
if not isinstance(tools, Sequence) or isinstance(tools, str | bytes):
return set()
return {self._get_tool_name(tool) for tool in tools}
tool_names: set[str] = set()
for tool_item in cast(Sequence[object], tools):
tool_names.add(self._get_tool_name(tool_item))
return tool_names
def _get_tool_name(self, tool: Any) -> str:
"""Get a stable name for a tool for runtime comparison."""
if isinstance(tool, FunctionTool):
return tool.name
if isinstance(tool, Mapping):
tool_type = tool.get("type")
tool_type = tool.get("type") # type: ignore[reportUnknownMemberType]
if tool_type == "function":
if isinstance(function_data := tool.get("function"), Mapping) and function_data.get("name"):
return str(function_data["name"])
if tool.get("name"):
return str(tool["name"])
if tool.get("name"):
return str(tool["name"])
if tool.get("server_label"):
return f"mcp:{tool['server_label']}"
function_data = tool.get("function") # type: ignore[reportUnknownMemberType]
if isinstance(function_data, Mapping) and (function_name := function_data.get("name")): # type: ignore[assignment]
return function_name # type: ignore[no-any-return]
if tool_name := tool.get("name"): # type: ignore[reportUnknownMemberType]
return tool_name # type: ignore[no-any-return]
if server_label := tool.get("server_label"): # type: ignore[reportUnknownMemberType]
return f"mcp:{server_label}"
if tool_type:
return str(tool_type)
if getattr(tool, "name", None):
return str(tool.name)
if getattr(tool, "server_label", None):
return f"mcp:{tool.server_label}"
if getattr(tool, "type", None):
return str(tool.type)
return tool_type # type: ignore[no-any-return]
raise ValueError("Dict based tool definitions must include a 'name' property for runtime comparison.")
if name_value := getattr(tool, "name", None):
return name_value # type: ignore[no-any-return]
if server_label_value := getattr(tool, "server_label", None):
return f"mcp:{server_label_value}"
if tool_type_value := getattr(tool, "type", None):
return tool_type_value # type: ignore[no-any-return]
return type(tool).__name__
def _get_structured_output_signature(self, chat_options: Mapping[str, Any] | None) -> str | None:
@@ -545,14 +550,14 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
return run_options
@override
def _check_model_presence(self, run_options: dict[str, Any]) -> None:
def _check_model_presence(self, options: dict[str, Any]) -> None:
# Skip model check for application endpoints - model is pre-configured on server
if self._is_application_endpoint:
return
if not run_options.get("model"):
if not options.get("model"):
if not self.model_id:
raise ValueError("model_deployment_name must be a non-empty string")
run_options["model"] = self.model_id
options["model"] = self.model_id
def _transform_input_for_azure_ai(self, input_items: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Transform input items to match Azure AI Projects expected schema.
@@ -575,15 +580,14 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
# Add 'annotations' only to output_text content items (assistant messages)
# User messages (input_text) do NOT support annotations in Azure AI
if "content" in new_item and isinstance(new_item["content"], list):
new_content: list[dict[str, Any] | Any] = []
for content_item in new_item["content"]:
if isinstance(content_item, dict):
new_content_item: dict[str, Any] = dict(content_item)
if (content := new_item.get("content")) and isinstance(content, list):
new_content: list[Any] = []
for content_item in content: # type: ignore[list-item]
if isinstance(content_item, MutableMapping):
# Only add annotations to output_text (assistant content)
if new_content_item.get("type") == "output_text" and "annotations" not in new_content_item:
new_content_item["annotations"] = []
new_content.append(new_content_item)
if content_item.get("type") == "output_text" and "annotations" not in content_item: # type: ignore[reportUnknownMemberType]
content_item["annotations"] = []
new_content.append(content_item)
else:
new_content.append(content_item)
new_item["content"] = new_content
@@ -721,9 +725,13 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
# Streaming "added" events send output as an empty list; skip.
continue
if output is not None:
urls = output.get("get_urls") if isinstance(output, dict) else output.get_urls
if urls and isinstance(urls, list):
get_urls.extend(urls)
urls = output.get("get_urls") if isinstance(output, Mapping) else getattr(output, "get_urls", None) # type: ignore
if isinstance(urls, list):
string_urls: list[str] = []
for url_item in urls: # type: ignore[list-item]
if isinstance(url_item, str):
string_urls.append(url_item)
get_urls.extend(string_urls)
return get_urls
def _get_search_doc_url(self, citation_title: str | None, get_urls: list[str]) -> str | None:
@@ -878,7 +886,7 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
contents=contents_list,
conversation_id=update.conversation_id,
response_id=update.response_id,
role=update.role,
role=update.role, # type: ignore[union-attr]
model_id=update.model_id,
continuation_token=update.continuation_token,
additional_properties=update.additional_properties,
@@ -186,7 +186,7 @@ class RawAzureAIInferenceEmbeddingClient(
values: Sequence[Content | str],
*,
options: AzureAIInferenceEmbeddingOptionsT | None = None,
) -> GeneratedEmbeddings[list[float]]:
) -> GeneratedEmbeddings[list[float], AzureAIInferenceEmbeddingOptionsT]:
"""Generate embeddings for text and/or image inputs.
Text inputs (``str`` or ``Content`` with ``type="text"``) are sent to the
@@ -224,7 +224,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
if isinstance(tool, MCPTool):
mcp_tools.append(tool)
elif isinstance(tool, (FunctionTool, MutableMapping)):
non_mcp_tools.append(tool)
non_mcp_tools.append(tool) # type: ignore[reportUnknownArgumentType]
# Connect MCP tools and discover their functions BEFORE creating the agent
# This is required because Azure AI Responses API doesn't accept tools at request time
@@ -79,7 +79,7 @@ class AzureAISettings(TypedDict, total=False):
model_deployment_name: str | None
def _extract_project_connection_id(additional_properties: dict[str, Any] | None) -> str | None:
def _extract_project_connection_id(additional_properties: Mapping[str, Any] | None) -> str | None:
"""Extract project_connection_id from tool additional_properties.
Checks for both direct 'project_connection_id' key (programmatic usage)
@@ -95,17 +95,18 @@ def _extract_project_connection_id(additional_properties: dict[str, Any] | None)
return None
# Check for direct project_connection_id (programmatic usage)
project_connection_id = additional_properties.get("project_connection_id")
if isinstance(project_connection_id, str):
return project_connection_id
if (proj_conn_id := additional_properties.get("project_connection_id")) and isinstance(proj_conn_id, str):
return proj_conn_id # type: ignore[no-any-return]
# Check for connection.name structure (declarative/YAML usage)
if "connection" in additional_properties:
conn = additional_properties["connection"]
if isinstance(conn, dict):
name = conn.get("name")
if isinstance(name, str):
return name
if (
(connection := additional_properties.get("connection"))
and isinstance(connection, Mapping)
and (name := connection.get("name")) # type: ignore
and isinstance(name, str)
):
return name # type: ignore[no-any-return]
return None
@@ -189,9 +190,9 @@ def to_azure_ai_agent_tools(
and tool.resources
and "mcp" not in tool.resources
):
if "tool_resources" not in run_options:
run_options["tool_resources"] = {}
run_options["tool_resources"].update(tool.resources)
run_options.setdefault("tool_resources", {})
if isinstance(tool.resources, Mapping):
run_options["tool_resources"].update(tool.resources)
elif isinstance(tool, (dict, MutableMapping)):
# Handle dict-based tools - pass through directly
tool_dict = tool if isinstance(tool, dict) else dict(tool)
@@ -422,9 +423,16 @@ def to_azure_ai_tools(
elif isinstance(tool, Tool):
# Pass through SDK Tool types directly (CodeInterpreterTool, FileSearchTool, etc.)
azure_tools.append(tool)
elif isinstance(tool, MutableMapping):
# Convert mutable mappings into plain dicts for stable typing.
tool_dict: dict[str, Any] = dict(tool)
if tool_dict.get("type") == "mcp":
azure_tools.append(_prepare_mcp_tool_dict_for_azure_ai(tool_dict))
else:
azure_tools.append(tool_dict)
else:
# Pass through dict-based tools directly
azure_tools.append(dict(tool) if isinstance(tool, MutableMapping) else tool) # type: ignore[arg-type]
# Pass through any other supported tool objects unchanged.
azure_tools.append(tool)
return azure_tools
@@ -446,7 +454,16 @@ def _prepare_mcp_tool_dict_for_azure_ai(tool_dict: dict[str, Any]) -> MCPTool:
mcp["server_description"] = description
# Check for project_connection_id
if project_connection_id := tool_dict.get("project_connection_id"):
project_connection_id = tool_dict.get("project_connection_id")
if not isinstance(project_connection_id, str):
additional_properties = tool_dict.get("additional_properties")
project_connection_id = (
_extract_project_connection_id(additional_properties) # pyright: ignore[reportUnknownArgumentType]
if isinstance(additional_properties, Mapping)
else None
)
if project_connection_id:
mcp["project_connection_id"] = project_connection_id
elif headers := tool_dict.get("headers"):
mcp["headers"] = headers
+2 -1
View File
@@ -61,6 +61,7 @@ omit = [
[tool.pyright]
extends = "../../pyproject.toml"
include = ["agent_framework_azure_ai"]
[tool.mypy]
plugins = ['pydantic.mypy']
@@ -86,7 +87,7 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_ai"
test = "pytest --cov=agent_framework_azure_ai --cov-report=term-missing:skip-covered tests"
test = "pytest -m \"not integration\" --cov=agent_framework_azure_ai --cov-report=term-missing:skip-covered tests"
[tool.poe.tasks.integration-tests]
cmd = """