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
@@ -2,8 +2,8 @@
import importlib.metadata
from ._chat_client import BedrockChatClient, BedrockChatOptions, BedrockGuardrailConfig, BedrockSettings
from ._embedding_client import BedrockEmbeddingClient, BedrockEmbeddingOptions, BedrockEmbeddingSettings
from ._chat_client import BedrockChatClient, BedrockChatOptions, BedrockGuardrailConfig, BedrockSettings # type: ignore
from ._embedding_client import BedrockEmbeddingClient, BedrockEmbeddingOptions, BedrockEmbeddingSettings # type: ignore
try:
__version__ = importlib.metadata.version(__name__)
@@ -1,5 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
# type: ignore
# Because the Bedrock client does not have typing, we are ignoring type issues in this module.
from __future__ import annotations
import asyncio
@@ -288,14 +289,16 @@ class BedrockChatClient(
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
if not settings.get("region"):
settings["region"] = DEFAULT_REGION
region = settings.get("region") or DEFAULT_REGION
chat_model_id = settings.get("chat_model_id")
if client is None:
if client:
self._bedrock_client = client
else:
session = boto3_session or self._create_session(settings)
client = session.client(
self._bedrock_client = session.client(
"bedrock-runtime",
region_name=settings["region"],
region_name=region,
config=BotoConfig(user_agent_extra=AGENT_FRAMEWORK_USER_AGENT),
)
@@ -304,20 +307,28 @@ class BedrockChatClient(
function_invocation_configuration=function_invocation_configuration,
**kwargs,
)
self._bedrock_client = client
self.model_id = settings["chat_model_id"]
self.region = settings["region"]
self.model_id = chat_model_id
self.region = region
@staticmethod
def _create_session(settings: BedrockSettings) -> Boto3Session:
session_kwargs: dict[str, Any] = {"region_name": settings.get("region") or DEFAULT_REGION}
if settings.get("access_key") and settings.get("secret_key"):
session_kwargs["aws_access_key_id"] = settings["access_key"].get_secret_value() # type: ignore[union-attr]
session_kwargs["aws_secret_access_key"] = settings["secret_key"].get_secret_value() # type: ignore[union-attr]
if settings.get("session_token"):
session_kwargs["aws_session_token"] = settings["session_token"].get_secret_value() # type: ignore[union-attr]
access_key = settings.get("access_key")
secret_key = settings.get("secret_key")
session_token = settings.get("session_token")
if access_key is not None and secret_key is not None:
session_kwargs["aws_access_key_id"] = access_key.get_secret_value()
session_kwargs["aws_secret_access_key"] = secret_key.get_secret_value()
if session_token is not None:
session_kwargs["aws_session_token"] = session_token.get_secret_value()
return Boto3Session(**session_kwargs)
def _invoke_converse(self, request: Mapping[str, Any]) -> dict[str, Any]:
response = self._bedrock_client.converse(**request)
if not isinstance(response, Mapping):
raise ChatClientInvalidResponseException("Bedrock converse response must be a mapping.")
return response
@override
def _inner_get_response(
self,
@@ -332,16 +343,20 @@ class BedrockChatClient(
if stream:
# Streaming mode - simulate streaming by yielding a single update
async def _stream() -> AsyncIterable[ChatResponseUpdate]:
response = await asyncio.to_thread(self._bedrock_client.converse, **request)
response = await asyncio.to_thread(self._invoke_converse, request)
parsed_response = self._process_converse_response(response)
contents = list(parsed_response.messages[0].contents if parsed_response.messages else [])
if parsed_response.usage_details:
contents.append(Content.from_usage(usage_details=parsed_response.usage_details)) # type: ignore[arg-type]
raw_finish_reason = (
parsed_response.finish_reason if isinstance(parsed_response.finish_reason, str) else None
)
finish_reason = self._map_finish_reason(raw_finish_reason)
yield ChatResponseUpdate(
response_id=parsed_response.response_id,
contents=contents,
model_id=parsed_response.model_id,
finish_reason=parsed_response.finish_reason,
finish_reason=finish_reason,
raw_representation=parsed_response.raw_representation,
)
@@ -349,7 +364,7 @@ class BedrockChatClient(
# Non-streaming mode
async def _get_response() -> ChatResponse:
raw_response = await asyncio.to_thread(self._bedrock_client.converse, **request)
raw_response = await asyncio.to_thread(self._invoke_converse, request)
return self._process_converse_response(raw_response)
return _get_response()
@@ -529,25 +544,25 @@ class BedrockChatClient(
def _convert_tool_result_to_blocks(self, result: Any) -> list[dict[str, Any]]:
prepared_result = result if isinstance(result, str) else FunctionTool.parse_result(result)
try:
parsed_result = json.loads(prepared_result)
parsed_result: object = json.loads(prepared_result)
except json.JSONDecodeError:
return [{"text": prepared_result}]
return self._convert_prepared_tool_result_to_blocks(parsed_result)
def _convert_prepared_tool_result_to_blocks(self, value: Any) -> list[dict[str, Any]]:
if isinstance(value, list):
def _convert_prepared_tool_result_to_blocks(self, value: object) -> list[dict[str, Any]]:
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
blocks: list[dict[str, Any]] = []
for item in value:
blocks.extend(self._convert_prepared_tool_result_to_blocks(item))
return blocks or [{"text": ""}]
return [self._normalize_tool_result_value(value)]
def _normalize_tool_result_value(self, value: Any) -> dict[str, Any]:
def _normalize_tool_result_value(self, value: object) -> dict[str, Any]:
if isinstance(value, dict):
return {"json": value}
if isinstance(value, (list, tuple)):
return {"json": list(value)}
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
return {"json": [item for item in value]}
if isinstance(value, str):
return {"text": value}
if isinstance(value, (int, float, bool)) or value is None:
@@ -586,12 +601,14 @@ class BedrockChatClient(
return f"tool-call-{uuid4().hex}"
def _process_converse_response(self, response: dict[str, Any]) -> ChatResponse:
output = response.get("output", {})
message = output.get("message", {})
content_blocks = message.get("content", []) or []
"""Convert Bedrock Converse API response to ChatResponse."""
output = response.get("output") or {}
message = output.get("message") or {}
content_blocks = message.get("content") or []
contents = self._parse_message_contents(content_blocks)
chat_message = Message(role="assistant", contents=contents, raw_representation=message)
usage_details = self._parse_usage(response.get("usage") or output.get("usage"))
usage_source = response.get("usage") or output.get("usage")
usage_details = self._parse_usage(usage_source)
finish_reason = self._map_finish_reason(output.get("completionReason") or response.get("stopReason"))
response_id = response.get("responseId") or message.get("id")
model_id = response.get("modelId") or output.get("modelId") or self.model_id
@@ -616,7 +633,7 @@ class BedrockChatClient(
details["total_token_count"] = total_tokens
return details
def _parse_message_contents(self, content_blocks: Sequence[MutableMapping[str, Any]]) -> list[Any]:
def _parse_message_contents(self, content_blocks: Sequence[dict[str, Any]]) -> list[Any]:
contents: list[Any] = []
for block in content_blocks:
if text_value := block.get("text"):
@@ -625,32 +642,50 @@ class BedrockChatClient(
if (json_value := block.get("json")) is not None:
contents.append(Content.from_text(text=json.dumps(json_value), raw_representation=block))
continue
tool_use = block.get("toolUse")
if isinstance(tool_use, MutableMapping):
tool_name = tool_use.get("name")
tool_use_value = block.get("toolUse")
tool_use = (
tool_use_value
if isinstance(tool_use_value, dict)
else dict(tool_use_value)
if isinstance(tool_use_value, Mapping)
else None
)
if tool_use is not None:
tool_name_value = tool_use.get("name")
tool_name = tool_name_value if isinstance(tool_name_value, str) else None
if not tool_name:
raise ChatClientInvalidResponseException(
"Bedrock response missing required tool name in toolUse block."
)
tool_use_id = tool_use.get("toolUseId")
contents.append(
Content.from_function_call(
call_id=tool_use.get("toolUseId") or self._generate_tool_call_id(),
call_id=tool_use_id if isinstance(tool_use_id, str) else self._generate_tool_call_id(),
name=tool_name,
arguments=tool_use.get("input"),
raw_representation=block,
)
)
continue
tool_result = block.get("toolResult")
if isinstance(tool_result, MutableMapping):
status = (tool_result.get("status") or "success").lower()
tool_result_value = block.get("toolResult")
tool_result = (
tool_result_value
if isinstance(tool_result_value, dict)
else dict(tool_result_value)
if isinstance(tool_result_value, Mapping)
else None
)
if tool_result is not None:
status_value = tool_result.get("status")
status = (status_value if isinstance(status_value, str) else "success").lower()
exception = None
if status not in {"success", "ok"}:
exception = RuntimeError(f"Bedrock tool result status: {status}")
result_value = self._convert_bedrock_tool_result_to_value(tool_result.get("content"))
tool_use_id = tool_result.get("toolUseId")
contents.append(
Content.from_function_result(
call_id=tool_result.get("toolUseId") or self._generate_tool_call_id(),
call_id=tool_use_id if isinstance(tool_use_id, str) else self._generate_tool_call_id(),
result=result_value,
exception=str(exception) if exception else None, # type: ignore[arg-type]
raw_representation=block,
@@ -673,24 +708,28 @@ class BedrockChatClient(
"""
return f"https://bedrock-runtime.{self.region}.amazonaws.com"
def _convert_bedrock_tool_result_to_value(self, content: Any) -> Any:
def _convert_bedrock_tool_result_to_value(self, content: object) -> object:
if not content:
return None
if isinstance(content, Sequence) and not isinstance(content, (str, bytes, bytearray)):
values: list[Any] = []
values: list[object] = []
for item in content:
if isinstance(item, MutableMapping):
if (text_value := item.get("text")) is not None:
item_dict = item if isinstance(item, dict) else dict(item) if isinstance(item, Mapping) else None
if item_dict is not None:
text_value = item_dict.get("text")
if isinstance(text_value, str):
values.append(text_value)
continue
if "json" in item:
values.append(item["json"])
if "json" in item_dict:
values.append(item_dict["json"])
continue
values.append(item)
return values[0] if len(values) == 1 else values
if isinstance(content, MutableMapping):
if (text_value := content.get("text")) is not None:
content_dict = content if isinstance(content, dict) else dict(content) if isinstance(content, Mapping) else None
if content_dict is not None:
text_value = content_dict.get("text")
if isinstance(text_value, str):
return text_value
if "json" in content:
return content["json"]
if "json" in content_dict:
return content_dict["json"]
return content
@@ -1,5 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
# type: ignore
# Because the Bedrock client does not have typing, we are ignoring type issues in this module.
from __future__ import annotations
import asyncio
@@ -122,25 +123,27 @@ class RawBedrockEmbeddingClient(
)
resolved_region = settings.get("region") or DEFAULT_REGION
if client is None:
if client:
self._bedrock_client = client
else:
if not boto3_session:
session_kwargs: dict[str, Any] = {}
if region := settings.get("region"):
session_kwargs["region_name"] = region
if (access_key := settings.get("access_key")) and (secret_key := settings.get("secret_key")):
session_kwargs["aws_access_key_id"] = access_key.get_secret_value() # type: ignore[union-attr]
session_kwargs["aws_secret_access_key"] = secret_key.get_secret_value() # type: ignore[union-attr]
session_kwargs["aws_access_key_id"] = access_key.get_secret_value()
session_kwargs["aws_secret_access_key"] = secret_key.get_secret_value()
if session_token := settings.get("session_token"):
session_kwargs["aws_session_token"] = session_token.get_secret_value() # type: ignore[union-attr]
session_kwargs["aws_session_token"] = session_token.get_secret_value()
boto3_session = Boto3Session(**session_kwargs)
client = boto3_session.client(
region_name = boto3_session.region_name
self._bedrock_client = boto3_session.client(
"bedrock-runtime",
region_name=boto3_session.region_name or resolved_region,
region_name=region_name or resolved_region,
config=BotoConfig(user_agent_extra=AGENT_FRAMEWORK_USER_AGENT),
)
self._bedrock_client = client
self.model_id = settings["embedding_model_id"] # type: ignore[assignment]
self.model_id: str = settings["embedding_model_id"] # type: ignore[assignment] # pyright: ignore[reportTypedDictNotRequiredAccess]
self.region = resolved_region
super().__init__(**kwargs)
@@ -153,7 +156,7 @@ class RawBedrockEmbeddingClient(
values: Sequence[str],
*,
options: BedrockEmbeddingOptionsT | None = None,
) -> GeneratedEmbeddings[list[float]]:
) -> GeneratedEmbeddings[list[float], BedrockEmbeddingOptionsT]:
"""Call the Bedrock invoke_model API for embeddings.
Uses the Amazon Titan Embeddings model format. Each value is embedded
@@ -211,7 +214,6 @@ class RawBedrockEmbeddingClient(
accept="application/json",
body=json.dumps(body),
)
response_body = json.loads(response["body"].read())
embedding = Embedding(
vector=response_body["embedding"],
+3 -2
View File
@@ -60,6 +60,7 @@ omit = [
[tool.pyright]
extends = "../../pyproject.toml"
include = ["agent_framework_bedrock"]
[tool.mypy]
plugins = ['pydantic.mypy']
@@ -85,8 +86,8 @@ include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_bedrock"
test = "pytest --cov=agent_framework_bedrock --cov-report=term-missing:skip-covered tests"
test = "pytest -m \"not integration\" --cov=agent_framework_bedrock --cov-report=term-missing:skip-covered tests"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
build-backend = "hatchling.build"