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
@@ -15,7 +15,6 @@ from agent_framework import (
from agent_framework import (
FunctionTool as AFFunctionTool,
)
from agent_framework._tools import _create_model_from_json_schema # type: ignore
from agent_framework.exceptions import AgentException
from dotenv import load_dotenv
@@ -34,7 +33,7 @@ from ._models import (
RemoteConnection,
Tool,
WebSearchTool,
_safe_mode_context,
_safe_mode_context, # type: ignore[reportPrivateUsage]
agent_schema_dispatch,
)
@@ -445,7 +444,7 @@ class AgentFactory:
if tools := self._parse_tools(prompt_agent.tools):
chat_options["tools"] = tools
if output_schema := prompt_agent.outputSchema:
chat_options["response_format"] = _create_model_from_json_schema("agent", output_schema.to_json_schema())
chat_options["response_format"] = output_schema.to_json_schema()
# Step 3: Create the agent instance
return Agent(
client=client,
@@ -563,7 +562,7 @@ class AgentFactory:
if tools := self._parse_tools(prompt_agent.tools):
chat_options["tools"] = tools
if output_schema := prompt_agent.outputSchema:
chat_options["response_format"] = _create_model_from_json_schema("agent", output_schema.to_json_schema())
chat_options["response_format"] = output_schema.to_json_schema()
return Agent(
client=client,
name=prompt_agent.name,
@@ -598,6 +597,9 @@ class AgentFactory:
case ApiKeyConnection():
if prompt_agent.model.connection.endpoint:
provider_kwargs["project_endpoint"] = prompt_agent.model.connection.endpoint
case ReferenceConnection():
# Reference connections are resolved by concrete providers when supported.
pass
# Create the provider and use it to create the agent
provider = provider_class(**provider_kwargs)
@@ -608,8 +610,7 @@ class AgentFactory:
# Parse response format into default_options
default_options: dict[str, Any] | None = None
if prompt_agent.outputSchema:
response_format = _create_model_from_json_schema("agent", prompt_agent.outputSchema.to_json_schema())
default_options = {"response_format": response_format}
default_options = {"response_format": prompt_agent.outputSchema.to_json_schema()}
# Create the agent using the provider
# The provider's create_agent returns a Agent directly
@@ -25,6 +25,7 @@ See: dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/
from __future__ import annotations
import locale
import logging
import sys
import uuid
@@ -103,6 +104,8 @@ DECLARATIVE_STATE_KEY = "_declarative_workflow_state"
# Types that PowerFx can serialize directly
# Note: Decimal is included because PowerFx returns Decimal for numeric values
_POWERFX_SAFE_TYPES = (str, int, float, bool, type(None), _Decimal)
_POWERFX_EVAL_LOCALE = "en-US"
_POWERFX_NUMERIC_LOCALE_CANDIDATES = ("en_US.UTF-8", "en_US", "C")
def _make_powerfx_safe(value: Any) -> Any:
@@ -121,10 +124,12 @@ def _make_powerfx_safe(value: Any) -> Any:
return value
if isinstance(value, dict):
return {k: _make_powerfx_safe(v) for k, v in value.items()}
value_dict = cast(Mapping[Any, Any], value)
return {str(k): _make_powerfx_safe(v) for k, v in value_dict.items()}
if isinstance(value, list):
return [_make_powerfx_safe(item) for item in value]
value_list = cast(list[Any], value) # type: ignore[redundant-cast]
return [_make_powerfx_safe(item) for item in value_list]
# Try to convert objects with __dict__ or dataclass-style attributes
if hasattr(value, "__dict__"):
@@ -382,21 +387,33 @@ class DeclarativeWorkflowState:
f"Install dotnet and the powerfx package for full PowerFx support."
)
engine = Engine()
symbols = self._to_powerfx_symbols()
# Use setlocale(category) query form so we can restore the exact prior value.
# getlocale() returns a normalized tuple and is not always a lossless
# round-trip for setlocale across platforms/locales.
original_numeric_locale = locale.setlocale(locale.LC_NUMERIC)
try:
from System.Globalization import CultureInfo
for locale_candidate in _POWERFX_NUMERIC_LOCALE_CANDIDATES:
try:
locale.setlocale(locale.LC_NUMERIC, locale_candidate)
break
except locale.Error:
continue
original_culture = CultureInfo.CurrentCulture
original_ui_culture = CultureInfo.CurrentUICulture
en_us_culture = CultureInfo("en-US")
CultureInfo.CurrentCulture = en_us_culture
CultureInfo.CurrentUICulture = en_us_culture
engine = Engine()
try:
return engine.eval(formula, symbols=symbols)
from System.Globalization import ( # pyright: ignore[reportMissingImports]
CultureInfo, # pyright: ignore[reportUnknownVariableType]
)
except ImportError:
return engine.eval(formula, symbols=symbols, locale=_POWERFX_EVAL_LOCALE)
original_culture = cast(Any, CultureInfo.CurrentCulture) # pyright: ignore[reportUnknownMemberType]
try:
CultureInfo.CurrentCulture = CultureInfo(_POWERFX_EVAL_LOCALE) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
return engine.eval(formula, symbols=symbols, locale=_POWERFX_EVAL_LOCALE)
finally:
CultureInfo.CurrentCulture = original_culture
CultureInfo.CurrentUICulture = original_ui_culture
CultureInfo.CurrentCulture = original_culture # pyright: ignore[reportUnknownMemberType]
except ValueError as e:
error_msg = str(e)
# Handle undefined variable errors gracefully by returning None
@@ -405,6 +422,8 @@ class DeclarativeWorkflowState:
logger.debug(f"PowerFx: undefined variable in expression '{formula}', returning None")
return None
raise
finally:
locale.setlocale(locale.LC_NUMERIC, original_numeric_locale)
def _eval_custom_function(self, formula: str) -> Any | None:
"""Handle custom functions not supported by the Python PowerFx library.
@@ -424,7 +443,7 @@ class DeclarativeWorkflowState:
args_str = match.group(1)
# Parse comma-separated arguments (handling nested parentheses)
args = self._parse_function_args(args_str)
evaluated_args = []
evaluated_args: list[str] = []
for arg in args:
arg = arg.strip()
if arg.startswith('"') and arg.endswith('"'):
@@ -576,37 +595,44 @@ class DeclarativeWorkflowState:
"""
messages: Any = self.eval(f"={inner_expr}")
if isinstance(messages, list) and messages:
last_msg: Any = messages[-1]
message_list = cast(list[Any], messages) # type: ignore[redundant-cast]
last_msg: Any = message_list[-1]
if isinstance(last_msg, dict):
last_msg_dict = cast(dict[str, Any], last_msg)
# Try "text" key first (simple dict format)
if "text" in last_msg:
return str(last_msg["text"])
if "text" in last_msg_dict:
return str(last_msg_dict["text"])
# Try extracting from "contents" (Message dict format)
# Message.text concatenates text from all TextContent items
contents = last_msg.get("contents", [])
if isinstance(contents, list):
text_parts = []
contents_obj = last_msg_dict.get("contents", [])
if isinstance(contents_obj, list):
contents = cast(list[Any], contents_obj) # type: ignore[redundant-cast]
text_parts: list[str] = []
for content in contents:
if isinstance(content, dict):
content_dict = cast(dict[str, Any], content)
# TextContent has a "text" key
if content.get("type") == "text" or "text" in content:
text_parts.append(str(content.get("text", "")))
elif hasattr(content, "text"):
text_parts.append(str(getattr(content, "text", "")))
if content_dict.get("type") == "text" or "text" in content_dict:
text_parts.append(str(content_dict.get("text", "")))
else:
content_obj: object = content
if hasattr(content_obj, "text"):
text_parts.append(str(getattr(content_obj, "text", "")))
if text_parts:
return " ".join(text_parts)
return ""
if hasattr(last_msg, "text"):
return str(getattr(last_msg, "text", ""))
last_msg_obj: object = last_msg
if hasattr(last_msg_obj, "text"):
return str(getattr(last_msg_obj, "text", ""))
return ""
def _parse_function_args(self, args_str: str) -> list[str]:
"""Parse comma-separated function arguments, handling nested parentheses and strings."""
args = []
current = []
args: list[str] = []
current: list[str] = []
depth = 0
in_string = False
string_char = None
string_char: str | None = None
for char in args_str:
if char in ('"', "'") and not in_string:
@@ -14,7 +14,7 @@ action definitions and creates a proper workflow graph with:
from __future__ import annotations
import logging
from typing import Any
from typing import Any, cast
from agent_framework import (
Workflow,
@@ -983,8 +983,9 @@ class DeclarativeWorkflowBuilder:
last_executor = chain[-1]
# Skip terminators — they handle their own control flow
action_def = getattr(last_executor, "_action_def", {})
if isinstance(action_def, dict) and action_def.get("kind", "") in TERMINATOR_ACTIONS:
action_def_obj = getattr(last_executor, "_action_def", {})
action_def = cast(dict[str, Any], action_def_obj) if isinstance(action_def_obj, dict) else {}
if action_def.get("kind", "") in TERMINATOR_ACTIONS:
return None
# Check if last executor is a structure with branch_exits
@@ -188,9 +188,9 @@ def _validate_conversation_history(messages: list[Message], agent_name: str) ->
tool_result_ids: set[str] = set()
for i, msg in enumerate(messages):
if not hasattr(msg, "contents") or msg.contents is None:
if not (contents := getattr(msg, "contents", None)):
continue
for content in msg.contents:
for content in contents:
if content.type == "function_call" and content.call_id:
tool_call_ids.add(content.call_id)
logger.debug(
@@ -7,7 +7,8 @@ Each action becomes a node in the workflow graph.
"""
import uuid
from typing import Any
from collections.abc import Mapping
from typing import Any, cast
from agent_framework import (
WorkflowContext,
@@ -28,9 +29,12 @@ def _get_variable_path(action_def: dict[str, Any], key: str = "variable") -> str
variable = action_def.get(key)
if isinstance(variable, str):
return variable
if isinstance(variable, dict):
return variable.get("path")
return action_def.get("path")
if isinstance(variable, Mapping):
path = variable.get("path") # type: ignore[reportUnknownVariableType]
return path if isinstance(path, str) else None
fallback_path = action_def.get("path")
return fallback_path if isinstance(fallback_path, str) else None
class SetValueExecutor(DeclarativeActionExecutor):
@@ -150,16 +154,23 @@ class SetMultipleVariablesExecutor(DeclarativeActionExecutor):
"""Handle the SetMultipleVariables action."""
state = await self._ensure_state_initialized(ctx, trigger)
assignments = self._action_def.get("assignments", [])
assignments = cast(
list[Mapping[str, Any]],
self._action_def.get("assignments") if isinstance(self._action_def.get("assignments"), list) else [],
)
for assignment in assignments:
if not isinstance(assignment, Mapping):
continue
variable = assignment.get("variable")
path: str | None
if isinstance(variable, str):
path = variable
elif isinstance(variable, dict):
path = variable.get("path")
elif isinstance(variable, Mapping):
path_value = variable.get("path") # type: ignore[reportUnknownMemberType]
path = path_value if isinstance(path_value, str) else None
else:
path = assignment.get("path")
fallback_path = assignment.get("path")
path = fallback_path if isinstance(fallback_path, str) else None
value = assignment.get("value")
if path:
evaluated_value = state.eval_if_expression(value)
@@ -249,7 +260,10 @@ class SendActivityExecutor(DeclarativeActionExecutor):
activity = self._action_def.get("activity", "")
# Activity can be a string directly or a dict with a "text" field
text = activity.get("text", "") if isinstance(activity, dict) else activity
if isinstance(activity, Mapping):
text: Any = activity.get("text", "") # type: ignore[reportUnknownMemberType]
else:
text = activity
if isinstance(text, str):
# First evaluate any =expression syntax
@@ -260,7 +274,7 @@ class SendActivityExecutor(DeclarativeActionExecutor):
# Yield the text as workflow output
if text:
await ctx.yield_output(str(text))
await ctx.yield_output(str(text)) # type: ignore[reportUnknownArgumentType]
await ctx.send_message(ActionComplete())
@@ -336,11 +350,14 @@ class EditTableExecutor(DeclarativeActionExecutor):
if table_path:
# Get current table value
current_table = state.get(table_path)
if current_table is None:
current_table_value = state.get(table_path)
current_table: list[Any]
if current_table_value is None:
current_table = []
elif not isinstance(current_table, list):
current_table = [current_table]
elif isinstance(current_table_value, list):
current_table = list(current_table_value) # type: ignore[reportUnknownArgumentType]
else:
current_table = [current_table_value]
if operation == "add" or operation == "insert":
evaluated_value = state.eval_if_expression(value)
@@ -413,11 +430,14 @@ class EditTableV2Executor(DeclarativeActionExecutor):
if table_path:
# Get current table value
current_table = state.get(table_path)
if current_table is None:
current_table_value = state.get(table_path)
current_table: list[Any]
if current_table_value is None:
current_table = []
elif not isinstance(current_table, list):
current_table = [current_table]
elif isinstance(current_table_value, list):
current_table = list(current_table_value) # type: ignore[reportUnknownArgumentType]
else:
current_table = [current_table_value]
if operation == "add":
evaluated_item = state.eval_if_expression(item)
@@ -433,9 +453,12 @@ class EditTableV2Executor(DeclarativeActionExecutor):
evaluated_item = state.eval_if_expression(item)
if key_field and isinstance(evaluated_item, dict):
# Remove by key match
key_value = evaluated_item.get(key_field)
evaluated_item_dict = cast(dict[str, Any], evaluated_item)
key_value = evaluated_item_dict.get(key_field)
current_table = [
r for r in current_table if not (isinstance(r, dict) and r.get(key_field) == key_value)
r
for r in current_table
if not (isinstance(r, dict) and cast(dict[str, Any], r).get(key_field) == key_value)
]
elif evaluated_item in current_table:
current_table.remove(evaluated_item)
@@ -451,11 +474,11 @@ class EditTableV2Executor(DeclarativeActionExecutor):
elif operation == "addorupdate":
evaluated_item = state.eval_if_expression(item)
if key_field and isinstance(evaluated_item, dict):
key_value = evaluated_item.get(key_field)
key_value = evaluated_item.get(key_field) # type: ignore[reportUnknownArgumentType]
# Find existing item with same key
found_idx = -1
for i, r in enumerate(current_table):
if isinstance(r, dict) and r.get(key_field) == key_value:
if isinstance(r, dict) and cast(dict[str, Any], r).get(key_field) == key_value:
found_idx = i
break
if found_idx >= 0:
@@ -476,9 +499,9 @@ class EditTableV2Executor(DeclarativeActionExecutor):
if 0 <= idx < len(current_table):
current_table[idx] = evaluated_item
elif key_field and isinstance(evaluated_item, dict):
key_value = evaluated_item.get(key_field)
key_value = evaluated_item.get(key_field) # type: ignore[reportUnknownArgumentType]
for i, r in enumerate(current_table):
if isinstance(r, dict) and r.get(key_field) == key_value:
if isinstance(r, dict) and cast(dict[str, Any], r).get(key_field) == key_value:
current_table[i] = evaluated_item
break
@@ -568,11 +591,13 @@ class ParseValueExecutor(DeclarativeActionExecutor):
if value is None:
return {}
if isinstance(value, dict):
return value
return cast(dict[str, Any], value)
if isinstance(value, str):
try:
parsed = json.loads(value)
return parsed if isinstance(parsed, dict) else {"value": parsed}
if isinstance(parsed, dict):
return cast(dict[str, Any], parsed)
return {"value": parsed}
except json.JSONDecodeError:
return {"value": value}
return {"value": value}
@@ -581,11 +606,13 @@ class ParseValueExecutor(DeclarativeActionExecutor):
if value is None:
return []
if isinstance(value, list):
return value
return cast(list[Any], value) # type: ignore[redundant-cast]
if isinstance(value, str):
try:
parsed = json.loads(value)
return parsed if isinstance(parsed, list) else [parsed]
if isinstance(parsed, list):
return cast(list[Any], parsed) # type: ignore[redundant-cast]
return [parsed]
except json.JSONDecodeError:
return [value]
return [value]
@@ -15,9 +15,11 @@ import json
import logging
import uuid
from abc import abstractmethod
from collections.abc import Mapping
from dataclasses import dataclass, field
from inspect import isawaitable
from typing import Any
from typing import Any, cast
from collections.abc import Callable
from agent_framework import (
Content,
@@ -127,7 +129,7 @@ class ToolInvocationResult:
success: bool
result: Any = None
error: str | None = None
messages: list[Message] = field(default_factory=list)
messages: list[Message] = field(default_factory=cast(Callable[..., list[Message]], list))
rejected: bool = False
rejection_reason: str | None = None
@@ -267,15 +269,14 @@ class BaseToolExecutor(DeclarativeActionExecutor):
Returns:
Tuple of (messages_var, result_var, auto_send)
"""
output_config = self._action_def.get("output", {})
output_config: dict[str, str | bool] = self._action_def.get("output", {})
if not isinstance(output_config, dict):
if not isinstance(output_config, Mapping):
return None, None, True
messages_var = output_config.get("messages")
result_var = output_config.get("result")
auto_send = bool(output_config.get("autoSend", True))
return (
str(messages_var) if messages_var else None,
str(result_var) if result_var else None,
@@ -494,7 +495,7 @@ class BaseToolExecutor(DeclarativeActionExecutor):
type(arguments_def).__name__,
)
elif isinstance(arguments_def, dict):
for key, value in arguments_def.items():
for key, value in arguments_def.items(): # type: ignore[reportUnknownVariableType]
arguments[key] = state.eval_if_expression(value)
# Check if approval is required
@@ -44,14 +44,16 @@ def message_text(messages: Any) -> str:
content: Any = messages_dict.get("content", "")
if isinstance(content, str):
return content
if hasattr(content, "text"):
return str(content.text)
text_attr = getattr(content, "text", None)
if text_attr is not None:
return str(text_attr)
return str(content) if content else ""
if isinstance(messages, list):
# List of messages - concatenate all text
texts: list[str] = []
for msg in messages:
message_list = cast(list[Any], messages) # type: ignore[redundant-cast]
for msg in message_list:
if isinstance(msg, str):
texts.append(msg)
elif isinstance(msg, dict):
@@ -61,14 +63,16 @@ def message_text(messages: Any) -> str:
texts.append(msg_content)
elif msg_content:
texts.append(str(msg_content))
elif hasattr(msg, "content"):
msg_obj_content: Any = msg.content
if isinstance(msg_obj_content, str):
texts.append(msg_obj_content)
elif hasattr(msg_obj_content, "text"):
texts.append(str(msg_obj_content.text))
elif msg_obj_content:
texts.append(str(msg_obj_content))
else:
msg_obj: object = msg
if hasattr(msg_obj, "content"):
msg_obj_content: Any = getattr(msg_obj, "content", None)
if isinstance(msg_obj_content, str):
texts.append(msg_obj_content)
elif (msg_obj_text := getattr(msg_obj_content, "text", None)) is not None:
texts.append(str(msg_obj_text))
elif msg_obj_content:
texts.append(str(msg_obj_content))
return " ".join(texts)
# Try to get text attribute
@@ -191,10 +195,8 @@ def is_blank(value: Any) -> bool:
return True
if isinstance(value, str) and not value.strip():
return True
if isinstance(value, list):
return len(value) == 0
if isinstance(value, dict):
return len(value) == 0
if isinstance(value, (list, dict)):
return len(value) == 0 # type: ignore[reportUnknownArgumentType]
return False
@@ -284,8 +284,9 @@ class WorkflowState:
if existing is None:
self.set(path, [value])
elif isinstance(existing, list):
existing.append(value)
self.set(path, existing)
existing_list = cast(list[Any], existing) # type: ignore[redundant-cast]
existing_list.append(value)
self.set(path, existing_list)
else:
raise ValueError(f"Cannot append to non-list at path '{path}'")
@@ -614,9 +615,9 @@ class WorkflowState:
if isinstance(value, str):
return self.eval(value)
if isinstance(value, dict):
return {str(k): self.eval_if_expression(v) for k, v in value.items()}
return {str(k): self.eval_if_expression(v) for k, v in value.items()} # type: ignore[reportUnknownVariableType]
if isinstance(value, list):
return [self.eval_if_expression(item) for item in value]
return [self.eval_if_expression(item) for item in value] # type: ignore[reportUnknownVariableType]
return value
def reset_local(self) -> None: