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/processor/pyproject.toml b/src/processor/pyproject.toml index b6c2c2aa..ae60afe0 100644 --- a/src/processor/pyproject.toml +++ b/src/processor/pyproject.toml @@ -5,10 +5,10 @@ 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.5", "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.1.0", "azure-appconfiguration==1.8.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..6a7e4409 100644 --- a/src/processor/src/libs/agent_framework/agent_builder.py +++ b/src/processor/src/libs/agent_framework/agent_builder.py @@ -1,20 +1,19 @@ # 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, + Agent, + AgentMiddleware, + BaseChatClient, + ChatMiddleware, ContextProvider, - Middleware, + FunctionTool, ToolMode, - ToolProtocol, ) from pydantic import BaseModel @@ -23,7 +22,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 +58,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 +69,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 +93,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 +178,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 +210,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 +226,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 +384,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 +421,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 +441,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 +476,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 +495,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 +538,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 +606,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 +631,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 +677,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 +752,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..4741b44c 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 @@ -443,9 +443,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..82f657b6 100644 --- a/src/processor/src/libs/agent_framework/agent_info.py +++ b/src/processor/src/libs/agent_framework/agent_info.py @@ -4,7 +4,8 @@ """Pydantic model describing an agent participant with Jinja2 template rendering.""" from typing import Any, Callable, MutableMapping, Sequence -from agent_framework import ToolProtocol + +from agent_framework import FunctionTool from jinja2 import Template from openai import BaseModel from pydantic import Field @@ -20,10 +21,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..63608969 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,8 @@ from datetime import datetime from typing import Any, Callable, Optional -from agent_framework import AgentRunContext, AgentMiddleware + +from agent_framework import AgentContext, AgentMiddleware class AgentSpeakingCaptureMiddleware(AgentMiddleware): @@ -72,7 +73,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 42f6422e..634d5dbd 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,758 +1,788 @@ -# 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 agent_framework.azure import AzureOpenAIResponsesClient -from tenacity import ( - AsyncRetrying, - retry_if_exception, - stop_after_attempt, -) -from tenacity.wait import wait_base - -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 - - # "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) - - 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, - **kwargs: Any, - ): - 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 - ) -> Any: - 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, chat_options=chat_options, **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, chat_options=chat_options, **kwargs - ), - config=self._retry_config, - ) - - async def _inner_get_streaming_response( - self, *, messages: MutableSequence[Any], chat_options: Any, **kwargs: Any - ) -> AsyncIterable[Any]: - # Conservative retry: only retries failures before the first yielded update. - attempts = self._retry_config.max_retries + 1 - - 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_streaming_response( - messages=effective_messages, chat_options=chat_options, **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) +# 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") + if role is None: + return None + return role if isinstance(role, str) else getattr(role, "value", str(role)) + role = getattr(message, "role", None) + if role is None: + return None + return role if isinstance(role, str) else getattr(role, "value", str(role)) + + +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", "contents"): + if hasattr(message, attr): + try: + setattr(message, attr, new_text) + except Exception: + pass + else: + if attr != "contents": + # Also update contents if it exists for agent-framework 1.3.0 compat + if hasattr(message, "contents"): + try: + setattr(message, "contents", new_text) + except Exception: + pass + return message + 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) 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..946575ea 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -19,25 +19,27 @@ 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 ( - AgentProtocol, - AgentRunUpdateEvent, - ChatAgent, - ChatMessage, + Agent, + AgentResponseUpdate, Executor, - GroupChatBuilder, - ManagerSelectionResponse, + Message, Role, + SupportsAgentRun, Workflow, - WorkflowOutputEvent, + WorkflowBuilder as GroupChatBuilder, + WorkflowEvent, ) 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 +89,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 +182,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 +190,7 @@ def __init__( self, name: str, process_id: str, - participants: Mapping[str, AgentProtocol | Executor] - | Sequence[AgentProtocol | Executor], + participants: Mapping[str, SupportsAgentRun | Executor], memory_client: AsyncMemory, coordinator_name: str = "Coordinator", max_rounds: int = 100, @@ -202,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 @@ -225,7 +226,7 @@ def __init__( self.result_format = result_output_format # Runtime state - self.agents: dict[str, ChatAgent] = 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 @@ -338,7 +339,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 +476,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 +490,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 +504,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, @@ -525,7 +526,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( @@ -542,8 +543,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 +643,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 +706,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 +718,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 +885,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 +895,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" @@ -988,14 +989,14 @@ async def _complete_agent_response( if agent_name != self.coordinator_name: self._progress_counter += 1 - # Detect manager termination signal (finish=true) from Coordinator. - # NOTE: The underlying GroupChatBuilder does not automatically stop on finish, + # Detect Coordinator termination signal (finish=true) via CoordinatorSelectionResponse. + # 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 +1123,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 +1221,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 +1229,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 +1242,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 +1297,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..bffb03f1 100644 --- a/src/processor/src/libs/agent_framework/middlewares.py +++ b/src/processor/src/libs/agent_framework/middlewares.py @@ -7,24 +7,27 @@ from collections.abc import Awaitable, Callable from agent_framework import ( + AgentContext, AgentMiddleware, - AgentRunContext, ChatContext, - ChatMessage, ChatMiddleware, FunctionInvocationContext, FunctionMiddleware, + Message, Role, ) +ROLE_USER = getattr(Role, "USER", "user") + + class DebuggingMiddleware(AgentMiddleware): """Class-based middleware that adds debugging information to chat responses.""" 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,17 +139,20 @@ 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: - original_text = message.text + original_text = message.text if message.text else ( + str(message.contents) if hasattr(message, "contents") and message.contents else None + ) + if message.role == ROLE_USER and original_text: updated_text = original_text if self.replacement: @@ -155,7 +161,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, contents=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..500ac544 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,13 @@ from collections.abc import MutableSequence, Sequence from typing import TYPE_CHECKING -from agent_framework import ChatMessage, Context, ContextProvider +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 @@ -49,6 +55,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 +98,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 +151,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 +260,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,17 +303,19 @@ 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, "contents") and message.contents: + return str(message.contents) if not isinstance(message.contents, str) else message.contents if hasattr(message, "content"): return str(message.content) if message.content else "" return str(message) if message else "" @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 fbcb39e2..420664a7 100644 --- a/src/processor/src/libs/base/orchestrator_base.py +++ b/src/processor/src/libs/base/orchestrator_base.py @@ -9,12 +9,15 @@ from abc import abstractmethod from typing import Any, Callable, Generic, MutableMapping, Sequence, TypeVar -from agent_framework import ChatAgent, ManagerSelectionResponse, ToolProtocol +from agent_framework import Agent, FunctionTool, ToolResultCompactionStrategy 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 +63,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 +93,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 +133,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 +147,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]() + ) -> dict[str, 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 +179,19 @@ async def create_agents( .with_temperature(0.0) .with_max_tokens(20_000) ) + # Prevent context window overflow by summarizing older tool results. + 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 ) @@ -294,7 +303,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..1ce73a68 100644 --- a/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py +++ b/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py @@ -12,7 +12,7 @@ from pathlib import Path from typing import Any, Callable, MutableMapping, Sequence -from agent_framework import MCPStdioTool, MCPStreamableHTTPTool, ToolProtocol +from agent_framework import FunctionTool, MCPStdioTool, MCPStreamableHTTPTool from libs.agent_framework.agent_info import AgentInfo from libs.agent_framework.groupchat_orchestrator import ( @@ -98,10 +98,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..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,11 +13,7 @@ from pathlib import Path from typing import Any, Callable, MutableMapping, Sequence -from agent_framework import ( - MCPStdioTool, - MCPStreamableHTTPTool, - ToolProtocol, -) +from agent_framework import FunctionTool, MCPStdioTool, MCPStreamableHTTPTool from libs.agent_framework.agent_info import AgentInfo from libs.agent_framework.groupchat_orchestrator import ( @@ -107,10 +103,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..b6d96efe 100644 --- a/src/processor/src/steps/design/orchestration/design_orchestrator.py +++ b/src/processor/src/steps/design/orchestration/design_orchestrator.py @@ -11,11 +11,7 @@ from pathlib import Path from typing import Any, Callable, MutableMapping, Sequence -from agent_framework import ( - MCPStdioTool, - MCPStreamableHTTPTool, - ToolProtocol, -) +from agent_framework import FunctionTool, MCPStdioTool, MCPStreamableHTTPTool from libs.agent_framework.agent_info import AgentInfo from libs.agent_framework.groupchat_orchestrator import ( @@ -98,10 +94,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..a16e6b4d 100644 --- a/src/processor/src/steps/documentation/orchestration/documentation_orchestrator.py +++ b/src/processor/src/steps/documentation/orchestration/documentation_orchestrator.py @@ -15,11 +15,7 @@ from pathlib import Path from typing import Any, Callable, MutableMapping, Sequence -from agent_framework import ( - MCPStdioTool, - MCPStreamableHTTPTool, - ToolProtocol, -) +from agent_framework import FunctionTool, MCPStdioTool, MCPStreamableHTTPTool from libs.agent_framework.agent_info import AgentInfo from libs.agent_framework.groupchat_orchestrator import ( @@ -112,10 +108,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..436757fa 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,15 @@ 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). + # 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.source_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.source_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 +397,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=executor_id, 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[executor_id] + if executor_id in step_start_perf else None ), ) @@ -423,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.source_executor_id or "unknown", + "executor_id": executor_id, "error_type": "WorkflowOutputMissing", "message": error_msg, "traceback": None, @@ -476,16 +466,14 @@ async def _generate_report_summary( "error": f"security evidence scan failed: {type(e).__name__}: {e}", } - report_collector.set_current_step( - event.source_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.source_executor_id} step" + or f"Hard terminated in {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 {executor_id} step", ) failure_details: Any = ( @@ -510,14 +498,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=executor_id, error_message=getattr(event.data, "reason", None) - or f"Hard terminated in {event.source_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.source_executor_id] - if event.source_executor_id in step_start_perf + - step_start_perf[executor_id] + if executor_id in step_start_perf else None ), ) @@ -533,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.source_executor_id, + step_name=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[executor_id] + if executor_id in step_start_perf else None ), ) - if event.source_executor_id in step_start_perf: + if executor_id in step_start_perf: report_collector.mark_step_completed( - event.source_executor_id, + executor_id, execution_time=time.perf_counter() - - step_start_perf[event.source_executor_id], + - step_start_perf[executor_id], ) try: @@ -572,10 +560,10 @@ async def _generate_report_summary( ) return event.data - elif isinstance(event, ExecutorFailedEvent): - pass - # will handle in WorkflowFailedEvent - elif isinstance(event, WorkflowFailedEvent): + elif event.type == "executor_failed": + # 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)", event.origin.value, @@ -644,7 +632,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 +663,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..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 @@ -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(ImportError): + 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(ImportError): + 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(ImportError): + 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 4939049b..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 @@ -17,20 +17,47 @@ import pytest -from libs.agent_framework.groupchat_orchestrator import ( +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 + + +_original_message = getattr(groupchat_module, "Message", None) +groupchat_module.Message = Message +from libs.agent_framework.groupchat_orchestrator import ( # noqa: E402 AgentResponse, GroupChatOrchestrator, OrchestrationResult, ) +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: + groupchat_module.Message = _original_message + + def _run(coro): return asyncio.run(coro) @dataclass class _Msg: - """Lightweight stand-in for a ChatMessage.""" + """Lightweight stand-in for a Message.""" source: str = "" content: str = "" @@ -601,30 +628,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 @@ -776,13 +800,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, @@ -797,14 +819,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, @@ -818,12 +838,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, @@ -837,12 +855,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( @@ -858,12 +874,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( @@ -915,8 +929,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 @@ -927,7 +939,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", ) @@ -936,8 +948,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 @@ -950,7 +960,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..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 @@ -4,16 +4,44 @@ import asyncio from types import SimpleNamespace -from agent_framework import ChatMessage, Role +import libs.agent_framework.middlewares as middlewares_module -from libs.agent_framework.middlewares import InputObserverMiddleware +ROLE_USER = "user" + + +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 + self.contents = contents + 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. +_original_message = getattr(middlewares_module, "Message", None) +middlewares_module.Message = Message +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: + middlewares_module.Message = _original_message 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 +52,7 @@ 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].role == ROLE_USER + assert ctx.messages[0].contents == "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..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 @@ -7,14 +7,43 @@ from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock -from agent_framework import ChatMessage, Role +import libs.agent_framework.middlewares as middlewares_module -from libs.agent_framework.middlewares import ( +ROLE_USER = "user" +ROLE_ASSISTANT = "assistant" + + +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 + self.contents = contents + 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. +_original_message = getattr(middlewares_module, "Message", None) +middlewares_module.Message = Message +from libs.agent_framework.middlewares import ( # noqa: E402 DebuggingMiddleware, LoggingFunctionMiddleware, ) +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: + middlewares_module.Message = _original_message + + def _run(coro): return asyncio.run(coro) @@ -86,24 +115,24 @@ 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", 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 = ChatMessage(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) _run(mw.process(ctx, AsyncMock())) - assert ctx.messages[0].text == "keep me" + assert ctx.messages[0].contents == "keep me" 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 0f3c189b..6da17eee 100644 --- a/src/processor/uv.lock +++ b/src/processor/uv.lock @@ -51,14 +51,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]] @@ -103,31 +103,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]] @@ -146,6 +144,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" @@ -159,6 +171,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" @@ -174,23 +199,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] @@ -198,18 +217,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]] @@ -256,6 +285,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" @@ -294,6 +380,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" @@ -485,7 +596,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.98.1" +version = "0.97.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -497,9 +608,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/60/a9e4426dfe594e5eec8a9757d48e3d8dcf529a0a35a4fc8aefa352bd95fe/anthropic-0.98.1.tar.gz", hash = "sha256:62205edec42f5877df63d58be8e9443843d3e032215836e228fba1f59514a433", size = 725085, upload-time = "2026-05-04T21:40:39.496Z" } +sdist = { url = "https://files.pythonhosted.org/packages/14/93/f66ea8bfe39f2e6bb9da8e27fa5457ad2520e8f7612dfc547b17fad55c4d/anthropic-0.97.0.tar.gz", hash = "sha256:021e79fd8e21e90ad94dc5ba2bbbd8b1599f424f5b1fab6c06204009cab764be", size = 669502, upload-time = "2026-04-23T20:52:34.445Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/6f/7f7f80f714e6de0784518f1999f71fd632076aefd3e22fe0ccd27ca9571f/anthropic-0.98.1-py3-none-any.whl", hash = "sha256:107ebf954415382fdcea6a94f9cf334a53199ad64794403590dc55366cefcc28", size = 699604, upload-time = "2026-05-04T21:40:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/53/b6/8e851369fa661ad0fef2ae6266bf3b7d52b78ccf011720058f4adaca59e2/anthropic-0.97.0-py3-none-any.whl", hash = "sha256:8a1a472dfabcfc0c52ff6a3eecf724ac7e07107a2f6e2367be55ceb42f5d5613", size = 662126, upload-time = "2026-04-23T20:52:32.377Z" }, ] [[package]] @@ -568,16 +679,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]] @@ -804,6 +915,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, ] +[[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" @@ -982,6 +1121,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" @@ -1017,55 +1172,55 @@ wheels = [ [[package]] name = "cryptography" -version = "48.0.0" +version = "47.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, - { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, - { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, - { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, - { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, - { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, - { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, - { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, - { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, - { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, - { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, - { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, - { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, - { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, - { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, - { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, - { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, - { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, - { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, - { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, - { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, - { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, - { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, - { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, - { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, - { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, - { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, - { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ef/b2/7ffa7fe8207a8c42147ffe70c3e360b228160c1d85dc3faff16aaa3244c0/cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", size = 830863, upload-time = "2026-04-24T19:54:57.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/98/40dfe932134bdcae4f6ab5927c87488754bf9eb79297d7e0070b78dd58e9/cryptography-47.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", size = 7912214, upload-time = "2026-04-24T19:53:03.864Z" }, + { url = "https://files.pythonhosted.org/packages/34/c6/2733531243fba725f58611b918056b277692f1033373dcc8bd01af1c05d4/cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", size = 4644617, upload-time = "2026-04-24T19:53:06.909Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/b27be1a670a9b87f855d211cf0e1174a5d721216b7616bd52d8581d912ed/cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", size = 4668186, upload-time = "2026-04-24T19:53:09.053Z" }, + { url = "https://files.pythonhosted.org/packages/81/b9/8443cfe5d17d482d348cee7048acf502bb89a51b6382f06240fd290d4ca3/cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", size = 4651244, upload-time = "2026-04-24T19:53:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/5d/5e/13ed0cdd0eb88ba159d6dd5ebfece8cb901dbcf1ae5ac4072e28b55d3153/cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", size = 5252906, upload-time = "2026-04-24T19:53:13.532Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/ed058e1df0f33d440217cd120d41d5dda9dd215a80b8187f68483185af82/cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", size = 4701842, upload-time = "2026-04-24T19:53:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3d30986b30fdbd9e969abbdf8ba00ed0618615144341faeb57f395a084fe/cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", size = 4289313, upload-time = "2026-04-24T19:53:17.755Z" }, + { url = "https://files.pythonhosted.org/packages/df/fd/32db38e3ad0cb331f0691cb4c7a8a6f176f679124dee746b3af6633db4d9/cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", size = 4650964, upload-time = "2026-04-24T19:53:20.062Z" }, + { url = "https://files.pythonhosted.org/packages/86/53/5395d944dfd48cb1f67917f533c609c34347185ef15eb4308024c876f274/cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", size = 5207817, upload-time = "2026-04-24T19:53:22.498Z" }, + { url = "https://files.pythonhosted.org/packages/34/4f/e5711b28e1901f7d480a2b1b688b645aa4c77c73f10731ed17e7f7db3f0d/cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", size = 4701544, upload-time = "2026-04-24T19:53:24.356Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/c8ddc25de3010fc8da447648f5a092c40e7a8fadf01dd6d255d9c0b9373d/cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", size = 4783536, upload-time = "2026-04-24T19:53:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/66/b6/d4a68f4ea999c6d89e8498579cba1c5fcba4276284de7773b17e4fa69293/cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", size = 4926106, upload-time = "2026-04-24T19:53:28.686Z" }, + { url = "https://files.pythonhosted.org/packages/54/ed/5f524db1fade9c013aa618e1c99c6ed05e8ffc9ceee6cda22fed22dda3f4/cryptography-47.0.0-cp311-abi3-win32.whl", hash = "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", size = 3258581, upload-time = "2026-04-24T19:53:31.058Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/1b901990b174786569029f67542b3edf72ac068b6c3c8683c17e6a2f5363/cryptography-47.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", size = 3775309, upload-time = "2026-04-24T19:53:33.054Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/7aa18ad9c11bc87689affa5ce4368d884b517502d75739d475fc6f4a03c7/cryptography-47.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", size = 7904299, upload-time = "2026-04-24T19:53:35.003Z" }, + { url = "https://files.pythonhosted.org/packages/07/55/c18f75724544872f234678fdedc871391722cb34a2aee19faa9f63100bb2/cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", size = 4631180, upload-time = "2026-04-24T19:53:37.517Z" }, + { url = "https://files.pythonhosted.org/packages/ee/65/31a5cc0eaca99cec5bafffe155d407115d96136bb161e8b49e0ef73f09a7/cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", size = 4653529, upload-time = "2026-04-24T19:53:39.775Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bc/641c0519a495f3bfd0421b48d7cd325c4336578523ccd76ea322b6c29c7a/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", size = 4638570, upload-time = "2026-04-24T19:53:42.129Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/300327b0a47f6dc94dd8b71b57052aefe178bb51745073d73d80604f11ab/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", size = 5238019, upload-time = "2026-04-24T19:53:44.577Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/5b5cf994391d4bf9d9c7efd4c66aabe4d95227256627f8fea6cff7dfadbd/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", size = 4686832, upload-time = "2026-04-24T19:53:47.015Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2c/ae950e28fd6475c852fc21a44db3e6b5bcc1261d1e370f2b6e42fa800fef/cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", size = 4269301, upload-time = "2026-04-24T19:53:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/67/fb/6a39782e150ffe5cc1b0018cb6ddc48bf7ca62b498d7539ffc8a758e977d/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", size = 4638110, upload-time = "2026-04-24T19:53:51.011Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d7/0b3c71090a76e5c203164a47688b697635ece006dcd2499ab3a4dbd3f0bd/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736", size = 5194988, upload-time = "2026-04-24T19:53:52.962Z" }, + { url = "https://files.pythonhosted.org/packages/63/33/63a961498a9df51721ab578c5a2622661411fc520e00bd83b0cc64eb20c4/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", size = 4686563, upload-time = "2026-04-24T19:53:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/5ee5b145248f92250de86145d1c1d6edebbd57a7fe7caa4dedb5d4cf06a1/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", size = 4770094, upload-time = "2026-04-24T19:53:57.753Z" }, + { url = "https://files.pythonhosted.org/packages/92/43/21d220b2da5d517773894dacdcdb5c682c28d3fffce65548cb06e87d5501/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", size = 4913811, upload-time = "2026-04-24T19:54:00.236Z" }, + { url = "https://files.pythonhosted.org/packages/31/98/dc4ad376ac5f1a1a7d4a83f7b0c6f2bcad36b5d2d8f30aeb482d3a7d9582/cryptography-47.0.0-cp314-cp314t-win32.whl", hash = "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", size = 3237158, upload-time = "2026-04-24T19:54:02.606Z" }, + { url = "https://files.pythonhosted.org/packages/bc/da/97f62d18306b5133468bc3f8cc73a3111e8cdc8cf8d3e69474d6e5fd2d1b/cryptography-47.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", size = 3758706, upload-time = "2026-04-24T19:54:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e0/34/a4fae8ae7c3bc227460c9ae43f56abf1b911da0ec29e0ebac53bb0a4b6b7/cryptography-47.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", size = 7904072, upload-time = "2026-04-24T19:54:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/01/64/d7b1e54fdb69f22d24a64bb3e88dc718b31c7fb10ef0b9691a3cf7eeea6e/cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", size = 4635767, upload-time = "2026-04-24T19:54:08.519Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7b/cca826391fb2a94efdcdfe4631eb69306ee1cff0b22f664a412c90713877/cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", size = 4654350, upload-time = "2026-04-24T19:54:10.795Z" }, + { url = "https://files.pythonhosted.org/packages/4c/65/4b57bcc823f42a991627c51c2f68c9fd6eb1393c1756aac876cba2accae2/cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", size = 4643394, upload-time = "2026-04-24T19:54:13.275Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/2c5fbeea70adbbca2bbae865e1d605d6a4a7f8dbd9d33eaf69645087f06c/cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", size = 5225777, upload-time = "2026-04-24T19:54:15.18Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b8/ac57107ef32749d2b244e36069bb688792a363aaaa3acc9e3cf84c130315/cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", size = 4688771, upload-time = "2026-04-24T19:54:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/56/fc/9f1de22ff8be99d991f240a46863c52d475404c408886c5a38d2b5c3bb26/cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", size = 4270753, upload-time = "2026-04-24T19:54:19.963Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/d70c852797aa68e8e48d12e5a87170c43f67bb4a59403627259dd57d15de/cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", size = 4642911, upload-time = "2026-04-24T19:54:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/a5/51/661cbee74f594c5d97ff82d34f10d5551c085ca4668645f4606ebd22bd5d/cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", size = 5181411, upload-time = "2026-04-24T19:54:24.376Z" }, + { url = "https://files.pythonhosted.org/packages/94/87/f2b6c374a82cf076cfa1416992ac8e8ec94d79facc37aec87c1a5cb72352/cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", size = 4688262, upload-time = "2026-04-24T19:54:26.946Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/8b7462f4acf21ec509616f0245018bb197194ab0b65c2ea21a0bdd53c0eb/cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", size = 4775506, upload-time = "2026-04-24T19:54:28.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/75/158e494e4c08dc05e039da5bb48553826bd26c23930cf8d3cd5f21fa8921/cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", size = 4912060, upload-time = "2026-04-24T19:54:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/bd/0a9d3edbf5eadbac926d7b9b3cd0c4be584eeeae4a003d24d9eda4affbbd/cryptography-47.0.0-cp38-abi3-win32.whl", hash = "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", size = 3248487, upload-time = "2026-04-24T19:54:33.494Z" }, + { url = "https://files.pythonhosted.org/packages/60/80/5681af756d0da3a599b7bdb586fac5a1540f1bcefd2717a20e611ddade45/cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", size = 3755737, upload-time = "2026-04-24T19:54:35.408Z" }, ] [[package]] @@ -1242,6 +1397,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" @@ -1344,6 +1512,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" @@ -1385,6 +1570,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5", size = 300743, upload-time = "2026-04-02T21:22:49.108Z" }, ] +[[package]] +name = "griffe" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, +] + [[package]] name = "griffelib" version = "2.0.2" @@ -1546,6 +1743,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 = "identify" version = "2.6.19" @@ -1720,6 +1946,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" @@ -1926,11 +2161,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" }, ] -[package.optional-dependencies] -ws = [ - { name = "websockets" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -2274,21 +2504,20 @@ wheels = [ [[package]] name = "openai-agents" -version = "0.15.1" +version = "0.8.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "griffelib" }, + { name = "griffe" }, { name = "mcp" }, { name = "openai" }, { name = "pydantic" }, { name = "requests" }, { name = "types-requests" }, { name = "typing-extensions" }, - { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/52/125891e56b67ec78bef08d91dc0a8d39457088cd0f59bf8e74a37e5e591c/openai_agents-0.15.1.tar.gz", hash = "sha256:78c3f1226e1d6d34dd7566e211c8345e996629d1704335153d1728995a3a7775", size = 5319915, upload-time = "2026-05-02T02:20:53.631Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/6b/f86002a00f16b387b0570860e461475660d81eb00e2817391926d3947933/openai_agents-0.8.3.tar.gz", hash = "sha256:07a6e900b0fe4b7fd8f91a06ed9ab4fec9df335ed676f1c9e1125f60cb57919b", size = 2378346, upload-time = "2026-02-10T00:11:07.048Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/7b/69a33975b3610300219e1d25185c46280814cdb87e69a779acf0c9b9166a/openai_agents-0.15.1-py3-none-any.whl", hash = "sha256:2d304a5dcb919bc4fa1de5c7c9c93a4353a8a79d87d582d6170929390b7b3cef", size = 818627, upload-time = "2026-05-02T02:20:50.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/38/d77602daf5308395ee067954ffa7e96cb9ecf9292ad3b5f398f1c77e0b36/openai_agents-0.8.3-py3-none-any.whl", hash = "sha256:e562ec1a70177abaa34ca6f0428241a9dbeb6b3d73f88a7f4ba3ee3d72b3b98d", size = 378042, upload-time = "2026-02-10T00:11:04.967Z" }, ] [[package]] @@ -2359,19 +2588,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" @@ -2523,10 +2739,10 @@ dev = [ [package.metadata] requires-dist = [ - { name = "agent-framework", specifier = "==1.0.0b260107" }, + { name = "agent-framework", specifier = "==1.3.0" }, { name = "aiohttp", specifier = "==3.13.5" }, { 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.1.0" }, { name = "azure-appconfiguration", specifier = "==1.8.0" }, @@ -3269,6 +3485,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.5" @@ -3553,7 +3781,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.3.1" +version = "21.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -3561,9 +3789,9 @@ dependencies = [ { name = "platformdirs" }, { name = "python-discovery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/0d/915c02c94d207b85580eb09bffab54438a709e7288524094fe781da526c2/virtualenv-21.3.1.tar.gz", hash = "sha256:c2305bc1fddeec40699b8370d13f8d431b0701f00ce895061ce493aeded4426b", size = 7613791, upload-time = "2026-05-05T01:34:31.402Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/8b/6331f7a7fe70131c301106ec1e7cf23e2501bf7d4ca3636805801ca191bb/virtualenv-21.3.0.tar.gz", hash = "sha256:733750db978ec95c2d8eb4feadaa57091002bce404cb39ba69899cf7bd28944e", size = 7614069, upload-time = "2026-04-27T17:05:58.927Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl", hash = "sha256:d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35", size = 7594539, upload-time = "2026-05-05T01:34:28.98Z" }, + { url = "https://files.pythonhosted.org/packages/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl", hash = "sha256:4d28ee41f6d9ec8f1f00cd472b9ffbcedda1b3d3b9a575b5c94a2d004fd51bd7", size = 7594690, upload-time = "2026-04-27T17:05:55.468Z" }, ] [[package]]