mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
ef44fb4960
commit
203fb7b1c4
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
+6
-21
@@ -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 = [
|
||||
|
||||
+1
-2
@@ -19,8 +19,7 @@ import time
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from .testutils import (
|
||||
from testutils import (
|
||||
SampleTestHelper,
|
||||
skip_if_azure_functions_integration_tests_disabled,
|
||||
)
|
||||
|
||||
+1
-2
@@ -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 = [
|
||||
|
||||
+1
-2
@@ -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 = [
|
||||
|
||||
+1
-2
@@ -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 = [
|
||||
|
||||
+1
-2
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
+8
-12
@@ -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}")])],
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user