From 46b4e823733df46606f6fce30e20be5a2dd0fe43 Mon Sep 17 00:00:00 2001 From: Dhanushree-Microsoft Date: Thu, 21 May 2026 11:44:16 +0530 Subject: [PATCH 01/25] Refactor code structure for improved readability and maintainability --- infra/vscode_web/endpoint-requirements.txt | 2 +- infra/vscode_web/requirements.txt | 2 +- src/backend-api/pyproject.toml | 2 +- src/backend-api/uv.lock | 8 +- src/processor/pyproject.toml | 6 +- .../src/libs/agent_framework/agent_builder.py | 120 +++---- .../agent_framework/agent_framework_helper.py | 48 ++- .../src/libs/agent_framework/agent_info.py | 10 +- .../agent_framework/agent_speaking_capture.py | 8 +- .../azure_openai_response_retry.py | 9 +- .../coordinator_selection_response.py | 11 + .../agent_framework/groupchat_orchestrator.py | 85 +++-- .../src/libs/agent_framework/middlewares.py | 48 ++- .../shared_memory_context_provider.py | 31 +- .../src/libs/base/orchestrator_base.py | 38 ++- .../src/libs/mcp_server/MCPBlobIOTool.py | 14 +- .../src/libs/mcp_server/MCPDatetimeTool.py | 12 +- .../src/libs/mcp_server/MCPMicrosoftDocs.py | 8 +- .../orchestration/analysis_orchestrator.py | 9 +- .../yaml_convert_orchestrator.py | 17 +- .../orchestration/design_orchestrator.py | 17 +- .../documentation_orchestrator.py | 17 +- .../src/steps/migration_processor.py | 63 ++-- .../agent_framework/test_agent_builder.py | 8 +- .../test_agent_framework_helper.py | 48 ++- .../test_groupchat_orchestrator_internals.py | 58 ++-- .../test_input_observer_middleware.py | 18 +- .../test_middlewares_extras.py | 21 +- .../test_shared_memory_context_provider.py | 2 +- .../steps/test_migration_processor_run.py | 87 +++-- src/processor/uv.lock | 322 +++++++++++++++--- 31 files changed, 746 insertions(+), 403 deletions(-) create mode 100644 src/processor/src/libs/agent_framework/coordinator_selection_response.py diff --git a/infra/vscode_web/endpoint-requirements.txt b/infra/vscode_web/endpoint-requirements.txt index 18d6803e..d7ff98e4 100644 --- a/infra/vscode_web/endpoint-requirements.txt +++ b/infra/vscode_web/endpoint-requirements.txt @@ -1,3 +1,3 @@ -azure-ai-projects==1.0.0b12 +azure-ai-projects==2.1.0 azure-identity==1.20.0 ansible-core~=2.17.0 \ No newline at end of file diff --git a/infra/vscode_web/requirements.txt b/infra/vscode_web/requirements.txt index 18d6803e..d7ff98e4 100644 --- a/infra/vscode_web/requirements.txt +++ b/infra/vscode_web/requirements.txt @@ -1,3 +1,3 @@ -azure-ai-projects==1.0.0b12 +azure-ai-projects==2.1.0 azure-identity==1.20.0 ansible-core~=2.17.0 \ No newline at end of file diff --git a/src/backend-api/pyproject.toml b/src/backend-api/pyproject.toml index 81c5c5b5..f65b4b9f 100644 --- a/src/backend-api/pyproject.toml +++ b/src/backend-api/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "aiofiles==24.1.0", - "azure-ai-agents==1.2.0b3", + "azure-ai-agents==1.2.0b6", "azure-appconfiguration==1.7.1", "azure-identity==1.25.0", "azure-monitor-opentelemetry==1.7.0", diff --git a/src/backend-api/uv.lock b/src/backend-api/uv.lock index 86f6b53f..e4e0c988 100644 --- a/src/backend-api/uv.lock +++ b/src/backend-api/uv.lock @@ -200,7 +200,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "aiofiles", specifier = "==24.1.0" }, - { name = "azure-ai-agents", specifier = "==1.2.0b3" }, + { name = "azure-ai-agents", specifier = "==1.2.0b6" }, { name = "azure-appconfiguration", specifier = "==1.7.1" }, { name = "azure-identity", specifier = "==1.25.0" }, { name = "azure-monitor-opentelemetry", specifier = "==1.7.0" }, @@ -311,16 +311,16 @@ wheels = [ [[package]] name = "azure-ai-agents" -version = "1.2.0b3" +version = "1.2.0b6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/52/3c1af9ed86582f09343f135d527ca26f0bf9659c01ccbddb650bbb952963/azure_ai_agents-1.2.0b3.tar.gz", hash = "sha256:440d7fca98c0b13654a57dcd159cdf64d1024f9baacd1a4354ce91a290d3741e", size = 362563, upload-time = "2025-08-22T22:41:58.609Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/32/f4e534dc05dfb714705df56a190d690c5452cd4dd7e936612cb1adddc44f/azure_ai_agents-1.2.0b6.tar.gz", hash = "sha256:d3c10848c3b19dec98a292f8c10cee4ba4aac1050d4faabf9c2e2456b727f528", size = 396865, upload-time = "2025-10-24T18:04:47.877Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/a4/c916745e150b5e157688da9a7965d62efb82ad940f2991260d1d2b79fcf1/azure_ai_agents-1.2.0b3-py3-none-any.whl", hash = "sha256:fec3e92fac5de2c18dee2d4def734825c2a4880bee39b3c237a7ad8079bfa8a7", size = 208129, upload-time = "2025-08-22T22:42:00.249Z" }, + { url = "https://files.pythonhosted.org/packages/96/d0/930c522f5fa9da163de057e57f8b44539424e13f46618c52624ebc712293/azure_ai_agents-1.2.0b6-py3-none-any.whl", hash = "sha256:ce23ad8fb9791118905be1ec8eae5c907cca2e536a455f1d3b830062c72cf2a7", size = 217950, upload-time = "2025-10-24T18:04:49.72Z" }, ] [[package]] diff --git a/src/processor/pyproject.toml b/src/processor/pyproject.toml index 1f36bdf2..2d622ad5 100644 --- a/src/processor/pyproject.toml +++ b/src/processor/pyproject.toml @@ -5,12 +5,12 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "agent-framework==1.0.0b260107", + "agent-framework==1.3.0", "aiohttp==3.13.4", "art==6.5", - "azure-ai-agents==1.2.0b5", + "azure-ai-agents==1.2.0b6", "azure-ai-inference==1.0.0b9", - "azure-ai-projects==2.0.0b3", + "azure-ai-projects==2.1.0", "azure-appconfiguration==1.7.2", "azure-core==1.38.0", "azure-cosmos==4.15.0", diff --git a/src/processor/src/libs/agent_framework/agent_builder.py b/src/processor/src/libs/agent_framework/agent_builder.py index 8b9c629e..6cae1a67 100644 --- a/src/processor/src/libs/agent_framework/agent_builder.py +++ b/src/processor/src/libs/agent_framework/agent_builder.py @@ -1,21 +1,31 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -"""Fluent builder for constructing ChatAgent instances with chainable configuration.""" +"""Fluent builder for constructing Agent instances with chainable configuration.""" from collections.abc import Callable, MutableMapping, Sequence from typing import Any, Literal -from agent_framework import ( - AggregateContextProvider, - ChatAgent, - ChatClientProtocol, - ChatMessageStoreProtocol, - ContextProvider, - Middleware, - ToolMode, - ToolProtocol, -) +try: + from agent_framework import ( + Agent, + AgentMiddleware, + BaseChatClient, + ChatMiddleware, + ContextProvider, + FunctionTool, + ToolMode, + ) +except ImportError: + from agent_framework import ( + AgentMiddleware, + BaseChatClient, + ChatAgent as Agent, + ChatMiddleware, + ContextProvider, + ToolMode, + ToolProtocol as FunctionTool, + ) from pydantic import BaseModel from libs.agent_framework.agent_info import AgentInfo @@ -23,7 +33,7 @@ class AgentBuilder: - """Fluent builder for creating ChatAgent instances with a chainable API. + """Fluent builder for creating Agent instances with a chainable API. This class provides two ways to create agents: 1. Fluent API with method chaining (recommended for readability) @@ -59,7 +69,7 @@ class AgentBuilder: ) """ - def __init__(self, chat_client: ChatClientProtocol): + def __init__(self, chat_client: BaseChatClient): """Initialize the builder with a chat client. Args: @@ -70,14 +80,15 @@ def __init__(self, chat_client: ChatClientProtocol): self._id: str | None = None self._name: str | None = None self._description: str | None = None - self._chat_message_store_factory: ( - Callable[[], ChatMessageStoreProtocol] | None - ) = None + self._chat_message_store_factory: Callable[[], Any] | None = None self._conversation_id: str | None = None - self._context_providers: ( - ContextProvider | list[ContextProvider] | AggregateContextProvider | None + self._context_providers: ContextProvider | list[ContextProvider] | None = None + self._middleware: ( + AgentMiddleware + | ChatMiddleware + | list[AgentMiddleware | ChatMiddleware] + | None ) = None - self._middleware: Middleware | list[Middleware] | None = None self._frequency_penalty: float | None = None self._logit_bias: dict[str | int, float] | None = None self._max_tokens: int | None = None @@ -93,10 +104,10 @@ def __init__(self, chat_client: ChatClientProtocol): ToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None ) = "auto" self._tools: ( - ToolProtocol + FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None ) = None self._top_p: float | None = None @@ -178,10 +189,10 @@ def with_max_tokens(self, max_tokens: int) -> "AgentBuilder": def with_tools( self, - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]], + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]], ) -> "AgentBuilder": """Set the tools available to the agent. @@ -210,7 +221,8 @@ def with_tool_choice( return self def with_middleware( - self, middleware: Middleware | list[Middleware] + self, + middleware: AgentMiddleware | ChatMiddleware | list[AgentMiddleware | ChatMiddleware], ) -> "AgentBuilder": """Set middleware for request/response processing. @@ -225,9 +237,7 @@ def with_middleware( def with_context_providers( self, - context_providers: ContextProvider - | list[ContextProvider] - | AggregateContextProvider, + context_providers: ContextProvider | list[ContextProvider], ) -> "AgentBuilder": """Set context providers for additional conversation context. @@ -385,7 +395,7 @@ def with_store(self, store: bool) -> "AgentBuilder": return self def with_message_store_factory( - self, factory: Callable[[], ChatMessageStoreProtocol] + self, factory: Callable[[], Any] ) -> "AgentBuilder": """Set the message store factory. @@ -422,11 +432,11 @@ def with_kwargs(self, **kwargs: Any) -> "AgentBuilder": self._kwargs.update(kwargs) return self - def build(self) -> ChatAgent: - """Build and return the configured ChatAgent. + def build(self) -> Agent: + """Build and return the configured Agent. Returns: - ChatAgent: Configured agent instance ready for use + Agent: Configured agent instance ready for use Example: .. code-block:: python @@ -442,7 +452,7 @@ def build(self) -> ChatAgent: async with agent: response = await agent.run("Hello!") """ - return ChatAgent( + return Agent( chat_client=self._chat_client, instructions=self._instructions, id=self._id, @@ -477,14 +487,10 @@ def create_agent_by_agentinfo( agent_info: AgentInfo, *, id: str | None = None, - chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] - | None = None, + chat_message_store_factory: Callable[[], Any] | None = None, conversation_id: str | None = None, - context_providers: ContextProvider - | list[ContextProvider] - | AggregateContextProvider - | None = None, - middleware: Middleware | list[Middleware] | None = None, + context_providers: ContextProvider | list[ContextProvider] | None = None, + middleware: AgentMiddleware | ChatMiddleware | list[AgentMiddleware | ChatMiddleware] | None = None, frequency_penalty: float | None = None, logit_bias: dict[str | int, float] | None = None, max_tokens: int | None = None, @@ -500,20 +506,20 @@ def create_agent_by_agentinfo( | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto", - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None = None, top_p: float | None = None, user: str | None = None, additional_chat_options: dict[str, Any] | None = None, **kwargs: Any, - ) -> ChatAgent: + ) -> Agent: """Create an agent using AgentInfo configuration with full parameter support. This method creates a chat client from the service configuration and then - creates a ChatAgent with the specified parameters. Agent name, description, + creates a Agent with the specified parameters. Agent name, description, and instructions are taken from AgentInfo but can be overridden via kwargs. Args: @@ -543,7 +549,7 @@ def create_agent_by_agentinfo( **kwargs: Additional keyword arguments Returns: - ChatAgent: Configured agent instance ready for use + Agent: Configured agent instance ready for use Example: .. code-block:: python @@ -611,20 +617,16 @@ def create_agent_by_agentinfo( @staticmethod def create_agent( - chat_client: ChatClientProtocol, + chat_client: BaseChatClient, instructions: str | None = None, *, id: str | None = None, name: str | None = None, description: str | None = None, - chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] - | None = None, + chat_message_store_factory: Callable[[], Any] | None = None, conversation_id: str | None = None, - context_providers: ContextProvider - | list[ContextProvider] - | AggregateContextProvider - | None = None, - middleware: Middleware | list[Middleware] | None = None, + context_providers: ContextProvider | list[ContextProvider] | None = None, + middleware: AgentMiddleware | ChatMiddleware | list[AgentMiddleware | ChatMiddleware] | None = None, frequency_penalty: float | None = None, logit_bias: dict[str | int, float] | None = None, max_tokens: int | None = None, @@ -640,19 +642,19 @@ def create_agent( | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto", - tools: ToolProtocol + tools: FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None = None, top_p: float | None = None, user: str | None = None, additional_chat_options: dict[str, Any] | None = None, **kwargs: Any, - ) -> ChatAgent: + ) -> Agent: """Create a Chat Client Agent. - Factory method that creates a ChatAgent instance with the specified configuration. + Factory method that creates a Agent instance with the specified configuration. The agent uses a chat client to interact with language models and supports tools (MCP tools, callable functions), context providers, middleware, and both streaming and non-streaming responses. @@ -686,7 +688,7 @@ def create_agent( **kwargs: Additional keyword arguments Returns: - ChatAgent: Configured chat agent instance that can be used directly or with async context manager + Agent: Configured chat agent instance that can be used directly or with async context manager Examples: Non-streaming example (from azure_response_client_basic.py): @@ -761,10 +763,10 @@ def create_agent( Note: When the agent has MCP tools or needs proper resource cleanup, use it with - ``async with`` to ensure proper initialization and cleanup via the ChatAgent's + ``async with`` to ensure proper initialization and cleanup via the Agent's async context manager protocol. """ - return ChatAgent( + return Agent( chat_client=chat_client, instructions=instructions, id=id, diff --git a/src/processor/src/libs/agent_framework/agent_framework_helper.py b/src/processor/src/libs/agent_framework/agent_framework_helper.py index 61da842a..e2609e04 100644 --- a/src/processor/src/libs/agent_framework/agent_framework_helper.py +++ b/src/processor/src/libs/agent_framework/agent_framework_helper.py @@ -27,12 +27,12 @@ ) if TYPE_CHECKING: - from agent_framework.azure import ( - AzureAIAgentClient, - AzureOpenAIAssistantsClient, - AzureOpenAIChatClient, - AzureOpenAIResponsesClient, - ) + from agent_framework.azure import DurableAIAgentClient + + # TODO: agent-framework 1.3.0 removed these azure clients with no replacement. + # from agent_framework.azure import AzureOpenAIAssistantsClient + # from agent_framework.azure import AzureOpenAIChatClient + # from agent_framework.azure import AzureOpenAIResponsesClient class ClientType(Enum): @@ -147,7 +147,7 @@ def create_client( env_file_path: str | None = None, env_file_encoding: str | None = None, instruction_role: str | None = None, - ) -> "AzureOpenAIChatClient": + ) -> Any: pass @overload @@ -171,7 +171,7 @@ def create_client( async_client: object | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, - ) -> "AzureOpenAIAssistantsClient": + ) -> Any: pass @overload @@ -193,7 +193,7 @@ def create_client( env_file_path: str | None = None, env_file_encoding: str | None = None, instruction_role: str | None = None, - ) -> "AzureOpenAIResponsesClient": + ) -> Any: pass @overload @@ -233,7 +233,7 @@ def create_client( async_credential: object | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, - ) -> "AzureAIAgentClient": + ) -> "DurableAIAgentClient": pass @staticmethod @@ -366,7 +366,12 @@ def create_client( "OpenAIResponsesClient is not implemented in this context." ) elif client_type == ClientType.AzureOpenAIChatCompletion: - from agent_framework.azure import AzureOpenAIChatClient + try: + from agent_framework.azure import AzureOpenAIChatClient + except ImportError as exc: + raise NotImplementedError( + "ClientType.AzureOpenAIChatCompletion is not supported in agent-framework 1.3.0; AzureOpenAIChatClient was removed." + ) from exc return AzureOpenAIChatClient( api_key=api_key, @@ -385,7 +390,12 @@ def create_client( instruction_role=instruction_role, ) elif client_type == ClientType.AzureOpenAIAssistant: - from agent_framework.azure import AzureOpenAIAssistantsClient + try: + from agent_framework.azure import AzureOpenAIAssistantsClient + except ImportError as exc: + raise NotImplementedError( + "ClientType.AzureOpenAIAssistant is not supported in agent-framework 1.3.0; AzureOpenAIAssistantsClient was removed." + ) from exc return AzureOpenAIAssistantsClient( deployment_name=deployment_name, @@ -406,7 +416,12 @@ def create_client( env_file_encoding=env_file_encoding, ) elif client_type == ClientType.AzureOpenAIResponse: - from agent_framework.azure import AzureOpenAIResponsesClient + try: + from agent_framework.azure import AzureOpenAIResponsesClient + except ImportError as exc: + raise NotImplementedError( + "ClientType.AzureOpenAIResponse is not supported in agent-framework 1.3.0; AzureOpenAIResponsesClient was removed." + ) from exc return AzureOpenAIResponsesClient( api_key=api_key, @@ -443,9 +458,12 @@ def create_client( retry_config=retry_config, ) elif client_type == ClientType.AzureOpenAIAgent: - from agent_framework.azure import AzureAIAgentClient + try: + from agent_framework.azure import DurableAIAgentClient + except ImportError: + from agent_framework.azure import AzureAIAgentClient as DurableAIAgentClient - return AzureAIAgentClient( + return DurableAIAgentClient( project_client=project_client, agent_id=agent_id, agent_name=agent_name, diff --git a/src/processor/src/libs/agent_framework/agent_info.py b/src/processor/src/libs/agent_framework/agent_info.py index 8eb18de3..6e3dfac0 100644 --- a/src/processor/src/libs/agent_framework/agent_info.py +++ b/src/processor/src/libs/agent_framework/agent_info.py @@ -4,7 +4,11 @@ """Pydantic model describing an agent participant with Jinja2 template rendering.""" from typing import Any, Callable, MutableMapping, Sequence -from agent_framework import ToolProtocol + +try: + from agent_framework import FunctionTool +except ImportError: + from agent_framework import ToolProtocol as FunctionTool from jinja2 import Template from openai import BaseModel from pydantic import Field @@ -20,10 +24,10 @@ class AgentInfo(BaseModel): agent_instruction: str | None = Field(default=None) agent_framework_helper: AgentFrameworkHelper | None = Field(default=None) tools: ( - ToolProtocol + FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None ) = Field(default=None) diff --git a/src/processor/src/libs/agent_framework/agent_speaking_capture.py b/src/processor/src/libs/agent_framework/agent_speaking_capture.py index 8243d755..5dac0cba 100644 --- a/src/processor/src/libs/agent_framework/agent_speaking_capture.py +++ b/src/processor/src/libs/agent_framework/agent_speaking_capture.py @@ -5,7 +5,11 @@ from datetime import datetime from typing import Any, Callable, Optional -from agent_framework import AgentRunContext, AgentMiddleware + +try: + from agent_framework import AgentContext, AgentMiddleware +except ImportError: + from agent_framework import AgentMiddleware, AgentRunContext as AgentContext class AgentSpeakingCaptureMiddleware(AgentMiddleware): @@ -72,7 +76,7 @@ def __init__( str, list[str] ] = {} # Buffer for streaming responses - async def process(self, context: AgentRunContext, next): + async def process(self, context: AgentContext, next): """Process the agent invocation and capture the response. Args: diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index 48b829b3..b51e324a 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -12,7 +12,14 @@ from dataclasses import dataclass from typing import Any, AsyncIterable, MutableSequence -from agent_framework.azure import AzureOpenAIResponsesClient +try: + from agent_framework.azure import AzureOpenAIResponsesClient +except ImportError: + class AzureOpenAIResponsesClient: + def __init__(self, *args: Any, **kwargs: Any): + raise NotImplementedError( + "AzureOpenAIResponsesClient was removed from agent_framework.azure in 1.3.0." + ) from tenacity import ( AsyncRetrying, retry_if_exception, diff --git a/src/processor/src/libs/agent_framework/coordinator_selection_response.py b/src/processor/src/libs/agent_framework/coordinator_selection_response.py new file mode 100644 index 00000000..5a4f4d25 --- /dev/null +++ b/src/processor/src/libs/agent_framework/coordinator_selection_response.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from pydantic import BaseModel, Field + + +class CoordinatorSelectionResponse(BaseModel): + selected_participant: str | None = Field(default=None) + instruction: str | None = Field(default=None) + finish: bool = Field(default=False) + final_message: str | None = Field(default=None) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 5cb63938..ab9975c1 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -21,23 +21,38 @@ from datetime import datetime from typing import Any, Awaitable, Callable, Generic, Mapping, Sequence, TypeVar -from agent_framework import ( - AgentProtocol, - AgentRunUpdateEvent, - ChatAgent, - ChatMessage, - Executor, - GroupChatBuilder, - ManagerSelectionResponse, - Role, - Workflow, - WorkflowOutputEvent, -) +try: + from agent_framework import ( + Agent, + AgentResponseUpdate, + Executor, + Message, + Role, + SupportsAgentRun, + Workflow, + WorkflowBuilder as GroupChatBuilder, + WorkflowEvent as WorkflowOutputEvent, + ) +except ImportError: + from agent_framework import ( + AgentProtocol as SupportsAgentRun, + AgentRunUpdateEvent as AgentResponseUpdate, + ChatAgent as Agent, + ChatMessage as Message, + Executor, + GroupChatBuilder, + Role, + Workflow, + WorkflowOutputEvent, + ) from mem0 import AsyncMemory from pydantic import BaseModel, ValidationError +from .coordinator_selection_response import CoordinatorSelectionResponse + logger = logging.getLogger(__name__) +ROLE_ASSISTANT = getattr(Role, "ASSISTANT", "assistant") # Generic type variables TInput = TypeVar("TInput") # Input type (str, dict, BaseModel, etc.) @@ -87,7 +102,7 @@ class OrchestrationResult(Generic[TOutput]): """Final workflow execution result with generic output type""" success: bool - conversation: list[ChatMessage] + conversation: list[Message] agent_responses: list[AgentResponse] tool_usage: dict[str, list[dict[str, Any]]] result: TOutput | None = None @@ -180,7 +195,7 @@ class GroupChatOrchestrator(ABC, Generic[TInput, TOutput]): Note: This orchestrator expects agents to be pre-created and passed in via - `participants`. Creation of `ChatAgent` instances (and wiring tools) + `participants`. Creation of `Agent` instances (and wiring tools) is handled elsewhere in the app. """ @@ -188,8 +203,8 @@ def __init__( self, name: str, process_id: str, - participants: Mapping[str, AgentProtocol | Executor] - | Sequence[AgentProtocol | Executor], + participants: Mapping[str, SupportsAgentRun | Executor] + | Sequence[SupportsAgentRun | Executor], memory_client: AsyncMemory, coordinator_name: str = "Coordinator", max_rounds: int = 100, @@ -225,7 +240,7 @@ def __init__( self.result_format = result_output_format # Runtime state - self.agents: dict[str, ChatAgent] = participants + self.agents: dict[str, Agent] = participants self.agent_tool_usage: dict[str, list[dict[str, Any]]] = {} self.agent_responses: list[AgentResponse] = [] self._initialized: bool = False @@ -338,7 +353,7 @@ def get_result_generator_name(self) -> str: """ return "ResultGenerator" - def _validate_sign_offs(self, conversation: list[ChatMessage]) -> tuple[bool, str]: + def _validate_sign_offs(self, conversation: list[Message]) -> tuple[bool, str]: """ Validate that all required reviewers have SIGN-OFF: PASS. @@ -475,7 +490,7 @@ async def run_stream( self._tool_call_emitted.clear() self._tool_call_recorded.clear() self._tool_call_index.clear() - self._conversation: list[ChatMessage] = [] # Track conversation during workflow + self._conversation: list[Message] = [] # Track conversation during workflow try: # Ensure initialized @@ -489,7 +504,7 @@ async def run_stream( group_chat_workflow = await self._build_groupchat() # Execute with streaming - conversation: list[ChatMessage] = [] + conversation: list[Message] = [] async for event in group_chat_workflow.run_stream(task_prompt): # Enforce wall-clock timeout if configured. @@ -503,7 +518,7 @@ async def run_stream( termination_type="hard_timeout", ) - if isinstance(event, AgentRunUpdateEvent): + if isinstance(event, AgentResponseUpdate): await self._handle_agent_update( event, stream_callback=on_agent_response_stream, @@ -542,8 +557,8 @@ async def run_stream( self._conversation = conversation # Update instance variable # Backfill tool usage from the final conversation (more reliable than streaming updates) - # AgentRunUpdateEvent may stream text only; tool calls are represented as FunctionCallContent - # items inside ChatMessage.contents. + # AgentResponseUpdate may stream text only; tool calls are represented as FunctionCallContent + # items inside Message.contents. self._backfill_tool_usage_from_conversation(conversation) # Post-workflow analysis (optional) @@ -642,7 +657,7 @@ async def run_stream( async def _handle_agent_update( self, - event: AgentRunUpdateEvent, + event: AgentResponseUpdate, stream_callback: AgentResponseStreamCallback | None = None, callback: AgentResponseCallback | None = None, ) -> None: @@ -705,7 +720,7 @@ async def _start_agent_if_needed( logger.info(f"\n[AGENT] {agent_name}:", extra={"agent_name": agent_name}) - def _append_text_chunk(self, event: AgentRunUpdateEvent) -> None: + def _append_text_chunk(self, event: AgentResponseUpdate) -> None: """Append streamed text chunks to the current agent buffer.""" if not hasattr(event.data, "text") or not event.data.text: return @@ -717,7 +732,7 @@ def _append_text_chunk(self, event: AgentRunUpdateEvent) -> None: async def _process_tool_calls( self, - event: AgentRunUpdateEvent, + event: AgentResponseUpdate, agent_name: str, stream_callback: AgentResponseStreamCallback | None, ) -> None: @@ -884,7 +899,7 @@ def _extract_function_calls(self, contents: Any) -> list[dict[str, Any]]: return calls def _backfill_tool_usage_from_conversation( - self, conversation: list[ChatMessage] + self, conversation: list[Message] ) -> None: """Populate `agent_tool_usage` from final conversation messages. @@ -894,7 +909,7 @@ def _backfill_tool_usage_from_conversation( for msg in conversation: try: role = getattr(msg, "role", None) - if role != Role.ASSISTANT: + if role != ROLE_ASSISTANT: continue agent_name = getattr(msg, "author_name", None) or "assistant" @@ -989,13 +1004,13 @@ async def _complete_agent_response( self._progress_counter += 1 # Detect manager termination signal (finish=true) from Coordinator. - # NOTE: The underlying GroupChatBuilder does not automatically stop on finish, + # NOTE: The underlying WorkflowBuilder does not automatically stop on finish, # so we enforce it here. if agent_name == self.coordinator_name: try: json_payload = self._extract_first_json_payload(complete_message) response_dict = json.loads(json_payload) - manager_response = ManagerSelectionResponse.model_validate( + manager_response = CoordinatorSelectionResponse.model_validate( response_dict ) manager_instruction = getattr(manager_response, "instruction", None) @@ -1122,7 +1137,7 @@ async def _build_groupchat(self) -> Workflow: async def _generate_final_result( self, - conversation: list[ChatMessage], + conversation: list[Message], result_format: type[TOutput], result_generator_name: str, ) -> TOutput: @@ -1220,7 +1235,7 @@ def _truncate_text( def _build_result_generator_conversation( self, - conversation: Iterable[ChatMessage], + conversation: Iterable[Message], *, exclude_authors: set[str] | None, max_messages: int, @@ -1228,7 +1243,7 @@ def _build_result_generator_conversation( max_chars_per_message: int, keep_head_chars: int, keep_tail_chars: int, - ) -> list[ChatMessage]: + ) -> list[Message]: """Build a size-bounded conversation slice for the ResultGenerator. The raw conversation can contain extremely large tool outputs or repeated @@ -1241,7 +1256,7 @@ def _build_result_generator_conversation( """ exclude = {a.lower() for a in (exclude_authors or set())} - selected: list[ChatMessage] = [] + selected: list[Message] = [] seen_fingerprints: set[tuple[str | None, str, str]] = set() total_chars = 0 @@ -1296,7 +1311,7 @@ def _build_result_generator_conversation( # Preserve role + author_name so downstream can attribute sign-offs. selected.append( - ChatMessage( + Message( role=role, text=truncated, author_name=author, diff --git a/src/processor/src/libs/agent_framework/middlewares.py b/src/processor/src/libs/agent_framework/middlewares.py index a24f5b00..0319a6a0 100644 --- a/src/processor/src/libs/agent_framework/middlewares.py +++ b/src/processor/src/libs/agent_framework/middlewares.py @@ -6,16 +6,31 @@ import time from collections.abc import Awaitable, Callable -from agent_framework import ( - AgentMiddleware, - AgentRunContext, - ChatContext, - ChatMessage, - ChatMiddleware, - FunctionInvocationContext, - FunctionMiddleware, - Role, -) +try: + from agent_framework import ( + AgentContext, + AgentMiddleware, + ChatContext, + ChatMiddleware, + FunctionInvocationContext, + FunctionMiddleware, + Message, + Role, + ) +except ImportError: + from agent_framework import ( + AgentMiddleware, + AgentRunContext as AgentContext, + ChatContext, + ChatMessage as Message, + ChatMiddleware, + FunctionInvocationContext, + FunctionMiddleware, + Role, + ) + + +ROLE_USER = getattr(Role, "USER", "user") class DebuggingMiddleware(AgentMiddleware): @@ -23,8 +38,8 @@ class DebuggingMiddleware(AgentMiddleware): async def process( self, - context: AgentRunContext, - next: Callable[[AgentRunContext], Awaitable[None]], + context: AgentContext, + next: Callable[[AgentContext], Awaitable[None]], ) -> None: """Run-level debugging middleware for troubleshooting specific runs.""" print("[Debug] Debug mode enabled for this run") @@ -136,16 +151,17 @@ async def process( for i, message in enumerate(context.messages): content = message.text if message.text else str(message.contents) - print(f" Message {i + 1} ({message.role.value}): {content}") + role_value = getattr(message.role, "value", message.role) + print(f" Message {i + 1} ({role_value}): {content}") print(f"[InputObserverMiddleware] Total messages: {len(context.messages)}") # Modify user messages by creating new messages with enhanced text - modified_messages: list[ChatMessage] = [] + modified_messages: list[Message] = [] modified_count = 0 for message in context.messages: - if message.role == Role.USER and message.text: + if message.role == ROLE_USER and message.text: original_text = message.text updated_text = original_text @@ -155,7 +171,7 @@ async def process( f"[InputObserverMiddleware] Updated: '{original_text}' -> '{updated_text}'" ) - modified_message = ChatMessage(role=message.role, text=updated_text) + modified_message = Message(role=message.role, text=updated_text) modified_messages.append(modified_message) modified_count += 1 else: diff --git a/src/processor/src/libs/agent_framework/shared_memory_context_provider.py b/src/processor/src/libs/agent_framework/shared_memory_context_provider.py index a143a88e..16b64e00 100644 --- a/src/processor/src/libs/agent_framework/shared_memory_context_provider.py +++ b/src/processor/src/libs/agent_framework/shared_memory_context_provider.py @@ -18,7 +18,17 @@ from collections.abc import MutableSequence, Sequence from typing import TYPE_CHECKING -from agent_framework import ChatMessage, Context, ContextProvider +try: + from agent_framework import Context, ContextProvider, Message +except ImportError: + try: + from agent_framework import ContextProvider, Message + except ImportError: + from agent_framework import ChatMessage as Message, ContextProvider + + class Context: + def __init__(self, instructions: str | None = None, **kwargs): + self.instructions = instructions if TYPE_CHECKING: from libs.agent_framework.qdrant_memory_store import QdrantMemoryStore @@ -49,6 +59,11 @@ class SharedMemoryContextProvider(ContextProvider): redundant embedding calls for intermediate turns) """ + DEFAULT_CONTEXT_PROMPT = ( + "The following are relevant memories from previous migration steps. " + "Use them as context to inform your current task:" + ) + def __init__( self, memory_store: QdrantMemoryStore, @@ -87,7 +102,7 @@ def __init__( async def invoking( self, - messages: ChatMessage | MutableSequence[ChatMessage], + messages: Message | MutableSequence[Message], **kwargs, ) -> Context: """Called before the agent's LLM call. Injects relevant shared memories. @@ -140,8 +155,8 @@ async def invoking( async def invoked( self, - request_messages: ChatMessage | Sequence[ChatMessage], - response_messages: ChatMessage | Sequence[ChatMessage] | None = None, + request_messages: Message | Sequence[Message], + response_messages: Message | Sequence[Message] | None = None, invoke_exception: Exception | None = None, **kwargs, ) -> None: @@ -249,7 +264,7 @@ async def _flush_memory(self) -> None: ) def _extract_query( - self, messages: ChatMessage | MutableSequence[ChatMessage] + self, messages: Message | MutableSequence[Message] ) -> str: """Extract a search query from the input messages. @@ -292,8 +307,8 @@ def _format_memories(self, memories: list) -> str: return "\n".join(lines) @staticmethod - def _get_text(message: ChatMessage) -> str: - """Extract text content from a ChatMessage.""" + def _get_text(message: Message) -> str: + """Extract text content from a Message.""" if hasattr(message, "text") and message.text: return message.text if hasattr(message, "content"): @@ -302,7 +317,7 @@ def _get_text(message: ChatMessage) -> str: @staticmethod def _extract_text( - messages: ChatMessage | Sequence[ChatMessage], + messages: Message | Sequence[Message], ) -> str: """Extract text content from response message(s).""" if not isinstance(messages, (list, Sequence)) or isinstance(messages, str): diff --git a/src/processor/src/libs/base/orchestrator_base.py b/src/processor/src/libs/base/orchestrator_base.py index 46dce8c6..2d6616e9 100644 --- a/src/processor/src/libs/base/orchestrator_base.py +++ b/src/processor/src/libs/base/orchestrator_base.py @@ -9,12 +9,23 @@ from abc import abstractmethod from typing import Any, Callable, Generic, MutableMapping, Sequence, TypeVar -from agent_framework import ChatAgent, ManagerSelectionResponse, ToolProtocol +try: + from agent_framework import Agent, FunctionTool, ToolResultCompactionStrategy +except ImportError: + from agent_framework import ChatAgent as Agent, ToolProtocol as FunctionTool + + try: + from agent_framework import ToolResultCompactionStrategy + except ImportError: + ToolResultCompactionStrategy = None # type: ignore[assignment,misc] from libs.agent_framework.agent_builder import AgentBuilder from libs.agent_framework.agent_framework_helper import ClientType from libs.agent_framework.agent_info import AgentInfo from libs.agent_framework.azure_openai_response_retry import RateLimitRetryConfig +from libs.agent_framework.coordinator_selection_response import ( + CoordinatorSelectionResponse, +) from libs.agent_framework.groupchat_orchestrator import ( AgentResponse, AgentResponseStream, @@ -60,10 +71,10 @@ def is_console_summarization_enabled(self) -> bool: async def initialize(self, process_id: str): self.mcp_tools: ( - ToolProtocol + FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] ) = await self.prepare_mcp_tools() self.agentinfos = await self.prepare_agent_infos() @@ -90,7 +101,7 @@ async def flush_agent_memories(self) -> None: is stored in the shared memory before the next step begins. """ for agent in (self.agents or {}).values(): - # ChatAgent stores providers in agent.context_provider (AggregateContextProvider) + # Agent stores providers in agent.context_provider (ContextProvider) # which has a .providers list of individual ContextProvider instances agg_provider = getattr(agent, "context_provider", None) if agg_provider is None: @@ -130,10 +141,10 @@ async def execute( async def prepare_mcp_tools( self, ) -> ( - ToolProtocol + FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] ): pass @@ -144,8 +155,8 @@ async def prepare_agent_infos(self) -> list[AgentInfo]: async def create_agents( self, agent_infos: list[AgentInfo], process_id: str - ) -> list[ChatAgent]: - agents = dict[str, ChatAgent]() + ) -> list[Agent]: + agents = dict[str, Agent]() agent_client = await self.get_client(thread_id=process_id) # Workspace context — injected into every agent's system instructions @@ -176,13 +187,20 @@ async def create_agents( .with_temperature(0.0) .with_max_tokens(20_000) ) + # Prevent context window overflow by summarizing older tool results. + if ToolResultCompactionStrategy is not None: + builder = builder.with_kwargs( + compaction_strategy=ToolResultCompactionStrategy( + keep_last_tool_call_groups=2 + ) + ) if agent_info.agent_name == "Coordinator": # Routing-only: keep deterministic. Needs enough tokens for long instructions. builder = ( builder .with_temperature(0.0) - .with_response_format(ManagerSelectionResponse) + .with_response_format(CoordinatorSelectionResponse) .with_max_tokens(4_000) .with_tools(agent_info.tools) # for checking file existence ) @@ -292,7 +310,7 @@ async def on_agent_response(self, response: AgentResponse): # print different information. from Coordinator's response structure try: response_dict = json.loads(response.message) - coordinator_response = ManagerSelectionResponse.model_validate( + coordinator_response = CoordinatorSelectionResponse.model_validate( response_dict ) diff --git a/src/processor/src/libs/mcp_server/MCPBlobIOTool.py b/src/processor/src/libs/mcp_server/MCPBlobIOTool.py index 40a68fe2..f821c925 100644 --- a/src/processor/src/libs/mcp_server/MCPBlobIOTool.py +++ b/src/processor/src/libs/mcp_server/MCPBlobIOTool.py @@ -22,14 +22,14 @@ from libs.mcp_server.MCPBlobIOTool import get_blob_file_mcp from libs.agent_framework.mcp_context import MCPContext - from agent_framework import ChatAgent + from agent_framework import Agent # Get the Blob Storage MCP tool blob_tool = get_blob_file_mcp() # Use with MCPContext for TaskGroup-safe management async with MCPContext(tools=[blob_tool]) as mcp_ctx: - async with ChatAgent(client, tools=mcp_ctx.tools) as agent: + async with Agent(client, tools=mcp_ctx.tools) as agent: response = await agent.run( "Upload the file 'data.csv' to my Azure storage container 'datasets'" ) @@ -76,7 +76,7 @@ def get_blob_file_mcp() -> MCPStdioTool: blob_tool = get_blob_file_mcp() async with blob_tool: - async with ChatAgent(client, tools=[blob_tool]) as agent: + async with Agent(client, tools=[blob_tool]) as agent: result = await agent.run( "Upload 'report.pdf' to container 'documents'" ) @@ -91,7 +91,7 @@ def get_blob_file_mcp() -> MCPStdioTool: blob_tool = get_blob_file_mcp() async with MCPContext(tools=[blob_tool]) as mcp_ctx: - async with ChatAgent(client, tools=mcp_ctx.tools) as agent: + async with Agent(client, tools=mcp_ctx.tools) as agent: # List all containers containers = await agent.run("List all my blob containers") print(containers) @@ -111,13 +111,13 @@ def get_blob_file_mcp() -> MCPStdioTool: async with MCPContext(tools=[blob_tool, datetime_tool]) as mcp_ctx: # Data processing agent - async with ChatAgent(client1, tools=mcp_ctx.tools) as processor: + async with Agent(client1, tools=mcp_ctx.tools) as processor: data = await processor.run( "Download 'raw_data.csv' from 'input-container'" ) # Analysis agent - async with ChatAgent(client2, tools=mcp_ctx.tools) as analyst: + async with Agent(client2, tools=mcp_ctx.tools) as analyst: result = await analyst.run( f"Analyze the data and upload results to 'output-container'" ) @@ -137,7 +137,7 @@ def get_blob_file_mcp() -> MCPStdioTool: blob_tool = get_blob_file_mcp() async with MCPContext(tools=[blob_tool]) as mcp_ctx: - async with ChatAgent(client, tools=mcp_ctx.tools) as agent: + async with Agent(client, tools=mcp_ctx.tools) as agent: response = await agent.run("Upload 'image.png' to 'media-container'") Note: diff --git a/src/processor/src/libs/mcp_server/MCPDatetimeTool.py b/src/processor/src/libs/mcp_server/MCPDatetimeTool.py index 83aca397..157d07a2 100644 --- a/src/processor/src/libs/mcp_server/MCPDatetimeTool.py +++ b/src/processor/src/libs/mcp_server/MCPDatetimeTool.py @@ -15,14 +15,14 @@ from libs.mcp_server.MCPDatetimeTool import get_datetime_mcp from libs.agent_framework.mcp_context import MCPContext - from agent_framework import ChatAgent + from agent_framework import Agent # Get the datetime MCP tool datetime_tool = get_datetime_mcp() # Use with MCPContext for TaskGroup-safe management async with MCPContext(tools=[datetime_tool]) as mcp_ctx: - async with ChatAgent(client, tools=mcp_ctx.tools) as agent: + async with Agent(client, tools=mcp_ctx.tools) as agent: response = await agent.run("What time is it right now?") print(response) """ @@ -60,7 +60,7 @@ def get_datetime_mcp() -> MCPStdioTool: datetime_tool = get_datetime_mcp() async with datetime_tool: - async with ChatAgent(client, tools=[datetime_tool]) as agent: + async with Agent(client, tools=[datetime_tool]) as agent: result = await agent.run("What's today's date?") print(result) @@ -74,7 +74,7 @@ def get_datetime_mcp() -> MCPStdioTool: weather_tool = get_weather_mcp() async with MCPContext(tools=[datetime_tool, weather_tool]) as mcp_ctx: - async with ChatAgent(client, tools=mcp_ctx.tools) as agent: + async with Agent(client, tools=mcp_ctx.tools) as agent: response = await agent.run( "What's the current time and what's the weather like?" ) @@ -88,10 +88,10 @@ def get_datetime_mcp() -> MCPStdioTool: async with MCPContext(tools=[datetime_tool]) as mcp_ctx: # Share tool across multiple agents - async with ChatAgent(client1, tools=mcp_ctx.tools) as agent1: + async with Agent(client1, tools=mcp_ctx.tools) as agent1: time_info = await agent1.run("Get the current time") - async with ChatAgent(client2, tools=mcp_ctx.tools) as agent2: + async with Agent(client2, tools=mcp_ctx.tools) as agent2: schedule = await agent2.run( f"Based on the time {time_info}, suggest a meeting slot" ) diff --git a/src/processor/src/libs/mcp_server/MCPMicrosoftDocs.py b/src/processor/src/libs/mcp_server/MCPMicrosoftDocs.py index d9a2ca0e..989f7d75 100644 --- a/src/processor/src/libs/mcp_server/MCPMicrosoftDocs.py +++ b/src/processor/src/libs/mcp_server/MCPMicrosoftDocs.py @@ -12,14 +12,14 @@ from libs.mcp_server.MCPMicrosoftDocs import get_microsoft_docs_mcp from libs.agent_framework.mcp_context import MCPContext - from agent_framework import ChatAgent + from agent_framework import Agent # Get the Microsoft Docs MCP tool docs_tool = get_microsoft_docs_mcp() # Use with MCPContext for TaskGroup-safe management async with MCPContext(tools=[docs_tool]) as mcp_ctx: - async with ChatAgent(client, tools=mcp_ctx.tools) as agent: + async with Agent(client, tools=mcp_ctx.tools) as agent: response = await agent.run("Search Microsoft Learn for Azure Functions best practices") print(response) """ @@ -47,7 +47,7 @@ def get_microsoft_docs_mcp() -> MCPStreamableHTTPTool: docs_tool = get_microsoft_docs_mcp() async with docs_tool: - async with ChatAgent(client, tools=[docs_tool]) as agent: + async with Agent(client, tools=[docs_tool]) as agent: result = await agent.run("Find documentation about Azure App Service") Advanced usage with multiple tools: @@ -60,7 +60,7 @@ def get_microsoft_docs_mcp() -> MCPStreamableHTTPTool: datetime_tool = MCPStdioTool(name="datetime", command="npx", args=["-y", "@modelcontextprotocol/server-datetime"]) async with MCPContext(tools=[docs_tool, datetime_tool]) as mcp_ctx: - async with ChatAgent(client, tools=mcp_ctx.tools) as agent: + async with Agent(client, tools=mcp_ctx.tools) as agent: response = await agent.run("What's the latest Azure Functions documentation?") Note: diff --git a/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py b/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py index 93f8f2f0..461005a6 100644 --- a/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py +++ b/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py @@ -12,7 +12,10 @@ from pathlib import Path from typing import Any, Callable, MutableMapping, Sequence -from agent_framework import MCPStdioTool, MCPStreamableHTTPTool, ToolProtocol +try: + from agent_framework import FunctionTool, MCPStdioTool, MCPStreamableHTTPTool +except ImportError: + from agent_framework import MCPStdioTool, MCPStreamableHTTPTool, ToolProtocol as FunctionTool from libs.agent_framework.agent_info import AgentInfo from libs.agent_framework.groupchat_orchestrator import ( @@ -98,10 +101,10 @@ async def execute( async def prepare_mcp_tools( self, ) -> ( - ToolProtocol + FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] ): """Create and return the MCP tools used by analysis agents. diff --git a/src/processor/src/steps/convert/orchestration/yaml_convert_orchestrator.py b/src/processor/src/steps/convert/orchestration/yaml_convert_orchestrator.py index f1fe8b4d..580e56b8 100644 --- a/src/processor/src/steps/convert/orchestration/yaml_convert_orchestrator.py +++ b/src/processor/src/steps/convert/orchestration/yaml_convert_orchestrator.py @@ -13,11 +13,14 @@ from pathlib import Path from typing import Any, Callable, MutableMapping, Sequence -from agent_framework import ( - MCPStdioTool, - MCPStreamableHTTPTool, - ToolProtocol, -) +try: + from agent_framework import FunctionTool, MCPStdioTool, MCPStreamableHTTPTool +except ImportError: + from agent_framework import ( + MCPStdioTool, + MCPStreamableHTTPTool, + ToolProtocol as FunctionTool, + ) from libs.agent_framework.agent_info import AgentInfo from libs.agent_framework.groupchat_orchestrator import ( @@ -107,10 +110,10 @@ async def execute( async def prepare_mcp_tools( self, ) -> ( - ToolProtocol + FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] ): """Create and return the MCP tools used by conversion agents.""" ms_doc_mcp_tool = MCPStreamableHTTPTool( diff --git a/src/processor/src/steps/design/orchestration/design_orchestrator.py b/src/processor/src/steps/design/orchestration/design_orchestrator.py index d2dd47f0..2e49c850 100644 --- a/src/processor/src/steps/design/orchestration/design_orchestrator.py +++ b/src/processor/src/steps/design/orchestration/design_orchestrator.py @@ -11,11 +11,14 @@ from pathlib import Path from typing import Any, Callable, MutableMapping, Sequence -from agent_framework import ( - MCPStdioTool, - MCPStreamableHTTPTool, - ToolProtocol, -) +try: + from agent_framework import FunctionTool, MCPStdioTool, MCPStreamableHTTPTool +except ImportError: + from agent_framework import ( + MCPStdioTool, + MCPStreamableHTTPTool, + ToolProtocol as FunctionTool, + ) from libs.agent_framework.agent_info import AgentInfo from libs.agent_framework.groupchat_orchestrator import ( @@ -98,10 +101,10 @@ async def execute( async def prepare_mcp_tools( self, ) -> ( - ToolProtocol + FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] ): """Create and return the MCP tools used by design agents.""" # Create MCP tools (not connected yet) diff --git a/src/processor/src/steps/documentation/orchestration/documentation_orchestrator.py b/src/processor/src/steps/documentation/orchestration/documentation_orchestrator.py index 0aa6c443..b623b9dd 100644 --- a/src/processor/src/steps/documentation/orchestration/documentation_orchestrator.py +++ b/src/processor/src/steps/documentation/orchestration/documentation_orchestrator.py @@ -15,11 +15,14 @@ from pathlib import Path from typing import Any, Callable, MutableMapping, Sequence -from agent_framework import ( - MCPStdioTool, - MCPStreamableHTTPTool, - ToolProtocol, -) +try: + from agent_framework import FunctionTool, MCPStdioTool, MCPStreamableHTTPTool +except ImportError: + from agent_framework import ( + MCPStdioTool, + MCPStreamableHTTPTool, + ToolProtocol as FunctionTool, + ) from libs.agent_framework.agent_info import AgentInfo from libs.agent_framework.groupchat_orchestrator import ( @@ -112,10 +115,10 @@ async def execute( async def prepare_mcp_tools( self, ) -> ( - ToolProtocol + FunctionTool | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] ): """Create and return the MCP tools used by documentation agents.""" ms_doc_mcp_tool = MCPStreamableHTTPTool( diff --git a/src/processor/src/steps/migration_processor.py b/src/processor/src/steps/migration_processor.py index 73b2954a..b1451ef3 100644 --- a/src/processor/src/steps/migration_processor.py +++ b/src/processor/src/steps/migration_processor.py @@ -32,16 +32,7 @@ from datetime import datetime from typing import Any -from agent_framework import ( - ExecutorCompletedEvent, - ExecutorFailedEvent, - ExecutorInvokedEvent, - Workflow, - WorkflowBuilder, - WorkflowFailedEvent, - WorkflowOutputEvent, - WorkflowStartedEvent, -) +from agent_framework import Workflow, WorkflowBuilder, WorkflowEvent from openai import AsyncAzureOpenAI @@ -368,7 +359,7 @@ async def _generate_report_summary( } async for event in self.workflow.run_stream(input_data): - if isinstance(event, WorkflowStartedEvent): + if event.type == "started": logger.info("Workflow started (%s)", event.origin.value) report_collector.set_current_step("analysis", step_phase="start") @@ -377,16 +368,16 @@ async def _generate_report_summary( await telemetry.init_process( process_id=input_data.process_id, step="analysis", phase="start" ) - elif isinstance(event, WorkflowOutputEvent): - # WorkflowOutputEvent carries the step output (success or hard-termination). + elif event.type == "output": + # WorkflowEvent carries the step output (success or hard-termination). # Note: a None payload is an error that must be surfaced clearly. if event.data is None: report_collector.set_current_step( - event.source_executor_id or "unknown" + event.executor_id or "unknown" ) # Build a meaningful error message instead of generic "Workflow output is None" - executor_id = event.source_executor_id or "unknown" + executor_id = event.executor_id or "unknown" error_msg = f"Step '{executor_id}' completed without producing output. This may be caused by context length overflow, agent timeout, or an internal orchestration error. Check processor logs for '[AOAI_CTX_TRIM_STREAM]' or exception details." report_collector.record_failure( @@ -407,13 +398,13 @@ async def _generate_report_summary( await telemetry.record_failure_outcome( process_id=input_data.process_id, - failed_step=event.source_executor_id or "unknown", + failed_step=event.executor_id or "unknown", error_message=error_msg, failure_details=failure_details, execution_time_seconds=( time.perf_counter() - - step_start_perf[event.source_executor_id] - if event.source_executor_id in step_start_perf + - step_start_perf[event.executor_id] + if event.executor_id in step_start_perf else None ), ) @@ -423,7 +414,7 @@ async def _generate_report_summary( # Raise a rich exception so the queue worker reports a meaningful reason. raise WorkflowExecutorFailedException({ - "executor_id": event.source_executor_id or "unknown", + "executor_id": event.executor_id or "unknown", "error_type": "WorkflowOutputMissing", "message": error_msg, "traceback": None, @@ -477,15 +468,15 @@ async def _generate_report_summary( } report_collector.set_current_step( - event.source_executor_id or "unknown" + event.executor_id or "unknown" ) report_collector.record_failure( exception=ValueError( getattr(event.data, "reason", None) - or f"Hard terminated in {event.source_executor_id} step" + or f"Hard terminated in {event.executor_id} step" ), custom_message=getattr(event.data, "reason", None) - or f"Hard terminated in {event.source_executor_id} step", + or f"Hard terminated in {event.executor_id} step", ) failure_details: Any = ( @@ -510,14 +501,14 @@ async def _generate_report_summary( await telemetry.record_failure_outcome( process_id=input_data.process_id, - failed_step=event.source_executor_id or "unknown", + failed_step=event.executor_id or "unknown", error_message=getattr(event.data, "reason", None) - or f"Hard terminated in {event.source_executor_id} step", + or f"Hard terminated in {event.executor_id} step", failure_details=failure_details, execution_time_seconds=( time.perf_counter() - - step_start_perf[event.source_executor_id] - if event.source_executor_id in step_start_perf + - step_start_perf[event.executor_id] + if event.executor_id in step_start_perf else None ), ) @@ -533,21 +524,21 @@ async def _generate_report_summary( logger.info("Workflow output (%s): %s", event.origin.value, event.data) await telemetry.record_step_result( process_id=input_data.process_id, - step_name=event.source_executor_id, + step_name=event.executor_id, step_result=event.data, execution_time_seconds=( time.perf_counter() - - step_start_perf[event.source_executor_id] - if event.source_executor_id in step_start_perf + - step_start_perf[event.executor_id] + if event.executor_id in step_start_perf else None ), ) - if event.source_executor_id in step_start_perf: + if event.executor_id in step_start_perf: report_collector.mark_step_completed( - event.source_executor_id, + event.executor_id, execution_time=time.perf_counter() - - step_start_perf[event.source_executor_id], + - step_start_perf[event.executor_id], ) try: @@ -572,10 +563,10 @@ async def _generate_report_summary( ) return event.data - elif isinstance(event, ExecutorFailedEvent): + elif event.type == "executor_failed": pass # will handle in WorkflowFailedEvent - elif isinstance(event, WorkflowFailedEvent): + elif event.type == "failed": logger.error( "Executor failed (%s): %s [%s]: %s (traceback: %s)", event.origin.value, @@ -644,7 +635,7 @@ async def _generate_report_summary( # Raise a rich exception containing the full WorkflowErrorDetails payload. raise WorkflowExecutorFailedException(event.details) - elif isinstance(event, ExecutorInvokedEvent): + elif event.type == "executor_invoked": # The bug. the first executor's event fired after completing execution. if event.executor_id != "analysis": telemetry: TelemetryManager = ( @@ -675,7 +666,7 @@ async def _generate_report_summary( # near-zero and incorrect. if event.executor_id not in step_start_perf: step_start_perf[event.executor_id] = time.perf_counter() - elif isinstance(event, ExecutorCompletedEvent): + elif event.type == "executor_completed": # print(f"Executor completed ({event.executor_id}): {event.data}") # Log shared memory stats after each step diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_agent_builder.py b/src/processor/src/tests/unit/libs/agent_framework/test_agent_builder.py index 26fcbfe5..cbfede63 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_agent_builder.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_agent_builder.py @@ -144,7 +144,7 @@ def test_chaining_returns_self_each_step(self): class TestBuild: def test_build_passes_all_state_to_chat_agent(self): chat_client = MagicMock() - with patch("libs.agent_framework.agent_builder.ChatAgent") as mock_chat: + with patch("libs.agent_framework.agent_builder.Agent") as mock_chat: agent = ( AgentBuilder(chat_client) .with_instructions("inst") @@ -172,7 +172,7 @@ def test_build_passes_all_state_to_chat_agent(self): class TestStaticFactories: def test_create_agent_invokes_chat_agent(self): chat_client = MagicMock() - with patch("libs.agent_framework.agent_builder.ChatAgent") as mock_chat: + with patch("libs.agent_framework.agent_builder.Agent") as mock_chat: agent = AgentBuilder.create_agent( chat_client=chat_client, instructions="i", @@ -206,7 +206,7 @@ def test_create_agent_by_agentinfo_uses_helper_and_creates_client(self): with patch( "libs.agent_framework.agent_builder.get_bearer_token_provider", return_value="token-provider", - ), patch("libs.agent_framework.agent_builder.ChatAgent") as mock_chat: + ), patch("libs.agent_framework.agent_builder.Agent") as mock_chat: agent = AgentBuilder.create_agent_by_agentinfo( service_id="default", agent_info=agent_info, @@ -241,7 +241,7 @@ def test_create_agent_by_agentinfo_falls_back_to_system_prompt(self): with patch( "libs.agent_framework.agent_builder.get_bearer_token_provider", return_value="tp", - ), patch("libs.agent_framework.agent_builder.ChatAgent") as mock_chat: + ), patch("libs.agent_framework.agent_builder.Agent") as mock_chat: AgentBuilder.create_agent_by_agentinfo( service_id="default", agent_info=agent_info ) diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_agent_framework_helper.py b/src/processor/src/tests/unit/libs/agent_framework/test_agent_framework_helper.py index 64a8d415..767d5359 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_agent_framework_helper.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_agent_framework_helper.py @@ -110,45 +110,41 @@ def test_default_token_provider_when_no_credential(self): assert mock_cls.call_args.kwargs["ad_token_provider"] == "default-token" def test_azure_openai_chat_completion(self): - # Patch the lazily imported module fake_module = types.ModuleType("agent_framework.azure") - fake_module.AzureOpenAIChatClient = MagicMock(return_value="chat_client") with patch.dict(sys.modules, {"agent_framework.azure": fake_module}): - client = AgentFrameworkHelper.create_client( - ClientType.AzureOpenAIChatCompletion, - endpoint="https://x", - deployment_name="gpt-4", - ad_token_provider="t", - ) - assert client == "chat_client" + with pytest.raises(NotImplementedError, match="AzureOpenAIChatClient was removed"): + AgentFrameworkHelper.create_client( + ClientType.AzureOpenAIChatCompletion, + endpoint="https://x", + deployment_name="gpt-4", + ad_token_provider="t", + ) def test_azure_openai_assistant(self): fake_module = types.ModuleType("agent_framework.azure") - fake_module.AzureOpenAIAssistantsClient = MagicMock(return_value="asst_client") with patch.dict(sys.modules, {"agent_framework.azure": fake_module}): - client = AgentFrameworkHelper.create_client( - ClientType.AzureOpenAIAssistant, - endpoint="https://x", - deployment_name="gpt-4", - ad_token_provider="t", - ) - assert client == "asst_client" + with pytest.raises(NotImplementedError, match="AzureOpenAIAssistantsClient was removed"): + AgentFrameworkHelper.create_client( + ClientType.AzureOpenAIAssistant, + endpoint="https://x", + deployment_name="gpt-4", + ad_token_provider="t", + ) def test_azure_openai_response(self): fake_module = types.ModuleType("agent_framework.azure") - fake_module.AzureOpenAIResponsesClient = MagicMock(return_value="resp_client") with patch.dict(sys.modules, {"agent_framework.azure": fake_module}): - client = AgentFrameworkHelper.create_client( - ClientType.AzureOpenAIResponse, - endpoint="https://x", - deployment_name="gpt-4", - ad_token_provider="t", - ) - assert client == "resp_client" + with pytest.raises(NotImplementedError, match="AzureOpenAIResponsesClient was removed"): + AgentFrameworkHelper.create_client( + ClientType.AzureOpenAIResponse, + endpoint="https://x", + deployment_name="gpt-4", + ad_token_provider="t", + ) def test_azure_openai_agent(self): fake_module = types.ModuleType("agent_framework.azure") - fake_module.AzureAIAgentClient = MagicMock(return_value="agent_client") + fake_module.DurableAIAgentClient = MagicMock(return_value="agent_client") with patch.dict(sys.modules, {"agent_framework.azure": fake_module}): client = AgentFrameworkHelper.create_client( ClientType.AzureOpenAIAgent, diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py index a95d9623..263b157e 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py @@ -17,6 +17,21 @@ import pytest +import libs.agent_framework.groupchat_orchestrator as groupchat_module + +ROLE_USER = "user" +ROLE_ASSISTANT = "assistant" + + +class Message: + def __init__(self, *, role, text=None, contents=None, author_name=None): + self.role = role + self.text = text + self.contents = contents + self.author_name = author_name + + +groupchat_module.Message = Message from libs.agent_framework.groupchat_orchestrator import ( AgentResponse, AgentResponseStream, @@ -31,7 +46,7 @@ def _run(coro): @dataclass class _Msg: - """Lightweight stand-in for a ChatMessage.""" + """Lightweight stand-in for a Message.""" source: str = "" content: str = "" @@ -602,30 +617,27 @@ def test_skips_unrelated(self): class TestBackfillToolUsage: def test_skips_non_assistant(self): - from agent_framework import Role orch = _make_orch() - msg = SimpleNamespace(role=Role.USER, contents=[]) + msg = SimpleNamespace(role=ROLE_USER, contents=[]) orch._backfill_tool_usage_from_conversation([msg]) assert orch.agent_tool_usage == {} def test_records_calls_from_assistant(self): - from agent_framework import Role orch = _make_orch() item = SimpleNamespace(name="t", call_id="c", arguments={"x": 1}) msg = SimpleNamespace( - role=Role.ASSISTANT, author_name="A", contents=[item] + role=ROLE_ASSISTANT, author_name="A", contents=[item] ) orch._backfill_tool_usage_from_conversation([msg]) assert orch.agent_tool_usage["A"][0]["tool_name"] == "t" def test_dedup_already_recorded(self): - from agent_framework import Role orch = _make_orch() # Pre-mark this call as already recorded orch._tool_call_recorded.add(("A", "c")) item = SimpleNamespace(name="t", call_id="c", arguments={}) msg = SimpleNamespace( - role=Role.ASSISTANT, author_name="A", contents=[item] + role=ROLE_ASSISTANT, author_name="A", contents=[item] ) orch._backfill_tool_usage_from_conversation([msg]) assert "A" in orch.agent_tool_usage @@ -777,13 +789,11 @@ def test_tail_zero_returns_head(self): class TestBuildResultGeneratorConversation: def test_excludes_named_authors(self): - from agent_framework import Role - from agent_framework import ChatMessage orch = _make_orch() msgs = [ - ChatMessage(role=Role.ASSISTANT, text="from coord", author_name="Coordinator"), - ChatMessage(role=Role.ASSISTANT, text="from architect", author_name="Architect"), + Message(role=ROLE_ASSISTANT, text="from coord", author_name="Coordinator"), + Message(role=ROLE_ASSISTANT, text="from architect", author_name="Architect"), ] out = orch._build_result_generator_conversation( msgs, @@ -798,14 +808,12 @@ def test_excludes_named_authors(self): assert all("Coordinator" != m.author_name for m in out) def test_dedupes_identical_payloads(self): - from agent_framework import Role - from agent_framework import ChatMessage orch = _make_orch() big = "X" * 1000 msgs = [ - ChatMessage(role=Role.ASSISTANT, text=big, author_name="A"), - ChatMessage(role=Role.ASSISTANT, text=big, author_name="A"), + Message(role=ROLE_ASSISTANT, text=big, author_name="A"), + Message(role=ROLE_ASSISTANT, text=big, author_name="A"), ] out = orch._build_result_generator_conversation( msgs, @@ -819,12 +827,10 @@ def test_dedupes_identical_payloads(self): assert len(out) == 1 def test_truncates_messages_to_per_message_budget(self): - from agent_framework import Role - from agent_framework import ChatMessage orch = _make_orch() msgs = [ - ChatMessage(role=Role.ASSISTANT, text="A" * 500, author_name="X"), + Message(role=ROLE_ASSISTANT, text="A" * 500, author_name="X"), ] out = orch._build_result_generator_conversation( msgs, @@ -838,12 +844,10 @@ def test_truncates_messages_to_per_message_budget(self): assert len(out[-1].text) <= 100 def test_total_budget_enforced(self): - from agent_framework import Role - from agent_framework import ChatMessage orch = _make_orch() msgs = [ - ChatMessage(role=Role.ASSISTANT, text="A" * 100, author_name=str(i)) + Message(role=ROLE_ASSISTANT, text="A" * 100, author_name=str(i)) for i in range(20) ] out = orch._build_result_generator_conversation( @@ -859,12 +863,10 @@ def test_total_budget_enforced(self): assert total <= 200 def test_max_messages_caps_count(self): - from agent_framework import Role - from agent_framework import ChatMessage orch = _make_orch() msgs = [ - ChatMessage(role=Role.ASSISTANT, text=f"m{i}", author_name=str(i)) + Message(role=ROLE_ASSISTANT, text=f"m{i}", author_name=str(i)) for i in range(20) ] out = orch._build_result_generator_conversation( @@ -916,8 +918,6 @@ def test_unknown_tool_name(self): class TestGenerateFinalResult: def test_parses_valid_json(self): from pydantic import BaseModel - from agent_framework import Role - from agent_framework import ChatMessage class Model(BaseModel): x: int @@ -928,7 +928,7 @@ class Model(BaseModel): orch = _make_orch(participants={"Coordinator": object(), "ResultGenerator": rg}, result_format=Model) out = _run( orch._generate_final_result( - conversation=[ChatMessage(role=Role.ASSISTANT, text="x", author_name="A")], + conversation=[Message(role=ROLE_ASSISTANT, text="x", author_name="A")], result_format=Model, result_generator_name="ResultGenerator", ) @@ -937,8 +937,6 @@ class Model(BaseModel): def test_retry_on_validation_error(self): from pydantic import BaseModel - from agent_framework import Role - from agent_framework import ChatMessage class Model(BaseModel): x: int @@ -951,7 +949,7 @@ class Model(BaseModel): orch = _make_orch(participants={"Coordinator": object(), "ResultGenerator": rg}, result_format=Model) out = _run( orch._generate_final_result( - conversation=[ChatMessage(role=Role.ASSISTANT, text="x", author_name="A")], + conversation=[Message(role=ROLE_ASSISTANT, text="x", author_name="A")], result_format=Model, result_generator_name="ResultGenerator", ) diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py b/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py index 7556b989..fca26fa8 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py @@ -4,8 +4,20 @@ import asyncio from types import SimpleNamespace -from agent_framework import ChatMessage, Role +import libs.agent_framework.middlewares as middlewares_module +ROLE_USER = "user" + + +class Message: + def __init__(self, *, role, text=None, contents=None, author_name=None): + self.role = role + self.text = text + self.contents = contents + self.author_name = author_name + + +middlewares_module.Message = Message from libs.agent_framework.middlewares import InputObserverMiddleware @@ -13,7 +25,7 @@ def test_input_observer_middleware_replaces_user_text_when_configured() -> None: async def _run() -> None: ctx = SimpleNamespace( messages=[ - ChatMessage(role=Role.USER, text="original"), + Message(role=ROLE_USER, text="original"), ] ) @@ -24,7 +36,7 @@ async def _next(_context): await mw.process(ctx, _next) - assert ctx.messages[0].role == Role.USER + assert ctx.messages[0].role == ROLE_USER assert ctx.messages[0].text == "replacement" asyncio.run(_run()) diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py b/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py index c4c32f5a..1b2e1e2f 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py @@ -7,8 +7,21 @@ from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock -from agent_framework import ChatMessage, Role +import libs.agent_framework.middlewares as middlewares_module +ROLE_USER = "user" +ROLE_ASSISTANT = "assistant" + + +class Message: + def __init__(self, *, role, text=None, contents=None, author_name=None): + self.role = role + self.text = text + self.contents = contents + self.author_name = author_name + + +middlewares_module.Message = Message from libs.agent_framework.middlewares import ( DebuggingMiddleware, LoggingFunctionMiddleware, @@ -86,8 +99,8 @@ class TestInputObserverMiddleware: def test_replaces_user_messages_when_replacement_set(self): from libs.agent_framework.middlewares import InputObserverMiddleware - msg_user = ChatMessage(role=Role.USER, text="orig user") - msg_assistant = ChatMessage(role=Role.ASSISTANT, text="hi") + msg_user = Message(role=ROLE_USER, text="orig user") + msg_assistant = Message(role=ROLE_ASSISTANT, text="hi") ctx = MagicMock() ctx.messages = [msg_user, msg_assistant] next_fn = AsyncMock() @@ -101,7 +114,7 @@ def test_replaces_user_messages_when_replacement_set(self): def test_no_replacement_keeps_text(self): from libs.agent_framework.middlewares import InputObserverMiddleware - msg = ChatMessage(role=Role.USER, text="keep me") + msg = Message(role=ROLE_USER, text="keep me") ctx = MagicMock() ctx.messages = [msg] mw = InputObserverMiddleware(replacement=None) diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_shared_memory_context_provider.py b/src/processor/src/tests/unit/libs/agent_framework/test_shared_memory_context_provider.py index 1d75ee7a..ab2bc8b2 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_shared_memory_context_provider.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_shared_memory_context_provider.py @@ -90,7 +90,7 @@ async def _run(): provider, _ = _make_provider() context = await provider.invoking([]) assert context.instructions is None - assert context.messages == [] + assert getattr(context, "messages", []) == [] asyncio.run(_run()) diff --git a/src/processor/src/tests/unit/steps/test_migration_processor_run.py b/src/processor/src/tests/unit/steps/test_migration_processor_run.py index acd4ee40..683fcc5d 100644 --- a/src/processor/src/tests/unit/steps/test_migration_processor_run.py +++ b/src/processor/src/tests/unit/steps/test_migration_processor_run.py @@ -11,14 +11,7 @@ import pytest -from agent_framework import ( - ExecutorCompletedEvent, - ExecutorFailedEvent, - ExecutorInvokedEvent, - WorkflowFailedEvent, - WorkflowOutputEvent, - WorkflowStartedEvent, -) +from agent_framework import WorkflowEvent from agent_framework._workflows._events import WorkflowErrorDetails from steps.analysis.models.step_param import Analysis_TaskParam @@ -79,11 +72,11 @@ class TestRunSuccessFlow: def test_workflow_started_then_normal_output_returns_data(self): data = SimpleNamespace(is_hard_terminated=False, value="ok") events = [ - WorkflowStartedEvent(), - ExecutorInvokedEvent(executor_id="analysis", data=_make_input()), - ExecutorCompletedEvent(executor_id="analysis", data={"r": 1}), - ExecutorInvokedEvent(executor_id="design", data=_make_input()), - WorkflowOutputEvent(data=data, source_executor_id="design"), + WorkflowEvent.started(), + WorkflowEvent.executor_invoked(executor_id="analysis", data=_make_input()), + WorkflowEvent.executor_completed(executor_id="analysis", data={"r": 1}), + WorkflowEvent.executor_invoked(executor_id="design", data=_make_input()), + WorkflowEvent.output(executor_id="design", data=data), ] proc = _make_processor(events) result = _run(proc.run(_make_input())) @@ -96,10 +89,10 @@ def test_workflow_started_then_normal_output_returns_data(self): def test_invoked_event_for_non_analysis_triggers_transition_phase(self): data = SimpleNamespace(is_hard_terminated=False) events = [ - WorkflowStartedEvent(), + WorkflowEvent.started(), # Documentation invocation should map to "Documentation" display - ExecutorInvokedEvent(executor_id="documentation", data=_make_input()), - WorkflowOutputEvent(data=data, source_executor_id="documentation"), + WorkflowEvent.executor_invoked(executor_id="documentation", data=_make_input()), + WorkflowEvent.output(executor_id="documentation", data=data), ] proc = _make_processor(events) _run(proc.run(_make_input())) @@ -112,9 +105,9 @@ def test_invoked_event_for_non_analysis_triggers_transition_phase(self): def test_invoked_event_unknown_executor_uses_capitalize(self): data = SimpleNamespace(is_hard_terminated=False) events = [ - WorkflowStartedEvent(), - ExecutorInvokedEvent(executor_id="custom", data=_make_input()), - WorkflowOutputEvent(data=data, source_executor_id="custom"), + WorkflowEvent.started(), + WorkflowEvent.executor_invoked(executor_id="custom", data=_make_input()), + WorkflowEvent.output(executor_id="custom", data=data), ] proc = _make_processor(events) _run(proc.run(_make_input())) @@ -132,8 +125,8 @@ def test_hard_terminated_returns_data_and_records_failure(self): blocking_issues=["NEED_HUMAN_REVIEW"], ) events = [ - WorkflowStartedEvent(), - WorkflowOutputEvent(data=data, source_executor_id="analysis"), + WorkflowEvent.started(), + WorkflowEvent.output(executor_id="analysis", data=data), ] proc = _make_processor(events) result = _run(proc.run(_make_input())) @@ -150,8 +143,8 @@ def test_hard_terminated_security_policy_collects_evidence(self): blocking_issues=["SECURITY_POLICY_VIOLATION"], ) events = [ - WorkflowStartedEvent(), - WorkflowOutputEvent(data=data, source_executor_id="analysis"), + WorkflowEvent.started(), + WorkflowEvent.output(executor_id="analysis", data=data), ] proc = _make_processor(events) @@ -181,8 +174,8 @@ def test_hard_terminated_security_policy_handles_collector_error(self): blocking_issues=["SECURITY_POLICY_VIOLATION"], ) events = [ - WorkflowStartedEvent(), - WorkflowOutputEvent(data=data, source_executor_id="analysis"), + WorkflowEvent.started(), + WorkflowEvent.output(executor_id="analysis", data=data), ] proc = _make_processor(events) with patch( @@ -198,8 +191,8 @@ def test_hard_terminated_security_policy_handles_collector_error(self): class TestRunOutputMissingFlow: def test_missing_output_raises_workflow_executor_failed_exception(self): events = [ - WorkflowStartedEvent(), - WorkflowOutputEvent(data=None, source_executor_id="analysis"), + WorkflowEvent.started(), + WorkflowEvent.output(executor_id="analysis", data=None), ] proc = _make_processor(events) with pytest.raises(WorkflowExecutorFailedException) as excinfo: @@ -209,8 +202,8 @@ def test_missing_output_raises_workflow_executor_failed_exception(self): def test_missing_output_with_none_source_uses_unknown(self): events = [ - WorkflowStartedEvent(), - WorkflowOutputEvent(data=None, source_executor_id=None), + WorkflowEvent.started(), + WorkflowEvent.output(executor_id=None, data=None), ] proc = _make_processor(events) with pytest.raises(WorkflowExecutorFailedException): @@ -226,9 +219,9 @@ def test_workflow_failed_event_raises_with_details(self): executor_id="yaml", ) events = [ - WorkflowStartedEvent(), - ExecutorInvokedEvent(executor_id="yaml", data=_make_input()), - WorkflowFailedEvent(details=details), + WorkflowEvent.started(), + WorkflowEvent.executor_invoked(executor_id="yaml", data=_make_input()), + WorkflowEvent.failed(details=details), ] proc = _make_processor(events) with pytest.raises(WorkflowExecutorFailedException) as excinfo: @@ -246,8 +239,8 @@ def test_workflow_failed_classifies_context_size_message(self): executor_id="design", ) events = [ - WorkflowStartedEvent(), - WorkflowFailedEvent(details=details), + WorkflowEvent.started(), + WorkflowEvent.failed(details=details), ] proc = _make_processor(events) with pytest.raises(WorkflowExecutorFailedException): @@ -261,8 +254,8 @@ def test_workflow_failed_classifies_context_error_type(self): executor_id="analysis", ) events = [ - WorkflowStartedEvent(), - WorkflowFailedEvent(details=details), + WorkflowEvent.started(), + WorkflowEvent.failed(details=details), ] proc = _make_processor(events) with pytest.raises(WorkflowExecutorFailedException): @@ -275,9 +268,9 @@ def test_executor_failed_event_is_silently_ignored(self): ) data = SimpleNamespace(is_hard_terminated=False) events = [ - WorkflowStartedEvent(), - ExecutorFailedEvent(executor_id="analysis", details=details), - WorkflowOutputEvent(data=data, source_executor_id="analysis"), + WorkflowEvent.started(), + WorkflowEvent.executor_failed(executor_id="analysis", details=details), + WorkflowEvent.output(executor_id="analysis", data=data), ] proc = _make_processor(events) result = _run(proc.run(_make_input())) @@ -288,9 +281,9 @@ class TestRunMemoryStoreLifecycle: def test_memory_store_is_registered_and_closed(self): data = SimpleNamespace(is_hard_terminated=False) events = [ - WorkflowStartedEvent(), - ExecutorCompletedEvent(executor_id="analysis", data=None), - WorkflowOutputEvent(data=data, source_executor_id="analysis"), + WorkflowEvent.started(), + WorkflowEvent.executor_completed(executor_id="analysis", data=None), + WorkflowEvent.output(executor_id="analysis", data=data), ] memory_store = MagicMock() memory_store.get_count = AsyncMock(return_value=3) @@ -304,8 +297,8 @@ def test_memory_store_is_registered_and_closed(self): def test_memory_store_close_error_is_swallowed(self): data = SimpleNamespace(is_hard_terminated=False) events = [ - WorkflowStartedEvent(), - WorkflowOutputEvent(data=data, source_executor_id="analysis"), + WorkflowEvent.started(), + WorkflowEvent.output(executor_id="analysis", data=data), ] memory_store = MagicMock() memory_store.get_count = AsyncMock(side_effect=RuntimeError("x")) @@ -318,11 +311,11 @@ def test_memory_store_close_error_is_swallowed(self): def test_executor_completed_with_memory_store_logs_count(self): data = SimpleNamespace(is_hard_terminated=False) events = [ - WorkflowStartedEvent(), - ExecutorCompletedEvent( + WorkflowEvent.started(), + WorkflowEvent.executor_completed( executor_id="analysis", data={"some": "result"} ), - WorkflowOutputEvent(data=data, source_executor_id="design"), + WorkflowEvent.output(executor_id="design", data=data), ] memory_store = MagicMock() memory_store.get_count = AsyncMock(return_value=7) diff --git a/src/processor/uv.lock b/src/processor/uv.lock index 2727133a..376ecbd9 100644 --- a/src/processor/uv.lock +++ b/src/processor/uv.lock @@ -50,14 +50,14 @@ wheels = [ [[package]] name = "agent-framework" -version = "1.0.0b260107" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "agent-framework-core", extra = ["all"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/e7/5ad52075da4e586ca94fb8806b3085ac5dea8059413e413bff88c0452e88/agent_framework-1.0.0b260107.tar.gz", hash = "sha256:a2f6508a0ca1df3b7ca4e3a64e45bac8e33cdfe02cf69e9056e37e881a58aad7", size = 2898189, upload-time = "2026-01-07T23:57:48.213Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/e8/c2ee1c4dae4a86b95091969426d11361232a0c554124ba321852a6b6b9bd/agent_framework-1.3.0.tar.gz", hash = "sha256:a13423aceaf587cf28180138151d445bd2d4ce82908cef4a6fbb85fa1771bac1", size = 5509571, upload-time = "2026-05-08T00:09:16.022Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/55/ffef27526cc26bf163ccf9d58ba87bf4e677bba343a542e7b666846f744d/agent_framework-1.0.0b260107-py3-none-any.whl", hash = "sha256:080deb32bff4ef07227a4ba709798c67079ff8a2997fe7a0aed0010adc0c18cf", size = 5554, upload-time = "2026-01-07T23:57:08.433Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/050f8f8bce8c629a88197837b4beb35cb287f880789fc01923fd5938f142/agent_framework-1.3.0-py3-none-any.whl", hash = "sha256:baaaa932639c87be99d43333f612c3b4112d6d976f0e1e72238e42a4bd572438", size = 5684, upload-time = "2026-05-08T00:09:54.064Z" }, ] [[package]] @@ -102,31 +102,29 @@ wheels = [ ] [[package]] -name = "agent-framework-azure-ai" +name = "agent-framework-azure-ai-search" version = "1.0.0b260130" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "agent-framework-core" }, - { name = "aiohttp" }, - { name = "azure-ai-agents" }, - { name = "azure-ai-projects" }, + { name = "azure-search-documents" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/ef/69ead4fcd2c21608ce35353a507df23df51872552747f803c43d1d81f612/agent_framework_azure_ai-1.0.0b260130.tar.gz", hash = "sha256:c571275089a801f961370ba824568c8b02143b1a6bb5b1d78b97c6debdf4906f", size = 32723, upload-time = "2026-01-30T18:56:41.07Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/63/81c7853aa526f3c3667871cea14667af73323c6c53d31c34be34926a9de4/agent_framework_azure_ai_search-1.0.0b260130.tar.gz", hash = "sha256:0a622fdddd7dc0287de693f2aa6f770ec52ea8d1eaca817c4276daa08001c10b", size = 13312, upload-time = "2026-01-30T19:01:08.046Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/8f/a1467c352fed5eb6ebb9567109251cc39b5b3ebb5137a2d14c71fea51bc8/agent_framework_azure_ai-1.0.0b260130-py3-none-any.whl", hash = "sha256:87f0248fe6d4f2f4146f0a56a53527af6365d4a377dc2e3d56c37cbb9deae098", size = 38542, upload-time = "2026-01-30T19:01:12.102Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ec/ac8143dbb1af2ec510f7772d712803193a6a0ad5f36b06e7ec7121df5c80/agent_framework_azure_ai_search-1.0.0b260130-py3-none-any.whl", hash = "sha256:0278c948696d7a00193a0271074c6057b57589ff98eda5544f2eafeac051d6e9", size = 13449, upload-time = "2026-01-30T19:01:23.262Z" }, ] [[package]] -name = "agent-framework-azure-ai-search" -version = "1.0.0b260130" +name = "agent-framework-azure-cosmos" +version = "1.0.0b260507" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "agent-framework-core" }, - { name = "azure-search-documents" }, + { name = "azure-cosmos" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/63/81c7853aa526f3c3667871cea14667af73323c6c53d31c34be34926a9de4/agent_framework_azure_ai_search-1.0.0b260130.tar.gz", hash = "sha256:0a622fdddd7dc0287de693f2aa6f770ec52ea8d1eaca817c4276daa08001c10b", size = 13312, upload-time = "2026-01-30T19:01:08.046Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/97/fd8b045fc4eb1d213d7a91eff6e48e030fdb67da30505f46f1ed20a7aa48/agent_framework_azure_cosmos-1.0.0b260507.tar.gz", hash = "sha256:2c8ec2d5eae52b9e92fd14b4adecd5a52a900a7897589549c32852d9488112c7", size = 10984, upload-time = "2026-05-08T00:09:22.016Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/ec/ac8143dbb1af2ec510f7772d712803193a6a0ad5f36b06e7ec7121df5c80/agent_framework_azure_ai_search-1.0.0b260130-py3-none-any.whl", hash = "sha256:0278c948696d7a00193a0271074c6057b57589ff98eda5544f2eafeac051d6e9", size = 13449, upload-time = "2026-01-30T19:01:23.262Z" }, + { url = "https://files.pythonhosted.org/packages/84/b9/6ac1960dae49ecde8ea906b302abe79b66d09d4cf74f8ed3f7dd9fc6230f/agent_framework_azure_cosmos-1.0.0b260507-py3-none-any.whl", hash = "sha256:c1d7ae4a560b592d2bff9c1ec75a7910101baf8c1778443644cc8cb81c82c1a1", size = 11989, upload-time = "2026-05-08T00:09:02.858Z" }, ] [[package]] @@ -145,6 +143,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/fa/200b40db670f79f561ff1e69e9626729ceb6486af970e3489f6c3a295d76/agent_framework_azurefunctions-1.0.0b260130-py3-none-any.whl", hash = "sha256:7d529a0bad67caa38d8823462c439e97de5e1cf364c0e9a0895df5fb44996f64", size = 17788, upload-time = "2026-01-30T18:56:45.741Z" }, ] +[[package]] +name = "agent-framework-bedrock" +version = "1.0.0b260507" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "boto3" }, + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/86/0b7dd9d1c043b251ff8bd0e037a20495c82c798914db0372040625cae889/agent_framework_bedrock-1.0.0b260507.tar.gz", hash = "sha256:38953ab30f7aff651a9c85c1ceeefd2ad85fa094b3316858930f1c18dcaff2c6", size = 17467, upload-time = "2026-05-08T00:09:24.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/b4/fc4277a50b7a0a7cd038e4511a0215fb98ab5e394f719506e30c31854335/agent_framework_bedrock-1.0.0b260507-py3-none-any.whl", hash = "sha256:28ce485c639e467ca4fae4d5b747cd7f9438b8145ca096c658ab5c694611edcc", size = 13907, upload-time = "2026-05-08T00:09:18.84Z" }, +] + [[package]] name = "agent-framework-chatkit" version = "1.0.0b260130" @@ -158,6 +170,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/f1/68496e52aa36e66cf2962b8a8c6937053e2e57ad5f135b6983d705172554/agent_framework_chatkit-1.0.0b260130-py3-none-any.whl", hash = "sha256:a7814a5b222de7a0ac57fb89f4a6e534521c7e58bdc86a6465885fb9d57e63f1", size = 11712, upload-time = "2026-01-30T18:56:49.14Z" }, ] +[[package]] +name = "agent-framework-claude" +version = "1.0.0b260507" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "claude-agent-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/1a/1a1c810e7c74075a4766ac0de66e3e510e0267533baa41a089ab1eb5bf01/agent_framework_claude-1.0.0b260507.tar.gz", hash = "sha256:0daccfef8141470fd206bb8b30925a44ba42ec6fb8946934dbcefe50cfeae14c", size = 11618, upload-time = "2026-05-08T00:08:57.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/f8/4977b7d7f1f2ea82c396de07b04f999c58475476722836f3ed0337722495/agent_framework_claude-1.0.0b260507-py3-none-any.whl", hash = "sha256:3ebd1d391b4413512970da62eb5377099ecd66305048594ec5b65cbdf141623f", size = 11588, upload-time = "2026-05-08T00:09:00.32Z" }, +] + [[package]] name = "agent-framework-copilotstudio" version = "1.0.0b260130" @@ -173,23 +198,17 @@ wheels = [ [[package]] name = "agent-framework-core" -version = "1.0.0b260107" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "azure-identity" }, - { name = "mcp", extra = ["ws"] }, - { name = "openai" }, { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, - { name = "opentelemetry-semantic-conventions-ai" }, - { name = "packaging" }, { name = "pydantic" }, - { name = "pydantic-settings" }, + { name = "python-dotenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/44/06f5d2c99dd7bdb82c2cb5cbc354b5bc6af72d1886d20eff1dff83508fae/agent_framework_core-1.0.0b260107.tar.gz", hash = "sha256:12636fb64664c6153546f0d85dafccdbe57226767c14b3f38985867389f980bb", size = 3574757, upload-time = "2026-01-07T23:57:16.113Z" } +sdist = { url = "https://files.pythonhosted.org/packages/90/59/4c212abdb93074677d643e31a3c21e33ff26a3ccc351145475cd1ffffad7/agent_framework_core-1.3.0.tar.gz", hash = "sha256:91c3659718b733f70dde6fb3626edb044733e0f7aa5f9726c9774e17fae328ef", size = 365395, upload-time = "2026-05-08T00:09:09.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5a/8c6315a2ca119ad48340344616d4b8e77fd68e2892f82c402069a52ad647/agent_framework_core-1.0.0b260107-py3-none-any.whl", hash = "sha256:5bd119b8d30dc2d5bee1c4a5c3597d7afc808a52e4de148725c4f2d9bcc7632b", size = 5687298, upload-time = "2026-01-07T23:57:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/56/f2/c4258333f2691ee10869bf72f51d423808962ccf0c195b1f893c06c348ad/agent_framework_core-1.3.0-py3-none-any.whl", hash = "sha256:b7a5baf2beb383e9042af057df79dae4fda0b836cbc8530b3b2a57a3c12bb7ac", size = 407978, upload-time = "2026-05-08T00:09:32.752Z" }, ] [package.optional-dependencies] @@ -197,18 +216,28 @@ all = [ { name = "agent-framework-a2a" }, { name = "agent-framework-ag-ui" }, { name = "agent-framework-anthropic" }, - { name = "agent-framework-azure-ai" }, { name = "agent-framework-azure-ai-search" }, + { name = "agent-framework-azure-cosmos" }, { name = "agent-framework-azurefunctions" }, + { name = "agent-framework-bedrock" }, { name = "agent-framework-chatkit" }, + { name = "agent-framework-claude" }, { name = "agent-framework-copilotstudio" }, { name = "agent-framework-declarative" }, { name = "agent-framework-devui" }, + { name = "agent-framework-durabletask" }, + { name = "agent-framework-foundry" }, + { name = "agent-framework-foundry-local" }, + { name = "agent-framework-github-copilot" }, + { name = "agent-framework-hyperlight", marker = "(python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'AMD64' and sys_platform == 'win32')" }, { name = "agent-framework-lab" }, { name = "agent-framework-mem0" }, { name = "agent-framework-ollama" }, + { name = "agent-framework-openai" }, + { name = "agent-framework-orchestrations" }, { name = "agent-framework-purview" }, { name = "agent-framework-redis" }, + { name = "mcp" }, ] [[package]] @@ -255,6 +284,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/22/122ed515935926137cc3c6ca795ef01b30feb82160cfc0f29a34f9d603de/agent_framework_durabletask-1.0.0b260130-py3-none-any.whl", hash = "sha256:a46e292800d10a62ce0923efe753594ddbf0bd6d1bb6e1258380f0dbf7d0302f", size = 36357, upload-time = "2026-01-30T19:01:24.057Z" }, ] +[[package]] +name = "agent-framework-foundry" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "agent-framework-openai" }, + { name = "azure-ai-inference" }, + { name = "azure-ai-projects" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/f6/8700acd779cbffd933dcb5dc878abce3e0a2f536962567665ccc49965715/agent_framework_foundry-1.3.0.tar.gz", hash = "sha256:8a4b137efa0a7000e60fb396ad90e01c271d14a52f1325f1f0a32177d944bcff", size = 32620, upload-time = "2026-05-08T00:09:04.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/53/9acf5831263d4fcd1d5b8d39af99ee430ec2710d2f9adeab5a1fe7559da0/agent_framework_foundry-1.3.0-py3-none-any.whl", hash = "sha256:49987bc01b077f6c60af33c475f9770a02b4ff6d6822aede18fc5471b46ffd41", size = 37052, upload-time = "2026-05-08T00:09:13.139Z" }, +] + +[[package]] +name = "agent-framework-foundry-local" +version = "1.0.0b260507" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "agent-framework-openai" }, + { name = "foundry-local-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/03/8f0b8a2209fd091903bbb068c4458f19c74e48d37f4fa08748d76c3f3091/agent_framework_foundry_local-1.0.0b260507.tar.gz", hash = "sha256:fc2d98ff1f98d0481544c3ad8453f2d56096203fd368d0b68f52ef6ae4c7b0a6", size = 6719, upload-time = "2026-05-08T00:09:35.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/07/1120c862714d89f40d4575a052a495f86bda0fdb4132d5c4597c7a735875/agent_framework_foundry_local-1.0.0b260507-py3-none-any.whl", hash = "sha256:515346ca7716d86c9a4110db9f5586a65c4970ac442aaa00725d27341c5825df", size = 7176, upload-time = "2026-05-08T00:09:28.74Z" }, +] + +[[package]] +name = "agent-framework-github-copilot" +version = "1.0.0b260507" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "github-copilot-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/0f/0cab3d20c84ff309f820d02e810c1fa17f1a6fc432775605e34f651955ae/agent_framework_github_copilot-1.0.0b260507.tar.gz", hash = "sha256:f8640d4a18beca67a83b833b5d23f873aa5e1d4e91423ee1923d650b7b97d06d", size = 12546, upload-time = "2026-05-08T00:08:59.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/75/c8747c30acf236daa97063763fd16e443a2734e80c5678c42e103d1b50d6/agent_framework_github_copilot-1.0.0b260507-py3-none-any.whl", hash = "sha256:53a5daae86824fce017f30637edd5e50675e4630da5be09bb259383713198f40", size = 12510, upload-time = "2026-05-08T00:09:42.889Z" }, +] + +[[package]] +name = "agent-framework-hyperlight" +version = "1.0.0b260507" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core", marker = "python_full_version < '3.14'" }, + { name = "hyperlight-sandbox", marker = "python_full_version < '3.14'" }, + { name = "hyperlight-sandbox-backend-wasm", marker = "(python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "hyperlight-sandbox-python-guest", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/1f/52a2541d4a0bc5657ca9c2ef4f85885fb323682052da3fc1451eabafb73d/agent_framework_hyperlight-1.0.0b260507.tar.gz", hash = "sha256:845baab7439ac7b94ee53805cf3d32d0eea3b77a040d0f1b367f0a395fd8c08b", size = 19057, upload-time = "2026-05-08T00:09:56.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/d8/c2e0d3f63ea53f9897bd6c31a3d07c41c48a7b30fd7a1c2b5182fffe32ca/agent_framework_hyperlight-1.0.0b260507-py3-none-any.whl", hash = "sha256:121b464edf32f3db0e5b2891525d8937f0854bc19102a7c50b1905ff29063da7", size = 19589, upload-time = "2026-05-08T00:09:52.71Z" }, +] + [[package]] name = "agent-framework-lab" version = "1.0.0b251024" @@ -293,6 +379,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/27/23e23a1919592dcf2aaf25aa9950a7dbda77c4ba03cba8843491b9f12024/agent_framework_ollama-1.0.0b260130-py3-none-any.whl", hash = "sha256:55e4e17f226ad61e8a9dcbbcc24ab006a3480043ecb4d32c12d2444f628054d6", size = 9167, upload-time = "2026-01-30T19:01:05.647Z" }, ] +[[package]] +name = "agent-framework-openai" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/54/26595b5fa394dd91a5bd434f87b1e7d781545efbf0bd8053de193f89ec63/agent_framework_openai-1.3.0.tar.gz", hash = "sha256:770828447875ee169dde8cd2f2a0343f427d856af7c83895ca12d59f8c24a7f2", size = 49146, upload-time = "2026-05-08T00:09:44.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/d8/a0e0af08123d3c2ff3f42b6976eed155536c73be4d61b898bc15cf31a38c/agent_framework_openai-1.3.0-py3-none-any.whl", hash = "sha256:1953dcb9f3e852362be84b4316ee69639313a7f119eab6ce8c88949e1f24aa4b", size = 54041, upload-time = "2026-05-08T00:09:17.744Z" }, +] + +[[package]] +name = "agent-framework-orchestrations" +version = "1.0.0b260507" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/84/1a26978d91c40f62ef472fd36d1502545bb7425b94b03765c41b322e3398/agent_framework_orchestrations-1.0.0b260507.tar.gz", hash = "sha256:3f17281a2603240e3eed26174cab6b3dca153cb18cec8380f4719e598a55013f", size = 55971, upload-time = "2026-05-08T00:09:37.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/f2df27ba789130470311e7487d19815483f837094672408a22655b33784a/agent_framework_orchestrations-1.0.0b260507-py3-none-any.whl", hash = "sha256:396a5ed962c2a3b1f09d8fc777933397df486bdae0a5f81cf63595c4c6f102de", size = 62074, upload-time = "2026-05-08T00:09:31.24Z" }, +] + [[package]] name = "agent-framework-purview" version = "1.0.0b260130" @@ -567,16 +678,16 @@ wheels = [ [[package]] name = "azure-ai-agents" -version = "1.2.0b5" +version = "1.2.0b6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/57/8adeed578fa8984856c67b4229e93a58e3f6024417d448d0037aafa4ee9b/azure_ai_agents-1.2.0b5.tar.gz", hash = "sha256:1a16ef3f305898aac552269f01536c34a00473dedee0bca731a21fdb739ff9d5", size = 394876, upload-time = "2025-09-30T01:55:02.328Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/32/f4e534dc05dfb714705df56a190d690c5452cd4dd7e936612cb1adddc44f/azure_ai_agents-1.2.0b6.tar.gz", hash = "sha256:d3c10848c3b19dec98a292f8c10cee4ba4aac1050d4faabf9c2e2456b727f528", size = 396865, upload-time = "2025-10-24T18:04:47.877Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/6d/15070d23d7a94833a210da09d5d7ed3c24838bb84f0463895e5d159f1695/azure_ai_agents-1.2.0b5-py3-none-any.whl", hash = "sha256:257d0d24a6bf13eed4819cfa5c12fb222e5908deafb3cbfd5711d3a511cc4e88", size = 217948, upload-time = "2025-09-30T01:55:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/96/d0/930c522f5fa9da163de057e57f8b44539424e13f46618c52624ebc712293/azure_ai_agents-1.2.0b6-py3-none-any.whl", hash = "sha256:ce23ad8fb9791118905be1ec8eae5c907cca2e536a455f1d3b830062c72cf2a7", size = 217950, upload-time = "2025-10-24T18:04:49.72Z" }, ] [[package]] @@ -595,7 +706,7 @@ wheels = [ [[package]] name = "azure-ai-projects" -version = "2.0.0b3" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -603,10 +714,11 @@ dependencies = [ { name = "azure-storage-blob" }, { name = "isodate" }, { name = "openai" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/e0/3512d3f07e9dd2eb4af684387c31598c435bd87833b6a81850972963cb9c/azure_ai_projects-2.0.0b3.tar.gz", hash = "sha256:6d09ad110086e450a47b991ee8a3644f1be97fa3085d5981d543f900d78f4505", size = 431749, upload-time = "2026-01-06T05:31:25.849Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/76/3fdede8eddfe5927a571898a15f0288ba30fee78e5ba099f88df3ded70af/azure_ai_projects-2.1.0.tar.gz", hash = "sha256:f0749fa9a174255aa1a5550fb6078208521518472907a4c6dd552767d9b39caa", size = 543343, upload-time = "2026-04-20T17:06:48.751Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/b6/8fbd4786bb5c0dd19eaff86ddce0fbfb53a6f90d712038272161067a076a/azure_ai_projects-2.0.0b3-py3-none-any.whl", hash = "sha256:3b3048a3ba3904d556ba392b7bd20b6e84c93bb39df6d43a6470cdb0ad08af8c", size = 240717, upload-time = "2026-01-06T05:31:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/f7/f6/4984e7772a97c7a9e6505a3de8e55a5070fa2b02cd7e980da91e0d9b9b97/azure_ai_projects-2.1.0-py3-none-any.whl", hash = "sha256:6f259d8eb9167d2dfd372006d0221a8118faeaeb05829fa898b595bc6f19c699", size = 274309, upload-time = "2026-04-20T17:06:50.542Z" }, ] [[package]] @@ -834,6 +946,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, ] +[[package]] +name = "boto3" +version = "1.43.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/27/ae1a71e945ce7bde39b0677b252fe7d8a0ad7fa3d6b724d78b81469c08fe/boto3-1.43.10.tar.gz", hash = "sha256:27342e5d5f6170fcc8d1e21cdd939af2448d58ac56b08d494250eaad998e30c7", size = 113159, upload-time = "2026-05-18T20:42:34.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/1b/439234598449f846b17333e67ec63c3dd8f8880c13de9089383b4bab58c3/boto3-1.43.10-py3-none-any.whl", hash = "sha256:83918184d95967e4c6e9ed1e9a2f58250b291e6ea2cb847ab0825d52596b39e5", size = 140534, upload-time = "2026-05-18T20:42:32.009Z" }, +] + +[[package]] +name = "botocore" +version = "1.43.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/4e/c127dd0628c551f10cb890e279a9c0e367523b880c4cd3e81a1e76886174/botocore-1.43.10.tar.gz", hash = "sha256:2f4af585b41dbccdfc9f49677d7bd72d713a12ef89a1dc9c8538a927649498bf", size = 15365344, upload-time = "2026-05-18T20:42:21.562Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/0e/41f64d6c267edf03f4fe8f461edc4c644243e77c8d5a1fef1e0166ac4ed0/botocore-1.43.10-py3-none-any.whl", hash = "sha256:8a0176d8c2f8bebe95d4f923a824a1ace04b02f360e220681c388e097f32c3b6", size = 15043571, upload-time = "2026-05-18T20:42:16.664Z" }, +] + [[package]] name = "cachetools" version = "7.1.1" @@ -1012,6 +1152,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] +[[package]] +name = "claude-agent-sdk" +version = "0.1.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "mcp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/dd/2818538efd18ed4ef72d4775efa75bb36cbea0fa418eda51df85ee9c2424/claude_agent_sdk-0.1.48.tar.gz", hash = "sha256:ee294d3f02936c0b826119ffbefcf88c67731cf8c2d2cb7111ccc97f76344272", size = 87375, upload-time = "2026-03-07T00:21:37.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/cf/bbbdee52ee0c63c8709b0ac03ce3c1da5bdc37def5da0eca63363448744f/claude_agent_sdk-0.1.48-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5761ff1d362e0f17c2b1bfd890d1c897f0aa81091e37bbd15b7d06f05ced552d", size = 57559306, upload-time = "2026-03-07T00:21:20.011Z" }, + { url = "https://files.pythonhosted.org/packages/57/d1/2179154b88d4cf6ba1cf6a15066ee8e96257aaeb1330e625e809ba2f28eb/claude_agent_sdk-0.1.48-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:39c1307daa17e42fa8a71180bb20af8a789d72d3891fc93519ff15540badcb83", size = 73980309, upload-time = "2026-03-07T00:21:24.592Z" }, + { url = "https://files.pythonhosted.org/packages/dc/99/55b0cd3bf54a7449e744d23cf50be104e9445cf623e1ed75722112aa6264/claude_agent_sdk-0.1.48-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:543d70acba468eccfff836965a14b8ac88cf90809aeeb88431dfcea3ee9a2fa9", size = 74583686, upload-time = "2026-03-07T00:21:28.969Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f6/4851bd9a238b7aadba7639eb906aca7da32a51f01563fa4488469c608b3a/claude_agent_sdk-0.1.48-py3-none-win_amd64.whl", hash = "sha256:0d37e60bd2b17efc3f927dccef080f14897ab62cd1d0d67a4abc8a0e2d4f1006", size = 74956045, upload-time = "2026-03-07T00:21:33.475Z" }, +] + [[package]] name = "click" version = "8.3.3" @@ -1280,6 +1436,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, ] +[[package]] +name = "foundry-local-sdk" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, + { name = "tqdm" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/6b/76a7fe8f9f4c52cc84eaa1cd1b66acddf993496d55d6ea587bf0d0854d1c/foundry_local_sdk-0.5.1-py3-none-any.whl", hash = "sha256:f3639a3666bc3a94410004a91671338910ac2e1b8094b1587cc4db0f4a7df07e", size = 14003, upload-time = "2025-11-21T05:39:58.099Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -1382,6 +1551,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/8c/dce3b1b7593858eba995b2dfdb833f872c7f863e3da92aab7128a6b11af4/furl-2.1.4-py2.py3-none-any.whl", hash = "sha256:da34d0b34e53ffe2d2e6851a7085a05d96922b5b578620a37377ff1dbeeb11c8", size = 27550, upload-time = "2025-03-09T05:36:19.928Z" }, ] +[[package]] +name = "github-copilot-sdk" +version = "1.0.0b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dateutil" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/fe/2cb98d4b9f57f8062ea72775bde72aed1958305016753f7296398e0ceb45/github_copilot_sdk-1.0.0b2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:1b5941d8b6e3d94d42a5bec6607a26f562e6535d5c981089d23d3d224b94601c", size = 67061619, upload-time = "2026-05-06T20:02:08.636Z" }, + { url = "https://files.pythonhosted.org/packages/57/45/76567821b2d36f81e6bca78c98d265e2762733f765fa51d69602b7f81867/github_copilot_sdk-1.0.0b2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b8f6a087a0cf02bb0d33976e8f8c009578d84d701a0b28d52051304791ac70", size = 63790955, upload-time = "2026-05-06T20:02:12.354Z" }, + { url = "https://files.pythonhosted.org/packages/15/67/684b0da0b1207a2bdf025c22ee075d34a1736d61a4973651035d4fd4d8dc/github_copilot_sdk-1.0.0b2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:f403638c11b82bddb81c94675fc4e8014a1bb2e86a679a39fa167dcc3ad5416a", size = 69538664, upload-time = "2026-05-06T20:02:16.363Z" }, + { url = "https://files.pythonhosted.org/packages/57/1d/80d88ecf83683535d1a16d4817f1683db3b125f52a924ebdfe9764f5e4c3/github_copilot_sdk-1.0.0b2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:433d16bb31171fee8d3a5b70259c527f63b297e83a8f8761ae1f16f14d641f32", size = 68163648, upload-time = "2026-05-06T20:02:21.139Z" }, + { url = "https://files.pythonhosted.org/packages/32/d3/b72aa2fbb3194b50b53e8cb1484f5606a1f8eedcdb0bfb5747da52079553/github_copilot_sdk-1.0.0b2-py3-none-win_amd64.whl", hash = "sha256:a6e9782dae4c3c2ab3527b45bb5de0f61998104c10e9ff64698280eaf37ab5dd", size = 62649144, upload-time = "2026-05-06T20:02:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e2/be95b8ea0ac11d1ca474e28a59284f4e395c2710734eadfb657f5de8ace2/github_copilot_sdk-1.0.0b2-py3-none-win_arm64.whl", hash = "sha256:2e97d0ce4bad67dc5929091cb429e7bbae7d4643e4908a6af256a41439000740", size = 60374365, upload-time = "2026-05-06T20:02:29.02Z" }, +] + [[package]] name = "google-api-core" version = "2.30.3" @@ -1587,6 +1773,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, ] +[[package]] +name = "hyperlight-sandbox" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/5e/14c69eac7e1c74fbd556c6f890729a3d232d32d65cd9f8cfde72c0534e61/hyperlight_sandbox-0.4.0.tar.gz", hash = "sha256:90d7b91d4d8e17054e282b0daed55c261392a748dafc57e6416d3184cdac910b", size = 9262, upload-time = "2026-05-02T00:00:02.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/e3/b8c106a274c08a30261105afa5511e0ec55960e86b2f6c51e3095e96647c/hyperlight_sandbox-0.4.0-py3-none-any.whl", hash = "sha256:7ae44d2448ed6ecadb368373c7e45eb395521e7774c86a1cbc1ef9cdfc25cd2a", size = 5723, upload-time = "2026-05-02T00:00:03.811Z" }, +] + +[[package]] +name = "hyperlight-sandbox-backend-wasm" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/e5/3cdf21594eb28de7ca1a5a1ade27e137c8f3d7ab48d65fed87a3b74c4039/hyperlight_sandbox_backend_wasm-0.4.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:ff4627950708909202ee24c6175dc41e9c05479f89393575e3de0f14e6f5a193", size = 3918189, upload-time = "2026-05-01T23:59:16.666Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/b1bb9893bbeb979d133dc542520125dcbf8394d1a2537e753118b37c7cab/hyperlight_sandbox_backend_wasm-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cce7dc28b9ded034a11a9a8cf7b9ffb838e29006be8d2e01646dd131ba501b73", size = 3383520, upload-time = "2026-05-01T23:59:27.261Z" }, + { url = "https://files.pythonhosted.org/packages/8c/29/deee4e31086628750f0ce1f67da1e28c613fd2df68465de130cbfe51e72d/hyperlight_sandbox_backend_wasm-0.4.0-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:88e194515e4784f68676b6906c98a4000f913c93172cf07981d8a977e756bbd6", size = 3917939, upload-time = "2026-05-01T23:59:14.805Z" }, + { url = "https://files.pythonhosted.org/packages/15/2a/6822aec3c04c46893406d0d6ed576dbdb4b5c1d76a0124dc220bb45b0d34/hyperlight_sandbox_backend_wasm-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:d1cd2269a5651ea9be1f94a3e3388f6af69e41dbc2b808c3b806481fe17ce163", size = 3383110, upload-time = "2026-05-01T23:59:23.736Z" }, +] + +[[package]] +name = "hyperlight-sandbox-python-guest" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fd/816d1f3f277ff149a45da5381967aa04c22bc7702b5c14f0acfd9db2cee7/hyperlight_sandbox_python_guest-0.4.0.tar.gz", hash = "sha256:64c3c6c13fe550bf5b680fa0b965cf62bc4668084cc275c3467e3c015e6ead36", size = 21657381, upload-time = "2026-05-01T23:59:46.589Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/ba/efb9aacf993f0ac142da5beb9177b221e49dc860c6ea398de236015a52a0/hyperlight_sandbox_python_guest-0.4.0-py3-none-any.whl", hash = "sha256:0789eb794b99606288402ed3921b5e2630800a69d24117ecd9b82e816568202d", size = 21822062, upload-time = "2026-05-01T23:59:50.99Z" }, +] + [[package]] name = "id" version = "1.6.1" @@ -1773,6 +1988,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, ] +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + [[package]] name = "joserfc" version = "1.6.4" @@ -1979,11 +2203,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076, upload-time = "2025-12-19T10:19:55.416Z" }, ] -[package.optional-dependencies] -ws = [ - { name = "websockets" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -2454,19 +2673,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/a6/83dc2ab6fa397ee66fba04fe2e74bdf7be3b3870005359ceb7689103c058/opentelemetry_semantic_conventions-0.62b1-py3-none-any.whl", hash = "sha256:cf506938103d331fbb78eded0d9788095f7fd59016f2bda813c3324e5a74a93c", size = 231620, upload-time = "2026-04-24T13:15:35.454Z" }, ] -[[package]] -name = "opentelemetry-semantic-conventions-ai" -version = "0.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-sdk" }, - { name = "opentelemetry-semantic-conventions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/24/02/10aeacc37a38a3a8fa16ff67bec1ae3bf882539f6f9efb0f70acf802ca2d/opentelemetry_semantic_conventions_ai-0.5.1.tar.gz", hash = "sha256:153906200d8c1d2f8e09bd78dbef526916023de85ac3dab35912bfafb69ff04c", size = 26533, upload-time = "2026-03-26T14:20:38.73Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/22/41fb05f1dc5fda2c468e05a41814c20859016c85117b66c8a257cae814f6/opentelemetry_semantic_conventions_ai-0.5.1-py3-none-any.whl", hash = "sha256:25aeb22bd261543b4898a73824026d96770e5351209c7d07a0b1314762b1f6e4", size = 11250, upload-time = "2026-03-26T14:20:37.108Z" }, -] - [[package]] name = "orderedmultidict" version = "1.0.2" @@ -2627,12 +2833,12 @@ dev = [ [package.metadata] requires-dist = [ - { name = "agent-framework", specifier = "==1.0.0b260107" }, + { name = "agent-framework", specifier = "==1.3.0" }, { name = "aiohttp", specifier = "==3.13.4" }, { name = "art", specifier = "==6.5" }, - { name = "azure-ai-agents", specifier = "==1.2.0b5" }, + { name = "azure-ai-agents", specifier = "==1.2.0b6" }, { name = "azure-ai-inference", specifier = "==1.0.0b9" }, - { name = "azure-ai-projects", specifier = "==2.0.0b3" }, + { name = "azure-ai-projects", specifier = "==2.1.0" }, { name = "azure-appconfiguration", specifier = "==1.7.2" }, { name = "azure-core", specifier = "==1.38.0" }, { name = "azure-cosmos", specifier = "==4.15.0" }, @@ -3437,6 +3643,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, ] +[[package]] +name = "s3transfer" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/ec/7c692cde9125b77e84b307354d4fb705f98b8ccad59a036d5957ca75bfc3/s3transfer-0.17.0.tar.gz", hash = "sha256:9edeb6d1c3c2f89d6050348548834ad8289610d886e5bf7b7207728bd43ce33a", size = 155337, upload-time = "2026-04-29T22:07:36.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/72/c6c32d2b657fa3dad1de340254e14390b1e334ce38268b7ad51abda3c8c2/s3transfer-0.17.0-py3-none-any.whl", hash = "sha256:ce3801712acf4ad3e89fb9990df97b4972e93f4b3b0004d214be5bce12814c20", size = 86811, upload-time = "2026-04-29T22:07:34.966Z" }, +] + [[package]] name = "sas-cosmosdb" version = "0.1.4" From 4e0a59aef22b8340fdae5c48572e802abd64222a Mon Sep 17 00:00:00 2001 From: Dhanushree-Microsoft Date: Thu, 21 May 2026 12:32:10 +0530 Subject: [PATCH 02/25] refactor: remove ImportError handling for agent_framework imports --- .../src/libs/agent_framework/agent_builder.py | 29 +++++---------- .../src/libs/agent_framework/agent_info.py | 5 +-- .../agent_framework/agent_speaking_capture.py | 5 +-- .../azure_openai_response_retry.py | 15 ++++---- .../agent_framework/groupchat_orchestrator.py | 35 ++++++------------- .../src/libs/agent_framework/middlewares.py | 32 ++++++----------- .../shared_memory_context_provider.py | 18 ++++------ .../src/libs/base/orchestrator_base.py | 10 +----- .../orchestration/analysis_orchestrator.py | 5 +-- .../yaml_convert_orchestrator.py | 9 +---- .../orchestration/design_orchestrator.py | 9 +---- .../documentation_orchestrator.py | 9 +---- 12 files changed, 51 insertions(+), 130 deletions(-) diff --git a/src/processor/src/libs/agent_framework/agent_builder.py b/src/processor/src/libs/agent_framework/agent_builder.py index 6cae1a67..6a7e4409 100644 --- a/src/processor/src/libs/agent_framework/agent_builder.py +++ b/src/processor/src/libs/agent_framework/agent_builder.py @@ -6,26 +6,15 @@ from collections.abc import Callable, MutableMapping, Sequence from typing import Any, Literal -try: - from agent_framework import ( - Agent, - AgentMiddleware, - BaseChatClient, - ChatMiddleware, - ContextProvider, - FunctionTool, - ToolMode, - ) -except ImportError: - from agent_framework import ( - AgentMiddleware, - BaseChatClient, - ChatAgent as Agent, - ChatMiddleware, - ContextProvider, - ToolMode, - ToolProtocol as FunctionTool, - ) +from agent_framework import ( + Agent, + AgentMiddleware, + BaseChatClient, + ChatMiddleware, + ContextProvider, + FunctionTool, + ToolMode, +) from pydantic import BaseModel from libs.agent_framework.agent_info import AgentInfo diff --git a/src/processor/src/libs/agent_framework/agent_info.py b/src/processor/src/libs/agent_framework/agent_info.py index 6e3dfac0..82f657b6 100644 --- a/src/processor/src/libs/agent_framework/agent_info.py +++ b/src/processor/src/libs/agent_framework/agent_info.py @@ -5,10 +5,7 @@ from typing import Any, Callable, MutableMapping, Sequence -try: - from agent_framework import FunctionTool -except ImportError: - from agent_framework import ToolProtocol as FunctionTool +from agent_framework import FunctionTool from jinja2 import Template from openai import BaseModel from pydantic import Field diff --git a/src/processor/src/libs/agent_framework/agent_speaking_capture.py b/src/processor/src/libs/agent_framework/agent_speaking_capture.py index 5dac0cba..63608969 100644 --- a/src/processor/src/libs/agent_framework/agent_speaking_capture.py +++ b/src/processor/src/libs/agent_framework/agent_speaking_capture.py @@ -6,10 +6,7 @@ from datetime import datetime from typing import Any, Callable, Optional -try: - from agent_framework import AgentContext, AgentMiddleware -except ImportError: - from agent_framework import AgentMiddleware, AgentRunContext as AgentContext +from agent_framework import AgentContext, AgentMiddleware class AgentSpeakingCaptureMiddleware(AgentMiddleware): diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index b51e324a..91cdf4c9 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -12,14 +12,13 @@ from dataclasses import dataclass from typing import Any, AsyncIterable, MutableSequence -try: - from agent_framework.azure import AzureOpenAIResponsesClient -except ImportError: - class AzureOpenAIResponsesClient: - def __init__(self, *args: Any, **kwargs: Any): - raise NotImplementedError( - "AzureOpenAIResponsesClient was removed from agent_framework.azure in 1.3.0." - ) +# agent_framework 1.3.0 removed AzureOpenAIResponsesClient from agent_framework.azure. +# Keep this stub so legacy code paths fail explicitly if invoked. +class AzureOpenAIResponsesClient: + def __init__(self, *args: Any, **kwargs: Any): + raise NotImplementedError( + "AzureOpenAIResponsesClient was removed from agent_framework.azure in 1.3.0." + ) from tenacity import ( AsyncRetrying, retry_if_exception, diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index ab9975c1..02be619e 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -21,30 +21,17 @@ from datetime import datetime from typing import Any, Awaitable, Callable, Generic, Mapping, Sequence, TypeVar -try: - from agent_framework import ( - Agent, - AgentResponseUpdate, - Executor, - Message, - Role, - SupportsAgentRun, - Workflow, - WorkflowBuilder as GroupChatBuilder, - WorkflowEvent as WorkflowOutputEvent, - ) -except ImportError: - from agent_framework import ( - AgentProtocol as SupportsAgentRun, - AgentRunUpdateEvent as AgentResponseUpdate, - ChatAgent as Agent, - ChatMessage as Message, - Executor, - GroupChatBuilder, - Role, - Workflow, - WorkflowOutputEvent, - ) +from agent_framework import ( + Agent, + AgentResponseUpdate, + Executor, + Message, + Role, + SupportsAgentRun, + Workflow, + WorkflowBuilder as GroupChatBuilder, + WorkflowEvent as WorkflowOutputEvent, +) from mem0 import AsyncMemory from pydantic import BaseModel, ValidationError diff --git a/src/processor/src/libs/agent_framework/middlewares.py b/src/processor/src/libs/agent_framework/middlewares.py index 0319a6a0..1f26d547 100644 --- a/src/processor/src/libs/agent_framework/middlewares.py +++ b/src/processor/src/libs/agent_framework/middlewares.py @@ -6,28 +6,16 @@ import time from collections.abc import Awaitable, Callable -try: - from agent_framework import ( - AgentContext, - AgentMiddleware, - ChatContext, - ChatMiddleware, - FunctionInvocationContext, - FunctionMiddleware, - Message, - Role, - ) -except ImportError: - from agent_framework import ( - AgentMiddleware, - AgentRunContext as AgentContext, - ChatContext, - ChatMessage as Message, - ChatMiddleware, - FunctionInvocationContext, - FunctionMiddleware, - Role, - ) +from agent_framework import ( + AgentContext, + AgentMiddleware, + ChatContext, + ChatMiddleware, + FunctionInvocationContext, + FunctionMiddleware, + Message, + Role, +) ROLE_USER = getattr(Role, "USER", "user") diff --git a/src/processor/src/libs/agent_framework/shared_memory_context_provider.py b/src/processor/src/libs/agent_framework/shared_memory_context_provider.py index 16b64e00..fd95a5de 100644 --- a/src/processor/src/libs/agent_framework/shared_memory_context_provider.py +++ b/src/processor/src/libs/agent_framework/shared_memory_context_provider.py @@ -18,17 +18,13 @@ from collections.abc import MutableSequence, Sequence from typing import TYPE_CHECKING -try: - from agent_framework import Context, ContextProvider, Message -except ImportError: - try: - from agent_framework import ContextProvider, Message - except ImportError: - from agent_framework import ChatMessage as Message, ContextProvider - - class Context: - def __init__(self, instructions: str | None = None, **kwargs): - self.instructions = instructions +from agent_framework import ContextProvider, Message + + +class Context: + def __init__(self, instructions: str | None = None, **kwargs): + self.instructions = instructions + if TYPE_CHECKING: from libs.agent_framework.qdrant_memory_store import QdrantMemoryStore diff --git a/src/processor/src/libs/base/orchestrator_base.py b/src/processor/src/libs/base/orchestrator_base.py index 2d6616e9..90b564a8 100644 --- a/src/processor/src/libs/base/orchestrator_base.py +++ b/src/processor/src/libs/base/orchestrator_base.py @@ -9,15 +9,7 @@ from abc import abstractmethod from typing import Any, Callable, Generic, MutableMapping, Sequence, TypeVar -try: - from agent_framework import Agent, FunctionTool, ToolResultCompactionStrategy -except ImportError: - from agent_framework import ChatAgent as Agent, ToolProtocol as FunctionTool - - try: - from agent_framework import ToolResultCompactionStrategy - except ImportError: - ToolResultCompactionStrategy = None # type: ignore[assignment,misc] +from agent_framework import Agent, FunctionTool, ToolResultCompactionStrategy from libs.agent_framework.agent_builder import AgentBuilder from libs.agent_framework.agent_framework_helper import ClientType diff --git a/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py b/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py index 461005a6..1ce73a68 100644 --- a/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py +++ b/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py @@ -12,10 +12,7 @@ from pathlib import Path from typing import Any, Callable, MutableMapping, Sequence -try: - from agent_framework import FunctionTool, MCPStdioTool, MCPStreamableHTTPTool -except ImportError: - from agent_framework import MCPStdioTool, MCPStreamableHTTPTool, ToolProtocol as FunctionTool +from agent_framework import FunctionTool, MCPStdioTool, MCPStreamableHTTPTool from libs.agent_framework.agent_info import AgentInfo from libs.agent_framework.groupchat_orchestrator import ( diff --git a/src/processor/src/steps/convert/orchestration/yaml_convert_orchestrator.py b/src/processor/src/steps/convert/orchestration/yaml_convert_orchestrator.py index 580e56b8..e3d6937b 100644 --- a/src/processor/src/steps/convert/orchestration/yaml_convert_orchestrator.py +++ b/src/processor/src/steps/convert/orchestration/yaml_convert_orchestrator.py @@ -13,14 +13,7 @@ from pathlib import Path from typing import Any, Callable, MutableMapping, Sequence -try: - from agent_framework import FunctionTool, MCPStdioTool, MCPStreamableHTTPTool -except ImportError: - from agent_framework import ( - MCPStdioTool, - MCPStreamableHTTPTool, - ToolProtocol as FunctionTool, - ) +from agent_framework import FunctionTool, MCPStdioTool, MCPStreamableHTTPTool from libs.agent_framework.agent_info import AgentInfo from libs.agent_framework.groupchat_orchestrator import ( diff --git a/src/processor/src/steps/design/orchestration/design_orchestrator.py b/src/processor/src/steps/design/orchestration/design_orchestrator.py index 2e49c850..b6d96efe 100644 --- a/src/processor/src/steps/design/orchestration/design_orchestrator.py +++ b/src/processor/src/steps/design/orchestration/design_orchestrator.py @@ -11,14 +11,7 @@ from pathlib import Path from typing import Any, Callable, MutableMapping, Sequence -try: - from agent_framework import FunctionTool, MCPStdioTool, MCPStreamableHTTPTool -except ImportError: - from agent_framework import ( - MCPStdioTool, - MCPStreamableHTTPTool, - ToolProtocol as FunctionTool, - ) +from agent_framework import FunctionTool, MCPStdioTool, MCPStreamableHTTPTool from libs.agent_framework.agent_info import AgentInfo from libs.agent_framework.groupchat_orchestrator import ( diff --git a/src/processor/src/steps/documentation/orchestration/documentation_orchestrator.py b/src/processor/src/steps/documentation/orchestration/documentation_orchestrator.py index b623b9dd..a16e6b4d 100644 --- a/src/processor/src/steps/documentation/orchestration/documentation_orchestrator.py +++ b/src/processor/src/steps/documentation/orchestration/documentation_orchestrator.py @@ -15,14 +15,7 @@ from pathlib import Path from typing import Any, Callable, MutableMapping, Sequence -try: - from agent_framework import FunctionTool, MCPStdioTool, MCPStreamableHTTPTool -except ImportError: - from agent_framework import ( - MCPStdioTool, - MCPStreamableHTTPTool, - ToolProtocol as FunctionTool, - ) +from agent_framework import FunctionTool, MCPStdioTool, MCPStreamableHTTPTool from libs.agent_framework.agent_info import AgentInfo from libs.agent_framework.groupchat_orchestrator import ( From b6001e8e479055f2b52ff45cce5d6b62e53a9013 Mon Sep 17 00:00:00 2001 From: Dhanushree-Microsoft Date: Thu, 21 May 2026 13:21:04 +0530 Subject: [PATCH 03/25] refactor: update Message stubs for consistency and clarity in tests --- .../azure_openai_response_retry.py | 15 +++++++++------ .../test_groupchat_orchestrator_internals.py | 2 +- .../test_input_observer_middleware.py | 6 +++++- .../agent_framework/test_middlewares_extras.py | 6 +++++- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index 91cdf4c9..d254bf72 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -12,6 +12,14 @@ from dataclasses import dataclass from typing import Any, AsyncIterable, MutableSequence +from tenacity import ( + AsyncRetrying, + retry_if_exception, + stop_after_attempt, +) +from tenacity.wait import wait_base + + # agent_framework 1.3.0 removed AzureOpenAIResponsesClient from agent_framework.azure. # Keep this stub so legacy code paths fail explicitly if invoked. class AzureOpenAIResponsesClient: @@ -19,12 +27,7 @@ def __init__(self, *args: Any, **kwargs: Any): raise NotImplementedError( "AzureOpenAIResponsesClient was removed from agent_framework.azure in 1.3.0." ) -from tenacity import ( - AsyncRetrying, - retry_if_exception, - stop_after_attempt, -) -from tenacity.wait import wait_base + logger = logging.getLogger(__name__) diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py index 263b157e..2e0068cf 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py @@ -32,7 +32,7 @@ def __init__(self, *, role, text=None, contents=None, author_name=None): groupchat_module.Message = Message -from libs.agent_framework.groupchat_orchestrator import ( +from libs.agent_framework.groupchat_orchestrator import ( # noqa: E402 AgentResponse, AgentResponseStream, GroupChatOrchestrator, diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py b/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py index fca26fa8..2db61a41 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py @@ -10,6 +10,8 @@ class Message: + """Test stub for Message - the real Message in 1.3.0 uses contents= instead of text=.""" + def __init__(self, *, role, text=None, contents=None, author_name=None): self.role = role self.text = text @@ -17,8 +19,10 @@ def __init__(self, *, role, text=None, contents=None, author_name=None): self.author_name = author_name +# Patch at module level: middleware code references Message at runtime for isinstance +# checks and construction. This is scoped to test execution only. middlewares_module.Message = Message -from libs.agent_framework.middlewares import InputObserverMiddleware +from libs.agent_framework.middlewares import InputObserverMiddleware # noqa: E402 def test_input_observer_middleware_replaces_user_text_when_configured() -> None: diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py b/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py index 1b2e1e2f..9648f581 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py @@ -14,6 +14,8 @@ class Message: + """Test stub for Message - the real Message in 1.3.0 uses contents= instead of text=.""" + def __init__(self, *, role, text=None, contents=None, author_name=None): self.role = role self.text = text @@ -21,8 +23,10 @@ def __init__(self, *, role, text=None, contents=None, author_name=None): self.author_name = author_name +# Patch at module level: middleware code references Message at runtime for isinstance +# checks and construction. This is scoped to test execution only. middlewares_module.Message = Message -from libs.agent_framework.middlewares import ( +from libs.agent_framework.middlewares import ( # noqa: E402 DebuggingMiddleware, LoggingFunctionMiddleware, ) From 64c78f49d5091860fa5baa0dd0d9553d452282ae Mon Sep 17 00:00:00 2001 From: Dhanushree-Microsoft Date: Thu, 21 May 2026 14:38:02 +0530 Subject: [PATCH 04/25] refactor: simplify client import handling in AgentFrameworkHelper --- .../agent_framework/agent_framework_helper.py | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/src/processor/src/libs/agent_framework/agent_framework_helper.py b/src/processor/src/libs/agent_framework/agent_framework_helper.py index e2609e04..4741b44c 100644 --- a/src/processor/src/libs/agent_framework/agent_framework_helper.py +++ b/src/processor/src/libs/agent_framework/agent_framework_helper.py @@ -366,12 +366,7 @@ def create_client( "OpenAIResponsesClient is not implemented in this context." ) elif client_type == ClientType.AzureOpenAIChatCompletion: - try: - from agent_framework.azure import AzureOpenAIChatClient - except ImportError as exc: - raise NotImplementedError( - "ClientType.AzureOpenAIChatCompletion is not supported in agent-framework 1.3.0; AzureOpenAIChatClient was removed." - ) from exc + from agent_framework.azure import AzureOpenAIChatClient return AzureOpenAIChatClient( api_key=api_key, @@ -390,12 +385,7 @@ def create_client( instruction_role=instruction_role, ) elif client_type == ClientType.AzureOpenAIAssistant: - try: - from agent_framework.azure import AzureOpenAIAssistantsClient - except ImportError as exc: - raise NotImplementedError( - "ClientType.AzureOpenAIAssistant is not supported in agent-framework 1.3.0; AzureOpenAIAssistantsClient was removed." - ) from exc + from agent_framework.azure import AzureOpenAIAssistantsClient return AzureOpenAIAssistantsClient( deployment_name=deployment_name, @@ -416,12 +406,7 @@ def create_client( env_file_encoding=env_file_encoding, ) elif client_type == ClientType.AzureOpenAIResponse: - try: - from agent_framework.azure import AzureOpenAIResponsesClient - except ImportError as exc: - raise NotImplementedError( - "ClientType.AzureOpenAIResponse is not supported in agent-framework 1.3.0; AzureOpenAIResponsesClient was removed." - ) from exc + from agent_framework.azure import AzureOpenAIResponsesClient return AzureOpenAIResponsesClient( api_key=api_key, From 7e514c5098537a363156cf8e6ec87d17d5eeb417 Mon Sep 17 00:00:00 2001 From: Dhanushree-Microsoft Date: Thu, 21 May 2026 14:46:52 +0530 Subject: [PATCH 05/25] refactor: update exception handling in client creation tests to use ImportError --- .../libs/agent_framework/test_agent_framework_helper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_agent_framework_helper.py b/src/processor/src/tests/unit/libs/agent_framework/test_agent_framework_helper.py index 767d5359..9392baaa 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_agent_framework_helper.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_agent_framework_helper.py @@ -112,7 +112,7 @@ def test_default_token_provider_when_no_credential(self): def test_azure_openai_chat_completion(self): fake_module = types.ModuleType("agent_framework.azure") with patch.dict(sys.modules, {"agent_framework.azure": fake_module}): - with pytest.raises(NotImplementedError, match="AzureOpenAIChatClient was removed"): + with pytest.raises(ImportError): AgentFrameworkHelper.create_client( ClientType.AzureOpenAIChatCompletion, endpoint="https://x", @@ -123,7 +123,7 @@ def test_azure_openai_chat_completion(self): def test_azure_openai_assistant(self): fake_module = types.ModuleType("agent_framework.azure") with patch.dict(sys.modules, {"agent_framework.azure": fake_module}): - with pytest.raises(NotImplementedError, match="AzureOpenAIAssistantsClient was removed"): + with pytest.raises(ImportError): AgentFrameworkHelper.create_client( ClientType.AzureOpenAIAssistant, endpoint="https://x", @@ -134,7 +134,7 @@ def test_azure_openai_assistant(self): def test_azure_openai_response(self): fake_module = types.ModuleType("agent_framework.azure") with patch.dict(sys.modules, {"agent_framework.azure": fake_module}): - with pytest.raises(NotImplementedError, match="AzureOpenAIResponsesClient was removed"): + with pytest.raises(ImportError): AgentFrameworkHelper.create_client( ClientType.AzureOpenAIResponse, endpoint="https://x", From aab94b902cca832e1d017f834fc8442746a32aa6 Mon Sep 17 00:00:00 2001 From: Dhanushree-Microsoft Date: Thu, 21 May 2026 15:23:32 +0530 Subject: [PATCH 06/25] refactor: normalize executor_id handling in MigrationProcessor for improved telemetry and error reporting --- .../src/steps/migration_processor.py | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/src/processor/src/steps/migration_processor.py b/src/processor/src/steps/migration_processor.py index b1451ef3..adeb031f 100644 --- a/src/processor/src/steps/migration_processor.py +++ b/src/processor/src/steps/migration_processor.py @@ -370,14 +370,13 @@ async def _generate_report_summary( ) elif event.type == "output": # WorkflowEvent carries the step output (success or hard-termination). + # Normalize executor_id once to avoid None in telemetry/reporting. + executor_id = event.executor_id or "unknown" # Note: a None payload is an error that must be surfaced clearly. if event.data is None: - report_collector.set_current_step( - event.executor_id or "unknown" - ) + report_collector.set_current_step(executor_id) # Build a meaningful error message instead of generic "Workflow output is None" - executor_id = event.executor_id or "unknown" error_msg = f"Step '{executor_id}' completed without producing output. This may be caused by context length overflow, agent timeout, or an internal orchestration error. Check processor logs for '[AOAI_CTX_TRIM_STREAM]' or exception details." report_collector.record_failure( @@ -398,13 +397,13 @@ async def _generate_report_summary( await telemetry.record_failure_outcome( process_id=input_data.process_id, - failed_step=event.executor_id or "unknown", + failed_step=executor_id, error_message=error_msg, failure_details=failure_details, execution_time_seconds=( time.perf_counter() - - step_start_perf[event.executor_id] - if event.executor_id in step_start_perf + - step_start_perf[executor_id] + if executor_id in step_start_perf else None ), ) @@ -414,7 +413,7 @@ async def _generate_report_summary( # Raise a rich exception so the queue worker reports a meaningful reason. raise WorkflowExecutorFailedException({ - "executor_id": event.executor_id or "unknown", + "executor_id": executor_id, "error_type": "WorkflowOutputMissing", "message": error_msg, "traceback": None, @@ -467,16 +466,14 @@ async def _generate_report_summary( "error": f"security evidence scan failed: {type(e).__name__}: {e}", } - report_collector.set_current_step( - event.executor_id or "unknown" - ) + report_collector.set_current_step(executor_id) report_collector.record_failure( exception=ValueError( getattr(event.data, "reason", None) - or f"Hard terminated in {event.executor_id} step" + or f"Hard terminated in {executor_id} step" ), custom_message=getattr(event.data, "reason", None) - or f"Hard terminated in {event.executor_id} step", + or f"Hard terminated in {executor_id} step", ) failure_details: Any = ( @@ -501,14 +498,14 @@ async def _generate_report_summary( await telemetry.record_failure_outcome( process_id=input_data.process_id, - failed_step=event.executor_id or "unknown", + failed_step=executor_id, error_message=getattr(event.data, "reason", None) - or f"Hard terminated in {event.executor_id} step", + or f"Hard terminated in {executor_id} step", failure_details=failure_details, execution_time_seconds=( time.perf_counter() - - step_start_perf[event.executor_id] - if event.executor_id in step_start_perf + - step_start_perf[executor_id] + if executor_id in step_start_perf else None ), ) @@ -524,21 +521,21 @@ async def _generate_report_summary( logger.info("Workflow output (%s): %s", event.origin.value, event.data) await telemetry.record_step_result( process_id=input_data.process_id, - step_name=event.executor_id, + step_name=executor_id, step_result=event.data, execution_time_seconds=( time.perf_counter() - - step_start_perf[event.executor_id] - if event.executor_id in step_start_perf + - step_start_perf[executor_id] + if executor_id in step_start_perf else None ), ) - if event.executor_id in step_start_perf: + if executor_id in step_start_perf: report_collector.mark_step_completed( - event.executor_id, + executor_id, execution_time=time.perf_counter() - - step_start_perf[event.executor_id], + - step_start_perf[executor_id], ) try: From 913bffa9f89352fe6ac9a068a708a6a101946aa9 Mon Sep 17 00:00:00 2001 From: Dhanushree-Microsoft Date: Thu, 21 May 2026 15:53:02 +0530 Subject: [PATCH 07/25] refactor: streamline WorkflowEvent handling in GroupChatOrchestrator and OrchestratorBase --- .../src/libs/agent_framework/groupchat_orchestrator.py | 4 ++-- src/processor/src/libs/base/orchestrator_base.py | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 02be619e..78fb899e 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -30,7 +30,7 @@ SupportsAgentRun, Workflow, WorkflowBuilder as GroupChatBuilder, - WorkflowEvent as WorkflowOutputEvent, + WorkflowEvent, ) from mem0 import AsyncMemory from pydantic import BaseModel, ValidationError @@ -527,7 +527,7 @@ async def run_stream( # If the Coordinator requested finish=true, stop immediately. if self._termination_requested: break - elif isinstance(event, WorkflowOutputEvent): + elif isinstance(event, WorkflowEvent) and getattr(event, "type", None) == "output": # Complete last agent's response before finishing if self._last_executor_id and self._current_agent_response: await self._complete_agent_response( diff --git a/src/processor/src/libs/base/orchestrator_base.py b/src/processor/src/libs/base/orchestrator_base.py index 90b564a8..81e489a2 100644 --- a/src/processor/src/libs/base/orchestrator_base.py +++ b/src/processor/src/libs/base/orchestrator_base.py @@ -180,12 +180,11 @@ async def create_agents( .with_max_tokens(20_000) ) # Prevent context window overflow by summarizing older tool results. - if ToolResultCompactionStrategy is not None: - builder = builder.with_kwargs( - compaction_strategy=ToolResultCompactionStrategy( - keep_last_tool_call_groups=2 - ) + builder = builder.with_kwargs( + compaction_strategy=ToolResultCompactionStrategy( + keep_last_tool_call_groups=2 ) + ) if agent_info.agent_name == "Coordinator": # Routing-only: keep deterministic. Needs enough tokens for long instructions. From f64adcdbf9f02134ab93604bcd2ab61fe76b25eb Mon Sep 17 00:00:00 2001 From: Dhanushree-Microsoft Date: Thu, 21 May 2026 17:52:12 +0530 Subject: [PATCH 08/25] refactor: enhance AzureOpenAIResponseClientWithRetry for legacy parameter support and backward compatibility --- .../azure_openai_response_retry.py | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index d254bf72..458d32a1 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -20,13 +20,8 @@ from tenacity.wait import wait_base -# agent_framework 1.3.0 removed AzureOpenAIResponsesClient from agent_framework.azure. -# Keep this stub so legacy code paths fail explicitly if invoked. -class AzureOpenAIResponsesClient: - def __init__(self, *args: Any, **kwargs: Any): - raise NotImplementedError( - "AzureOpenAIResponsesClient was removed from agent_framework.azure in 1.3.0." - ) +# agent_framework 1.3.0 removed AzureOpenAIResponsesClient; alias to its replacement. +from agent_framework.openai import OpenAIChatClient as AzureOpenAIResponsesClient logger = logging.getLogger(__name__) @@ -525,15 +520,31 @@ def __init__( self, *args: Any, retry_config: RateLimitRetryConfig | None = None, + # Legacy parameter names (mapped to OpenAIChatClient equivalents) + deployment_name: str | None = None, + endpoint: str | None = None, + ad_token: str | None = None, + ad_token_provider: object | None = None, + token_endpoint: str | None = None, **kwargs: Any, ): + # Map legacy params to OpenAIChatClient params + if deployment_name and "model" not in kwargs: + kwargs["model"] = deployment_name + if endpoint and "azure_endpoint" not in kwargs: + kwargs["azure_endpoint"] = endpoint + if ad_token_provider and "credential" not in kwargs: + kwargs["credential"] = ad_token_provider + super().__init__(*args, **kwargs) self._retry_config = retry_config or RateLimitRetryConfig.from_env() self._context_trim_config = ContextTrimConfig.from_env() async def _inner_get_response( - self, *, messages: MutableSequence[Any], chat_options: Any, **kwargs: Any + self, *, messages: MutableSequence[Any], chat_options: Any = None, options: Any = None, stream: bool = False, **kwargs: Any ) -> Any: + # Support both old (chat_options) and new (options) parameter names + effective_options = options if options is not None else chat_options parent_inner_get_response = super( AzureOpenAIResponseClientWithRetry, self )._inner_get_response @@ -559,7 +570,7 @@ async def _inner_get_response( try: return await _retry_call( lambda: parent_inner_get_response( - messages=effective_messages, chat_options=chat_options, **kwargs + messages=effective_messages, options=effective_options, stream=stream, **kwargs ), config=self._retry_config, ) @@ -607,16 +618,22 @@ async def _inner_get_response( await asyncio.sleep(trim_delay) return await _retry_call( lambda: parent_inner_get_response( - messages=trimmed, chat_options=chat_options, **kwargs + messages=trimmed, options=effective_options, stream=stream, **kwargs ), config=self._retry_config, ) async def _inner_get_streaming_response( - self, *, messages: MutableSequence[Any], chat_options: Any, **kwargs: Any + self, *, messages: MutableSequence[Any], chat_options: Any = None, options: Any = None, **kwargs: Any ) -> AsyncIterable[Any]: + """Streaming with retry. Delegates to parent._inner_get_response(stream=True). + + This method is kept for backward compatibility in case any internal code path + calls it directly. The new framework uses _inner_get_response(stream=True). + """ # Conservative retry: only retries failures before the first yielded update. attempts = self._retry_config.max_retries + 1 + effective_options = options if options is not None else chat_options effective_messages: MutableSequence[Any] | list[Any] = messages if self._context_trim_config.enabled: @@ -639,8 +656,8 @@ async def _inner_get_streaming_response( for attempt_index in range(attempts): stream = super( AzureOpenAIResponseClientWithRetry, self - )._inner_get_streaming_response( - messages=effective_messages, chat_options=chat_options, **kwargs + )._inner_get_response( + messages=effective_messages, options=effective_options, stream=True, **kwargs ) iterator = stream.__aiter__() From 0ad41f16830be9a569f6cfa36981f7d1dc277e21 Mon Sep 17 00:00:00 2001 From: Priyanka-Microsoft Date: Tue, 2 Jun 2026 12:46:44 +0530 Subject: [PATCH 09/25] code quality issue --- src/backend-api/src/tests/base/test_sk_logic_base.py | 1 - src/frontend/src/components/batchHistoryPanel.tsx | 2 +- .../unit/libs/application/test_application_context_extras.py | 4 ++-- .../src/tests/unit/services/test_queue_service_internals.py | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/backend-api/src/tests/base/test_sk_logic_base.py b/src/backend-api/src/tests/base/test_sk_logic_base.py index 594342b3..119d17fc 100644 --- a/src/backend-api/src/tests/base/test_sk_logic_base.py +++ b/src/backend-api/src/tests/base/test_sk_logic_base.py @@ -10,7 +10,6 @@ import importlib import sys import types -from typing import Type from unittest.mock import MagicMock import pytest diff --git a/src/frontend/src/components/batchHistoryPanel.tsx b/src/frontend/src/components/batchHistoryPanel.tsx index d180b78c..49fd10cb 100644 --- a/src/frontend/src/components/batchHistoryPanel.tsx +++ b/src/frontend/src/components/batchHistoryPanel.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from "react"; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { Card, Spinner, Tooltip } from "@fluentui/react-components"; import { useNavigate } from "react-router-dom"; import ConfirmationDialog from "../commonComponents/ConfirmationDialog/confirmationDialogue"; diff --git a/src/processor/src/tests/unit/libs/application/test_application_context_extras.py b/src/processor/src/tests/unit/libs/application/test_application_context_extras.py index b51b8b2a..e121254c 100644 --- a/src/processor/src/tests/unit/libs/application/test_application_context_extras.py +++ b/src/processor/src/tests/unit/libs/application/test_application_context_extras.py @@ -118,7 +118,7 @@ async def _run(): def test_create_async_instance_with_callable_factory(): async def _run(): - ctx = AppContext().add_async_singleton(_AsyncSvc, lambda: _AsyncSvc()) + ctx = AppContext().add_async_singleton(_AsyncSvc, _AsyncSvc) a = await ctx.get_service_async(_AsyncSvc) assert isinstance(a, _AsyncSvc) assert a.entered is True @@ -151,7 +151,7 @@ async def _run(): def test_create_instance_with_factory_callable(): - ctx = AppContext().add_singleton(_S, lambda: _S()) + ctx = AppContext().add_singleton(_S, _S) a = ctx.get_service(_S) assert isinstance(a, _S) diff --git a/src/processor/src/tests/unit/services/test_queue_service_internals.py b/src/processor/src/tests/unit/services/test_queue_service_internals.py index 47a8d701..6ebe0cf1 100644 --- a/src/processor/src/tests/unit/services/test_queue_service_internals.py +++ b/src/processor/src/tests/unit/services/test_queue_service_internals.py @@ -189,7 +189,7 @@ async def _go(): try: await task except (asyncio.CancelledError, Exception): - pass + pass # Expected during task cancellation cleanup _run(_go()) From 26f584c951dc79b54019540933c4d08784dc68af Mon Sep 17 00:00:00 2001 From: Dhanushree-Microsoft Date: Tue, 2 Jun 2026 13:18:16 +0530 Subject: [PATCH 10/25] chore: update werkzeug to 3.1.6 and add idna 3.15 to dependencies --- src/backend-api/pyproject.toml | 3 ++- src/backend-api/uv.lock | 15 ++++++++------- src/processor/pyproject.toml | 1 + src/processor/uv.lock | 7 ++++--- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/backend-api/pyproject.toml b/src/backend-api/pyproject.toml index 5b658d47..a877c5d9 100644 --- a/src/backend-api/pyproject.toml +++ b/src/backend-api/pyproject.toml @@ -38,10 +38,11 @@ override-dependencies = [ "azure-core==1.38.0", "urllib3==2.7.0", "requests==2.33.0", - "werkzeug==3.1.4", + "werkzeug==3.1.6", "pygments==2.20.0", "black==26.3.1", "cryptography==46.0.7", "pyjwt==2.12.0", "pyopenssl==26.0.0", + "idna==3.15", ] diff --git a/src/backend-api/uv.lock b/src/backend-api/uv.lock index cb73228e..54c9a421 100644 --- a/src/backend-api/uv.lock +++ b/src/backend-api/uv.lock @@ -14,13 +14,14 @@ overrides = [ { name = "azure-core", specifier = "==1.38.0" }, { name = "black", specifier = "==26.3.1" }, { name = "cryptography", specifier = "==46.0.7" }, + { name = "idna", specifier = "==3.15" }, { name = "pygments", specifier = "==2.20.0" }, { name = "pyjwt", specifier = "==2.12.0" }, { name = "pyopenssl", specifier = "==26.0.0" }, { name = "requests", specifier = "==2.33.0" }, { name = "starlette", specifier = "==0.49.1" }, { name = "urllib3", specifier = "==2.7.0" }, - { name = "werkzeug", specifier = "==3.1.4" }, + { name = "werkzeug", specifier = "==3.1.6" }, ] [[package]] @@ -1276,11 +1277,11 @@ wheels = [ [[package]] name = "idna" -version = "3.13" +version = "3.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] [[package]] @@ -3301,14 +3302,14 @@ wheels = [ [[package]] name = "werkzeug" -version = "3.1.4" +version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/ea/b0f8eeb287f8df9066e56e831c7824ac6bab645dd6c7a8f4b2d767944f9b/werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e", size = 864687, upload-time = "2025-11-29T02:15:22.841Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/f9/9e082990c2585c744734f85bec79b5dae5df9c974ffee58fe421652c8e91/werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905", size = 224960, upload-time = "2025-11-29T02:15:21.13Z" }, + { url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" }, ] [[package]] diff --git a/src/processor/pyproject.toml b/src/processor/pyproject.toml index 43241009..b6c2c2aa 100644 --- a/src/processor/pyproject.toml +++ b/src/processor/pyproject.toml @@ -62,4 +62,5 @@ prerelease = "allow" override-dependencies = [ "urllib3==2.7.0", "authlib==1.7.1", + "idna==3.15", ] diff --git a/src/processor/uv.lock b/src/processor/uv.lock index 7c68e2ab..0f3c189b 100644 --- a/src/processor/uv.lock +++ b/src/processor/uv.lock @@ -13,6 +13,7 @@ prerelease-mode = "allow" [manifest] overrides = [ { name = "authlib", specifier = "==1.7.1" }, + { name = "idna", specifier = "==3.15" }, { name = "urllib3", specifier = "==2.7.0" }, ] @@ -1556,11 +1557,11 @@ wheels = [ [[package]] name = "idna" -version = "3.13" +version = "3.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] [[package]] From 8371c224d14807895b44bcb07f9557c258f90bd6 Mon Sep 17 00:00:00 2001 From: Dhanushree-Microsoft Date: Thu, 4 Jun 2026 12:34:33 +0530 Subject: [PATCH 11/25] fix: check credential value instead of key presence When callers pass credential=None explicitly, the key exists in kwargs but the ad_token_provider mapping was skipped. Use kwargs.get() is None to correctly handle this case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure_openai_response_retry.py | 776 ------------------ 1 file changed, 776 deletions(-) diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index 458d32a1..e69de29b 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -1,776 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -"""Azure OpenAI Responses client wrapper with rate-limit-aware retry logic.""" - -from __future__ import annotations - -import asyncio -import logging -import os -import random -from dataclasses import dataclass -from typing import Any, AsyncIterable, MutableSequence - -from tenacity import ( - AsyncRetrying, - retry_if_exception, - stop_after_attempt, -) -from tenacity.wait import wait_base - - -# agent_framework 1.3.0 removed AzureOpenAIResponsesClient; alias to its replacement. -from agent_framework.openai import OpenAIChatClient as AzureOpenAIResponsesClient - - -logger = logging.getLogger(__name__) - - -def _format_exc_brief(exc: BaseException) -> str: - name = type(exc).__name__ - msg = str(exc) - return f"{name}: {msg}" if msg else name - - -@dataclass(frozen=True) -class RateLimitRetryConfig: - max_retries: int = 8 - base_delay_seconds: float = 5.0 - max_delay_seconds: float = 120.0 - - @staticmethod - def from_env( - max_retries_env: str = "AOAI_429_MAX_RETRIES", - base_delay_env: str = "AOAI_429_BASE_DELAY_SECONDS", - max_delay_env: str = "AOAI_429_MAX_DELAY_SECONDS", - ) -> "RateLimitRetryConfig": - def _int(name: str, default: int) -> int: - try: - return int(os.getenv(name, str(default))) - except Exception: - return default - - def _float(name: str, default: float) -> float: - try: - return float(os.getenv(name, str(default))) - except Exception: - return default - - return RateLimitRetryConfig( - max_retries=max(0, _int(max_retries_env, 8)), - base_delay_seconds=max(0.0, _float(base_delay_env, 5.0)), - max_delay_seconds=max(0.0, _float(max_delay_env, 120.0)), - ) - - -def _looks_like_rate_limit(error: BaseException) -> bool: - msg = str(error).lower() - if any(s in msg for s in ["too many requests", "rate limit", "429", "throttle"]): - return True - - status = getattr(error, "status_code", None) or getattr(error, "status", None) - if status == 429: - return True - - # Treat empty error messages as transient (likely connection reset or - # incomplete response from Azure front-end) — worth retrying. - if not msg or msg == str(type(error).__name__).lower(): - return True - - # Server errors (5xx) are transient and should be retried. - if isinstance(status, int) and 500 <= status < 600: - return True - - cause = getattr(error, "__cause__", None) - if cause and cause is not error: - return _looks_like_rate_limit(cause) - - return False - - -def _looks_like_context_length(error: BaseException) -> bool: - msg = str(error).lower() - if any( - s in msg - for s in [ - "exceeds the context window", - "maximum context length", - "context length", - "too many tokens", - "prompt is too long", - "input is too long", - "please reduce the length", - ] - ): - return True - - status = getattr(error, "status_code", None) or getattr(error, "status", None) - if status in (400, 413): - # Only treat 400/413 as context-length if the message actually mentions it. - # Generic 400s (e.g. "No tool output found") must NOT trigger trim retries. - context_keywords = [ - "context window", - "context length", - "too many tokens", - "prompt is too long", - "input is too long", - "reduce the length", - "maximum.*length", - "token limit", - ] - if any(kw in msg for kw in context_keywords): - return True - - cause = getattr(error, "__cause__", None) - if cause and cause is not error: - return _looks_like_context_length(cause) - - return False - - -def _safe_str(val: Any) -> str: - if val is None: - return "" - if isinstance(val, str): - return val - return str(val) - - -def _looks_like_tool_result(text: str) -> bool: - """Heuristic: detect tool/function result messages by content patterns.""" - if not text or len(text) < 50: - return False - # Common patterns in tool results from blob operations - indicators = [ - '"blob_name"', - '"container_name"', - '"folder_path"', - '"content":', - '"size":', - '"last_modified":', - "BlobProperties", - "Successfully saved", - "# ", - "## ", # Markdown headers from read_blob_content - ] - return any(ind in text[:500] for ind in indicators) - - -def _looks_like_save_blob_call(text: str) -> bool: - """Detect save_content_to_blob tool calls with large content arguments.""" - if not text: - return False - return "save_content_to_blob" in text[:200] and len(text) > 1000 - - -def _summarize_save_blob(text: str, max_chars: int) -> str: - """Extract blob name and size from save_content_to_blob call.""" - import re - - blob_match = re.search(r'"blob_name"\s*:\s*"([^"]+)"', text) - blob_name = blob_match.group(1) if blob_match else "unknown" - return f"[saved {blob_name} to blob storage ({len(text)} chars)]" - - -def _truncate_text( - text: str, *, max_chars: int, keep_head_chars: int, keep_tail_chars: int -) -> str: - if max_chars <= 0: - return "" - if not text: - return "" - if len(text) <= max_chars: - return text - - head = text[: max(0, min(keep_head_chars, max_chars))] - remaining = max_chars - len(head) - if remaining <= 0: - return head - - tail_len = max(0, min(keep_tail_chars, remaining)) - if tail_len <= 0: - return head - - tail = text[-tail_len:] - omitted = len(text) - (len(head) + len(tail)) - marker = f"\n... [TRUNCATED {omitted} CHARS] ...\n" - - budget = max_chars - (len(head) + len(tail)) - if budget <= 0: - return head + tail - if len(marker) > budget: - marker = marker[:budget] - - return head + marker + tail - - -def _estimate_message_text(message: Any) -> str: - if message is None: - return "" - - if isinstance(message, dict): - # Common shapes: {role, content}, {role, text}, {role, contents} - for key in ("content", "text", "contents"): - if key in message: - return _safe_str(message.get(key)) - return _safe_str(message) - - # Attribute-based objects. - for attr in ("content", "text", "contents"): - if hasattr(message, attr): - return _safe_str(getattr(message, attr)) - return _safe_str(message) - - -def _get_message_role(message: Any) -> str | None: - if message is None: - return None - if isinstance(message, dict): - role = message.get("role") - return role if isinstance(role, str) else None - role = getattr(message, "role", None) - return role if isinstance(role, str) else None - - -def _set_message_text(message: Any, new_text: str) -> Any: - """Best-effort setter for message text. - - - For dict messages: returns a shallow-copied dict with content/text updated. - - For objects: tries to set .content or .text; if that fails, returns original. - """ - if isinstance(message, dict): - out = dict(message) - if "content" in out: - out["content"] = new_text - elif "text" in out: - out["text"] = new_text - elif "contents" in out: - out["contents"] = new_text - else: - out["content"] = new_text - return out - - for attr in ("content", "text"): - if hasattr(message, attr): - try: - setattr(message, attr, new_text) - return message - except Exception: - pass - return message - - -@dataclass(frozen=True) -class ContextTrimConfig: - """Character-budget based context trimming. - - This is a defensive control to prevent hard failures like - "input exceeds the context window" when upstream accidentally injects - huge blobs (telemetry JSON, repeated instructions, etc.). - """ - - enabled: bool = True - # GPT-5.1 supports 272K input tokens (~800K chars). With workspace context - # injected into system instructions (never trimmed) and Qdrant shared memory - # providing cross-step context, we can keep fewer conversation messages. - max_total_chars: int = 400_000 - max_message_chars: int = 0 # Disabled — with keep_last_messages=15, per-message truncation is unnecessary - keep_last_messages: int = 15 - keep_head_chars: int = 12_000 - keep_tail_chars: int = 4_000 - keep_system_messages: bool = True - retry_on_context_error: bool = True - - @staticmethod - def from_env( - enabled_env: str = "AOAI_CTX_TRIM_ENABLED", - max_total_chars_env: str = "AOAI_CTX_MAX_TOTAL_CHARS", - max_message_chars_env: str = "AOAI_CTX_MAX_MESSAGE_CHARS", - keep_last_messages_env: str = "AOAI_CTX_KEEP_LAST_MESSAGES", - keep_head_chars_env: str = "AOAI_CTX_KEEP_HEAD_CHARS", - keep_tail_chars_env: str = "AOAI_CTX_KEEP_TAIL_CHARS", - keep_system_messages_env: str = "AOAI_CTX_KEEP_SYSTEM_MESSAGES", - retry_on_context_error_env: str = "AOAI_CTX_RETRY_ON_CONTEXT_ERROR", - ) -> "ContextTrimConfig": - def _int(name: str, default: int) -> int: - try: - return int(os.getenv(name, str(default))) - except Exception: - return default - - def _bool(name: str, default: bool) -> bool: - raw = os.getenv(name) - if raw is None: - return default - return str(raw).strip().lower() in ("1", "true", "yes", "y", "on") - - return ContextTrimConfig( - enabled=_bool(enabled_env, True), - max_total_chars=max(0, _int(max_total_chars_env, 240_000)), - max_message_chars=max(0, _int(max_message_chars_env, 20_000)), - keep_last_messages=max(1, _int(keep_last_messages_env, 15)), - keep_head_chars=max(0, _int(keep_head_chars_env, 10_000)), - keep_tail_chars=max(0, _int(keep_tail_chars_env, 3_000)), - keep_system_messages=_bool(keep_system_messages_env, True), - retry_on_context_error=_bool(retry_on_context_error_env, True), - ) - - -def _trim_messages( - messages: MutableSequence[Any], *, cfg: ContextTrimConfig -) -> list[Any]: - if not cfg.enabled: - return list(messages) - - # ────────────────────────────────────────────────────────────────────── - # Phase 0: Summarize large save_content_to_blob calls. - # Write payloads are redundant once persisted — replace with a short - # summary. Read tool results are never truncated so the model always - # has the full file content to reason about. - # ────────────────────────────────────────────────────────────────────── - SAVE_ARG_MAX_CHARS = 200 # Truncate save_content_to_blob arguments - - for i, m in enumerate(messages): - text = _estimate_message_text(m) - if _looks_like_save_blob_call(text) and len(text) > SAVE_ARG_MAX_CHARS: - summary = _summarize_save_blob(text, SAVE_ARG_MAX_CHARS) - messages[i] = _set_message_text(m, summary) - - # Keep last N messages; optionally keep system messages from the head. - system_messages: list[Any] = [] - tail: list[Any] = list(messages) - - if cfg.keep_system_messages: - for m in messages: - if _get_message_role(m) == "system": - system_messages.append(m) - else: - break - - if cfg.keep_last_messages > 0: - tail = tail[-cfg.keep_last_messages :] - - # De-dupe large repeated blobs using author-less fingerprint on head/tail text. - seen_fingerprints: set[tuple[str, str]] = set() - cleaned: list[Any] = [] - - for idx, m in enumerate(tail): - text = _estimate_message_text(m) - fp = (text[:200], text[-200:]) - if fp in seen_fingerprints: - continue - seen_fingerprints.add(fp) - - # Never truncate the last message — the agent needs it in full - # to reason about the most recent tool result or instruction. - is_last = idx == len(tail) - 1 - if ( - not is_last - and cfg.max_message_chars > 0 - and len(text) > cfg.max_message_chars - ): - text = _truncate_text( - text, - max_chars=cfg.max_message_chars, - keep_head_chars=cfg.keep_head_chars, - keep_tail_chars=cfg.keep_tail_chars, - ) - m = _set_message_text(m, text) - cleaned.append(m) - - # Enforce overall budget by trimming oldest messages from the non-system tail. - combined: list[Any] = system_messages + cleaned - if cfg.max_total_chars <= 0: - return combined - - def _total_chars(msgs: list[Any]) -> int: - return sum(len(_estimate_message_text(x)) for x in msgs) - - while combined and _total_chars(combined) > cfg.max_total_chars: - # Prefer dropping earliest non-system message. - drop_index = 0 - if cfg.keep_system_messages and system_messages: - drop_index = len(system_messages) - if drop_index >= len(combined): - # If only system messages remain, truncate the last one. - last = combined[-1] - text = _estimate_message_text(last) - text = _truncate_text( - text, - max_chars=cfg.max_total_chars, - keep_head_chars=min(cfg.keep_head_chars, cfg.max_total_chars), - keep_tail_chars=min(cfg.keep_tail_chars, cfg.max_total_chars), - ) - combined[-1] = _set_message_text(last, text) - break - combined.pop(drop_index) - - return combined - - -def _try_get_retry_after_seconds(error: BaseException) -> float | None: - inner = getattr(error, "inner_exception", None) - if isinstance(inner, BaseException) and inner is not error: - inner_retry = _try_get_retry_after_seconds(inner) - if inner_retry is not None: - return inner_retry - - candidates: list[Any] = [] - candidates.append(getattr(error, "retry_after", None)) - - response = getattr(error, "response", None) - if response is not None: - candidates.append(getattr(response, "headers", None)) - - headers = getattr(error, "headers", None) - if headers is not None: - candidates.append(headers) - - for item in candidates: - if item is None: - continue - if isinstance(item, (int, float)): - return float(item) - if isinstance(item, str): - try: - return float(item) - except Exception: - continue - if isinstance(item, dict): - for key in ("retry-after", "Retry-After"): - if key in item: - try: - return float(item[key]) - except Exception: - pass - return None - - -async def _retry_call(coro_factory, *, config: RateLimitRetryConfig): - def _log_before_sleep(retry_state) -> None: - exc = None - if retry_state.outcome is not None and retry_state.outcome.failed: - exc = retry_state.outcome.exception() - - # Tenacity sets next_action when it's about to sleep. - sleep_s = None - next_action = getattr(retry_state, "next_action", None) - if next_action is not None: - sleep_s = getattr(next_action, "sleep", None) - - retry_after = _try_get_retry_after_seconds(exc) if exc is not None else None - status = getattr(exc, "status_code", None) or getattr(exc, "status", None) - attempt = getattr(retry_state, "attempt_number", None) - max_attempts = config.max_retries + 1 - - logger.warning( - "[AOAI_RETRY] attempt %s/%s; sleeping=%ss; retry_after=%s; status=%s; error=%s", - attempt, - max_attempts, - None if sleep_s is None else round(float(sleep_s), 3), - None if retry_after is None else round(float(retry_after), 3), - status, - None if exc is None else _format_exc_brief(exc), - ) - - class _WaitRetryAfterOrExpJitter(wait_base): - def __init__(self, retry_config: RateLimitRetryConfig): - self._cfg = retry_config - - def __call__(self, retry_state) -> float: - exc = None - if retry_state.outcome is not None and retry_state.outcome.failed: - exc = retry_state.outcome.exception() - - if exc is not None: - retry_after = _try_get_retry_after_seconds(exc) - if retry_after is not None and retry_after >= 0: - return float(retry_after) - - attempt_index = max(0, retry_state.attempt_number - 1) - delay = self._cfg.base_delay_seconds * (2**attempt_index) - delay = min(delay, self._cfg.max_delay_seconds) - delay = delay + random.uniform(0.0, 0.25 * max(delay, 0.1)) - return float(delay) - - retrying = AsyncRetrying( - retry=retry_if_exception(_looks_like_rate_limit), - stop=stop_after_attempt(config.max_retries + 1), - wait=_WaitRetryAfterOrExpJitter(config), - before_sleep=_log_before_sleep, - reraise=True, - ) - - async for attempt in retrying: - with attempt: - return await coro_factory() - - raise RuntimeError("Retry loop exhausted unexpectedly") - - -class AzureOpenAIResponseClientWithRetry(AzureOpenAIResponsesClient): - """Azure OpenAI Responses client with 429 retry at the request boundary. - - Retry is centralized in the client layer (not in orchestrators) by retrying the - underlying Responses calls made by `OpenAIBaseResponsesClient`. - """ - - def __init__( - self, - *args: Any, - retry_config: RateLimitRetryConfig | None = None, - # Legacy parameter names (mapped to OpenAIChatClient equivalents) - deployment_name: str | None = None, - endpoint: str | None = None, - ad_token: str | None = None, - ad_token_provider: object | None = None, - token_endpoint: str | None = None, - **kwargs: Any, - ): - # Map legacy params to OpenAIChatClient params - if deployment_name and "model" not in kwargs: - kwargs["model"] = deployment_name - if endpoint and "azure_endpoint" not in kwargs: - kwargs["azure_endpoint"] = endpoint - if ad_token_provider and "credential" not in kwargs: - kwargs["credential"] = ad_token_provider - - super().__init__(*args, **kwargs) - self._retry_config = retry_config or RateLimitRetryConfig.from_env() - self._context_trim_config = ContextTrimConfig.from_env() - - async def _inner_get_response( - self, *, messages: MutableSequence[Any], chat_options: Any = None, options: Any = None, stream: bool = False, **kwargs: Any - ) -> Any: - # Support both old (chat_options) and new (options) parameter names - effective_options = options if options is not None else chat_options - parent_inner_get_response = super( - AzureOpenAIResponseClientWithRetry, self - )._inner_get_response - - effective_messages: MutableSequence[Any] | list[Any] = messages - if self._context_trim_config.enabled: - approx_chars = sum(len(_estimate_message_text(m)) for m in messages) - if ( - self._context_trim_config.max_total_chars > 0 - and approx_chars > self._context_trim_config.max_total_chars - ): - effective_messages = _trim_messages( - messages, cfg=self._context_trim_config - ) - logger.warning( - "[AOAI_CTX_TRIM] pre-trimmed request messages: approx_chars=%s -> %s; count=%s -> %s", - approx_chars, - sum(len(_estimate_message_text(m)) for m in effective_messages), - len(messages), - len(effective_messages), - ) - - try: - return await _retry_call( - lambda: parent_inner_get_response( - messages=effective_messages, options=effective_options, stream=stream, **kwargs - ), - config=self._retry_config, - ) - except Exception as e: - if not ( - self._context_trim_config.enabled - and self._context_trim_config.retry_on_context_error - and _looks_like_context_length(e) - ): - raise - - trimmed = _trim_messages( - messages, - cfg=ContextTrimConfig( - enabled=True, - max_total_chars=max( - 50_000, self._context_trim_config.max_total_chars - 80_000 - ), - max_message_chars=max( - 3_000, self._context_trim_config.max_message_chars - 6_000 - ), - keep_last_messages=max( - 6, self._context_trim_config.keep_last_messages - 12 - ), - keep_head_chars=max( - 1_000, self._context_trim_config.keep_head_chars - 4_000 - ), - keep_tail_chars=self._context_trim_config.keep_tail_chars, - keep_system_messages=True, - retry_on_context_error=True, - ), - ) - logger.warning( - "[AOAI_CTX_TRIM] retrying after context-length error; count=%s -> %s", - len(messages), - len(trimmed), - ) - # Cool down before retrying to avoid triggering 429s immediately. - trim_delay = self._retry_config.base_delay_seconds - trim_delay = min(trim_delay, self._retry_config.max_delay_seconds) - logger.info( - "[AOAI_CTX_TRIM] sleeping %ss before retry", - round(trim_delay, 1), - ) - await asyncio.sleep(trim_delay) - return await _retry_call( - lambda: parent_inner_get_response( - messages=trimmed, options=effective_options, stream=stream, **kwargs - ), - config=self._retry_config, - ) - - async def _inner_get_streaming_response( - self, *, messages: MutableSequence[Any], chat_options: Any = None, options: Any = None, **kwargs: Any - ) -> AsyncIterable[Any]: - """Streaming with retry. Delegates to parent._inner_get_response(stream=True). - - This method is kept for backward compatibility in case any internal code path - calls it directly. The new framework uses _inner_get_response(stream=True). - """ - # Conservative retry: only retries failures before the first yielded update. - attempts = self._retry_config.max_retries + 1 - effective_options = options if options is not None else chat_options - - effective_messages: MutableSequence[Any] | list[Any] = messages - if self._context_trim_config.enabled: - approx_chars = sum(len(_estimate_message_text(m)) for m in messages) - if ( - self._context_trim_config.max_total_chars > 0 - and approx_chars > self._context_trim_config.max_total_chars - ): - effective_messages = _trim_messages( - messages, cfg=self._context_trim_config - ) - logger.warning( - "[AOAI_CTX_TRIM] pre-trimmed streaming request messages: approx_chars=%s -> %s; count=%s -> %s", - approx_chars, - sum(len(_estimate_message_text(m)) for m in effective_messages), - len(messages), - len(effective_messages), - ) - - for attempt_index in range(attempts): - stream = super( - AzureOpenAIResponseClientWithRetry, self - )._inner_get_response( - messages=effective_messages, options=effective_options, stream=True, **kwargs - ) - - iterator = stream.__aiter__() - try: - first = await iterator.__anext__() - - async def _tail(): - yield first - async for item in iterator: - yield item - - async for item in _tail(): - yield item - return - except StopAsyncIteration: - return - except Exception as e: - close = getattr(stream, "aclose", None) - if callable(close): - try: - await close() - except Exception: - logger.debug("Best-effort close of response stream failed", exc_info=True) - - # Progressive retry for context-length failures. - if ( - self._context_trim_config.enabled - and self._context_trim_config.retry_on_context_error - and _looks_like_context_length(e) - ): - # Make trimming progressively more aggressive on each retry - # GPT-5.1: 272K input tokens ≈ 800K chars. Scale down from 600K default. - scale = attempt_index + 1 - aggressive_cfg = ContextTrimConfig( - enabled=True, - max_total_chars=max( - 30_000, - self._context_trim_config.max_total_chars - scale * 100_000, - ), - max_message_chars=max( - 2_000, - self._context_trim_config.max_message_chars - scale * 8_000, - ), - keep_last_messages=max( - 4, - self._context_trim_config.keep_last_messages - scale * 8, - ), - keep_head_chars=max( - 500, - self._context_trim_config.keep_head_chars - scale * 3_000, - ), - keep_tail_chars=max( - 500, - self._context_trim_config.keep_tail_chars - scale * 1_000, - ), - keep_system_messages=True, - retry_on_context_error=True, - ) - trimmed = _trim_messages(effective_messages, cfg=aggressive_cfg) - logger.warning( - "[AOAI_CTX_TRIM_STREAM] retrying after context-length error (attempt %s); count=%s -> %s, budget=%s", - attempt_index + 1, - len(effective_messages), - len(trimmed), - aggressive_cfg.max_total_chars, - ) - effective_messages = trimmed - if attempt_index >= attempts - 1: - # No more retries available. - raise - - # Cool down before retrying — immediate retries after trimming - # tend to trigger 429s because the API hasn't recovered yet. - trim_delay = self._retry_config.base_delay_seconds * ( - 2**attempt_index - ) - trim_delay = min(trim_delay, self._retry_config.max_delay_seconds) - logger.info( - "[AOAI_CTX_TRIM_STREAM] sleeping %ss before retry", - round(trim_delay, 1), - ) - await asyncio.sleep(trim_delay) - continue - - if not _looks_like_rate_limit(e) or attempt_index >= attempts - 1: - if _looks_like_rate_limit(e): - logger.warning( - "[AOAI_RETRY_STREAM] giving up after %s/%s attempts; error=%s", - attempt_index + 1, - attempts, - _format_exc_brief(e) - if isinstance(e, BaseException) - else str(e), - ) - raise - - retry_after = _try_get_retry_after_seconds(e) - if retry_after is not None and retry_after >= 0: - delay = retry_after - else: - delay = self._retry_config.base_delay_seconds * (2**attempt_index) - delay = min(delay, self._retry_config.max_delay_seconds) - delay = delay + random.uniform(0.0, 0.25 * max(delay, 0.1)) - - status = getattr(e, "status_code", None) or getattr(e, "status", None) - logger.warning( - "[AOAI_RETRY_STREAM] attempt %s/%s; sleeping=%ss; retry_after=%s; status=%s; error=%s", - attempt_index + 1, - attempts, - round(float(delay), 3), - None if retry_after is None else round(float(retry_after), 3), - status, - _format_exc_brief(e) if isinstance(e, BaseException) else str(e), - ) - - await asyncio.sleep(delay) From 1033890b8522ed08bdb2a5ab3707e04fe1eb19dd Mon Sep 17 00:00:00 2001 From: Dhanushree-Microsoft Date: Thu, 4 Jun 2026 12:35:21 +0530 Subject: [PATCH 12/25] fix: check credential value instead of key presence When callers pass credential=None explicitly, the key exists in kwargs but the ad_token_provider mapping was skipped. Use kwargs.get() is None to correctly handle this case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure_openai_response_retry.py | 775 ++++++++++++++++++ 1 file changed, 775 insertions(+) diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index e69de29b..04d5a15b 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -0,0 +1,775 @@ +# Licensed under the MIT License. + +"""Azure OpenAI Responses client wrapper with rate-limit-aware retry logic.""" + +from __future__ import annotations + +import asyncio +import logging +import os +import random +from dataclasses import dataclass +from typing import Any, AsyncIterable, MutableSequence + +from tenacity import ( + AsyncRetrying, + retry_if_exception, + stop_after_attempt, +) +from tenacity.wait import wait_base + + +# agent_framework 1.3.0 removed AzureOpenAIResponsesClient; alias to its replacement. +from agent_framework.openai import OpenAIChatClient as AzureOpenAIResponsesClient + + +logger = logging.getLogger(__name__) + + +def _format_exc_brief(exc: BaseException) -> str: + name = type(exc).__name__ + msg = str(exc) + return f"{name}: {msg}" if msg else name + + +@dataclass(frozen=True) +class RateLimitRetryConfig: + max_retries: int = 8 + base_delay_seconds: float = 5.0 + max_delay_seconds: float = 120.0 + + @staticmethod + def from_env( + max_retries_env: str = "AOAI_429_MAX_RETRIES", + base_delay_env: str = "AOAI_429_BASE_DELAY_SECONDS", + max_delay_env: str = "AOAI_429_MAX_DELAY_SECONDS", + ) -> "RateLimitRetryConfig": + def _int(name: str, default: int) -> int: + try: + return int(os.getenv(name, str(default))) + except Exception: + return default + + def _float(name: str, default: float) -> float: + try: + return float(os.getenv(name, str(default))) + except Exception: + return default + + return RateLimitRetryConfig( + max_retries=max(0, _int(max_retries_env, 8)), + base_delay_seconds=max(0.0, _float(base_delay_env, 5.0)), + max_delay_seconds=max(0.0, _float(max_delay_env, 120.0)), + ) + + +def _looks_like_rate_limit(error: BaseException) -> bool: + msg = str(error).lower() + if any(s in msg for s in ["too many requests", "rate limit", "429", "throttle"]): + return True + + status = getattr(error, "status_code", None) or getattr(error, "status", None) + if status == 429: + return True + + # Treat empty error messages as transient (likely connection reset or + # incomplete response from Azure front-end) — worth retrying. + if not msg or msg == str(type(error).__name__).lower(): + return True + + # Server errors (5xx) are transient and should be retried. + if isinstance(status, int) and 500 <= status < 600: + return True + + cause = getattr(error, "__cause__", None) + if cause and cause is not error: + return _looks_like_rate_limit(cause) + + return False + + +def _looks_like_context_length(error: BaseException) -> bool: + msg = str(error).lower() + if any( + s in msg + for s in [ + "exceeds the context window", + "maximum context length", + "context length", + "too many tokens", + "prompt is too long", + "input is too long", + "please reduce the length", + ] + ): + return True + + status = getattr(error, "status_code", None) or getattr(error, "status", None) + if status in (400, 413): + # Only treat 400/413 as context-length if the message actually mentions it. + # Generic 400s (e.g. "No tool output found") must NOT trigger trim retries. + context_keywords = [ + "context window", + "context length", + "too many tokens", + "prompt is too long", + "input is too long", + "reduce the length", + "maximum.*length", + "token limit", + ] + if any(kw in msg for kw in context_keywords): + return True + + cause = getattr(error, "__cause__", None) + if cause and cause is not error: + return _looks_like_context_length(cause) + + return False + + +def _safe_str(val: Any) -> str: + if val is None: + return "" + if isinstance(val, str): + return val + return str(val) + + +def _looks_like_tool_result(text: str) -> bool: + """Heuristic: detect tool/function result messages by content patterns.""" + if not text or len(text) < 50: + return False + # Common patterns in tool results from blob operations + indicators = [ + '"blob_name"', + '"container_name"', + '"folder_path"', + '"content":', + '"size":', + '"last_modified":', + "BlobProperties", + "Successfully saved", + "# ", + "## ", # Markdown headers from read_blob_content + ] + return any(ind in text[:500] for ind in indicators) + + +def _looks_like_save_blob_call(text: str) -> bool: + """Detect save_content_to_blob tool calls with large content arguments.""" + if not text: + return False + return "save_content_to_blob" in text[:200] and len(text) > 1000 + + +def _summarize_save_blob(text: str, max_chars: int) -> str: + """Extract blob name and size from save_content_to_blob call.""" + import re + + blob_match = re.search(r'"blob_name"\s*:\s*"([^"]+)"', text) + blob_name = blob_match.group(1) if blob_match else "unknown" + return f"[saved {blob_name} to blob storage ({len(text)} chars)]" + + +def _truncate_text( + text: str, *, max_chars: int, keep_head_chars: int, keep_tail_chars: int +) -> str: + if max_chars <= 0: + return "" + if not text: + return "" + if len(text) <= max_chars: + return text + + head = text[: max(0, min(keep_head_chars, max_chars))] + remaining = max_chars - len(head) + if remaining <= 0: + return head + + tail_len = max(0, min(keep_tail_chars, remaining)) + if tail_len <= 0: + return head + + tail = text[-tail_len:] + omitted = len(text) - (len(head) + len(tail)) + marker = f"\n... [TRUNCATED {omitted} CHARS] ...\n" + + budget = max_chars - (len(head) + len(tail)) + if budget <= 0: + return head + tail + if len(marker) > budget: + marker = marker[:budget] + + return head + marker + tail + + +def _estimate_message_text(message: Any) -> str: + if message is None: + return "" + + if isinstance(message, dict): + # Common shapes: {role, content}, {role, text}, {role, contents} + for key in ("content", "text", "contents"): + if key in message: + return _safe_str(message.get(key)) + return _safe_str(message) + + # Attribute-based objects. + for attr in ("content", "text", "contents"): + if hasattr(message, attr): + return _safe_str(getattr(message, attr)) + return _safe_str(message) + + +def _get_message_role(message: Any) -> str | None: + if message is None: + return None + if isinstance(message, dict): + role = message.get("role") + return role if isinstance(role, str) else None + role = getattr(message, "role", None) + return role if isinstance(role, str) else None + + +def _set_message_text(message: Any, new_text: str) -> Any: + """Best-effort setter for message text. + + - For dict messages: returns a shallow-copied dict with content/text updated. + - For objects: tries to set .content or .text; if that fails, returns original. + """ + if isinstance(message, dict): + out = dict(message) + if "content" in out: + out["content"] = new_text + elif "text" in out: + out["text"] = new_text + elif "contents" in out: + out["contents"] = new_text + else: + out["content"] = new_text + return out + + for attr in ("content", "text"): + if hasattr(message, attr): + try: + setattr(message, attr, new_text) + return message + except Exception: + pass + return message + + +@dataclass(frozen=True) +class ContextTrimConfig: + """Character-budget based context trimming. + + This is a defensive control to prevent hard failures like + "input exceeds the context window" when upstream accidentally injects + huge blobs (telemetry JSON, repeated instructions, etc.). + """ + + enabled: bool = True + # GPT-5.1 supports 272K input tokens (~800K chars). With workspace context + # injected into system instructions (never trimmed) and Qdrant shared memory + # providing cross-step context, we can keep fewer conversation messages. + max_total_chars: int = 400_000 + max_message_chars: int = 0 # Disabled — with keep_last_messages=15, per-message truncation is unnecessary + keep_last_messages: int = 15 + keep_head_chars: int = 12_000 + keep_tail_chars: int = 4_000 + keep_system_messages: bool = True + retry_on_context_error: bool = True + + @staticmethod + def from_env( + enabled_env: str = "AOAI_CTX_TRIM_ENABLED", + max_total_chars_env: str = "AOAI_CTX_MAX_TOTAL_CHARS", + max_message_chars_env: str = "AOAI_CTX_MAX_MESSAGE_CHARS", + keep_last_messages_env: str = "AOAI_CTX_KEEP_LAST_MESSAGES", + keep_head_chars_env: str = "AOAI_CTX_KEEP_HEAD_CHARS", + keep_tail_chars_env: str = "AOAI_CTX_KEEP_TAIL_CHARS", + keep_system_messages_env: str = "AOAI_CTX_KEEP_SYSTEM_MESSAGES", + retry_on_context_error_env: str = "AOAI_CTX_RETRY_ON_CONTEXT_ERROR", + ) -> "ContextTrimConfig": + def _int(name: str, default: int) -> int: + try: + return int(os.getenv(name, str(default))) + except Exception: + return default + + def _bool(name: str, default: bool) -> bool: + raw = os.getenv(name) + if raw is None: + return default + return str(raw).strip().lower() in ("1", "true", "yes", "y", "on") + + return ContextTrimConfig( + enabled=_bool(enabled_env, True), + max_total_chars=max(0, _int(max_total_chars_env, 240_000)), + max_message_chars=max(0, _int(max_message_chars_env, 20_000)), + keep_last_messages=max(1, _int(keep_last_messages_env, 15)), + keep_head_chars=max(0, _int(keep_head_chars_env, 10_000)), + keep_tail_chars=max(0, _int(keep_tail_chars_env, 3_000)), + keep_system_messages=_bool(keep_system_messages_env, True), + retry_on_context_error=_bool(retry_on_context_error_env, True), + ) + + +def _trim_messages( + messages: MutableSequence[Any], *, cfg: ContextTrimConfig +) -> list[Any]: + if not cfg.enabled: + return list(messages) + + # ────────────────────────────────────────────────────────────────────── + # Phase 0: Summarize large save_content_to_blob calls. + # Write payloads are redundant once persisted — replace with a short + # summary. Read tool results are never truncated so the model always + # has the full file content to reason about. + # ────────────────────────────────────────────────────────────────────── + SAVE_ARG_MAX_CHARS = 200 # Truncate save_content_to_blob arguments + + for i, m in enumerate(messages): + text = _estimate_message_text(m) + if _looks_like_save_blob_call(text) and len(text) > SAVE_ARG_MAX_CHARS: + summary = _summarize_save_blob(text, SAVE_ARG_MAX_CHARS) + messages[i] = _set_message_text(m, summary) + + # Keep last N messages; optionally keep system messages from the head. + system_messages: list[Any] = [] + tail: list[Any] = list(messages) + + if cfg.keep_system_messages: + for m in messages: + if _get_message_role(m) == "system": + system_messages.append(m) + else: + break + + if cfg.keep_last_messages > 0: + tail = tail[-cfg.keep_last_messages :] + + # De-dupe large repeated blobs using author-less fingerprint on head/tail text. + seen_fingerprints: set[tuple[str, str]] = set() + cleaned: list[Any] = [] + + for idx, m in enumerate(tail): + text = _estimate_message_text(m) + fp = (text[:200], text[-200:]) + if fp in seen_fingerprints: + continue + seen_fingerprints.add(fp) + + # Never truncate the last message — the agent needs it in full + # to reason about the most recent tool result or instruction. + is_last = idx == len(tail) - 1 + if ( + not is_last + and cfg.max_message_chars > 0 + and len(text) > cfg.max_message_chars + ): + text = _truncate_text( + text, + max_chars=cfg.max_message_chars, + keep_head_chars=cfg.keep_head_chars, + keep_tail_chars=cfg.keep_tail_chars, + ) + m = _set_message_text(m, text) + cleaned.append(m) + + # Enforce overall budget by trimming oldest messages from the non-system tail. + combined: list[Any] = system_messages + cleaned + if cfg.max_total_chars <= 0: + return combined + + def _total_chars(msgs: list[Any]) -> int: + return sum(len(_estimate_message_text(x)) for x in msgs) + + while combined and _total_chars(combined) > cfg.max_total_chars: + # Prefer dropping earliest non-system message. + drop_index = 0 + if cfg.keep_system_messages and system_messages: + drop_index = len(system_messages) + if drop_index >= len(combined): + # If only system messages remain, truncate the last one. + last = combined[-1] + text = _estimate_message_text(last) + text = _truncate_text( + text, + max_chars=cfg.max_total_chars, + keep_head_chars=min(cfg.keep_head_chars, cfg.max_total_chars), + keep_tail_chars=min(cfg.keep_tail_chars, cfg.max_total_chars), + ) + combined[-1] = _set_message_text(last, text) + break + combined.pop(drop_index) + + return combined + + +def _try_get_retry_after_seconds(error: BaseException) -> float | None: + inner = getattr(error, "inner_exception", None) + if isinstance(inner, BaseException) and inner is not error: + inner_retry = _try_get_retry_after_seconds(inner) + if inner_retry is not None: + return inner_retry + + candidates: list[Any] = [] + candidates.append(getattr(error, "retry_after", None)) + + response = getattr(error, "response", None) + if response is not None: + candidates.append(getattr(response, "headers", None)) + + headers = getattr(error, "headers", None) + if headers is not None: + candidates.append(headers) + + for item in candidates: + if item is None: + continue + if isinstance(item, (int, float)): + return float(item) + if isinstance(item, str): + try: + return float(item) + except Exception: + continue + if isinstance(item, dict): + for key in ("retry-after", "Retry-After"): + if key in item: + try: + return float(item[key]) + except Exception: + pass + return None + + +async def _retry_call(coro_factory, *, config: RateLimitRetryConfig): + def _log_before_sleep(retry_state) -> None: + exc = None + if retry_state.outcome is not None and retry_state.outcome.failed: + exc = retry_state.outcome.exception() + + # Tenacity sets next_action when it's about to sleep. + sleep_s = None + next_action = getattr(retry_state, "next_action", None) + if next_action is not None: + sleep_s = getattr(next_action, "sleep", None) + + retry_after = _try_get_retry_after_seconds(exc) if exc is not None else None + status = getattr(exc, "status_code", None) or getattr(exc, "status", None) + attempt = getattr(retry_state, "attempt_number", None) + max_attempts = config.max_retries + 1 + + logger.warning( + "[AOAI_RETRY] attempt %s/%s; sleeping=%ss; retry_after=%s; status=%s; error=%s", + attempt, + max_attempts, + None if sleep_s is None else round(float(sleep_s), 3), + None if retry_after is None else round(float(retry_after), 3), + status, + None if exc is None else _format_exc_brief(exc), + ) + + class _WaitRetryAfterOrExpJitter(wait_base): + def __init__(self, retry_config: RateLimitRetryConfig): + self._cfg = retry_config + + def __call__(self, retry_state) -> float: + exc = None + if retry_state.outcome is not None and retry_state.outcome.failed: + exc = retry_state.outcome.exception() + + if exc is not None: + retry_after = _try_get_retry_after_seconds(exc) + if retry_after is not None and retry_after >= 0: + return float(retry_after) + + attempt_index = max(0, retry_state.attempt_number - 1) + delay = self._cfg.base_delay_seconds * (2**attempt_index) + delay = min(delay, self._cfg.max_delay_seconds) + delay = delay + random.uniform(0.0, 0.25 * max(delay, 0.1)) + return float(delay) + + retrying = AsyncRetrying( + retry=retry_if_exception(_looks_like_rate_limit), + stop=stop_after_attempt(config.max_retries + 1), + wait=_WaitRetryAfterOrExpJitter(config), + before_sleep=_log_before_sleep, + reraise=True, + ) + + async for attempt in retrying: + with attempt: + return await coro_factory() + + raise RuntimeError("Retry loop exhausted unexpectedly") + + +class AzureOpenAIResponseClientWithRetry(AzureOpenAIResponsesClient): + """Azure OpenAI Responses client with 429 retry at the request boundary. + + Retry is centralized in the client layer (not in orchestrators) by retrying the + underlying Responses calls made by `OpenAIBaseResponsesClient`. + """ + + def __init__( + self, + *args: Any, + retry_config: RateLimitRetryConfig | None = None, + # Legacy parameter names (mapped to OpenAIChatClient equivalents) + deployment_name: str | None = None, + endpoint: str | None = None, + ad_token: str | None = None, + ad_token_provider: object | None = None, + token_endpoint: str | None = None, + **kwargs: Any, + ): + # Map legacy params to OpenAIChatClient params + if deployment_name and "model" not in kwargs: + kwargs["model"] = deployment_name + if endpoint and "azure_endpoint" not in kwargs: + kwargs["azure_endpoint"] = endpoint + if ad_token_provider and kwargs.get("credential") is None: + kwargs["credential"] = ad_token_provider + + super().__init__(*args, **kwargs) + self._retry_config = retry_config or RateLimitRetryConfig.from_env() + self._context_trim_config = ContextTrimConfig.from_env() + + async def _inner_get_response( + self, *, messages: MutableSequence[Any], chat_options: Any = None, options: Any = None, stream: bool = False, **kwargs: Any + ) -> Any: + # Support both old (chat_options) and new (options) parameter names + effective_options = options if options is not None else chat_options + parent_inner_get_response = super( + AzureOpenAIResponseClientWithRetry, self + )._inner_get_response + + effective_messages: MutableSequence[Any] | list[Any] = messages + if self._context_trim_config.enabled: + approx_chars = sum(len(_estimate_message_text(m)) for m in messages) + if ( + self._context_trim_config.max_total_chars > 0 + and approx_chars > self._context_trim_config.max_total_chars + ): + effective_messages = _trim_messages( + messages, cfg=self._context_trim_config + ) + logger.warning( + "[AOAI_CTX_TRIM] pre-trimmed request messages: approx_chars=%s -> %s; count=%s -> %s", + approx_chars, + sum(len(_estimate_message_text(m)) for m in effective_messages), + len(messages), + len(effective_messages), + ) + + try: + return await _retry_call( + lambda: parent_inner_get_response( + messages=effective_messages, options=effective_options, stream=stream, **kwargs + ), + config=self._retry_config, + ) + except Exception as e: + if not ( + self._context_trim_config.enabled + and self._context_trim_config.retry_on_context_error + and _looks_like_context_length(e) + ): + raise + + trimmed = _trim_messages( + messages, + cfg=ContextTrimConfig( + enabled=True, + max_total_chars=max( + 50_000, self._context_trim_config.max_total_chars - 80_000 + ), + max_message_chars=max( + 3_000, self._context_trim_config.max_message_chars - 6_000 + ), + keep_last_messages=max( + 6, self._context_trim_config.keep_last_messages - 12 + ), + keep_head_chars=max( + 1_000, self._context_trim_config.keep_head_chars - 4_000 + ), + keep_tail_chars=self._context_trim_config.keep_tail_chars, + keep_system_messages=True, + retry_on_context_error=True, + ), + ) + logger.warning( + "[AOAI_CTX_TRIM] retrying after context-length error; count=%s -> %s", + len(messages), + len(trimmed), + ) + # Cool down before retrying to avoid triggering 429s immediately. + trim_delay = self._retry_config.base_delay_seconds + trim_delay = min(trim_delay, self._retry_config.max_delay_seconds) + logger.info( + "[AOAI_CTX_TRIM] sleeping %ss before retry", + round(trim_delay, 1), + ) + await asyncio.sleep(trim_delay) + return await _retry_call( + lambda: parent_inner_get_response( + messages=trimmed, options=effective_options, stream=stream, **kwargs + ), + config=self._retry_config, + ) + + async def _inner_get_streaming_response( + self, *, messages: MutableSequence[Any], chat_options: Any = None, options: Any = None, **kwargs: Any + ) -> AsyncIterable[Any]: + """Streaming with retry. Delegates to parent._inner_get_response(stream=True). + + This method is kept for backward compatibility in case any internal code path + calls it directly. The new framework uses _inner_get_response(stream=True). + """ + # Conservative retry: only retries failures before the first yielded update. + attempts = self._retry_config.max_retries + 1 + effective_options = options if options is not None else chat_options + + effective_messages: MutableSequence[Any] | list[Any] = messages + if self._context_trim_config.enabled: + approx_chars = sum(len(_estimate_message_text(m)) for m in messages) + if ( + self._context_trim_config.max_total_chars > 0 + and approx_chars > self._context_trim_config.max_total_chars + ): + effective_messages = _trim_messages( + messages, cfg=self._context_trim_config + ) + logger.warning( + "[AOAI_CTX_TRIM] pre-trimmed streaming request messages: approx_chars=%s -> %s; count=%s -> %s", + approx_chars, + sum(len(_estimate_message_text(m)) for m in effective_messages), + len(messages), + len(effective_messages), + ) + + for attempt_index in range(attempts): + stream = super( + AzureOpenAIResponseClientWithRetry, self + )._inner_get_response( + messages=effective_messages, options=effective_options, stream=True, **kwargs + ) + + iterator = stream.__aiter__() + try: + first = await iterator.__anext__() + + async def _tail(): + yield first + async for item in iterator: + yield item + + async for item in _tail(): + yield item + return + except StopAsyncIteration: + return + except Exception as e: + close = getattr(stream, "aclose", None) + if callable(close): + try: + await close() + except Exception: + logger.debug("Best-effort close of response stream failed", exc_info=True) + + # Progressive retry for context-length failures. + if ( + self._context_trim_config.enabled + and self._context_trim_config.retry_on_context_error + and _looks_like_context_length(e) + ): + # Make trimming progressively more aggressive on each retry + # GPT-5.1: 272K input tokens ≈ 800K chars. Scale down from 600K default. + scale = attempt_index + 1 + aggressive_cfg = ContextTrimConfig( + enabled=True, + max_total_chars=max( + 30_000, + self._context_trim_config.max_total_chars - scale * 100_000, + ), + max_message_chars=max( + 2_000, + self._context_trim_config.max_message_chars - scale * 8_000, + ), + keep_last_messages=max( + 4, + self._context_trim_config.keep_last_messages - scale * 8, + ), + keep_head_chars=max( + 500, + self._context_trim_config.keep_head_chars - scale * 3_000, + ), + keep_tail_chars=max( + 500, + self._context_trim_config.keep_tail_chars - scale * 1_000, + ), + keep_system_messages=True, + retry_on_context_error=True, + ) + trimmed = _trim_messages(effective_messages, cfg=aggressive_cfg) + logger.warning( + "[AOAI_CTX_TRIM_STREAM] retrying after context-length error (attempt %s); count=%s -> %s, budget=%s", + attempt_index + 1, + len(effective_messages), + len(trimmed), + aggressive_cfg.max_total_chars, + ) + effective_messages = trimmed + if attempt_index >= attempts - 1: + # No more retries available. + raise + + # Cool down before retrying — immediate retries after trimming + # tend to trigger 429s because the API hasn't recovered yet. + trim_delay = self._retry_config.base_delay_seconds * ( + 2**attempt_index + ) + trim_delay = min(trim_delay, self._retry_config.max_delay_seconds) + logger.info( + "[AOAI_CTX_TRIM_STREAM] sleeping %ss before retry", + round(trim_delay, 1), + ) + await asyncio.sleep(trim_delay) + continue + + if not _looks_like_rate_limit(e) or attempt_index >= attempts - 1: + if _looks_like_rate_limit(e): + logger.warning( + "[AOAI_RETRY_STREAM] giving up after %s/%s attempts; error=%s", + attempt_index + 1, + attempts, + _format_exc_brief(e) + if isinstance(e, BaseException) + else str(e), + ) + raise + + retry_after = _try_get_retry_after_seconds(e) + if retry_after is not None and retry_after >= 0: + delay = retry_after + else: + delay = self._retry_config.base_delay_seconds * (2**attempt_index) + delay = min(delay, self._retry_config.max_delay_seconds) + delay = delay + random.uniform(0.0, 0.25 * max(delay, 0.1)) + + status = getattr(e, "status_code", None) or getattr(e, "status", None) + logger.warning( + "[AOAI_RETRY_STREAM] attempt %s/%s; sleeping=%ss; retry_after=%s; status=%s; error=%s", + attempt_index + 1, + attempts, + round(float(delay), 3), + None if retry_after is None else round(float(retry_after), 3), + status, + _format_exc_brief(e) if isinstance(e, BaseException) else str(e), + ) + + await asyncio.sleep(delay) From d86c63e82926a008629e99295761b15fe1de7dee Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 4 Jun 2026 13:06:16 +0530 Subject: [PATCH 13/25] fix: address review comments - type annotations, test cleanup, and code clarity - Fix create_agents return type annotation to dict[str, Agent] - Narrow participants param to Mapping only (Sequence was unused) - Normalize self.agents with dict() and correct value type - Replace redundant pass with continue and clarifying comment - Add teardown_module to test files to restore patched Message class Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/libs/agent_framework/groupchat_orchestrator.py | 9 ++++----- src/processor/src/libs/base/orchestrator_base.py | 2 +- src/processor/src/steps/migration_processor.py | 4 ++-- .../test_groupchat_orchestrator_internals.py | 7 +++++++ .../agent_framework/test_input_observer_middleware.py | 7 +++++++ .../unit/libs/agent_framework/test_middlewares_extras.py | 7 +++++++ 6 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 78fb899e..d064c3f7 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -19,7 +19,7 @@ from collections.abc import Iterable from dataclasses import asdict, dataclass, is_dataclass from datetime import datetime -from typing import Any, Awaitable, Callable, Generic, Mapping, Sequence, TypeVar +from typing import Any, Awaitable, Callable, Generic, Mapping, TypeVar from agent_framework import ( Agent, @@ -190,8 +190,7 @@ def __init__( self, name: str, process_id: str, - participants: Mapping[str, SupportsAgentRun | Executor] - | Sequence[SupportsAgentRun | Executor], + participants: Mapping[str, SupportsAgentRun | Executor], memory_client: AsyncMemory, coordinator_name: str = "Coordinator", max_rounds: int = 100, @@ -204,7 +203,7 @@ def __init__( Args: name: Friendly workflow name (used for logging/diagnostics) process_id: Workflow/process identifier (used for tracing) - participants: Mapping/sequence of pre-created agents (including the Coordinator) + participants: Mapping of pre-created agents (including the Coordinator) memory_client: Mem0 async memory client for multi-agent memory (may be None depending on runtime) coordinator_name: Name of the coordinator/manager agent max_rounds: Maximum conversation rounds before termination @@ -227,7 +226,7 @@ def __init__( self.result_format = result_output_format # Runtime state - self.agents: dict[str, Agent] = participants + self.agents: dict[str, SupportsAgentRun | Executor] = dict(participants) self.agent_tool_usage: dict[str, list[dict[str, Any]]] = {} self.agent_responses: list[AgentResponse] = [] self._initialized: bool = False diff --git a/src/processor/src/libs/base/orchestrator_base.py b/src/processor/src/libs/base/orchestrator_base.py index 8e32d6e8..420664a7 100644 --- a/src/processor/src/libs/base/orchestrator_base.py +++ b/src/processor/src/libs/base/orchestrator_base.py @@ -147,7 +147,7 @@ async def prepare_agent_infos(self) -> list[AgentInfo]: async def create_agents( self, agent_infos: list[AgentInfo], process_id: str - ) -> list[Agent]: + ) -> dict[str, Agent]: agents = dict[str, Agent]() agent_client = await self.get_client(thread_id=process_id) diff --git a/src/processor/src/steps/migration_processor.py b/src/processor/src/steps/migration_processor.py index adeb031f..436757fa 100644 --- a/src/processor/src/steps/migration_processor.py +++ b/src/processor/src/steps/migration_processor.py @@ -561,8 +561,8 @@ async def _generate_report_summary( return event.data elif event.type == "executor_failed": - pass - # will handle in WorkflowFailedEvent + # Intentionally ignored — actionable details arrive in the subsequent "failed" event. + continue elif event.type == "failed": logger.error( "Executor failed (%s): %s [%s]: %s (traceback: %s)", diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py index 1103aff2..70be9619 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py @@ -31,6 +31,7 @@ def __init__(self, *, role, text=None, contents=None, author_name=None): self.author_name = author_name +_original_message = getattr(groupchat_module, "Message", None) groupchat_module.Message = Message from libs.agent_framework.groupchat_orchestrator import ( # noqa: E402 AgentResponse, @@ -39,6 +40,12 @@ def __init__(self, *, role, text=None, contents=None, author_name=None): ) +def teardown_module(): + """Restore the original Message class to avoid leaking into other tests.""" + if _original_message is not None: + groupchat_module.Message = _original_message + + def _run(coro): return asyncio.run(coro) diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py b/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py index 2db61a41..8fcaf7ed 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py @@ -21,10 +21,17 @@ def __init__(self, *, role, text=None, contents=None, author_name=None): # Patch at module level: middleware code references Message at runtime for isinstance # checks and construction. This is scoped to test execution only. +_original_message = getattr(middlewares_module, "Message", None) middlewares_module.Message = Message from libs.agent_framework.middlewares import InputObserverMiddleware # noqa: E402 +def teardown_module(): + """Restore the original Message class to avoid leaking into other tests.""" + if _original_message is not None: + middlewares_module.Message = _original_message + + def test_input_observer_middleware_replaces_user_text_when_configured() -> None: async def _run() -> None: ctx = SimpleNamespace( diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py b/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py index 9648f581..b6f63f7a 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py @@ -25,6 +25,7 @@ def __init__(self, *, role, text=None, contents=None, author_name=None): # Patch at module level: middleware code references Message at runtime for isinstance # checks and construction. This is scoped to test execution only. +_original_message = getattr(middlewares_module, "Message", None) middlewares_module.Message = Message from libs.agent_framework.middlewares import ( # noqa: E402 DebuggingMiddleware, @@ -32,6 +33,12 @@ def __init__(self, *, role, text=None, contents=None, author_name=None): ) +def teardown_module(): + """Restore the original Message class to avoid leaking into other tests.""" + if _original_message is not None: + middlewares_module.Message = _original_message + + def _run(coro): return asyncio.run(coro) From d60d5b14e9a287a655fc2cd6b445c6d9f9b05b00 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 4 Jun 2026 13:58:21 +0530 Subject: [PATCH 14/25] fix: use contents= for Message construction in agent-framework 1.3.0 - Update InputObserverMiddleware to use Message(contents=) instead of Message(text=) since agent-framework 1.3.0 renamed the parameter - Update corresponding tests to verify contents field - Fix teardown_module signature to accept optional module parameter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/libs/agent_framework/middlewares.py | 2 +- .../test_groupchat_orchestrator_internals.py | 2 +- .../test_input_observer_middleware.py | 2 +- .../libs/agent_framework/test_middlewares_extras.py | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/processor/src/libs/agent_framework/middlewares.py b/src/processor/src/libs/agent_framework/middlewares.py index 1f26d547..e7e3b045 100644 --- a/src/processor/src/libs/agent_framework/middlewares.py +++ b/src/processor/src/libs/agent_framework/middlewares.py @@ -159,7 +159,7 @@ async def process( f"[InputObserverMiddleware] Updated: '{original_text}' -> '{updated_text}'" ) - modified_message = Message(role=message.role, text=updated_text) + modified_message = Message(role=message.role, contents=updated_text) modified_messages.append(modified_message) modified_count += 1 else: diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py index 70be9619..431ad94a 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py @@ -40,7 +40,7 @@ def __init__(self, *, role, text=None, contents=None, author_name=None): ) -def teardown_module(): +def teardown_module(module=None): """Restore the original Message class to avoid leaking into other tests.""" if _original_message is not None: groupchat_module.Message = _original_message diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py b/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py index 8fcaf7ed..5ecbc2b0 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py @@ -26,7 +26,7 @@ def __init__(self, *, role, text=None, contents=None, author_name=None): from libs.agent_framework.middlewares import InputObserverMiddleware # noqa: E402 -def teardown_module(): +def teardown_module(module=None): """Restore the original Message class to avoid leaking into other tests.""" if _original_message is not None: middlewares_module.Message = _original_message diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py b/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py index b6f63f7a..793b3faa 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py @@ -33,7 +33,7 @@ def __init__(self, *, role, text=None, contents=None, author_name=None): ) -def teardown_module(): +def teardown_module(module=None): """Restore the original Message class to avoid leaking into other tests.""" if _original_message is not None: middlewares_module.Message = _original_message @@ -110,22 +110,22 @@ class TestInputObserverMiddleware: def test_replaces_user_messages_when_replacement_set(self): from libs.agent_framework.middlewares import InputObserverMiddleware - msg_user = Message(role=ROLE_USER, text="orig user") - msg_assistant = Message(role=ROLE_ASSISTANT, text="hi") + msg_user = Message(role=ROLE_USER, text="orig user", contents="orig user") + msg_assistant = Message(role=ROLE_ASSISTANT, text="hi", contents="hi") ctx = MagicMock() ctx.messages = [msg_user, msg_assistant] next_fn = AsyncMock() mw = InputObserverMiddleware(replacement="REDACTED") _run(mw.process(ctx, next_fn)) # First message replaced, second untouched - assert ctx.messages[0].text == "REDACTED" - assert ctx.messages[1].text == "hi" + assert ctx.messages[0].contents == "REDACTED" + assert ctx.messages[1].contents == "hi" next_fn.assert_awaited_once() def test_no_replacement_keeps_text(self): from libs.agent_framework.middlewares import InputObserverMiddleware - msg = Message(role=ROLE_USER, text="keep me") + msg = Message(role=ROLE_USER, text="keep me", contents="keep me") ctx = MagicMock() ctx.messages = [msg] mw = InputObserverMiddleware(replacement=None) From cba537ead734b7af1ed0e2f0fc988d8bb77eb10a Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 4 Jun 2026 14:14:42 +0530 Subject: [PATCH 15/25] fix: add setup_module to prevent test ordering issues with Message patch The teardown_module from one test file was restoring the real Message before another test file's tests ran. Adding setup_module ensures the stub is re-applied before each module's tests execute. Also fix test assertion to check contents instead of text since the middleware now uses Message(contents=). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../test_groupchat_orchestrator_internals.py | 5 +++++ .../libs/agent_framework/test_input_observer_middleware.py | 5 +++++ .../unit/libs/agent_framework/test_middlewares_extras.py | 7 ++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py index 431ad94a..5ccbce22 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py @@ -40,6 +40,11 @@ def __init__(self, *, role, text=None, contents=None, author_name=None): ) +def setup_module(module=None): + """Re-apply the Message patch in case another module's teardown restored it.""" + groupchat_module.Message = Message + + def teardown_module(module=None): """Restore the original Message class to avoid leaking into other tests.""" if _original_message is not None: diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py b/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py index 5ecbc2b0..bcb7d552 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py @@ -26,6 +26,11 @@ def __init__(self, *, role, text=None, contents=None, author_name=None): from libs.agent_framework.middlewares import InputObserverMiddleware # noqa: E402 +def setup_module(module=None): + """Re-apply the Message patch in case another module's teardown restored it.""" + middlewares_module.Message = Message + + def teardown_module(module=None): """Restore the original Message class to avoid leaking into other tests.""" if _original_message is not None: diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py b/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py index 793b3faa..39ec2ca9 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py @@ -33,6 +33,11 @@ def __init__(self, *, role, text=None, contents=None, author_name=None): ) +def setup_module(module=None): + """Re-apply the Message patch in case another module's teardown restored it.""" + middlewares_module.Message = Message + + def teardown_module(module=None): """Restore the original Message class to avoid leaking into other tests.""" if _original_message is not None: @@ -130,4 +135,4 @@ def test_no_replacement_keeps_text(self): ctx.messages = [msg] mw = InputObserverMiddleware(replacement=None) _run(mw.process(ctx, AsyncMock())) - assert ctx.messages[0].text == "keep me" + assert ctx.messages[0].contents == "keep me" From 271fc276b22fdf468ae0fbff375a9c7cec5568a4 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 4 Jun 2026 14:57:37 +0530 Subject: [PATCH 16/25] fix: assert contents instead of text in input observer test The middleware now uses Message(contents=) so the test must verify the contents field, not text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../unit/libs/agent_framework/test_input_observer_middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py b/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py index bcb7d552..7dd364c6 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py @@ -53,6 +53,6 @@ async def _next(_context): await mw.process(ctx, _next) assert ctx.messages[0].role == ROLE_USER - assert ctx.messages[0].text == "replacement" + assert ctx.messages[0].contents == "replacement" asyncio.run(_run()) From 75b3e643e75ca0231ef4cdfe8c189802ba4bdc4d Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 4 Jun 2026 15:08:31 +0530 Subject: [PATCH 17/25] fix: set both text and contents on Message, restore copyright header - Set both text= and contents= when constructing Message in InputObserverMiddleware for compatibility with downstream code - Restore missing copyright header in azure_openai_response_retry.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/libs/agent_framework/azure_openai_response_retry.py | 1 + src/processor/src/libs/agent_framework/middlewares.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index 04d5a15b..e19a1bfe 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -1,3 +1,4 @@ +# Copyright (c) Microsoft Corporation. # Licensed under the MIT License. """Azure OpenAI Responses client wrapper with rate-limit-aware retry logic.""" diff --git a/src/processor/src/libs/agent_framework/middlewares.py b/src/processor/src/libs/agent_framework/middlewares.py index e7e3b045..e12f38d0 100644 --- a/src/processor/src/libs/agent_framework/middlewares.py +++ b/src/processor/src/libs/agent_framework/middlewares.py @@ -159,7 +159,7 @@ async def process( f"[InputObserverMiddleware] Updated: '{original_text}' -> '{updated_text}'" ) - modified_message = Message(role=message.role, contents=updated_text) + modified_message = Message(role=message.role, text=updated_text, contents=updated_text) modified_messages.append(modified_message) modified_count += 1 else: From ed3d29c9a5eb30dbec07f57c1d7378cdeb1ff524 Mon Sep 17 00:00:00 2001 From: Dhanushree-Microsoft Date: Thu, 4 Jun 2026 15:34:23 +0530 Subject: [PATCH 18/25] fix: remove unused modified_count variable (F841) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/processor/src/libs/agent_framework/middlewares.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/processor/src/libs/agent_framework/middlewares.py b/src/processor/src/libs/agent_framework/middlewares.py index e12f38d0..a17b6040 100644 --- a/src/processor/src/libs/agent_framework/middlewares.py +++ b/src/processor/src/libs/agent_framework/middlewares.py @@ -146,7 +146,6 @@ async def process( # Modify user messages by creating new messages with enhanced text modified_messages: list[Message] = [] - modified_count = 0 for message in context.messages: if message.role == ROLE_USER and message.text: @@ -161,7 +160,6 @@ async def process( modified_message = Message(role=message.role, text=updated_text, contents=updated_text) modified_messages.append(modified_message) - modified_count += 1 else: modified_messages.append(message) From d7055f2dfad08d085b918e5f9f07ad6b767a5810 Mon Sep 17 00:00:00 2001 From: Dhanushree-Microsoft Date: Thu, 4 Jun 2026 15:34:34 +0530 Subject: [PATCH 19/25] fix: remove unused Agent import (F401) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/processor/src/libs/agent_framework/groupchat_orchestrator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index d064c3f7..ebebeceb 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -22,7 +22,6 @@ from typing import Any, Awaitable, Callable, Generic, Mapping, TypeVar from agent_framework import ( - Agent, AgentResponseUpdate, Executor, Message, From ebbba82544345025081521462a42e05932848734 Mon Sep 17 00:00:00 2001 From: Dhanushree-Microsoft Date: Fri, 5 Jun 2026 11:25:10 +0530 Subject: [PATCH 20/25] fix: remove unused WorkflowEvent import and restore 'invalid content' retry check - Remove unused WorkflowEvent import from migration_processor.py (fixes lint F401) - Restore 'model produced invalid content' transient error retry in azure_openai_response_retry.py (regression from agent-framework 1.3.0 refactor) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../libs/agent_framework/azure_openai_response_retry.py | 8 ++++++++ src/processor/src/steps/migration_processor.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index e19a1bfe..93695000 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -82,6 +82,14 @@ def _looks_like_rate_limit(error: BaseException) -> bool: if isinstance(status, int) and 500 <= status < 600: return True + # "The model produced invalid content" is a transient error from Azure OpenAI + # when the model output fails content/schema validation — worth retrying. + if any( + s in msg + for s in ["model produced invalid content", "invalid content"] + ): + return True + cause = getattr(error, "__cause__", None) if cause and cause is not error: return _looks_like_rate_limit(cause) diff --git a/src/processor/src/steps/migration_processor.py b/src/processor/src/steps/migration_processor.py index 436757fa..c73f570a 100644 --- a/src/processor/src/steps/migration_processor.py +++ b/src/processor/src/steps/migration_processor.py @@ -32,7 +32,7 @@ from datetime import datetime from typing import Any -from agent_framework import Workflow, WorkflowBuilder, WorkflowEvent +from agent_framework import Workflow, WorkflowBuilder from openai import AsyncAzureOpenAI From 226c84584e7a324aff46a46372854d5cee61c4d4 Mon Sep 17 00:00:00 2001 From: Dhanushree-Microsoft Date: Tue, 9 Jun 2026 10:51:42 +0530 Subject: [PATCH 21/25] fix: enhance rate limit detection for Azure OpenAI transient errors --- .../libs/agent_framework/azure_openai_response_retry.py | 8 ++++++++ src/processor/src/steps/migration_processor.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index e19a1bfe..93695000 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -82,6 +82,14 @@ def _looks_like_rate_limit(error: BaseException) -> bool: if isinstance(status, int) and 500 <= status < 600: return True + # "The model produced invalid content" is a transient error from Azure OpenAI + # when the model output fails content/schema validation — worth retrying. + if any( + s in msg + for s in ["model produced invalid content", "invalid content"] + ): + return True + cause = getattr(error, "__cause__", None) if cause and cause is not error: return _looks_like_rate_limit(cause) diff --git a/src/processor/src/steps/migration_processor.py b/src/processor/src/steps/migration_processor.py index 436757fa..c73f570a 100644 --- a/src/processor/src/steps/migration_processor.py +++ b/src/processor/src/steps/migration_processor.py @@ -32,7 +32,7 @@ from datetime import datetime from typing import Any -from agent_framework import Workflow, WorkflowBuilder, WorkflowEvent +from agent_framework import Workflow, WorkflowBuilder from openai import AsyncAzureOpenAI From 7e6647bb38882d3153e0ae9f42d9ba52236cc89f Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Tue, 9 Jun 2026 11:25:08 +0530 Subject: [PATCH 22/25] fix: update step names for Docker image builds in workflow --- .github/workflows/docker-build-and-push.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-build-and-push.yml b/.github/workflows/docker-build-and-push.yml index 397bea6f..c64748aa 100644 --- a/.github/workflows/docker-build-and-push.yml +++ b/.github/workflows/docker-build-and-push.yml @@ -86,7 +86,7 @@ jobs: echo "DATE_TAG=${DATE_TAG}" >> $GITHUB_ENV echo "Base tag: $BASE_TAG, Date tag: $DATE_TAG" - - name: Build and Push ContentProcessorAPI Docker image + - name: Build and Push Backend API Docker image uses: docker/build-push-action@v7 with: context: ./src/backend-api @@ -97,7 +97,7 @@ jobs: ${{ steps.registry.outputs.ext_registry }}/backend-api:${{ env.BASE_TAG }} ${{ steps.registry.outputs.ext_registry }}/backend-api:${{ env.DATE_TAG }} - - name: Build and Push ContentProcessor Docker image + - name: Build and Push Processor Docker image uses: docker/build-push-action@v7 with: context: ./src/processor @@ -108,7 +108,7 @@ jobs: ${{ steps.registry.outputs.ext_registry }}/processor:${{ env.BASE_TAG }} ${{ steps.registry.outputs.ext_registry }}/processor:${{ env.DATE_TAG }} - - name: Build and Push ContentProcessorWeb Docker image + - name: Build and Push Frontend Docker image uses: docker/build-push-action@v7 with: context: ./src/frontend From 3970438712314ac78c3753392ff0a9e39637fee0 Mon Sep 17 00:00:00 2001 From: Dhanushree-Microsoft Date: Tue, 9 Jun 2026 18:35:51 +0530 Subject: [PATCH 23/25] evening changes --- .../src/libs/agent_framework/agent_builder.py | 139 +++++++++++++----- .../src/libs/agent_framework/agent_info.py | 10 +- .../azure_openai_response_retry.py | 17 ++- .../agent_framework/groupchat_orchestrator.py | 11 +- .../shared_memory_context_provider.py | 57 ++++--- .../src/steps/migration_processor.py | 36 ++--- 6 files changed, 166 insertions(+), 104 deletions(-) diff --git a/src/processor/src/libs/agent_framework/agent_builder.py b/src/processor/src/libs/agent_framework/agent_builder.py index 6a7e4409..c9b747d0 100644 --- a/src/processor/src/libs/agent_framework/agent_builder.py +++ b/src/processor/src/libs/agent_framework/agent_builder.py @@ -11,6 +11,7 @@ AgentMiddleware, BaseChatClient, ChatMiddleware, + ChatOptions, ContextProvider, FunctionTool, ToolMode, @@ -441,32 +442,61 @@ def build(self) -> Agent: async with agent: response = await agent.run("Hello!") """ + # Build default_options from model parameters + options_dict: dict[str, Any] = {} + if self._frequency_penalty is not None: + options_dict["frequency_penalty"] = self._frequency_penalty + if self._logit_bias is not None: + options_dict["logit_bias"] = self._logit_bias + if self._max_tokens is not None: + options_dict["max_tokens"] = self._max_tokens + if self._metadata is not None: + options_dict["metadata"] = self._metadata + if self._model_id is not None: + options_dict["model"] = self._model_id + if self._presence_penalty is not None: + options_dict["presence_penalty"] = self._presence_penalty + if self._response_format is not None: + options_dict["response_format"] = self._response_format + if self._seed is not None: + options_dict["seed"] = self._seed + if self._stop is not None: + options_dict["stop"] = self._stop + if self._store is not None: + options_dict["store"] = self._store + if self._temperature is not None: + options_dict["temperature"] = self._temperature + if self._tool_choice is not None: + options_dict["tool_choice"] = self._tool_choice + if self._top_p is not None: + options_dict["top_p"] = self._top_p + if self._user is not None: + options_dict["user"] = self._user + if self._additional_chat_options: + options_dict.update(self._additional_chat_options) + + default_options = ChatOptions(**options_dict) if options_dict else None + + # Agent expects context_providers as a Sequence; wrap single instance in a list + ctx_providers = self._context_providers + if ctx_providers is not None and not isinstance(ctx_providers, list): + ctx_providers = [ctx_providers] + + # Agent expects middleware as a Sequence; wrap single instance in a list + mw = self._middleware + if mw is not None and not isinstance(mw, list): + mw = [mw] + return Agent( - chat_client=self._chat_client, + self._chat_client, instructions=self._instructions, id=self._id, name=self._name, description=self._description, - chat_message_store_factory=self._chat_message_store_factory, - conversation_id=self._conversation_id, - context_providers=self._context_providers, - middleware=self._middleware, - frequency_penalty=self._frequency_penalty, - logit_bias=self._logit_bias, - max_tokens=self._max_tokens, - metadata=self._metadata, - model_id=self._model_id, - presence_penalty=self._presence_penalty, - response_format=self._response_format, - seed=self._seed, - stop=self._stop, - store=self._store, - temperature=self._temperature, - tool_choice=self._tool_choice, tools=self._tools, - top_p=self._top_p, - user=self._user, - additional_chat_options=self._additional_chat_options, + default_options=default_options, + context_providers=ctx_providers, + middleware=mw, **self._kwargs, ) @@ -755,31 +785,60 @@ def create_agent( ``async with`` to ensure proper initialization and cleanup via the Agent's async context manager protocol. """ + # Build default_options from model parameters + opts: dict[str, Any] = {} + if frequency_penalty is not None: + opts["frequency_penalty"] = frequency_penalty + if logit_bias is not None: + opts["logit_bias"] = logit_bias + if max_tokens is not None: + opts["max_tokens"] = max_tokens + if metadata is not None: + opts["metadata"] = metadata + if model_id is not None: + opts["model"] = model_id + if presence_penalty is not None: + opts["presence_penalty"] = presence_penalty + if response_format is not None: + opts["response_format"] = response_format + if seed is not None: + opts["seed"] = seed + if stop is not None: + opts["stop"] = stop + if store is not None: + opts["store"] = store + if temperature is not None: + opts["temperature"] = temperature + if tool_choice is not None: + opts["tool_choice"] = tool_choice + if top_p is not None: + opts["top_p"] = top_p + if user is not None: + opts["user"] = user + if additional_chat_options: + opts.update(additional_chat_options) + + default_options = ChatOptions(**opts) if opts else None + + # Agent expects context_providers as a Sequence; wrap single instance in a list + ctx_providers = context_providers + if ctx_providers is not None and not isinstance(ctx_providers, list): + ctx_providers = [ctx_providers] + + # Agent expects middleware as a Sequence; wrap single instance in a list + mw = middleware + if mw is not None and not isinstance(mw, list): + mw = [mw] + return Agent( - chat_client=chat_client, + chat_client, instructions=instructions, id=id, name=name, description=description, - chat_message_store_factory=chat_message_store_factory, - conversation_id=conversation_id, - context_providers=context_providers, - middleware=middleware, - frequency_penalty=frequency_penalty, - logit_bias=logit_bias, - max_tokens=max_tokens, - metadata=metadata, - model_id=model_id, - presence_penalty=presence_penalty, - response_format=response_format, - seed=seed, - stop=stop, - store=store, - temperature=temperature, - tool_choice=tool_choice, tools=tools, - top_p=top_p, - user=user, - additional_chat_options=additional_chat_options, + default_options=default_options, + context_providers=ctx_providers, + middleware=mw, **kwargs, ) diff --git a/src/processor/src/libs/agent_framework/agent_info.py b/src/processor/src/libs/agent_framework/agent_info.py index 82f657b6..1ae3def7 100644 --- a/src/processor/src/libs/agent_framework/agent_info.py +++ b/src/processor/src/libs/agent_framework/agent_info.py @@ -5,13 +5,15 @@ from typing import Any, Callable, MutableMapping, Sequence -from agent_framework import FunctionTool +from agent_framework import FunctionTool, MCPStdioTool, MCPStreamableHTTPTool from jinja2 import Template from openai import BaseModel from pydantic import Field from .agent_framework_helper import AgentFrameworkHelper, ClientType +ToolType = FunctionTool | MCPStreamableHTTPTool | MCPStdioTool | Callable[..., Any] | MutableMapping[str, Any] + class AgentInfo(BaseModel): agent_name: str @@ -21,10 +23,8 @@ class AgentInfo(BaseModel): agent_instruction: str | None = Field(default=None) agent_framework_helper: AgentFrameworkHelper | None = Field(default=None) tools: ( - FunctionTool - | Callable[..., Any] - | MutableMapping[str, Any] - | Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] + ToolType + | Sequence[ToolType] | None ) = Field(default=None) diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index 93695000..4bcd5c0f 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -539,12 +539,27 @@ def __init__( # Map legacy params to OpenAIChatClient params if deployment_name and "model" not in kwargs: kwargs["model"] = deployment_name - if endpoint and "azure_endpoint" not in kwargs: + if endpoint and not kwargs.get("azure_endpoint"): kwargs["azure_endpoint"] = endpoint if ad_token_provider and kwargs.get("credential") is None: kwargs["credential"] = ad_token_provider + # Remove None-valued keys that would conflict with env-based settings + for k in list(kwargs): + if kwargs[k] is None: + del kwargs[k] + super().__init__(*args, **kwargs) + + # OpenAIChatClient appends /v1/ to azure_endpoint but Azure AI Foundry + # endpoints expect /openai/responses (without /v1/). Fix the base URL. + if hasattr(self, "client") and self.client is not None: + base = str(self.client.base_url) + if "/openai/v1/" in base: + import httpx + corrected = base.replace("/openai/v1/", "/openai/") + self.client._base_url = httpx.URL(corrected) + self._retry_config = retry_config or RateLimitRetryConfig.from_env() self._context_trim_config = ContextTrimConfig.from_env() diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index ebebeceb..eb731dde 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -28,9 +28,9 @@ Role, SupportsAgentRun, Workflow, - WorkflowBuilder as GroupChatBuilder, WorkflowEvent, ) +from agent_framework_orchestrations import GroupChatBuilder from mem0 import AsyncMemory from pydantic import BaseModel, ValidationError @@ -491,7 +491,7 @@ async def run_stream( # Execute with streaming conversation: list[Message] = [] - async for event in group_chat_workflow.run_stream(task_prompt): + async for event in group_chat_workflow.run(task_prompt, stream=True): # Enforce wall-clock timeout if configured. if self.max_seconds is not None: elapsed = (datetime.now() - start_time).total_seconds() @@ -1114,9 +1114,10 @@ async def _build_groupchat(self) -> Workflow: ] return ( - GroupChatBuilder() - .set_manager(coordinator) - .participants(participants) + GroupChatBuilder( + participants=participants, + orchestrator_agent=coordinator, + ) .build() ) diff --git a/src/processor/src/libs/agent_framework/shared_memory_context_provider.py b/src/processor/src/libs/agent_framework/shared_memory_context_provider.py index fd95a5de..df78ebe0 100644 --- a/src/processor/src/libs/agent_framework/shared_memory_context_provider.py +++ b/src/processor/src/libs/agent_framework/shared_memory_context_provider.py @@ -77,6 +77,7 @@ def __init__( top_k: Number of relevant memories to retrieve per turn. score_threshold: Minimum similarity score for memory retrieval. """ + super().__init__(source_id=f"shared_memory_{agent_name}_{step}") self._memory_store = memory_store self._agent_name = agent_name self._step = step @@ -96,11 +97,14 @@ def __init__( break self._prior_steps = _STEP_ORDER[:step_idx] if step_idx else [] - async def invoking( + async def before_run( self, - messages: Message | MutableSequence[Message], - **kwargs, - ) -> Context: + *, + agent, + session, + context, + state, + ) -> None: """Called before the agent's LLM call. Injects relevant shared memories. Only searches memories from PREVIOUS steps. Within the current step, @@ -108,12 +112,13 @@ async def invoking( """ # Skip if this is the first step (no prior memories exist) if not self._prior_steps: - return Context() + return - # Extract query from the most recent messages + # Extract query from the most recent messages in context + messages = context.get_messages() query = self._extract_query(messages) if not query: - return Context() + return try: memories = await self._memory_store.search( @@ -127,15 +132,15 @@ async def invoking( self._agent_name, e, ) - return Context() + return if not memories: - return Context() + return # Format memories into context instructions formatted = self._format_memories(memories) if not formatted: - return Context() + return instructions = f"{self.DEFAULT_CONTEXT_PROMPT}\n\n{formatted}" @@ -147,14 +152,15 @@ async def invoking( len(instructions), ) - return Context(instructions=instructions) + context.extend_instructions(self.source_id, instructions) - async def invoked( + async def after_run( self, - request_messages: Message | Sequence[Message], - response_messages: Message | Sequence[Message] | None = None, - invoke_exception: Exception | None = None, - **kwargs, + *, + agent, + session, + context, + state, ) -> None: """Called after the agent's LLM response. Buffers the response for storage. @@ -163,33 +169,26 @@ async def invoked( This means only the agent's last response per step gets stored, which is the most complete and useful summary. """ - if invoke_exception is not None: - logger.debug( - "[MEMORY] invoked() skipped for %s — exception: %s", - self._agent_name, - invoke_exception, - ) - return - - if response_messages is None: + response = context.response + if response is None: logger.debug( - "[MEMORY] invoked() skipped for %s — no response_messages", + "[MEMORY] after_run() skipped for %s — no response", self._agent_name, ) return # Extract text from response - content = self._extract_text(response_messages) + content = response.text if hasattr(response, "text") else None if not content or len(content) < MIN_CONTENT_LENGTH_TO_STORE: logger.debug( - "[MEMORY] invoked() skipped for %s — content too short (%d chars)", + "[MEMORY] after_run() skipped for %s — content too short (%d chars)", self._agent_name, len(content) if content else 0, ) return logger.info( - "[MEMORY] invoked() buffering for %s (step=%s, %d chars)", + "[MEMORY] after_run() buffering for %s (step=%s, %d chars)", self._agent_name, self._step, len(content), diff --git a/src/processor/src/steps/migration_processor.py b/src/processor/src/steps/migration_processor.py index c73f570a..7c7328fa 100644 --- a/src/processor/src/steps/migration_processor.py +++ b/src/processor/src/steps/migration_processor.py @@ -157,30 +157,18 @@ def _init_workflow(self) -> Workflow: Workflow The built workflow ready to execute. """ + analysis = AnalysisExecutor(id="analysis", app_context=self.app_context) + design = DesignExecutor(id="design", app_context=self.app_context) + yaml_convert = YamlConvertExecutor(id="yaml", app_context=self.app_context) + documentation = DocumentationExecutor( + id="documentation", app_context=self.app_context + ) + workflow = ( - WorkflowBuilder() - .register_executor( - lambda: AnalysisExecutor(id="analysis", app_context=self.app_context), - name="analysis", - ) - .register_executor( - lambda: DesignExecutor(id="design", app_context=self.app_context), - name="design", - ) - .register_executor( - lambda: YamlConvertExecutor(id="yaml", app_context=self.app_context), - name="yaml", - ) - .register_executor( - lambda: DocumentationExecutor( - id="documentation", app_context=self.app_context - ), - name="documentation", - ) - .set_start_executor("analysis") - .add_edge("analysis", "design") - .add_edge("design", "yaml") - .add_edge("yaml", "documentation") + WorkflowBuilder(start_executor=analysis) + .add_edge(analysis, design) + .add_edge(design, yaml_convert) + .add_edge(yaml_convert, documentation) .build() ) @@ -358,7 +346,7 @@ async def _generate_report_summary( "top_remediations": remediation_titles, } - async for event in self.workflow.run_stream(input_data): + async for event in self.workflow.run(input_data, stream=True): if event.type == "started": logger.info("Workflow started (%s)", event.origin.value) From 418000d32ad8708d8e3d1acce0188168febeead1 Mon Sep 17 00:00:00 2001 From: Dhanushree-Microsoft Date: Tue, 9 Jun 2026 23:34:23 +0530 Subject: [PATCH 24/25] fix: enhance message handling and context management in orchestrators --- .../azure_openai_response_retry.py | 116 ++++++++++++++++++ .../agent_framework/groupchat_orchestrator.py | 4 +- .../src/libs/base/orchestrator_base.py | 26 ++-- 3 files changed, 132 insertions(+), 14 deletions(-) diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index 4bcd5c0f..c691ae7b 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -325,6 +325,117 @@ def _bool(name: str, default: bool) -> bool: ) +def _get_content_items(message: Any) -> list[Any]: + """Return the list of content items from a message, or empty list.""" + contents = None + if isinstance(message, dict): + contents = message.get("contents") or message.get("content") + else: + contents = getattr(message, "contents", None) or getattr(message, "content", None) + if isinstance(contents, list): + return contents + return [] + + +def _remove_orphan_tool_messages(messages: list[Any]) -> list[Any]: + """Remove messages with orphaned function_call or function_result items. + + The Responses API requires every function_call in the input to have a + corresponding function_call_output (function_result). If context trimming + breaks these pairs, the API rejects the request. + """ + # Collect call_ids for function_calls and function_results + call_ids_with_call: set[str] = set() + call_ids_with_result: set[str] = set() + + for m in messages: + for item in _get_content_items(m): + item_type = None + call_id = None + if isinstance(item, dict): + item_type = item.get("type") + call_id = item.get("call_id") + else: + item_type = getattr(item, "type", None) + call_id = getattr(item, "call_id", None) + if not call_id: + continue + if item_type == "function_call": + call_ids_with_call.add(call_id) + elif item_type == "function_result": + call_ids_with_result.add(call_id) + + # Identify orphaned call_ids + orphaned_calls = call_ids_with_call - call_ids_with_result + orphaned_results = call_ids_with_result - call_ids_with_call + + if not orphaned_calls and not orphaned_results: + return messages + + logger.warning( + "[AOAI_CTX_TRIM] removing orphaned tool messages: %d orphaned calls, %d orphaned results", + len(orphaned_calls), + len(orphaned_results), + ) + + # Remove messages that ONLY contain orphaned tool items + cleaned: list[Any] = [] + for m in messages: + items = _get_content_items(m) + if not items: + cleaned.append(m) + continue + + has_orphan = False + has_non_orphan = False + for item in items: + item_type = None + call_id = None + if isinstance(item, dict): + item_type = item.get("type") + call_id = item.get("call_id") + else: + item_type = getattr(item, "type", None) + call_id = getattr(item, "call_id", None) + if call_id and item_type == "function_call" and call_id in orphaned_calls: + has_orphan = True + elif call_id and item_type == "function_result" and call_id in orphaned_results: + has_orphan = True + else: + has_non_orphan = True + + if has_orphan and not has_non_orphan: + # Message contains ONLY orphaned tool items — drop it entirely + continue + elif has_orphan and has_non_orphan: + # Message has both orphan and non-orphan content. + # Drop orphaned items if possible, keeping the rest. + if isinstance(items, list) and not isinstance(m, dict): + # Filter out orphaned content items from the message + filtered = [] + for item in items: + item_type = getattr(item, "type", None) + call_id = getattr(item, "call_id", None) + if call_id and item_type == "function_call" and call_id in orphaned_calls: + continue + if call_id and item_type == "function_result" and call_id in orphaned_results: + continue + filtered.append(item) + if filtered: + try: + m.contents = filtered + except Exception: + pass + cleaned.append(m) + # else: drop message entirely if no content remains + else: + cleaned.append(m) + else: + cleaned.append(m) + + return cleaned + + def _trim_messages( messages: MutableSequence[Any], *, cfg: ContextTrimConfig ) -> list[Any]: @@ -414,6 +525,11 @@ def _total_chars(msgs: list[Any]) -> int: break combined.pop(drop_index) + # Phase final: Remove orphaned tool call / tool result messages. + # The Responses API requires every function_call to have a matching + # function_call_output. Trimming may break these pairs. + combined = _remove_orphan_tool_messages(combined) + return combined diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index eb731dde..cc38a086 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -1142,7 +1142,7 @@ async def _generate_final_result( result = await result_generator.run( final_conversation, - response_format=result_format, + options={"response_format": result_format}, ) text = result.messages[-1].text @@ -1175,7 +1175,7 @@ async def _generate_final_result( ) retry_result = await result_generator.run( retry_conversation, - response_format=result_format, + options={"response_format": result_format}, ) retry_text = retry_result.messages[-1].text retry_json_payload = self._extract_first_json_payload(retry_text) diff --git a/src/processor/src/libs/base/orchestrator_base.py b/src/processor/src/libs/base/orchestrator_base.py index 420664a7..5c53a961 100644 --- a/src/processor/src/libs/base/orchestrator_base.py +++ b/src/processor/src/libs/base/orchestrator_base.py @@ -9,7 +9,7 @@ from abc import abstractmethod from typing import Any, Callable, Generic, MutableMapping, Sequence, TypeVar -from agent_framework import Agent, FunctionTool, ToolResultCompactionStrategy +from agent_framework import Agent, FunctionTool, InMemoryHistoryProvider, ToolResultCompactionStrategy from libs.agent_framework.agent_builder import AgentBuilder from libs.agent_framework.agent_framework_helper import ClientType @@ -169,6 +169,7 @@ async def create_agents( AgentBuilder(agent_client) .with_name(agent_info.agent_name) .with_instructions(instruction) + .with_store(False) ) # Only attach tools when provided. (Coordinator should typically have none.) @@ -206,18 +207,19 @@ async def create_agents( .with_tool_choice("none") ) - # Attach shared memory context provider to expert agents + # Attach context providers to expert agents # (not Coordinator, not ResultGenerator — they don't need memory) - if ( - self.memory_store is not None - and agent_info.agent_name not in ("Coordinator", "ResultGenerator") - ): - memory_provider = SharedMemoryContextProvider( - memory_store=self.memory_store, - agent_name=agent_info.agent_name, - step=self.step_name, - ) - builder = builder.with_context_providers(memory_provider) + if agent_info.agent_name not in ("Coordinator", "ResultGenerator"): + providers: list = [InMemoryHistoryProvider()] + if self.memory_store is not None: + providers.append( + SharedMemoryContextProvider( + memory_store=self.memory_store, + agent_name=agent_info.agent_name, + step=self.step_name, + ) + ) + builder = builder.with_context_providers(providers) agent = builder.build() agents[agent_info.agent_name] = agent From 6f360410ec465d9c8bce7eae24e207e3c6558422 Mon Sep 17 00:00:00 2001 From: Dhanushree-Microsoft Date: Wed, 10 Jun 2026 11:58:21 +0530 Subject: [PATCH 25/25] fix: update test methods to use new workflow run interface and improve context handling --- .../agent_framework/test_agent_builder.py | 22 ++-- .../test_groupchat_orchestrator_internals.py | 8 +- .../test_shared_memory_context_provider.py | 119 ++++++++++-------- .../steps/test_migration_processor_run.py | 4 +- 4 files changed, 87 insertions(+), 66 deletions(-) diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_agent_builder.py b/src/processor/src/tests/unit/libs/agent_framework/test_agent_builder.py index cbfede63..f6e99c36 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_agent_builder.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_agent_builder.py @@ -157,15 +157,17 @@ def test_build_passes_all_state_to_chat_agent(self): .build() ) assert agent is mock_chat.return_value + args = mock_chat.call_args.args kwargs = mock_chat.call_args.kwargs - assert kwargs["chat_client"] is chat_client + assert args[0] is chat_client assert kwargs["instructions"] == "inst" assert kwargs["id"] == "id1" assert kwargs["name"] == "name1" assert kwargs["description"] == "desc1" - assert kwargs["temperature"] == 0.3 - assert kwargs["max_tokens"] == 100 - assert kwargs["tool_choice"] == "auto" + opts = kwargs["default_options"] + assert opts["temperature"] == 0.3 + assert opts["max_tokens"] == 100 + assert opts["tool_choice"] == "auto" assert kwargs["extra"] == 42 @@ -180,11 +182,13 @@ def test_create_agent_invokes_chat_agent(self): temperature=0.4, ) assert agent is mock_chat.return_value + args = mock_chat.call_args.args kwargs = mock_chat.call_args.kwargs - assert kwargs["chat_client"] is chat_client + assert args[0] is chat_client assert kwargs["instructions"] == "i" assert kwargs["name"] == "n" - assert kwargs["temperature"] == 0.4 + opts = kwargs["default_options"] + assert opts["temperature"] == 0.4 def test_create_agent_by_agentinfo_uses_helper_and_creates_client(self): # Build a fake AgentInfo with the minimum surface used by the method @@ -215,12 +219,14 @@ def test_create_agent_by_agentinfo_uses_helper_and_creates_client(self): assert agent is mock_chat.return_value helper.settings.get_service_config.assert_called_once_with("default") helper.create_client.assert_called_once() + args = mock_chat.call_args.args ck = mock_chat.call_args.kwargs - assert ck["chat_client"] == "client-instance" + assert args[0] == "client-instance" assert ck["instructions"] == "instr" assert ck["name"] == "A" assert ck["description"] == "D" - assert ck["temperature"] == 0.2 + opts = ck["default_options"] + assert opts["temperature"] == 0.2 def test_create_agent_by_agentinfo_falls_back_to_system_prompt(self): helper = MagicMock() diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py index 5ccbce22..1b2bb182 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py @@ -743,16 +743,14 @@ def test_build_groupchat_invokes_builder(self): }) with patch("libs.agent_framework.groupchat_orchestrator.GroupChatBuilder") as MockBuilder: built = MagicMock() - built.set_manager.return_value = built - built.participants.return_value = built built.build.return_value = "wf" MockBuilder.return_value = built wf = _run(orch._build_groupchat()) assert wf == "wf" # ResultGenerator excluded from participants - kwargs = built.participants.call_args.args[0] - assert "arch" in kwargs - assert "rg" not in kwargs + kwargs = MockBuilder.call_args.kwargs + assert "arch" in kwargs["participants"] + assert "rg" not in kwargs["participants"] # ----------------------------------------------------------------------------- diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_shared_memory_context_provider.py b/src/processor/src/tests/unit/libs/agent_framework/test_shared_memory_context_provider.py index ab2bc8b2..3398c764 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_shared_memory_context_provider.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_shared_memory_context_provider.py @@ -61,8 +61,36 @@ def _make_provider(store=None): ), store +def _make_context(messages=None, response_text=None): + """Create a mock SessionContext for before_run/after_run calls.""" + ctx = MagicMock() + ctx.get_messages = MagicMock(return_value=messages or []) + ctx.extend_instructions = MagicMock() + if response_text is not None: + ctx.response = MagicMock() + ctx.response.text = response_text + else: + ctx.response = None + return ctx + + +async def _call_before_run(provider, messages): + """Helper to call before_run and return the instructions that were injected.""" + ctx = _make_context(messages=messages) + await provider.before_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) + if ctx.extend_instructions.called: + return ctx.extend_instructions.call_args[0][1] # second positional arg = instructions + return None + + +async def _call_after_run(provider, response_text): + """Helper to call after_run with a response.""" + ctx = _make_context(response_text=response_text) + await provider.after_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) + + # --------------------------------------------------------------------------- -# invoking() — Pre-LLM memory injection +# before_run() — Pre-LLM memory injection # --------------------------------------------------------------------------- @@ -75,11 +103,11 @@ async def _run(): ] messages = [_make_chat_message("How should we handle storage configuration?")] - context = await provider.invoking(messages) + instructions = await _call_before_run(provider, messages) - assert context.instructions is not None - assert "GKE Filestore CSI" in context.instructions - assert "Azure Files for AKS" in context.instructions + assert instructions is not None + assert "GKE Filestore CSI" in instructions + assert "Azure Files for AKS" in instructions store.search.assert_called_once() asyncio.run(_run()) @@ -88,9 +116,8 @@ async def _run(): def test_invoking_empty_messages_returns_empty(): async def _run(): provider, _ = _make_provider() - context = await provider.invoking([]) - assert context.instructions is None - assert getattr(context, "messages", []) == [] + instructions = await _call_before_run(provider, []) + assert instructions is None asyncio.run(_run()) @@ -101,8 +128,8 @@ async def _run(): store.search.return_value = [] messages = [_make_chat_message("What is the overall migration plan for AKS?")] - context = await provider.invoking(messages) - assert context.instructions is None + instructions = await _call_before_run(provider, messages) + assert instructions is None asyncio.run(_run()) @@ -113,8 +140,8 @@ async def _run(): store.search.side_effect = Exception("search failed") messages = [_make_chat_message("What is the networking plan for AKS?")] - context = await provider.invoking(messages) - assert context.instructions is None + instructions = await _call_before_run(provider, messages) + assert instructions is None asyncio.run(_run()) @@ -125,7 +152,7 @@ async def _run(): long_text = "x" * 5000 messages = [_make_chat_message(long_text)] - await provider.invoking(messages) + await _call_before_run(provider, messages) query = store.search.call_args.kwargs["query"] assert len(query) <= 2000 @@ -142,7 +169,7 @@ async def _run(): _make_chat_message("Latest question about storage"), ] - await provider.invoking(messages) + await _call_before_run(provider, messages) query = store.search.call_args.kwargs["query"] assert "Latest question about storage" in query @@ -159,10 +186,10 @@ async def _run(): store.search.return_value = large_memories messages = [_make_chat_message("What storage configuration should we use for persistent volumes?")] - context = await provider.invoking(messages) + instructions = await _call_before_run(provider, messages) - assert context.instructions is not None - assert len(context.instructions) <= MAX_MEMORY_CONTEXT_CHARS + 200 + assert instructions is not None + assert len(instructions) <= MAX_MEMORY_CONTEXT_CHARS + 200 asyncio.run(_run()) @@ -175,10 +202,10 @@ async def _run(): ] messages = [_make_chat_message("What storage class should we choose for the cluster?")] - context = await provider.invoking(messages) + instructions = await _call_before_run(provider, messages) - assert "Chief Architect" in context.instructions - assert "design" in context.instructions + assert "Chief Architect" in instructions + assert "design" in instructions asyncio.run(_run()) @@ -189,26 +216,25 @@ async def _run(): store.search.return_value = [_make_memory_entry("some memory")] single = _make_chat_message("What about networking configuration for AKS?") - context = await provider.invoking(single) + instructions = await _call_before_run(provider, [single]) - assert context.instructions is not None + assert instructions is not None store.search.assert_called_once() asyncio.run(_run()) # --------------------------------------------------------------------------- -# invoked() — Post-LLM memory storage +# after_run() — Post-LLM memory storage # --------------------------------------------------------------------------- def test_invoked_stores_response(): async def _run(): provider, store = _make_provider() - request = [_make_chat_message("What is the networking plan for AKS?")] - response = [_make_chat_message("We should use Azure CNI for networking configuration in the AKS cluster")] + response_text = "We should use Azure CNI for networking configuration in the AKS cluster" - await provider.invoked(request, response) + await _call_after_run(provider, response_text) await provider.flush() store.add.assert_called_once() @@ -222,10 +248,9 @@ async def _run(): def test_invoked_skips_on_exception(): async def _run(): provider, store = _make_provider() - request = [_make_chat_message("Q")] - response = [_make_chat_message("A" * 100)] - - await provider.invoked(request, response, invoke_exception=Exception("fail")) + # after_run with no response simulates exception path + ctx = _make_context(response_text=None) + await provider.after_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) store.add.assert_not_called() asyncio.run(_run()) @@ -234,9 +259,8 @@ async def _run(): def test_invoked_skips_none_response(): async def _run(): provider, store = _make_provider() - request = [_make_chat_message("Q")] - - await provider.invoked(request, None) + ctx = _make_context(response_text=None) + await provider.after_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) store.add.assert_not_called() asyncio.run(_run()) @@ -245,10 +269,8 @@ async def _run(): def test_invoked_skips_short_response(): async def _run(): provider, store = _make_provider() - request = [_make_chat_message("Q")] - short = [_make_chat_message("x" * (MIN_CONTENT_LENGTH_TO_STORE - 1))] - - await provider.invoked(request, short) + short_text = "x" * (MIN_CONTENT_LENGTH_TO_STORE - 1) + await _call_after_run(provider, short_text) store.add.assert_not_called() asyncio.run(_run()) @@ -257,10 +279,8 @@ async def _run(): def test_invoked_stores_long_response(): async def _run(): provider, store = _make_provider() - request = [_make_chat_message("Q")] - long_resp = [_make_chat_message("x" * (MIN_CONTENT_LENGTH_TO_STORE + 1))] - - await provider.invoked(request, long_resp) + long_text = "x" * (MIN_CONTENT_LENGTH_TO_STORE + 1) + await _call_after_run(provider, long_text) await provider.flush() store.add.assert_called_once() @@ -270,11 +290,10 @@ async def _run(): def test_invoked_increments_turn_counter(): async def _run(): provider, store = _make_provider() - request = [_make_chat_message("Q")] - response = [_make_chat_message("A" * 100)] + response_text = "A" * 100 - await provider.invoked(request, response) - await provider.invoked(request, response) + await _call_after_run(provider, response_text) + await _call_after_run(provider, response_text) assert provider._turn_counter == 2 asyncio.run(_run()) @@ -284,10 +303,9 @@ def test_invoked_store_failure_does_not_raise(): async def _run(): provider, store = _make_provider() store.add.side_effect = Exception("store failed") - request = [_make_chat_message("Q")] - response = [_make_chat_message("A" * 100)] + response_text = "A" * 100 - await provider.invoked(request, response) + await _call_after_run(provider, response_text) await provider.flush() # Should not raise asyncio.run(_run()) @@ -296,10 +314,9 @@ async def _run(): def test_invoked_with_single_message(): async def _run(): provider, store = _make_provider() - request = _make_chat_message("What is the question about networking?") - response = _make_chat_message("We should use Azure CNI Overlay for the networking configuration in AKS") + response_text = "We should use Azure CNI Overlay for the networking configuration in AKS" - await provider.invoked(request, response) + await _call_after_run(provider, response_text) await provider.flush() store.add.assert_called_once() diff --git a/src/processor/src/tests/unit/steps/test_migration_processor_run.py b/src/processor/src/tests/unit/steps/test_migration_processor_run.py index 683fcc5d..b73abc8b 100644 --- a/src/processor/src/tests/unit/steps/test_migration_processor_run.py +++ b/src/processor/src/tests/unit/steps/test_migration_processor_run.py @@ -54,12 +54,12 @@ def _make_processor(events: list, memory_store=None) -> MigrationProcessor: proc._telemetry = telemetry # expose for assertions - async def _stream(_input): + async def _stream(_input, **kwargs): for ev in events: yield ev workflow = MagicMock() - workflow.run_stream = _stream + workflow.run = _stream proc.workflow = workflow # Patch _create_memory_store as an AsyncMock returning the provided value.