Python: [BREAKING]: removed display_name, renamed context_providers, middleware and AggregateContextProvider (#3139)

* removed display_name, renamed context_providers, middleware and AggregateContextProvider

* fixes

* fixed test

* testfix

* removed mistakenly put back test

* updated new test

* rename middlewares to middleware

* middleware fixes
This commit is contained in:
Eduard van Valkenburg
2026-01-13 03:24:07 +01:00
committed by GitHub
Unverified
parent ef44fb4960
commit 203fb7b1c4
80 changed files with 596 additions and 838 deletions
-1
View File
@@ -1 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
@@ -9,13 +9,13 @@ from typing import Any
from agent_framework import (
ChatMessage,
ChatOptions,
ChatResponse,
ChatResponseUpdate,
FunctionCallContent,
Role,
TextContent,
ai_function,
)
from agent_framework._types import ChatResponse
from pytest import MonkeyPatch
from agent_framework_ag_ui._client import AGUIChatClient, ServerFunctionCallContent
@@ -9,12 +9,11 @@ from pathlib import Path
from typing import Any
import pytest
from agent_framework import ChatAgent, ChatMessage, ChatOptions, TextContent
from agent_framework._types import ChatResponseUpdate
from agent_framework import ChatAgent, ChatMessage, ChatOptions, ChatResponseUpdate, TextContent
from pydantic import BaseModel
sys.path.insert(0, str(Path(__file__).parent))
from test_helpers_ag_ui import StreamingChatClientStub
from utils_test_ag_ui import StreamingChatClientStub
async def test_agent_initialization_basic():
@@ -3,8 +3,7 @@
"""Tests for document writer predictive state flow with confirm_changes."""
from ag_ui.core import EventType, StateDeltaEvent, ToolCallArgsEvent, ToolCallEndEvent, ToolCallStartEvent
from agent_framework import FunctionCallContent, FunctionResultContent, TextContent
from agent_framework._types import AgentRunResponseUpdate
from agent_framework import AgentRunResponseUpdate, FunctionCallContent, FunctionResultContent, TextContent
from agent_framework_ag_ui._events import AgentFrameworkEventBridge
+2 -3
View File
@@ -6,8 +6,7 @@ import json
import sys
from pathlib import Path
from agent_framework import ChatAgent, TextContent
from agent_framework._types import ChatResponseUpdate
from agent_framework import ChatAgent, ChatResponseUpdate, TextContent
from fastapi import FastAPI
from fastapi.testclient import TestClient
@@ -15,7 +14,7 @@ from agent_framework_ag_ui._agent import AgentFrameworkAgent
from agent_framework_ag_ui._endpoint import add_agent_framework_fastapi_endpoint
sys.path.insert(0, str(Path(__file__).parent))
from test_helpers_ag_ui import StreamingChatClientStub, stream_from_updates
from utils_test_ag_ui import StreamingChatClientStub, stream_from_updates
def build_chat_client(response_text: str = "Test response") -> StreamingChatClientStub:
@@ -2,8 +2,7 @@
"""Tests for human in the loop (function approval requests)."""
from agent_framework import FunctionApprovalRequestContent, FunctionCallContent
from agent_framework._types import AgentRunResponseUpdate
from agent_framework import AgentRunResponseUpdate, FunctionApprovalRequestContent, FunctionCallContent
from agent_framework_ag_ui._events import AgentFrameworkEventBridge
@@ -6,8 +6,7 @@ from collections.abc import AsyncGenerator
from types import SimpleNamespace
from typing import Any
from agent_framework import AgentRunResponseUpdate, TextContent, ai_function
from agent_framework._tools import FunctionInvocationConfiguration
from agent_framework import AgentRunResponseUpdate, FunctionInvocationConfiguration, TextContent, ai_function
from agent_framework_ag_ui._agent import AgentConfig
from agent_framework_ag_ui._orchestrators import DefaultOrchestrator, ExecutionContext
@@ -20,7 +20,7 @@ from agent_framework_ag_ui._agent import AgentConfig
from agent_framework_ag_ui._orchestrators import DefaultOrchestrator, HumanInTheLoopOrchestrator
sys.path.insert(0, str(Path(__file__).parent))
from test_helpers_ag_ui import StubAgent, TestExecutionContext
from utils_test_ag_ui import StubAgent, TestExecutionContext
@ai_function(approval_mode="always_require")
@@ -8,14 +8,13 @@ from typing import Any
import pytest
from ag_ui.core import StateSnapshotEvent
from agent_framework import ChatAgent, TextContent
from agent_framework._types import ChatResponseUpdate
from agent_framework import ChatAgent, ChatResponseUpdate, TextContent
from agent_framework_ag_ui._agent import AgentFrameworkAgent
from agent_framework_ag_ui._events import AgentFrameworkEventBridge
sys.path.insert(0, str(Path(__file__).parent))
from test_helpers_ag_ui import StreamingChatClientStub, stream_from_updates
from utils_test_ag_ui import StreamingChatClientStub, stream_from_updates
@pytest.fixture
@@ -8,12 +8,11 @@ from collections.abc import AsyncIterator, MutableSequence
from pathlib import Path
from typing import Any
from agent_framework import ChatAgent, ChatMessage, ChatOptions, TextContent
from agent_framework._types import ChatResponseUpdate
from agent_framework import ChatAgent, ChatMessage, ChatOptions, ChatResponseUpdate, TextContent
from pydantic import BaseModel
sys.path.insert(0, str(Path(__file__).parent))
from test_helpers_ag_ui import StreamingChatClientStub, stream_from_updates
from utils_test_ag_ui import StreamingChatClientStub, stream_from_updates
class RecipeOutput(BaseModel):
@@ -11,12 +11,13 @@ from agent_framework import (
AgentRunResponse,
AgentRunResponseUpdate,
AgentThread,
BaseChatClient,
ChatMessage,
ChatOptions,
ChatResponse,
ChatResponseUpdate,
TextContent,
)
from agent_framework._clients import BaseChatClient
from agent_framework._types import ChatResponse, ChatResponseUpdate
from agent_framework_ag_ui._message_adapters import _deduplicate_messages, _sanitize_tool_history
from agent_framework_ag_ui._orchestrators import ExecutionContext
@@ -79,31 +80,15 @@ class StubAgent(AgentProtocol):
chat_options: Any | None = None,
chat_client: Any | None = None,
) -> None:
self._id = agent_id
self._name = agent_name
self._description = "stub agent"
self.id = agent_id
self.name = agent_name
self.description = "stub agent"
self.updates = updates or [AgentRunResponseUpdate(contents=[TextContent(text="response")], role="assistant")]
self.chat_options = chat_options or SimpleNamespace(tools=None, response_format=None)
self.chat_client = chat_client or SimpleNamespace(function_invocation_configuration=None)
self.messages_received: list[Any] = []
self.tools_received: list[Any] | None = None
@property
def id(self) -> str:
return self._id
@property
def name(self) -> str | None:
return self._name
@property
def display_name(self) -> str:
return self._name or self._id
@property
def description(self) -> str | None:
return self._description
async def run(
self,
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
@@ -182,32 +182,11 @@ class DurableAIAgent(AgentProtocol):
"""
self.context = context
self.agent_name = agent_name
self._id = str(uuid.uuid4())
self._name = agent_name
self._display_name = agent_name
self._description = f"Durable agent proxy for {agent_name}"
self.id = str(uuid.uuid4())
self.name = agent_name
self.description = f"Durable agent proxy for {agent_name}"
logger.debug("[DurableAIAgent] Initialized for agent: %s", agent_name)
@property
def id(self) -> str:
"""Get the unique identifier for this agent."""
return self._id
@property
def name(self) -> str | None:
"""Get the name of the agent."""
return self._name
@property
def display_name(self) -> str:
"""Get the display name of the agent."""
return self._display_name
@property
def description(self) -> str | None:
"""Get the description of the agent."""
return self._description
# We return an AgentTask here which is a TaskBase subclass.
# This is an intentional deviation from AgentProtocol which defines run() as async.
# The AgentTask can be yielded in Durable Functions orchestrations and will provide
@@ -1 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
@@ -6,13 +6,18 @@ This module provides fixtures and configuration for pytest.
"""
import subprocess
import sys
from collections.abc import Iterator, Mapping
from pathlib import Path
from typing import Any
import pytest
import requests
from .testutils import (
# Add the integration_tests directory to the path so testutils can be imported
sys.path.insert(0, str(Path(__file__).parent))
from testutils import (
FunctionAppStartupError,
build_base_url,
cleanup_function_app,
@@ -15,11 +15,10 @@ Usage:
"""
import pytest
from testutils import SampleTestHelper, skip_if_azure_functions_integration_tests_disabled
from agent_framework_azurefunctions._constants import THREAD_ID_HEADER
from .testutils import SampleTestHelper, skip_if_azure_functions_integration_tests_disabled
# Module-level markers - applied to all tests in this file
pytestmark = [
pytest.mark.sample("01_single_agent"),
@@ -15,8 +15,7 @@ Usage:
"""
import pytest
from .testutils import SampleTestHelper, skip_if_azure_functions_integration_tests_disabled
from testutils import SampleTestHelper, skip_if_azure_functions_integration_tests_disabled
# Module-level markers - applied to all tests in this file
pytestmark = [
@@ -19,8 +19,7 @@ import time
import pytest
import requests
from .testutils import (
from testutils import (
SampleTestHelper,
skip_if_azure_functions_integration_tests_disabled,
)
@@ -19,8 +19,7 @@ Usage:
"""
import pytest
from .testutils import SampleTestHelper, skip_if_azure_functions_integration_tests_disabled
from testutils import SampleTestHelper, skip_if_azure_functions_integration_tests_disabled
# Module-level markers - applied to all tests in this file
pytestmark = [
@@ -19,8 +19,7 @@ Usage:
"""
import pytest
from .testutils import SampleTestHelper, skip_if_azure_functions_integration_tests_disabled
from testutils import SampleTestHelper, skip_if_azure_functions_integration_tests_disabled
# Module-level markers - applied to all tests in this file
pytestmark = [
@@ -19,8 +19,7 @@ Usage:
"""
import pytest
from .testutils import SampleTestHelper, skip_if_azure_functions_integration_tests_disabled
from testutils import SampleTestHelper, skip_if_azure_functions_integration_tests_disabled
# Module-level markers - applied to all tests in this file
pytestmark = [
@@ -21,8 +21,7 @@ Usage:
import time
import pytest
from .testutils import SampleTestHelper, skip_if_azure_functions_integration_tests_disabled
from testutils import SampleTestHelper, skip_if_azure_functions_integration_tests_disabled
# Module-level markers - applied to all tests in this file
pytestmark = [
@@ -218,12 +218,10 @@ class TestDurableAIAgent:
assert hasattr(agent, "id")
assert hasattr(agent, "name")
assert hasattr(agent, "description")
assert hasattr(agent, "display_name")
# Verify values
assert agent.name == "TestAgent"
assert agent.description == "Durable agent proxy for TestAgent"
assert agent.display_name == "TestAgent"
assert agent.id is not None # Auto-generated UUID
def test_get_new_thread(self) -> None:
@@ -4,11 +4,10 @@ from collections.abc import AsyncIterable
from typing import Any, ClassVar
from agent_framework import (
AgentMiddlewares,
AgentMiddlewareTypes,
AgentRunResponse,
AgentRunResponseUpdate,
AgentThread,
AggregateContextProvider,
BaseAgent,
ChatMessage,
ContextProvider,
@@ -79,8 +78,8 @@ class CopilotStudioAgent(BaseAgent):
id: str | None = None,
name: str | None = None,
description: str | None = None,
context_providers: ContextProvider | list[ContextProvider] | AggregateContextProvider | None = None,
middleware: AgentMiddlewares | list[AgentMiddlewares] | None = None,
context_provider: ContextProvider | None = None,
middleware: list[AgentMiddlewareTypes] | None = None,
environment_id: str | None = None,
agent_identifier: str | None = None,
client_id: str | None = None,
@@ -107,8 +106,8 @@ class CopilotStudioAgent(BaseAgent):
id: id of the CopilotAgent
name: Name of the CopilotAgent
description: Description of the CopilotAgent
context_providers: Context Providers, to be used by the copilot agent.
middleware: Agent middlewares used by the agent.
context_provider: Context Provider, to be used by the copilot agent.
middleware: Agent middleware used by the agent, should be a list of AgentMiddlewareTypes.
environment_id: Environment ID of the Power Platform environment containing
the Copilot Studio app. Can also be set via COPILOTSTUDIOAGENT__ENVIRONMENTID
environment variable.
@@ -138,7 +137,7 @@ class CopilotStudioAgent(BaseAgent):
id=id,
name=name,
description=description,
context_providers=context_providers,
context_provider=context_provider,
middleware=middleware,
)
if not client:
+20 -73
View File
@@ -18,7 +18,7 @@ from pydantic import BaseModel, Field, create_model
from ._clients import BaseChatClient, ChatClientProtocol
from ._logging import get_logger
from ._mcp import LOG_LEVEL_MAPPING, MCPTool
from ._memory import AggregateContextProvider, Context, ContextProvider
from ._memory import Context, ContextProvider
from ._middleware import Middleware, use_agent_middleware
from ._serialization import SerializationMixin
from ._threads import AgentThread, ChatMessageStoreProtocol
@@ -116,24 +116,9 @@ class AgentProtocol(Protocol):
# No need to inherit from AgentProtocol or use any framework classes
class CustomAgent:
def __init__(self):
self._id = "custom-agent-001"
self._name = "Custom Agent"
@property
def id(self) -> str:
return self._id
@property
def name(self) -> str | None:
return self._name
@property
def display_name(self) -> str:
return self.name or self.id
@property
def description(self) -> str | None:
return "A fully custom agent implementation"
self.id = "custom-agent-001"
self.name = "Custom Agent"
self.description = "A fully custom agent implementation"
async def run(self, messages=None, *, thread=None, **kwargs):
# Your custom implementation
@@ -160,25 +145,9 @@ class AgentProtocol(Protocol):
assert isinstance(instance, AgentProtocol)
"""
@property
def id(self) -> str:
"""Returns the ID of the agent."""
...
@property
def name(self) -> str | None:
"""Returns the name of the agent."""
...
@property
def display_name(self) -> str:
"""Returns the display name of the agent."""
...
@property
def description(self) -> str | None:
"""Returns the description of the agent."""
...
id: str
name: str | None
description: str | None
async def run(
self,
@@ -289,7 +258,6 @@ class BaseAgent(SerializationMixin):
# Access agent properties
print(agent.id) # Custom or auto-generated UUID
print(agent.display_name) # Returns name or id
"""
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"additional_properties"}
@@ -300,8 +268,8 @@ class BaseAgent(SerializationMixin):
id: str | None = None,
name: str | None = None,
description: str | None = None,
context_providers: ContextProvider | Sequence[ContextProvider] | None = None,
middleware: Middleware | Sequence[Middleware] | None = None,
context_provider: ContextProvider | None = None,
middleware: Sequence[Middleware] | None = None,
additional_properties: MutableMapping[str, Any] | None = None,
**kwargs: Any,
) -> None:
@@ -312,8 +280,8 @@ class BaseAgent(SerializationMixin):
a new UUID will be generated.
name: The name of the agent, can be None.
description: The description of the agent.
context_providers: The collection of multiple context providers to include during agent invocation.
middleware: List of middleware to intercept agent and function invocations.
context_provider: The context provider to include during agent invocation.
middleware: List of middleware.
additional_properties: Additional properties set on the agent.
kwargs: Additional keyword arguments (merged into additional_properties).
"""
@@ -322,11 +290,10 @@ class BaseAgent(SerializationMixin):
self.id = id
self.name = name
self.description = description
self.context_provider = self._prepare_context_providers(context_providers)
if middleware is None or isinstance(middleware, Sequence):
self.middleware: list[Middleware] | None = cast(list[Middleware], middleware) if middleware else None
else:
self.middleware = [middleware]
self.context_provider = context_provider
self.middleware: list[Middleware] | None = (
cast(list[Middleware], middleware) if middleware is not None else None
)
# Merge kwargs into additional_properties
self.additional_properties: dict[str, Any] = cast(dict[str, Any], additional_properties or {})
@@ -356,14 +323,6 @@ class BaseAgent(SerializationMixin):
if thread.context_provider:
await thread.context_provider.invoked(input_messages, response_messages, **kwargs)
@property
def display_name(self) -> str:
"""Returns the display name of the agent.
This is the name if present, otherwise the id.
"""
return self.name or self.id
def get_new_thread(self, **kwargs: Any) -> AgentThread:
"""Return a new AgentThread instance that is compatible with the agent.
@@ -499,18 +458,6 @@ class BaseAgent(SerializationMixin):
return [ChatMessage(role=Role.USER, text=msg) if isinstance(msg, str) else msg for msg in messages]
def _prepare_context_providers(
self,
context_providers: ContextProvider | Sequence[ContextProvider] | None = None,
) -> AggregateContextProvider | None:
if not context_providers:
return None
if isinstance(context_providers, AggregateContextProvider):
return context_providers
return AggregateContextProvider(context_providers)
# region ChatAgent
@@ -594,8 +541,8 @@ class ChatAgent(BaseAgent): # type: ignore[misc]
name: str | None = None,
description: str | None = None,
chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] | None = None,
context_providers: ContextProvider | list[ContextProvider] | AggregateContextProvider | None = None,
middleware: Middleware | list[Middleware] | None = None,
context_provider: ContextProvider | None = None,
middleware: Sequence[Middleware] | None = None,
# chat options
allow_multiple_tool_calls: bool | None = None,
conversation_id: str | None = None,
@@ -639,8 +586,8 @@ class ChatAgent(BaseAgent): # type: ignore[misc]
description: A brief description of the agent's purpose.
chat_message_store_factory: Factory function to create an instance of ChatMessageStoreProtocol.
If not provided, the default in-memory store will be used.
context_providers: The collection of multiple context providers to include during agent invocation.
middleware: List of middleware to intercept agent and function invocations.
context_provider: The context provider to include during agent invocation.
middleware: List of middleware to intercept agent, chat and function invocations.
allow_multiple_tool_calls: Whether to allow multiple tool calls in a single response.
conversation_id: The conversation ID for service-managed threads.
Cannot be used together with chat_message_store_factory.
@@ -683,7 +630,7 @@ class ChatAgent(BaseAgent): # type: ignore[misc]
id=id,
name=name,
description=description,
context_providers=context_providers,
context_provider=context_provider,
middleware=middleware,
**kwargs,
)
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, Literal, Protocol, TypeVar, run
from pydantic import BaseModel
from ._logging import get_logger
from ._memory import AggregateContextProvider, ContextProvider
from ._memory import ContextProvider
from ._middleware import (
ChatMiddleware,
ChatMiddlewareCallable,
@@ -336,12 +336,7 @@ class BaseChatClient(SerializationMixin, ABC):
self,
*,
middleware: (
ChatMiddleware
| ChatMiddlewareCallable
| FunctionMiddleware
| FunctionMiddlewareCallable
| list[ChatMiddleware | ChatMiddlewareCallable | FunctionMiddleware | FunctionMiddlewareCallable]
| None
Sequence[ChatMiddleware | ChatMiddlewareCallable | FunctionMiddleware | FunctionMiddlewareCallable] | None
) = None,
additional_properties: dict[str, Any] | None = None,
**kwargs: Any,
@@ -722,8 +717,8 @@ class BaseChatClient(SerializationMixin, ABC):
description: str | None = None,
instructions: str | None = None,
chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] | None = None,
context_providers: ContextProvider | list[ContextProvider] | AggregateContextProvider | None = None,
middleware: Middleware | list[Middleware] | None = None,
context_provider: ContextProvider | None = None,
middleware: Sequence[Middleware] | None = None,
allow_multiple_tool_calls: bool | None = None,
conversation_id: str | None = None,
frequency_penalty: float | None = None,
@@ -761,8 +756,8 @@ class BaseChatClient(SerializationMixin, ABC):
These will be put into the messages sent to the chat client service as a system message.
chat_message_store_factory: Factory function to create an instance of ChatMessageStoreProtocol.
If not provided, the default in-memory store will be used.
context_providers: Context providers to include during agent invocation.
middleware: List of middleware to intercept agent and function invocations.
context_provider: Context provider to include during agent invocation.
middleware: List of middleware to intercept chat and function invocations.
allow_multiple_tool_calls: Whether to allow multiple tool calls per agent turn.
conversation_id: The conversation ID to associate with the agent's messages.
frequency_penalty: The frequency penalty to use.
@@ -813,7 +808,7 @@ class BaseChatClient(SerializationMixin, ABC):
description=description,
instructions=instructions,
chat_message_store_factory=chat_message_store_factory,
context_providers=context_providers,
context_provider=context_provider,
middleware=middleware,
allow_multiple_tool_calls=allow_multiple_tool_calls,
conversation_id=conversation_id,
+3 -136
View File
@@ -1,22 +1,16 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import sys
from abc import ABC, abstractmethod
from collections.abc import MutableSequence, Sequence
from contextlib import AsyncExitStack
from types import TracebackType
from typing import TYPE_CHECKING, Any, Final, cast
from typing import TYPE_CHECKING, Any, Final
from ._types import ChatMessage
if TYPE_CHECKING:
from ._tools import ToolProtocol
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
else:
from typing_extensions import override # type: ignore[import] # pragma: no cover
if sys.version_info >= (3, 11):
from typing import Self # pragma: no cover
else:
@@ -24,7 +18,7 @@ else:
# region Context
__all__ = ["AggregateContextProvider", "Context", "ContextProvider"]
__all__ = ["Context", "ContextProvider"]
class Context:
@@ -100,7 +94,7 @@ class ContextProvider(ABC):
# Use with a chat agent
async with CustomContextProvider() as provider:
agent = ChatAgent(chat_client=client, name="assistant", context_providers=provider)
agent = ChatAgent(chat_client=client, name="assistant", context_provider=provider)
"""
# Default prompt to be used by all context providers when assembling memories/instructions
@@ -183,130 +177,3 @@ class ContextProvider(ABC):
exc_tb: The exception traceback if an exception occurred, None otherwise.
"""
pass
# region AggregateContextProvider
class AggregateContextProvider(ContextProvider):
"""A ContextProvider that contains multiple context providers.
It delegates events to multiple context providers and aggregates responses from those
events before returning. This allows you to combine multiple context providers into a
single provider.
Note:
An AggregateContextProvider is created automatically when you pass a single context
provider or a sequence of context providers to the agent constructor.
Examples:
.. code-block:: python
from agent_framework import AggregateContextProvider, ChatAgent
# Create multiple context providers
provider1 = CustomContextProvider1()
provider2 = CustomContextProvider2()
provider3 = CustomContextProvider3()
# Pass them to the agent - AggregateContextProvider is created automatically
agent = ChatAgent(chat_client=client, name="assistant", context_providers=[provider1, provider2, provider3])
# Verify that an AggregateContextProvider was created
assert isinstance(agent.context_providers, AggregateContextProvider)
# Add additional providers to the agent
provider4 = CustomContextProvider4()
agent.context_providers.add(provider4)
"""
def __init__(self, context_providers: ContextProvider | Sequence[ContextProvider] | None = None) -> None:
"""Initialize the AggregateContextProvider with context providers.
Args:
context_providers: The context provider(s) to add.
"""
if isinstance(context_providers, ContextProvider):
self.providers = [context_providers]
else:
self.providers = cast(list[ContextProvider], context_providers) or []
self._exit_stack: AsyncExitStack | None = None
def add(self, context_provider: ContextProvider) -> None:
"""Add a new context provider.
Args:
context_provider: The context provider to add.
"""
self.providers.append(context_provider)
@override
async def thread_created(self, thread_id: str | None = None) -> None:
await asyncio.gather(*[x.thread_created(thread_id) for x in self.providers])
@override
async def invoking(self, messages: ChatMessage | MutableSequence[ChatMessage], **kwargs: Any) -> Context:
contexts = await asyncio.gather(*[provider.invoking(messages, **kwargs) for provider in self.providers])
instructions: str = ""
return_messages: list[ChatMessage] = []
tools: list["ToolProtocol"] = []
for ctx in contexts:
if ctx.instructions:
instructions += ctx.instructions
if ctx.messages:
return_messages.extend(ctx.messages)
if ctx.tools:
tools.extend(ctx.tools)
return Context(instructions=instructions, messages=return_messages, tools=tools)
@override
async def invoked(
self,
request_messages: ChatMessage | Sequence[ChatMessage],
response_messages: ChatMessage | Sequence[ChatMessage] | None = None,
invoke_exception: Exception | None = None,
**kwargs: Any,
) -> None:
await asyncio.gather(*[
x.invoked(
request_messages=request_messages,
response_messages=response_messages,
invoke_exception=invoke_exception,
**kwargs,
)
for x in self.providers
])
@override
async def __aenter__(self) -> "Self":
"""Enter the async context manager and set up all providers.
Returns:
The AggregateContextProvider instance for chaining.
"""
self._exit_stack = AsyncExitStack()
await self._exit_stack.__aenter__()
# Enter all context providers
for provider in self.providers:
await self._exit_stack.enter_async_context(provider)
return self
@override
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
"""Exit the async context manager and clean up all providers.
Args:
exc_type: The exception type if an exception occurred, None otherwise.
exc_val: The exception value if an exception occurred, None otherwise.
exc_tb: The exception traceback if an exception occurred, None otherwise.
"""
if self._exit_stack is not None:
await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb)
self._exit_stack = None
@@ -2,10 +2,10 @@
import inspect
from abc import ABC, abstractmethod
from collections.abc import AsyncIterable, Awaitable, Callable, MutableSequence
from collections.abc import AsyncIterable, Awaitable, Callable, MutableSequence, Sequence
from enum import Enum
from functools import update_wrapper
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeAlias, TypeVar
from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeAlias, TypedDict, TypeVar
from ._serialization import SerializationMixin
from ._types import AgentRunResponse, AgentRunResponseUpdate, ChatMessage, prepare_messages
@@ -23,7 +23,7 @@ if TYPE_CHECKING:
__all__ = [
"AgentMiddleware",
"AgentMiddlewares",
"AgentMiddlewareTypes",
"AgentRunContext",
"ChatContext",
"ChatMiddleware",
@@ -305,7 +305,7 @@ class AgentMiddleware(ABC):
# Use with an agent
agent = ChatAgent(chat_client=client, name="assistant", middleware=RetryMiddleware())
agent = ChatAgent(chat_client=client, name="assistant", middleware=[RetryMiddleware()])
"""
@abstractmethod
@@ -373,7 +373,7 @@ class FunctionMiddleware(ABC):
# Use with an agent
agent = ChatAgent(chat_client=client, name="assistant", middleware=CachingMiddleware())
agent = ChatAgent(chat_client=client, name="assistant", middleware=[CachingMiddleware()])
"""
@abstractmethod
@@ -432,7 +432,9 @@ class ChatMiddleware(ABC):
# Use with an agent
agent = ChatAgent(
chat_client=client, name="assistant", middleware=SystemPromptMiddleware("You are a helpful assistant.")
chat_client=client,
name="assistant",
middleware=[SystemPromptMiddleware("You are a helpful assistant.")],
)
"""
@@ -480,7 +482,7 @@ Middleware: TypeAlias = (
| ChatMiddleware
| ChatMiddlewareCallable
)
AgentMiddlewares: TypeAlias = AgentMiddleware | AgentMiddlewareCallable
AgentMiddlewareTypes: TypeAlias = AgentMiddleware | AgentMiddlewareCallable
# region Middleware type markers for decorators
@@ -511,7 +513,7 @@ def agent_middleware(func: AgentMiddlewareCallable) -> AgentMiddlewareCallable:
# Use with an agent
agent = ChatAgent(chat_client=client, name="assistant", middleware=logging_middleware)
agent = ChatAgent(chat_client=client, name="assistant", middleware=[logging_middleware])
"""
# Add marker attribute to identify this as agent middleware
func._middleware_type: MiddlewareType = MiddlewareType.AGENT # type: ignore
@@ -544,7 +546,7 @@ def function_middleware(func: FunctionMiddlewareCallable) -> FunctionMiddlewareC
# Use with an agent
agent = ChatAgent(chat_client=client, name="assistant", middleware=logging_middleware)
agent = ChatAgent(chat_client=client, name="assistant", middleware=[logging_middleware])
"""
# Add marker attribute to identify this as function middleware
func._middleware_type: MiddlewareType = MiddlewareType.FUNCTION # type: ignore
@@ -577,7 +579,7 @@ def chat_middleware(func: ChatMiddlewareCallable) -> ChatMiddlewareCallable:
# Use with an agent
agent = ChatAgent(chat_client=client, name="assistant", middleware=logging_middleware)
agent = ChatAgent(chat_client=client, name="assistant", middleware=[logging_middleware])
"""
# Add marker attribute to identify this as chat middleware
func._middleware_type: MiddlewareType = MiddlewareType.CHAT # type: ignore
@@ -609,7 +611,7 @@ class BaseMiddlewarePipeline(ABC):
def __init__(self) -> None:
"""Initialize the base middleware pipeline."""
self._middlewares: list[Any] = []
self._middleware: list[Any] = []
@abstractmethod
def _register_middleware(self, middleware: Any) -> None:
@@ -624,12 +626,12 @@ class BaseMiddlewarePipeline(ABC):
@property
def has_middlewares(self) -> bool:
"""Check if there are any middlewares registered.
"""Check if there are any middleware registered.
Returns:
True if middlewares are registered, False otherwise.
True if middleware are registered, False otherwise.
"""
return bool(self._middlewares)
return bool(self._middleware)
def _register_middleware_with_wrapper(
self,
@@ -645,9 +647,9 @@ class BaseMiddlewarePipeline(ABC):
expected_type: The expected middleware base class type.
"""
if isinstance(middleware, expected_type):
self._middlewares.append(middleware)
self._middleware.append(middleware)
elif callable(middleware):
self._middlewares.append(MiddlewareWrapper(middleware)) # type: ignore[arg-type]
self._middleware.append(MiddlewareWrapper(middleware)) # type: ignore[arg-type]
def _create_handler_chain(
self,
@@ -667,7 +669,7 @@ class BaseMiddlewarePipeline(ABC):
"""
def create_next_handler(index: int) -> Callable[[Any], Awaitable[None]]:
if index >= len(self._middlewares):
if index >= len(self._middleware):
async def final_wrapper(c: Any) -> None:
# Execute actual handler and populate context for observability
@@ -677,7 +679,7 @@ class BaseMiddlewarePipeline(ABC):
return final_wrapper
middleware = self._middlewares[index]
middleware = self._middleware[index]
next_handler = create_next_handler(index + 1)
async def current_handler(c: Any) -> None:
@@ -705,7 +707,7 @@ class BaseMiddlewarePipeline(ABC):
"""
def create_next_handler(index: int) -> Callable[[Any], Awaitable[None]]:
if index >= len(self._middlewares):
if index >= len(self._middleware):
async def final_wrapper(c: Any) -> None:
# If terminate was set, skip execution
@@ -724,7 +726,7 @@ class BaseMiddlewarePipeline(ABC):
return final_wrapper
middleware = self._middlewares[index]
middleware = self._middleware[index]
next_handler = create_next_handler(index + 1)
async def current_handler(c: Any) -> None:
@@ -745,20 +747,20 @@ class AgentMiddlewarePipeline(BaseMiddlewarePipeline):
to process the agent invocation and pass control to the next middleware in the chain.
"""
def __init__(self, middlewares: list[AgentMiddleware | AgentMiddlewareCallable] | None = None):
def __init__(self, middleware: Sequence[AgentMiddlewareTypes] | None = None):
"""Initialize the agent middleware pipeline.
Args:
middlewares: The list of agent middleware to include in the pipeline.
middleware: The list of agent middleware to include in the pipeline.
"""
super().__init__()
self._middlewares: list[AgentMiddleware] = []
self._middleware: list[AgentMiddleware] = []
if middlewares:
for middleware in middlewares:
self._register_middleware(middleware)
if middleware:
for mdlware in middleware:
self._register_middleware(mdlware)
def _register_middleware(self, middleware: AgentMiddleware | AgentMiddlewareCallable) -> None:
def _register_middleware(self, middleware: AgentMiddlewareTypes) -> None:
"""Register an agent middleware item.
Args:
@@ -789,7 +791,7 @@ class AgentMiddlewarePipeline(BaseMiddlewarePipeline):
context.messages = messages
context.is_streaming = False
if not self._middlewares:
if not self._middleware:
return await final_handler(context)
# Store the final result
@@ -841,7 +843,7 @@ class AgentMiddlewarePipeline(BaseMiddlewarePipeline):
context.messages = messages
context.is_streaming = True
if not self._middlewares:
if not self._middleware:
async for update in final_handler(context):
yield update
return
@@ -874,18 +876,18 @@ class FunctionMiddlewarePipeline(BaseMiddlewarePipeline):
to process the function invocation and pass control to the next middleware in the chain.
"""
def __init__(self, middlewares: list[FunctionMiddleware | FunctionMiddlewareCallable] | None = None):
def __init__(self, middleware: Sequence[FunctionMiddleware | FunctionMiddlewareCallable] | None = None):
"""Initialize the function middleware pipeline.
Args:
middlewares: The list of function middleware to include in the pipeline.
middleware: The list of function middleware to include in the pipeline.
"""
super().__init__()
self._middlewares: list[FunctionMiddleware] = []
self._middleware: list[FunctionMiddleware] = []
if middlewares:
for middleware in middlewares:
self._register_middleware(middleware)
if middleware:
for mdlware in middleware:
self._register_middleware(mdlware)
def _register_middleware(self, middleware: FunctionMiddleware | FunctionMiddlewareCallable) -> None:
"""Register a function middleware item.
@@ -917,7 +919,7 @@ class FunctionMiddlewarePipeline(BaseMiddlewarePipeline):
context.function = function
context.arguments = arguments
if not self._middlewares:
if not self._middleware:
return await final_handler(context)
# Store the final result
@@ -947,18 +949,18 @@ class ChatMiddlewarePipeline(BaseMiddlewarePipeline):
to process the chat request and pass control to the next middleware in the chain.
"""
def __init__(self, middlewares: list[ChatMiddleware | ChatMiddlewareCallable] | None = None):
def __init__(self, middleware: Sequence[ChatMiddleware | ChatMiddlewareCallable] | None = None):
"""Initialize the chat middleware pipeline.
Args:
middlewares: The list of chat middleware to include in the pipeline.
middleware: The list of chat middleware to include in the pipeline.
"""
super().__init__()
self._middlewares: list[ChatMiddleware] = []
self._middleware: list[ChatMiddleware] = []
if middlewares:
for middleware in middlewares:
self._register_middleware(middleware)
if middleware:
for mdlware in middleware:
self._register_middleware(mdlware)
def _register_middleware(self, middleware: ChatMiddleware | ChatMiddlewareCallable) -> None:
"""Register a chat middleware item.
@@ -995,7 +997,7 @@ class ChatMiddlewarePipeline(BaseMiddlewarePipeline):
context.messages = messages
context.chat_options = chat_options
if not self._middlewares:
if not self._middleware:
return await final_handler(context)
# Store the final result
@@ -1045,7 +1047,7 @@ class ChatMiddlewarePipeline(BaseMiddlewarePipeline):
context.chat_options = chat_options
context.is_streaming = True
if not self._middlewares:
if not self._middleware:
async for update in final_handler(context):
yield update
return
@@ -1182,8 +1184,8 @@ def use_agent_middleware(agent_class: type[TAgent]) -> type[TAgent]:
original_run_stream = agent_class.run_stream # type: ignore[attr-defined]
def _build_middleware_pipelines(
agent_level_middlewares: Middleware | list[Middleware] | None,
run_level_middlewares: Middleware | list[Middleware] | None = None,
agent_level_middlewares: Sequence[Middleware] | None,
run_level_middlewares: Sequence[Middleware] | None = None,
) -> tuple[AgentMiddlewarePipeline, FunctionMiddlewarePipeline, list[ChatMiddleware | ChatMiddlewareCallable]]:
"""Build fresh agent and function middleware pipelines from the provided middleware lists.
@@ -1191,7 +1193,7 @@ def use_agent_middleware(agent_class: type[TAgent]) -> type[TAgent]:
agent_level_middlewares: Agent-level middleware (executed first)
run_level_middlewares: Run-level middleware (executed after agent middleware)
"""
middleware = categorize_middleware(agent_level_middlewares, run_level_middlewares)
middleware = categorize_middleware(*(agent_level_middlewares or ()), *(run_level_middlewares or ()))
return (
AgentMiddlewarePipeline(middleware["agent"]), # type: ignore[arg-type]
@@ -1204,7 +1206,7 @@ def use_agent_middleware(agent_class: type[TAgent]) -> type[TAgent]:
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
*,
thread: Any = None,
middleware: Middleware | list[Middleware] | None = None,
middleware: Sequence[Middleware] | None = None,
**kwargs: Any,
) -> AgentRunResponse:
"""Middleware-enabled run method."""
@@ -1253,7 +1255,7 @@ def use_agent_middleware(agent_class: type[TAgent]) -> type[TAgent]:
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
*,
thread: Any = None,
middleware: Middleware | list[Middleware] | None = None,
middleware: Sequence[Middleware] | None = None,
**kwargs: Any,
) -> AsyncIterable[AgentRunResponseUpdate]:
"""Middleware-enabled run_stream method."""
@@ -1461,9 +1463,15 @@ def use_chat_middleware(chat_client_class: type[TChatClient]) -> type[TChatClien
return chat_client_class
class MiddlewareDict(TypedDict):
agent: list[AgentMiddleware | AgentMiddlewareCallable]
function: list[FunctionMiddleware | FunctionMiddlewareCallable]
chat: list[ChatMiddleware | ChatMiddlewareCallable]
def categorize_middleware(
*middleware_sources: Any | list[Any] | None,
) -> dict[str, list[Any]]:
*middleware_sources: Middleware | None,
) -> MiddlewareDict:
"""Categorize middleware from multiple sources into agent, function, and chat types.
Args:
@@ -1472,7 +1480,7 @@ def categorize_middleware(
Returns:
Dict with keys "agent", "function", "chat" containing lists of categorized middleware.
"""
result: dict[str, list[Any]] = {"agent": [], "function": [], "chat": []}
result: MiddlewareDict = {"agent": [], "function": [], "chat": []}
# Merge all middleware sources into a single list
all_middleware: list[Any] = []
@@ -1495,11 +1503,11 @@ def categorize_middleware(
# Always call _determine_middleware_type to ensure proper validation
middleware_type = _determine_middleware_type(middleware)
if middleware_type == MiddlewareType.AGENT:
result["agent"].append(middleware)
result["agent"].append(middleware) # type: ignore
elif middleware_type == MiddlewareType.FUNCTION:
result["function"].append(middleware)
result["function"].append(middleware) # type: ignore
elif middleware_type == MiddlewareType.CHAT:
result["chat"].append(middleware)
result["chat"].append(middleware) # type: ignore
else:
# Fallback to agent middleware for unknown types
result["agent"].append(middleware)
@@ -1508,7 +1516,7 @@ def categorize_middleware(
def create_function_middleware_pipeline(
*middleware_sources: list[Middleware] | None,
*middleware_sources: Middleware,
) -> FunctionMiddlewarePipeline | None:
"""Create a function middleware pipeline from multiple middleware sources.
@@ -1518,28 +1526,10 @@ def create_function_middleware_pipeline(
Returns:
A FunctionMiddlewarePipeline if function middleware is found, None otherwise.
"""
middleware = categorize_middleware(*middleware_sources)
function_middlewares = middleware["function"]
function_middlewares = categorize_middleware(*middleware_sources)["function"]
return FunctionMiddlewarePipeline(function_middlewares) if function_middlewares else None # type: ignore[arg-type]
def _merge_and_filter_chat_middleware(
instance_middleware: Any | list[Any] | None,
call_middleware: Any | list[Any] | None,
) -> list[ChatMiddleware | ChatMiddlewareCallable]:
"""Merge instance-level and call-level middleware, filtering for chat middleware only.
Args:
instance_middleware: Middleware defined at the instance level.
call_middleware: Middleware provided at the call level.
Returns:
A merged list of chat middleware only.
"""
middleware = categorize_middleware(instance_middleware, call_middleware)
return middleware["chat"] # type: ignore[return-value]
def extract_and_merge_function_middleware(
chat_client: Any, kwargs: dict[str, Any]
) -> "FunctionMiddlewarePipeline | None":
@@ -1556,7 +1546,7 @@ def extract_and_merge_function_middleware(
existing_pipeline: FunctionMiddlewarePipeline | None = kwargs.get("_function_middleware_pipeline")
# Get middleware sources
client_middleware = getattr(chat_client, "middleware", None) if hasattr(chat_client, "middleware") else None
client_middleware = getattr(chat_client, "middleware", None)
run_level_middleware = kwargs.get("middleware")
# If we have an existing pipeline but no additional middleware sources, return it directly
@@ -1564,15 +1554,15 @@ def extract_and_merge_function_middleware(
return existing_pipeline
# If we have an existing pipeline with additional middleware, we need to merge
# Extract existing pipeline middlewares if present - cast to list[Middleware] for type compatibility
existing_middlewares: list[Middleware] | None = list(existing_pipeline._middlewares) if existing_pipeline else None
# Extract existing pipeline middleware if present - cast to list[Middleware] for type compatibility
existing_middleware: list[Middleware] | None = list(existing_pipeline._middleware) if existing_pipeline else None
# Create combined pipeline from all sources using existing helper
combined_pipeline = create_function_middleware_pipeline(
client_middleware, run_level_middleware, existing_middlewares
*(client_middleware or ()), *(run_level_middleware or ()), *(existing_middleware or ())
)
# If we have an existing pipeline but combined is None (no new middlewares), return existing
# If we have an existing pipeline but combined is None (no new middleware), return existing
if existing_pipeline and combined_pipeline is None:
return existing_pipeline
@@ -259,7 +259,7 @@ class SerializationMixin:
agent = CustomAgent(
context_providers=[...],
context_provider=[...],
middleware=[...]
)
@@ -3,7 +3,7 @@
from collections.abc import MutableMapping, Sequence
from typing import Any, Protocol, TypeVar
from ._memory import AggregateContextProvider
from ._memory import ContextProvider
from ._serialization import SerializationMixin
from ._types import ChatMessage
from .exceptions import AgentThreadException
@@ -327,7 +327,7 @@ class AgentThread:
*,
service_thread_id: str | None = None,
message_store: ChatMessageStoreProtocol | None = None,
context_provider: AggregateContextProvider | None = None,
context_provider: ContextProvider | None = None,
) -> None:
"""Initialize an AgentThread, do not use this method manually, always use: ``agent.get_new_thread()``.
@@ -110,7 +110,7 @@ def _clone_chat_agent(agent: ChatAgent) -> ChatAgent:
name=agent.name,
description=agent.description,
chat_message_store_factory=agent.chat_message_store_factory,
context_providers=agent.context_provider,
context_provider=agent.context_provider,
middleware=middleware,
# Disable parallel tool calls to prevent the agent from invoking multiple handoff tools at once.
allow_multiple_tool_calls=False,
@@ -1041,7 +1041,7 @@ class HandoffBuilder:
Each participant must have a unique identifier (name for agents, id for executors).
The workflow will automatically create an alias map so agents can be referenced by
their name, display_name, or executor id when routing.
their name, id, or executor id when routing.
Args:
participants: Sequence of AgentProtocol or Executor instances. Each must have
@@ -1091,7 +1091,7 @@ class HandoffBuilder:
if isinstance(participant, Executor):
identifier = participant.id
elif isinstance(participant, AgentProtocol):
identifier = participant.display_name
identifier = participant.name or participant.id
else:
raise TypeError(
f"Participants must be AgentProtocol or Executor instances. Got {type(participant).__name__}."
@@ -1838,7 +1838,7 @@ class HandoffBuilder:
if isinstance(instance, Executor):
identifier = instance.id
elif isinstance(instance, AgentProtocol):
identifier = instance.display_name
identifier = instance.name or instance.id
else:
raise TypeError(
f"Participants must be AgentProtocol or Executor instances. Got {type(instance).__name__}."
@@ -2026,9 +2026,9 @@ class HandoffBuilder:
tool_targets = self._apply_auto_tools(cloned_agent, target_agents)
if tool_targets:
middleware = _AutoHandoffMiddleware(tool_targets)
existing_middleware = list(cloned_agent.middleware or [])
existing_middleware.append(middleware)
cloned_agent.middleware = existing_middleware
existing_middlewares = list(cloned_agent.middleware or [])
existing_middlewares.append(middleware)
cloned_agent.middleware = existing_middlewares
new_executor = AgentExecutor(
cloned_agent,
@@ -53,7 +53,7 @@ def wrap_participant(participant: AgentProtocol | Executor, *, executor_id: str
f"Participants must implement AgentProtocol or be Executor instances. Got {type(participant).__name__}."
)
executor_id = executor_id or participant.display_name
executor_id = executor_id or participant.name or participant.id
return AgentExecutor(participant, id=executor_id)
@@ -89,11 +89,11 @@ def build_alias_map(participant: AgentProtocol | Executor, executor: Executor) -
if isinstance(participant, AgentProtocol):
name = getattr(participant, "name", None)
display = getattr(participant, "display_name", None)
_register([name, display])
agent_id = getattr(participant, "id", None)
_register([name, agent_id])
else:
display = getattr(participant, "display_name", None)
_register([display])
participant_id = getattr(participant, "id", None)
_register([participant_id])
return aliases
@@ -290,7 +290,7 @@ class SequentialBuilder:
for p in participants:
if isinstance(p, (AgentProtocol, AgentExecutor)):
label = p.id if isinstance(p, AgentExecutor) else p.display_name
label = p.id if isinstance(p, AgentExecutor) else p.name
if self._request_info_enabled:
# Insert request info interceptor BEFORE the agent
@@ -1346,7 +1346,7 @@ def _trace_agent_run(
operation_name=OtelAttr.AGENT_INVOKE_OPERATION,
provider_name=provider_name,
agent_id=self.id,
agent_name=self.display_name,
agent_name=self.name or self.id,
agent_description=self.description,
thread_id=thread.service_thread_id if thread else None,
chat_options=getattr(self, "chat_options", None),
@@ -1418,7 +1418,7 @@ def _trace_agent_run_stream(
operation_name=OtelAttr.AGENT_INVOKE_OPERATION,
provider_name=provider_name,
agent_id=self.id,
agent_name=self.display_name,
agent_name=self.name or self.id,
agent_description=self.description,
thread_id=thread.service_thread_id if thread else None,
chat_options=getattr(self, "chat_options", None),
@@ -221,11 +221,6 @@ class MockAgent(AgentProtocol):
"""Returns the name of the agent."""
return "Name"
@property
def display_name(self) -> str:
"""Returns the name of the agent."""
return "Display Name"
@property
def description(self) -> str | None:
return "Description"
+7 -48
View File
@@ -13,7 +13,6 @@ from agent_framework import (
AgentRunResponse,
AgentRunResponseUpdate,
AgentThread,
AggregateContextProvider,
ChatAgent,
ChatClientProtocol,
ChatMessage,
@@ -66,7 +65,6 @@ async def test_chat_client_agent_init(chat_client: ChatClientProtocol) -> None:
assert agent.id == agent_id
assert agent.name is None
assert agent.description == "Test"
assert agent.display_name == agent_id # Display name defaults to id if name is None
async def test_chat_client_agent_init_with_name(chat_client: ChatClientProtocol) -> None:
@@ -76,7 +74,6 @@ async def test_chat_client_agent_init_with_name(chat_client: ChatClientProtocol)
assert agent.id == agent_id
assert agent.name == "Test Agent"
assert agent.description == "Test"
assert agent.display_name == "Test Agent" # Display name is the name if present
async def test_chat_client_agent_run(chat_client: ChatClientProtocol) -> None:
@@ -255,7 +252,7 @@ class MockContextProvider(ContextProvider):
async def test_chat_agent_context_providers_model_invoking(chat_client: ChatClientProtocol) -> None:
"""Test that context providers' invoking is called during agent run."""
mock_provider = MockContextProvider(messages=[ChatMessage(role=Role.SYSTEM, text="Test context instructions")])
agent = ChatAgent(chat_client=chat_client, context_providers=mock_provider)
agent = ChatAgent(chat_client=chat_client, context_provider=mock_provider)
await agent.run("Hello")
@@ -272,7 +269,7 @@ async def test_chat_agent_context_providers_thread_created(chat_client_base: Cha
)
]
agent = ChatAgent(chat_client=chat_client_base, context_providers=mock_provider)
agent = ChatAgent(chat_client=chat_client_base, context_provider=mock_provider)
await agent.run("Hello")
@@ -283,7 +280,7 @@ async def test_chat_agent_context_providers_thread_created(chat_client_base: Cha
async def test_chat_agent_context_providers_messages_adding(chat_client: ChatClientProtocol) -> None:
"""Test that context providers' invoked is called during agent run."""
mock_provider = MockContextProvider()
agent = ChatAgent(chat_client=chat_client, context_providers=mock_provider)
agent = ChatAgent(chat_client=chat_client, context_provider=mock_provider)
await agent.run("Hello")
@@ -295,7 +292,7 @@ async def test_chat_agent_context_providers_messages_adding(chat_client: ChatCli
async def test_chat_agent_context_instructions_in_messages(chat_client: ChatClientProtocol) -> None:
"""Test that AI context instructions are included in messages."""
mock_provider = MockContextProvider(messages=[ChatMessage(role="system", text="Context-specific instructions")])
agent = ChatAgent(chat_client=chat_client, instructions="Agent instructions", context_providers=mock_provider)
agent = ChatAgent(chat_client=chat_client, instructions="Agent instructions", context_provider=mock_provider)
# We need to test the _prepare_thread_and_messages method directly
_, _, messages = await agent._prepare_thread_and_messages( # type: ignore[reportPrivateUsage]
@@ -314,7 +311,7 @@ async def test_chat_agent_context_instructions_in_messages(chat_client: ChatClie
async def test_chat_agent_no_context_instructions(chat_client: ChatClientProtocol) -> None:
"""Test behavior when AI context has no instructions."""
mock_provider = MockContextProvider()
agent = ChatAgent(chat_client=chat_client, instructions="Agent instructions", context_providers=mock_provider)
agent = ChatAgent(chat_client=chat_client, instructions="Agent instructions", context_provider=mock_provider)
_, _, messages = await agent._prepare_thread_and_messages( # type: ignore[reportPrivateUsage]
thread=None, input_messages=[ChatMessage(role=Role.USER, text="Hello")]
@@ -329,7 +326,7 @@ async def test_chat_agent_no_context_instructions(chat_client: ChatClientProtoco
async def test_chat_agent_run_stream_context_providers(chat_client: ChatClientProtocol) -> None:
"""Test that context providers work with run_stream method."""
mock_provider = MockContextProvider(messages=[ChatMessage(role=Role.SYSTEM, text="Stream context instructions")])
agent = ChatAgent(chat_client=chat_client, context_providers=mock_provider)
agent = ChatAgent(chat_client=chat_client, context_provider=mock_provider)
# Collect all stream updates
updates: list[AgentRunResponseUpdate] = []
@@ -343,44 +340,6 @@ async def test_chat_agent_run_stream_context_providers(chat_client: ChatClientPr
assert mock_provider.invoked_called
async def test_chat_agent_multiple_context_providers(chat_client: ChatClientProtocol) -> None:
"""Test that multiple context providers work together."""
provider1 = MockContextProvider(messages=[ChatMessage(role=Role.SYSTEM, text="First provider instructions")])
provider2 = MockContextProvider(messages=[ChatMessage(role=Role.SYSTEM, text="Second provider instructions")])
agent = ChatAgent(chat_client=chat_client, context_providers=[provider1, provider2])
await agent.run("Hello")
# Both providers should be called
assert provider1.invoking_called
assert not provider1.thread_created_called
assert provider1.invoked_called
assert provider2.invoking_called
assert not provider2.thread_created_called
assert provider2.invoked_called
async def test_chat_agent_aggregate_context_provider_combines_instructions() -> None:
"""Test that AggregateContextProvider combines instructions from multiple providers."""
provider1 = MockContextProvider(messages=[ChatMessage(role=Role.SYSTEM, text="First instruction")])
provider2 = MockContextProvider(messages=[ChatMessage(role=Role.SYSTEM, text="Second instruction")])
aggregate = AggregateContextProvider()
aggregate.providers.append(provider1)
aggregate.providers.append(provider2)
# Test invoking combines instructions
result = await aggregate.invoking([ChatMessage(role=Role.USER, text="Test")])
assert result.messages
assert isinstance(result.messages[0], ChatMessage)
assert isinstance(result.messages[1], ChatMessage)
assert result.messages[0].text == "First instruction"
assert result.messages[1].text == "Second instruction"
async def test_chat_agent_context_providers_with_thread_service_id(chat_client_base: ChatClientProtocol) -> None:
"""Test context providers with service-managed thread."""
mock_provider = MockContextProvider()
@@ -391,7 +350,7 @@ async def test_chat_agent_context_providers_with_thread_service_id(chat_client_b
)
]
agent = ChatAgent(chat_client=chat_client_base, context_providers=mock_provider)
agent = ChatAgent(chat_client=chat_client_base, context_provider=mock_provider)
# Use existing service-managed thread
thread = agent.get_new_thread(service_thread_id="existing-thread-id")
+38 -241
View File
@@ -2,10 +2,9 @@
from collections.abc import MutableSequence
from typing import Any
from unittest.mock import AsyncMock, Mock
from agent_framework import ChatMessage, Role, TextContent
from agent_framework._memory import AggregateContextProvider, Context, ContextProvider
from agent_framework import ChatMessage, Role
from agent_framework._memory import Context, ContextProvider
class MockContextProvider(ContextProvider):
@@ -45,252 +44,50 @@ class MockContextProvider(ContextProvider):
return context
class TestAggregateContextProvider:
"""Tests for AggregateContextProvider class."""
class TestContext:
"""Tests for Context class."""
def test_init_with_no_providers(self) -> None:
"""Test initialization with no providers."""
aggregate = AggregateContextProvider()
assert aggregate.providers == []
def test_context_default_values(self) -> None:
"""Test Context has correct default values."""
context = Context()
assert context.instructions is None
assert context.messages == []
assert context.tools == []
def test_init_with_none_providers(self) -> None:
"""Test initialization with None providers."""
aggregate = AggregateContextProvider(None)
assert aggregate.providers == []
def test_context_with_values(self) -> None:
"""Test Context can be initialized with values."""
messages = [ChatMessage(role=Role.USER, text="Test message")]
context = Context(instructions="Test instructions", messages=messages)
assert context.instructions == "Test instructions"
assert len(context.messages) == 1
assert context.messages[0].text == "Test message"
def test_init_with_providers(self) -> None:
"""Test initialization with providers."""
provider1 = MockContextProvider(messages=[ChatMessage(role="user", text="Instructions 1")])
provider2 = MockContextProvider(messages=[ChatMessage(role="user", text="Instructions 2")])
provider3 = MockContextProvider(messages=[ChatMessage(role="user", text="Instructions 3")])
providers = [provider1, provider2, provider3]
aggregate = AggregateContextProvider(providers)
assert len(aggregate.providers) == 3
assert aggregate.providers[0] is provider1
assert aggregate.providers[1] is provider2
assert aggregate.providers[2] is provider3
def test_add_provider(self) -> None:
"""Test adding a provider."""
aggregate = AggregateContextProvider()
provider = MockContextProvider(messages=[ChatMessage(role="user", text="Instructions")])
aggregate.add(provider)
assert len(aggregate.providers) == 1
assert aggregate.providers[0] is provider
def test_add_multiple_providers(self) -> None:
"""Test adding multiple providers."""
aggregate = AggregateContextProvider()
provider1 = MockContextProvider(messages=[ChatMessage(role="user", text="Instructions 1")])
provider2 = MockContextProvider(messages=[ChatMessage(role="user", text="Instructions 2")])
aggregate.add(provider1)
aggregate.add(provider2)
assert len(aggregate.providers) == 2
assert aggregate.providers[0] is provider1
assert aggregate.providers[1] is provider2
async def test_thread_created_with_no_providers(self) -> None:
"""Test thread_created with no providers."""
aggregate = AggregateContextProvider()
# Should not raise an exception
await aggregate.thread_created("thread-123")
async def test_thread_created_with_providers(self) -> None:
"""Test thread_created calls all providers."""
provider1 = MockContextProvider(messages=[ChatMessage(role="user", text="Instructions 1")])
provider2 = MockContextProvider(messages=[ChatMessage(role="user", text="Instructions 2")])
aggregate = AggregateContextProvider([provider1, provider2])
thread_id = "thread-123"
await aggregate.thread_created(thread_id)
assert provider1.thread_created_called
assert provider1.thread_created_thread_id == thread_id
assert provider2.thread_created_called
assert provider2.thread_created_thread_id == thread_id
async def test_thread_created_with_none_thread_id(self) -> None:
"""Test thread_created with None thread_id."""
provider = MockContextProvider(messages=[ChatMessage(role="user", text="Instructions")])
aggregate = AggregateContextProvider([provider])
await aggregate.thread_created(None)
class TestContextProvider:
"""Tests for ContextProvider class."""
async def test_thread_created(self) -> None:
"""Test thread_created is called."""
provider = MockContextProvider()
await provider.thread_created("test-thread-id")
assert provider.thread_created_called
assert provider.thread_created_thread_id is None
async def test_messages_adding_with_no_providers(self) -> None:
"""Test invoked with no providers."""
aggregate = AggregateContextProvider()
message = ChatMessage(text="Hello", role=Role.USER)
# Should not raise an exception
await aggregate.invoked(message)
async def test_messages_adding_with_single_message(self) -> None:
"""Test invoked with a single message."""
provider1 = MockContextProvider(messages=[ChatMessage(role="user", text="Instructions 1")])
provider2 = MockContextProvider(messages=[ChatMessage(role="user", text="Instructions 2")])
aggregate = AggregateContextProvider([provider1, provider2])
message = ChatMessage(text="Hello", role=Role.USER)
await aggregate.invoked(message)
assert provider1.invoked_called
assert provider1.new_messages == message
assert provider2.invoked_called
assert provider2.new_messages == message
async def test_messages_adding_with_message_sequence(self) -> None:
"""Test invoked with a sequence of messages."""
provider = MockContextProvider(messages=[ChatMessage(role="user", text="Instructions")])
aggregate = AggregateContextProvider([provider])
messages = [
ChatMessage(text="Hello", role=Role.USER),
ChatMessage(text="Hi there", role=Role.ASSISTANT),
]
await aggregate.invoked(messages)
assert provider.thread_created_thread_id == "test-thread-id"
async def test_invoked(self) -> None:
"""Test invoked is called."""
provider = MockContextProvider()
message = ChatMessage(role=Role.USER, text="Test message")
await provider.invoked(message)
assert provider.invoked_called
assert provider.new_messages == messages
async def test_model_invoking_with_no_providers(self) -> None:
"""Test invoking with no providers."""
aggregate = AggregateContextProvider()
message = ChatMessage(text="Hello", role=Role.USER)
context = await aggregate.invoking(message)
assert isinstance(context, Context)
assert not context.messages
async def test_model_invoking_with_single_provider(self) -> None:
"""Test invoking with a single provider."""
provider = MockContextProvider(messages=[ChatMessage(role="user", text="Test instructions")])
aggregate = AggregateContextProvider([provider])
message = [ChatMessage(text="Hello", role=Role.USER)]
context = await aggregate.invoking(message)
assert provider.new_messages == message
async def test_invoking(self) -> None:
"""Test invoking is called and returns context."""
provider = MockContextProvider(messages=[ChatMessage(role=Role.USER, text="Context message")])
message = ChatMessage(role=Role.USER, text="Test message")
context = await provider.invoking(message)
assert provider.invoking_called
assert provider.model_invoking_messages == message
assert isinstance(context, Context)
assert context.messages
assert isinstance(context.messages[0].contents[0], TextContent)
assert context.messages[0].text == "Test instructions"
async def test_model_invoking_with_multiple_providers(self) -> None:
"""Test invoking combines contexts from multiple providers."""
provider1 = MockContextProvider(messages=[ChatMessage(role="user", text="Instructions 1")])
provider2 = MockContextProvider(messages=[ChatMessage(role="user", text="Instructions 2")])
provider3 = MockContextProvider(messages=[ChatMessage(role="user", text="Instructions 3")])
aggregate = AggregateContextProvider([provider1, provider2, provider3])
messages = [ChatMessage(text="Hello", role=Role.USER)]
context = await aggregate.invoking(messages)
assert provider1.invoking_called
assert provider1.model_invoking_messages == messages
assert provider2.invoking_called
assert provider2.model_invoking_messages == messages
assert provider3.invoking_called
assert provider3.model_invoking_messages == messages
assert isinstance(context, Context)
assert context.messages
assert isinstance(context.messages[0].contents[0], TextContent)
assert isinstance(context.messages[1].contents[0], TextContent)
assert isinstance(context.messages[2].contents[0], TextContent)
assert context.messages[0].text == "Instructions 1"
assert context.messages[1].text == "Instructions 2"
assert context.messages[2].text == "Instructions 3"
async def test_model_invoking_with_none_instructions(self) -> None:
"""Test invoking filters out None instructions."""
provider1 = MockContextProvider(messages=[ChatMessage(role="user", text="Instructions 1")])
provider2 = MockContextProvider(messages=None) # None instructions
provider3 = MockContextProvider(messages=[ChatMessage(role="user", text="Instructions 3")])
aggregate = AggregateContextProvider([provider1, provider2, provider3])
message = ChatMessage(text="Hello", role=Role.USER)
context = await aggregate.invoking(message)
assert isinstance(context, Context)
assert context.messages
assert isinstance(context.messages[0].contents[0], TextContent)
assert isinstance(context.messages[1].contents[0], TextContent)
assert context.messages[0].text == "Instructions 1"
assert context.messages[1].text == "Instructions 3"
async def test_model_invoking_with_all_none_instructions(self) -> None:
"""Test invoking when all providers return None instructions."""
provider1 = MockContextProvider(None)
provider2 = MockContextProvider(None)
aggregate = AggregateContextProvider([provider1, provider2])
message = ChatMessage(text="Hello", role=Role.USER)
context = await aggregate.invoking(message)
assert isinstance(context, Context)
assert not context.messages
async def test_model_invoking_with_mutable_sequence(self) -> None:
"""Test invoking with MutableSequence of messages."""
provider = MockContextProvider(messages=[ChatMessage(role="user", text="Test instructions")])
aggregate = AggregateContextProvider([provider])
messages = [ChatMessage(text="Hello", role=Role.USER)]
context = await aggregate.invoking(messages)
assert provider.invoking_called
assert provider.model_invoking_messages == messages
assert isinstance(context, Context)
assert context.messages
assert isinstance(context.messages[0].contents[0], TextContent)
assert context.messages[0].text == "Test instructions"
async def test_async_methods_concurrent_execution(self) -> None:
"""Test that async methods execute providers concurrently."""
# Use AsyncMock to verify concurrent execution
provider1 = Mock(spec=ContextProvider)
provider1.thread_created = AsyncMock()
provider1.invoked = AsyncMock()
provider1.invoking = AsyncMock(return_value=Context(messages=[ChatMessage(role="user", text="Test 1")]))
provider2 = Mock(spec=ContextProvider)
provider2.thread_created = AsyncMock()
provider2.invoked = AsyncMock()
provider2.invoking = AsyncMock(return_value=Context(messages=[ChatMessage(role="user", text="Test 2")]))
aggregate = AggregateContextProvider([provider1, provider2])
# Test thread_created
await aggregate.thread_created("thread-123")
provider1.thread_created.assert_called_once_with("thread-123")
provider2.thread_created.assert_called_once_with("thread-123")
# Test invoked
message = ChatMessage(text="Hello", role=Role.USER)
await aggregate.invoked(message)
provider1.invoked.assert_called_once_with(
request_messages=message, response_messages=None, invoke_exception=None
)
provider2.invoked.assert_called_once_with(
request_messages=message, response_messages=None, invoke_exception=None
)
# Test invoking
context = await aggregate.invoking(message)
provider1.invoking.assert_called_once_with(message)
provider2.invoking.assert_called_once_with(message)
assert context.messages
assert context.messages[0].text == "Test 1"
assert context.messages[1].text == "Test 2"
assert context.messages is not None
assert len(context.messages) == 1
assert context.messages[0].text == "Context message"
@@ -148,7 +148,7 @@ class TestAgentMiddlewarePipeline:
context.terminate = True
def test_init_empty(self) -> None:
"""Test AgentMiddlewarePipeline initialization with no middlewares."""
"""Test AgentMiddlewarePipeline initialization with no middleware."""
pipeline = AgentMiddlewarePipeline()
assert not pipeline.has_middlewares
@@ -457,7 +457,7 @@ class TestFunctionMiddlewarePipeline:
assert execution_order == ["handler"]
def test_init_empty(self) -> None:
"""Test FunctionMiddlewarePipeline initialization with no middlewares."""
"""Test FunctionMiddlewarePipeline initialization with no middleware."""
pipeline = FunctionMiddlewarePipeline()
assert not pipeline.has_middlewares
@@ -539,7 +539,7 @@ class TestChatMiddlewarePipeline:
context.terminate = True
def test_init_empty(self) -> None:
"""Test ChatMiddlewarePipeline initialization with no middlewares."""
"""Test ChatMiddlewarePipeline initialization with no middleware."""
pipeline = ChatMiddlewarePipeline()
assert not pipeline.has_middlewares
@@ -979,7 +979,7 @@ class TestMultipleMiddlewareOrdering:
"""Test cases for multiple middleware execution order."""
async def test_agent_middleware_execution_order(self, mock_agent: AgentProtocol) -> None:
"""Test that multiple agent middlewares execute in registration order."""
"""Test that multiple agent middleware execute in registration order."""
execution_order: list[str] = []
class FirstMiddleware(AgentMiddleware):
@@ -1006,8 +1006,8 @@ class TestMultipleMiddlewareOrdering:
await next(context)
execution_order.append("third_after")
middlewares = [FirstMiddleware(), SecondMiddleware(), ThirdMiddleware()]
pipeline = AgentMiddlewarePipeline(middlewares) # type: ignore
middleware = [FirstMiddleware(), SecondMiddleware(), ThirdMiddleware()]
pipeline = AgentMiddlewarePipeline(middleware) # type: ignore
messages = [ChatMessage(role=Role.USER, text="test")]
context = AgentRunContext(agent=mock_agent, messages=messages)
@@ -1030,7 +1030,7 @@ class TestMultipleMiddlewareOrdering:
assert execution_order == expected_order
async def test_function_middleware_execution_order(self, mock_function: AIFunction[Any, Any]) -> None:
"""Test that multiple function middlewares execute in registration order."""
"""Test that multiple function middleware execute in registration order."""
execution_order: list[str] = []
class FirstMiddleware(FunctionMiddleware):
@@ -1053,8 +1053,8 @@ class TestMultipleMiddlewareOrdering:
await next(context)
execution_order.append("second_after")
middlewares = [FirstMiddleware(), SecondMiddleware()]
pipeline = FunctionMiddlewarePipeline(middlewares) # type: ignore
middleware = [FirstMiddleware(), SecondMiddleware()]
pipeline = FunctionMiddlewarePipeline(middleware) # type: ignore
arguments = FunctionTestArgs(name="test")
context = FunctionInvocationContext(function=mock_function, arguments=arguments)
@@ -1069,7 +1069,7 @@ class TestMultipleMiddlewareOrdering:
assert execution_order == expected_order
async def test_chat_middleware_execution_order(self, mock_chat_client: Any) -> None:
"""Test that multiple chat middlewares execute in registration order."""
"""Test that multiple chat middleware execute in registration order."""
execution_order: list[str] = []
class FirstChatMiddleware(ChatMiddleware):
@@ -1090,8 +1090,8 @@ class TestMultipleMiddlewareOrdering:
await next(context)
execution_order.append("third_after")
middlewares = [FirstChatMiddleware(), SecondChatMiddleware(), ThirdChatMiddleware()]
pipeline = ChatMiddlewarePipeline(middlewares) # type: ignore
middleware = [FirstChatMiddleware(), SecondChatMiddleware(), ThirdChatMiddleware()]
pipeline = ChatMiddlewarePipeline(middleware) # type: ignore
messages = [ChatMessage(role=Role.USER, text="test")]
chat_options = ChatOptions()
context = ChatContext(chat_client=mock_chat_client, messages=messages, chat_options=chat_options)
@@ -1542,7 +1542,7 @@ class TestMiddlewareExecutionControl:
assert context.result is None
async def test_multiple_middlewares_early_stop(self, mock_agent: AgentProtocol) -> None:
"""Test that when first middleware doesn't call next(), subsequent middlewares are not called."""
"""Test that when first middleware doesn't call next(), subsequent middleware are not called."""
execution_order: list[str] = []
class FirstMiddleware(AgentMiddleware):
@@ -1641,7 +1641,7 @@ class TestMiddlewareExecutionControl:
assert context.result is None
async def test_multiple_chat_middlewares_early_stop(self, mock_chat_client: Any) -> None:
"""Test that when first chat middleware doesn't call next(), subsequent middlewares are not called."""
"""Test that when first chat middleware doesn't call next(), subsequent middleware are not called."""
execution_order: list[str] = []
class FirstChatMiddleware(ChatMiddleware):
@@ -418,7 +418,7 @@ class TestChatAgentMultipleMiddlewareOrdering:
"""Test cases for multiple middleware execution order with ChatAgent."""
async def test_multiple_agent_middleware_execution_order(self, chat_client: "MockChatClient") -> None:
"""Test that multiple agent middlewares execute in correct order with ChatAgent."""
"""Test that multiple agent middleware execute in correct order with ChatAgent."""
execution_order: list[str] = []
class OrderedMiddleware(AgentMiddleware):
@@ -432,12 +432,12 @@ class TestChatAgentMultipleMiddlewareOrdering:
await next(context)
execution_order.append(f"{self.name}_after")
# Create multiple middlewares
# Create multiple middleware
middleware1 = OrderedMiddleware("first")
middleware2 = OrderedMiddleware("second")
middleware3 = OrderedMiddleware("third")
# Create ChatAgent with multiple middlewares
# Create ChatAgent with multiple middleware
agent = ChatAgent(chat_client=chat_client, middleware=[middleware1, middleware2, middleware3])
# Execute the agent
@@ -453,7 +453,7 @@ class TestChatAgentMultipleMiddlewareOrdering:
assert execution_order == expected_order
async def test_mixed_middleware_types_with_chat_agent(self, chat_client: "MockChatClient") -> None:
"""Test mixed class and function-based middlewares with ChatAgent."""
"""Test mixed class and function-based middleware with ChatAgent."""
execution_order: list[str] = []
class ClassAgentMiddleware(AgentMiddleware):
@@ -507,8 +507,8 @@ class TestChatAgentMultipleMiddlewareOrdering:
assert response is not None
assert chat_client.call_count == 1
# Verify that agent middlewares were executed in correct order
# (Function middlewares won't execute since no functions are called)
# Verify that agent middleware were executed in correct order
# (Function middleware won't execute since no functions are called)
expected_order = ["class_agent_before", "function_agent_before", "function_agent_after", "class_agent_after"]
assert execution_order == expected_order
@@ -999,7 +999,7 @@ class TestRunLevelMiddleware:
# Clear execution log
execution_log.clear()
# Fourth run with both run middlewares - should see both
# Fourth run with both run middleware - should see both
await agent.run("Test message 4", middleware=[run_middleware1, run_middleware2])
assert execution_log == ["run1_start", "run2_start", "run2_end", "run1_end"]
@@ -342,7 +342,6 @@ def test_agent_decorator_with_valid_class():
def __init__(self):
self.id = "test_agent_id"
self.name = "test_agent"
self.display_name = "Test Agent"
self.description = "Test agent description"
async def run(self, messages=None, *, thread=None, **kwargs):
@@ -384,7 +383,6 @@ def test_agent_decorator_with_partial_methods():
def __init__(self):
self.id = "test_agent_id"
self.name = "test_agent"
self.display_name = "Test Agent"
async def run(self, messages=None, *, thread=None, **kwargs):
return Mock()
@@ -406,7 +404,6 @@ def mock_chat_agent():
def __init__(self):
self.id = "test_agent_id"
self.name = "test_agent"
self.display_name = "Test Agent"
self.description = "Test agent description"
self.chat_options = ChatOptions(model_id="TestModel")
@@ -441,10 +438,10 @@ async def test_agent_instrumentation_enabled(
spans = span_exporter.get_finished_spans()
assert len(spans) == 1
span = spans[0]
assert span.name == "invoke_agent Test Agent"
assert span.name == "invoke_agent test_agent"
assert span.attributes[OtelAttr.OPERATION.value] == OtelAttr.AGENT_INVOKE_OPERATION
assert span.attributes[OtelAttr.AGENT_ID] == "test_agent_id"
assert span.attributes[OtelAttr.AGENT_NAME] == "Test Agent"
assert span.attributes[OtelAttr.AGENT_NAME] == "test_agent"
assert span.attributes[OtelAttr.AGENT_DESCRIPTION] == "Test agent description"
assert span.attributes[SpanAttributes.LLM_REQUEST_MODEL] == "TestModel"
assert span.attributes[OtelAttr.INPUT_TOKENS] == 15
@@ -469,10 +466,10 @@ async def test_agent_streaming_response_with_diagnostics_enabled_via_decorator(
spans = span_exporter.get_finished_spans()
assert len(spans) == 1
span = spans[0]
assert span.name == "invoke_agent Test Agent"
assert span.name == "invoke_agent test_agent"
assert span.attributes[OtelAttr.OPERATION.value] == OtelAttr.AGENT_INVOKE_OPERATION
assert span.attributes[OtelAttr.AGENT_ID] == "test_agent_id"
assert span.attributes[OtelAttr.AGENT_NAME] == "Test Agent"
assert span.attributes[OtelAttr.AGENT_NAME] == "test_agent"
assert span.attributes[OtelAttr.AGENT_DESCRIPTION] == "Test agent description"
assert span.attributes[SpanAttributes.LLM_REQUEST_MODEL] == "TestModel"
if enable_sensitive_data:
@@ -38,7 +38,7 @@ class _CountingAgent(BaseAgent):
) -> AgentRunResponse:
self.call_count += 1
return AgentRunResponse(
messages=[ChatMessage(role=Role.ASSISTANT, text=f"Response #{self.call_count}: {self.display_name}")]
messages=[ChatMessage(role=Role.ASSISTANT, text=f"Response #{self.call_count}: {self.name}")]
)
async def run_stream( # type: ignore[override]
@@ -49,7 +49,7 @@ class _CountingAgent(BaseAgent):
**kwargs: Any,
) -> AsyncIterable[AgentRunResponseUpdate]:
self.call_count += 1
yield AgentRunResponseUpdate(contents=[TextContent(text=f"Response #{self.call_count}: {self.display_name}")])
yield AgentRunResponseUpdate(contents=[TextContent(text=f"Response #{self.call_count}: {self.name}")])
async def test_agent_executor_checkpoint_stores_and_restores_state() -> None:
@@ -78,7 +78,7 @@ class _RecordingAgent(BaseAgent):
text_handoff: bool = False,
extra_properties: dict[str, object] | None = None,
) -> None:
super().__init__(id=name, name=name, display_name=name)
super().__init__(id=name, name=name)
self._agent_name = name
self.handoff_to = handoff_to
self.calls: list[list[ChatMessage]] = []
@@ -102,7 +102,7 @@ class _RecordingAgent(BaseAgent):
reply = ChatMessage(
role=Role.ASSISTANT,
contents=contents,
author_name=self.display_name,
author_name=self.name,
additional_properties=additional_properties,
)
return AgentRunResponse(messages=[reply])
@@ -35,7 +35,7 @@ class _EchoAgent(BaseAgent):
thread: AgentThread | None = None,
**kwargs: Any,
) -> AgentRunResponse:
return AgentRunResponse(messages=[ChatMessage(role=Role.ASSISTANT, text=f"{self.display_name} reply")])
return AgentRunResponse(messages=[ChatMessage(role=Role.ASSISTANT, text=f"{self.name} reply")])
async def run_stream( # type: ignore[override]
self,
@@ -45,7 +45,7 @@ class _EchoAgent(BaseAgent):
**kwargs: Any,
) -> AsyncIterable[AgentRunResponseUpdate]:
# Minimal async generator with one assistant update
yield AgentRunResponseUpdate(contents=[TextContent(text=f"{self.display_name} reply")])
yield AgentRunResponseUpdate(contents=[TextContent(text=f"{self.name} reply")])
class _SummarizerExec(Executor):
@@ -60,7 +60,7 @@ class _KwargsCapturingAgent(BaseAgent):
**kwargs: Any,
) -> AgentRunResponse:
self.captured_kwargs.append(dict(kwargs))
return AgentRunResponse(messages=[ChatMessage(role=Role.ASSISTANT, text=f"{self.display_name} response")])
return AgentRunResponse(messages=[ChatMessage(role=Role.ASSISTANT, text=f"{self.name} response")])
async def run_stream(
self,
@@ -70,7 +70,7 @@ class _KwargsCapturingAgent(BaseAgent):
**kwargs: Any,
) -> AsyncIterable[AgentRunResponseUpdate]:
self.captured_kwargs.append(dict(kwargs))
yield AgentRunResponseUpdate(contents=[TextContent(text=f"{self.display_name} response")])
yield AgentRunResponseUpdate(contents=[TextContent(text=f"{self.name} response")])
class _EchoAgent(BaseAgent):
@@ -83,7 +83,7 @@ class _EchoAgent(BaseAgent):
thread: AgentThread | None = None,
**kwargs: Any,
) -> AgentRunResponse:
return AgentRunResponse(messages=[ChatMessage(role=Role.ASSISTANT, text=f"{self.display_name} reply")])
return AgentRunResponse(messages=[ChatMessage(role=Role.ASSISTANT, text=f"{self.name} reply")])
async def run_stream(
self,
@@ -92,7 +92,7 @@ class _EchoAgent(BaseAgent):
thread: AgentThread | None = None,
**kwargs: Any,
) -> AsyncIterable[AgentRunResponseUpdate]:
yield AgentRunResponseUpdate(contents=[TextContent(text=f"{self.display_name} reply")])
yield AgentRunResponseUpdate(contents=[TextContent(text=f"{self.name} reply")])
# region Sequential Builder Tests
@@ -346,8 +346,8 @@ class EntityDiscovery:
instructions = None
model = None
chat_client_type = None
context_providers_list = None
middleware_list = None
context_provider_list = None
middlewares_list = None
if entity_type == "agent":
from ._utils import extract_agent_metadata
@@ -356,8 +356,8 @@ class EntityDiscovery:
instructions = agent_meta["instructions"]
model = agent_meta["model"]
chat_client_type = agent_meta["chat_client_type"]
context_providers_list = agent_meta["context_providers"]
middleware_list = agent_meta["middleware"]
context_provider_list = agent_meta["context_provider"]
middlewares_list = agent_meta["middleware"]
# Log helpful info about agent capabilities (before creating EntityInfo)
if entity_type == "agent":
@@ -395,8 +395,8 @@ class EntityDiscovery:
instructions=instructions,
model_id=model,
chat_client_type=chat_client_type,
context_providers=context_providers_list,
middleware=middleware_list,
context_provider=context_provider_list,
middleware=middlewares_list,
executors=tools_list if entity_type == "workflow" else [],
input_schema={"type": "string"}, # Default schema
start_executor_id=tools_list[0] if tools_list and entity_type == "workflow" else None,
@@ -829,8 +829,8 @@ class EntityDiscovery:
instructions = None
model = None
chat_client_type = None
context_providers_list = None
middleware_list = None
context_provider_list = None
middlewares_list = None
if obj_type == "agent":
from ._utils import extract_agent_metadata
@@ -839,8 +839,8 @@ class EntityDiscovery:
instructions = agent_meta["instructions"]
model = agent_meta["model"]
chat_client_type = agent_meta["chat_client_type"]
context_providers_list = agent_meta["context_providers"]
middleware_list = agent_meta["middleware"]
context_provider_list = agent_meta["context_provider"]
middlewares_list = agent_meta["middleware"]
entity_info = EntityInfo(
id=entity_id,
@@ -852,8 +852,8 @@ class EntityDiscovery:
instructions=instructions,
model_id=model,
chat_client_type=chat_client_type,
context_providers=context_providers_list,
middleware=middleware_list,
context_provider=context_provider_list,
middleware=middlewares_list,
metadata={
"module_path": module_path,
"entity_type": obj_type,
@@ -32,7 +32,7 @@ def extract_agent_metadata(entity_object: Any) -> dict[str, Any]:
"instructions": None,
"model": None,
"chat_client_type": None,
"context_providers": None,
"context_provider": None,
"middleware": None,
}
@@ -60,20 +60,20 @@ def extract_agent_metadata(entity_object: Any) -> dict[str, Any]:
and entity_object.context_provider
and hasattr(entity_object.context_provider, "__class__")
):
metadata["context_providers"] = [entity_object.context_provider.__class__.__name__] # type: ignore
metadata["context_provider"] = [entity_object.context_provider.__class__.__name__] # type: ignore
# Try to get middleware
if hasattr(entity_object, "middleware") and entity_object.middleware:
middleware_list: list[str] = []
middlewares_list: list[str] = []
for m in entity_object.middleware:
# Try multiple ways to get a good name for middleware
if hasattr(m, "__name__"): # Function or callable
middleware_list.append(m.__name__)
middlewares_list.append(m.__name__)
elif hasattr(m, "__class__"): # Class instance
middleware_list.append(m.__class__.__name__)
middlewares_list.append(m.__class__.__name__)
else:
middleware_list.append(str(m))
metadata["middleware"] = middleware_list # type: ignore
middlewares_list.append(str(m))
metadata["middleware"] = middlewares_list # type: ignore
return metadata
@@ -43,7 +43,7 @@ class EntityInfo(BaseModel):
instructions: str | None = None
model_id: str | None = None
chat_client_type: str | None = None
context_providers: list[str] | None = None
context_provider: list[str] | None = None
middleware: list[str] | None = None
# Workflow-specific fields (populated only for detailed info requests)
@@ -179,10 +179,10 @@ export function AgentDetailsModal({
</DetailCard>
)}
{/* Middleware */}
{/* Middlewares */}
{agent.middleware && agent.middleware.length > 0 && (
<DetailCard
title={`Middleware (${agent.middleware.length})`}
title={`Middlewares (${agent.middleware.length})`}
icon={<Package className="h-4 w-4 text-muted-foreground" />}
>
<ul className="space-y-1">
@@ -195,20 +195,16 @@ export function AgentDetailsModal({
</DetailCard>
)}
{/* Context Providers */}
{agent.context_providers && agent.context_providers.length > 0 && (
{/* Context Provider */}
{agent.context_provider && (
<DetailCard
title={`Context Providers (${agent.context_providers.length})`}
title="Context Provider"
icon={<Database className="h-4 w-4 text-muted-foreground" />}
className={!agent.middleware || agent.middleware.length === 0 ? "md:col-start-2" : ""}
>
<ul className="space-y-1">
{agent.context_providers.map((cp, index) => (
<li key={index} className="font-mono text-xs text-foreground">
{cp}
</li>
))}
</ul>
<div className="font-mono text-xs text-foreground">
{agent.context_provider}
</div>
</DetailCard>
)}
</div>
@@ -42,7 +42,7 @@ interface BackendEntityInfo {
instructions?: string;
model_id?: string;
chat_client_type?: string;
context_providers?: string[];
context_provider?: string[];
middleware?: string[];
// Workflow-specific fields (present when type === "workflow")
executors?: string[];
@@ -77,7 +77,7 @@ const MAX_RETRY_ATTEMPTS = 10; // Max 10 retries (~30 seconds with exponential b
function getBackendUrl(): string {
const stored = localStorage.getItem("devui_backend_url");
if (stored) return stored;
return DEFAULT_API_BASE_URL;
}
@@ -221,13 +221,13 @@ class ApiClient {
instructions: entity.instructions,
model_id: entity.model_id,
chat_client_type: entity.chat_client_type,
context_providers: entity.context_providers,
context_provider: entity.context_provider,
middleware: entity.middleware,
};
} else {
// Workflow - prefer executors field, fall back to tools for backward compatibility
const executorList = entity.executors || entity.tools || [];
// Determine start_executor_id: use entity value, or first executor if it's a string
let startExecutorId = entity.start_executor_id || "";
if (!startExecutorId && executorList.length > 0) {
@@ -236,7 +236,7 @@ class ApiClient {
startExecutorId = firstExecutor;
}
}
return {
id: entity.id,
name: entity.name,
@@ -493,10 +493,10 @@ class ApiClient {
if (!resumeResponseId) {
currentResponseId = storedState.responseId;
}
lastSequenceNumber = storedState.lastSequenceNumber;
lastMessageId = storedState.lastMessageId;
// Replay stored events only if we're not explicitly resuming
// (explicit resume means the caller already has the events)
if (!resumeResponseId) {
@@ -39,8 +39,8 @@ export interface AgentInfo {
instructions?: string;
model_id?: string;
chat_client_type?: string;
context_providers?: string[];
middleware?: string[];
context_provider?: string | undefined;
middleware?: string[] | undefined;
}
// JSON Schema types for workflow input
@@ -91,10 +91,6 @@ class NonStreamingAgent:
name = "Non-Streaming Agent"
description = "Agent without run_stream"
@property
def display_name(self):
return self.name
async def run(self, messages=None, *, thread=None, **kwargs):
return AgentRunResponse(
messages=[ChatMessage(
@@ -575,10 +575,6 @@ async def test_executor_handles_non_streaming_agent():
name = "Non-Streaming Test Agent"
description = "Test agent without run_stream()"
@property
def display_name(self):
return self.name
async def run(self, messages=None, *, thread=None, **kwargs):
return AgentRunResponse(
messages=[ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text=f"Processed: {messages}")])],
+4 -6
View File
@@ -135,11 +135,11 @@ class MyCustomCache(CacheProvider):
async def get(self, key: str) -> Any | None:
# Your implementation
pass
async def set(self, key: str, value: Any, ttl_seconds: int | None = None) -> None:
# Your implementation
pass
async def remove(self, key: str) -> None:
# Your implementation
pass
@@ -295,9 +295,9 @@ All exceptions inherit from `PurviewServiceError`. You can catch specific except
```python
from agent_framework.microsoft import (
PurviewPaymentRequiredError,
PurviewAuthenticationError,
PurviewAuthenticationError,
PurviewRateLimitError,
PurviewRequestError,
PurviewRequestError,
PurviewServiceError
)
@@ -321,5 +321,3 @@ except (PurviewAuthenticationError, PurviewRateLimitError, PurviewRequestError,
- **Error Handling**: Use `ignore_exceptions` and `ignore_payment_required` settings for graceful degradation. When enabled, errors are logged but don't fail the request.
- **Caching**: Protection scopes responses and 402 errors are cached by default with a 4-hour TTL. Cache is automatically invalidated when protection scope state changes.
- **Background Processing**: Content Activities and offline Process Content requests are handled asynchronously using background tasks to avoid blocking the main execution flow.