mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [BREAKING] Remove deprecated kwargs compatibility paths (#4858)
* [BREAKING] Remove deprecated kwargs compatibility paths Remove the deprecated kwargs compatibility shims across core agents, clients, tools, middleware, and telemetry. Keep workflow kwargs behavior intact in this branch and follow up separately in #4850. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix PR CI fallout for kwargs removal Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * updates * Fix Azure AI CI fallout Remove the stale _get_current_conversation_id override from the Azure AI client after the OpenAI base helper was deleted. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fixed new classes * Fix Assistants deprecated import gating Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix integration replay regressions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Switch multi-agent hosting samples to Azure chat completions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Simplify Azure multi-agent sample config Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
ca6cdd142e
commit
b1b528e4a8
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
import warnings
|
||||
from collections.abc import Awaitable, Callable, Mapping, MutableMapping, Sequence
|
||||
from contextlib import AbstractAsyncContextManager, AsyncExitStack
|
||||
from copy import deepcopy
|
||||
@@ -248,7 +247,6 @@ class SupportsAgentRun(Protocol):
|
||||
session: AgentSession | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[AgentResponse[Any]]:
|
||||
"""Get a response from the agent (non-streaming)."""
|
||||
...
|
||||
@@ -262,7 +260,6 @@ class SupportsAgentRun(Protocol):
|
||||
session: AgentSession | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
|
||||
"""Get a streaming response from the agent."""
|
||||
...
|
||||
@@ -275,7 +272,6 @@ class SupportsAgentRun(Protocol):
|
||||
session: AgentSession | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
|
||||
"""Get a response from the agent.
|
||||
|
||||
@@ -291,7 +287,6 @@ class SupportsAgentRun(Protocol):
|
||||
session: The conversation session associated with the message(s).
|
||||
function_invocation_kwargs: Keyword arguments forwarded to tool invocation.
|
||||
client_kwargs: Additional client-specific keyword arguments.
|
||||
kwargs: Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
When stream=False: An AgentResponse with the final result.
|
||||
@@ -334,7 +329,15 @@ class BaseAgent(SerializationMixin):
|
||||
|
||||
# Create a concrete subclass that implements the protocol
|
||||
class SimpleAgent(BaseAgent):
|
||||
async def run(self, messages=None, *, stream=False, session=None, **kwargs):
|
||||
async def run(
|
||||
self,
|
||||
messages=None,
|
||||
*,
|
||||
stream=False,
|
||||
session=None,
|
||||
function_invocation_kwargs=None,
|
||||
client_kwargs=None,
|
||||
):
|
||||
if stream:
|
||||
|
||||
async def _stream():
|
||||
@@ -373,7 +376,6 @@ class BaseAgent(SerializationMixin):
|
||||
context_providers: Sequence[BaseContextProvider] | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
additional_properties: MutableMapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize a BaseAgent instance.
|
||||
|
||||
@@ -385,15 +387,7 @@ class BaseAgent(SerializationMixin):
|
||||
context_providers: Context providers 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).
|
||||
"""
|
||||
if kwargs:
|
||||
warnings.warn(
|
||||
"Passing additional properties as direct keyword arguments to BaseAgent is deprecated; "
|
||||
"pass them via additional_properties instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
if id is None:
|
||||
id = str(uuid4())
|
||||
self.id = id
|
||||
@@ -403,10 +397,7 @@ class BaseAgent(SerializationMixin):
|
||||
self.middleware: list[MiddlewareTypes] | None = (
|
||||
cast(list[MiddlewareTypes], 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 {})
|
||||
self.additional_properties.update(kwargs)
|
||||
|
||||
def create_session(self, *, session_id: str | None = None) -> AgentSession:
|
||||
"""Create a new lightweight session.
|
||||
@@ -666,9 +657,10 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
|
||||
default_options: OptionsCoT | None = None,
|
||||
context_providers: Sequence[BaseContextProvider] | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
**kwargs: Any,
|
||||
additional_properties: MutableMapping[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Initialize a Agent instance.
|
||||
|
||||
@@ -695,7 +687,7 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
If both this and a compaction_strategy on the underlying client are set, this one is used.
|
||||
tokenizer: Optional agent-level tokenizer.
|
||||
If both this and a tokenizer on the underlying client are set, this one is used.
|
||||
kwargs: Any additional keyword arguments. Will be stored as ``additional_properties``.
|
||||
additional_properties: Additional properties stored on the agent.
|
||||
"""
|
||||
opts = dict(default_options) if default_options else {}
|
||||
|
||||
@@ -709,7 +701,8 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
name=name,
|
||||
description=description,
|
||||
context_providers=context_providers,
|
||||
**kwargs,
|
||||
middleware=middleware,
|
||||
additional_properties=additional_properties,
|
||||
)
|
||||
self.client = client
|
||||
self.compaction_strategy = compaction_strategy
|
||||
@@ -812,7 +805,6 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[AgentResponse[ResponseModelBoundT]]: ...
|
||||
|
||||
@overload
|
||||
@@ -828,7 +820,6 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[AgentResponse[Any]]: ...
|
||||
|
||||
@overload
|
||||
@@ -844,7 +835,6 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
|
||||
|
||||
def run(
|
||||
@@ -859,7 +849,6 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
|
||||
"""Run the agent with the given messages and options.
|
||||
|
||||
@@ -890,21 +879,12 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
is used, falling back to the client default.
|
||||
function_invocation_kwargs: Keyword arguments forwarded to tool invocation.
|
||||
client_kwargs: Additional client-specific keyword arguments for the chat client.
|
||||
kwargs: Deprecated additional keyword arguments for the agent.
|
||||
They are forwarded to both tool invocation and the chat client for compatibility.
|
||||
|
||||
Returns:
|
||||
When stream=False: An Awaitable[AgentResponse] containing the agent's response.
|
||||
When stream=True: A ResponseStream of AgentResponseUpdate items with
|
||||
``get_final_response()`` for the final AgentResponse.
|
||||
"""
|
||||
if kwargs:
|
||||
warnings.warn(
|
||||
"Passing runtime keyword arguments directly to run() is deprecated; pass tool values via "
|
||||
"function_invocation_kwargs and client-specific values via client_kwargs instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if not stream:
|
||||
|
||||
async def _run_non_streaming() -> AgentResponse[Any]:
|
||||
@@ -915,7 +895,6 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
options=options,
|
||||
compaction_strategy=compaction_strategy,
|
||||
tokenizer=tokenizer,
|
||||
legacy_kwargs=kwargs,
|
||||
function_invocation_kwargs=function_invocation_kwargs,
|
||||
client_kwargs=client_kwargs,
|
||||
)
|
||||
@@ -1003,7 +982,6 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
options=options,
|
||||
compaction_strategy=compaction_strategy,
|
||||
tokenizer=tokenizer,
|
||||
legacy_kwargs=kwargs,
|
||||
function_invocation_kwargs=function_invocation_kwargs,
|
||||
client_kwargs=client_kwargs,
|
||||
)
|
||||
@@ -1103,7 +1081,6 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
options: Mapping[str, Any] | None,
|
||||
compaction_strategy: CompactionStrategy | None,
|
||||
tokenizer: TokenizerProtocol | None,
|
||||
legacy_kwargs: Mapping[str, Any],
|
||||
function_invocation_kwargs: Mapping[str, Any] | None,
|
||||
client_kwargs: Mapping[str, Any] | None,
|
||||
) -> _RunContext:
|
||||
@@ -1176,12 +1153,9 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
duplicate_error_message=mcp_duplicate_message,
|
||||
)
|
||||
|
||||
# TODO(Copilot): Delete once direct ``run(**kwargs)`` compatibility is removed.
|
||||
# Legacy compatibility still fans out direct run kwargs into tool runtime kwargs.
|
||||
effective_function_invocation_kwargs = {
|
||||
**dict(legacy_kwargs),
|
||||
**(dict(function_invocation_kwargs) if function_invocation_kwargs is not None else {}),
|
||||
}
|
||||
effective_function_invocation_kwargs = (
|
||||
dict(function_invocation_kwargs) if function_invocation_kwargs is not None else {}
|
||||
)
|
||||
additional_function_arguments = {**effective_function_invocation_kwargs, **existing_additional_args}
|
||||
|
||||
# Build options dict from run() options merged with provided options
|
||||
@@ -1214,12 +1188,7 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
# Build session_messages from session context: context messages + input messages
|
||||
session_messages: list[Message] = session_context.get_messages(include_input=True)
|
||||
|
||||
# TODO(Copilot): Delete once direct ``run(**kwargs)`` compatibility is removed.
|
||||
# Legacy compatibility still fans out direct run kwargs into client kwargs.
|
||||
effective_client_kwargs = {
|
||||
**dict(legacy_kwargs),
|
||||
**(dict(client_kwargs) if client_kwargs is not None else {}),
|
||||
}
|
||||
effective_client_kwargs = dict(client_kwargs) if client_kwargs is not None else {}
|
||||
if active_session is not None:
|
||||
effective_client_kwargs["session"] = active_session
|
||||
|
||||
@@ -1499,9 +1468,29 @@ class Agent(
|
||||
*,
|
||||
stream: Literal[False] = ...,
|
||||
session: AgentSession | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
|
||||
options: ChatOptions[ResponseModelBoundT],
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
) -> Awaitable[AgentResponse[ResponseModelBoundT]]: ...
|
||||
|
||||
@overload
|
||||
def run(
|
||||
self,
|
||||
messages: AgentRunInputs | None = None,
|
||||
*,
|
||||
stream: Literal[False] = ...,
|
||||
session: AgentSession | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
|
||||
options: OptionsCoT | ChatOptions[None] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[AgentResponse[Any]]: ...
|
||||
|
||||
@overload
|
||||
@@ -1511,9 +1500,13 @@ class Agent(
|
||||
*,
|
||||
stream: Literal[True],
|
||||
session: AgentSession | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
|
||||
options: OptionsCoT | ChatOptions[Any] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
|
||||
|
||||
def run(
|
||||
@@ -1523,10 +1516,12 @@ class Agent(
|
||||
stream: bool = False,
|
||||
session: AgentSession | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
|
||||
options: OptionsCoT | ChatOptions[Any] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
|
||||
"""Run the agent."""
|
||||
super_run = cast(
|
||||
@@ -1538,10 +1533,12 @@ class Agent(
|
||||
stream=stream,
|
||||
session=session,
|
||||
middleware=middleware,
|
||||
tools=tools,
|
||||
options=options,
|
||||
compaction_strategy=compaction_strategy,
|
||||
tokenizer=tokenizer,
|
||||
function_invocation_kwargs=function_invocation_kwargs,
|
||||
client_kwargs=client_kwargs,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@@ -1558,7 +1555,7 @@ class Agent(
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
**kwargs: Any,
|
||||
additional_properties: MutableMapping[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Initialize a Agent instance."""
|
||||
super().__init__(
|
||||
@@ -1573,7 +1570,7 @@ class Agent(
|
||||
middleware=middleware,
|
||||
compaction_strategy=compaction_strategy,
|
||||
tokenizer=tokenizer,
|
||||
**kwargs,
|
||||
additional_properties=additional_properties,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import warnings
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import (
|
||||
AsyncIterable,
|
||||
@@ -139,7 +138,8 @@ class SupportsChatGetResponse(Protocol[OptionsContraT]):
|
||||
options: ChatOptions[ResponseModelBoundT],
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
**kwargs: Any,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
) -> Awaitable[ChatResponse[ResponseModelBoundT]]: ...
|
||||
|
||||
@overload
|
||||
@@ -153,7 +153,6 @@ class SupportsChatGetResponse(Protocol[OptionsContraT]):
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[ChatResponse[Any]]: ...
|
||||
|
||||
@overload
|
||||
@@ -167,7 +166,6 @@ class SupportsChatGetResponse(Protocol[OptionsContraT]):
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ResponseStream[ChatResponseUpdate, ChatResponse[Any]]: ...
|
||||
|
||||
def get_response(
|
||||
@@ -180,7 +178,6 @@ class SupportsChatGetResponse(Protocol[OptionsContraT]):
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[ChatResponse[Any]] | ResponseStream[ChatResponseUpdate, ChatResponse[Any]]:
|
||||
"""Send input and return the response.
|
||||
|
||||
@@ -192,7 +189,6 @@ class SupportsChatGetResponse(Protocol[OptionsContraT]):
|
||||
tokenizer: Optional per-call tokenizer override.
|
||||
function_invocation_kwargs: Keyword arguments forwarded only to tool invocation layers.
|
||||
client_kwargs: Additional client-specific keyword arguments.
|
||||
**kwargs: Deprecated additional client-specific keyword arguments.
|
||||
|
||||
Returns:
|
||||
When stream=False: An awaitable ChatResponse from the client.
|
||||
@@ -296,7 +292,6 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]):
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize a BaseChatClient instance.
|
||||
|
||||
@@ -304,19 +299,10 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]):
|
||||
compaction_strategy: Optional compaction strategy to apply before model calls.
|
||||
tokenizer: Optional tokenizer used by token-aware compaction strategies.
|
||||
additional_properties: Additional properties for the client.
|
||||
kwargs: Additional keyword arguments (merged into additional_properties for now).
|
||||
"""
|
||||
self.additional_properties = additional_properties or {}
|
||||
self.compaction_strategy = compaction_strategy
|
||||
self.tokenizer = tokenizer
|
||||
if kwargs:
|
||||
warnings.warn(
|
||||
"Passing additional properties as direct keyword arguments to BaseChatClient is deprecated; "
|
||||
"pass them via additional_properties instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
self.additional_properties.update(kwargs)
|
||||
super().__init__()
|
||||
|
||||
def to_dict(self, *, exclude: set[str] | None = None, exclude_none: bool = True) -> dict[str, Any]:
|
||||
@@ -457,7 +443,8 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]):
|
||||
options: ChatOptions[ResponseModelBoundT],
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
**kwargs: Any,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
) -> Awaitable[ChatResponse[ResponseModelBoundT]]: ...
|
||||
|
||||
@overload
|
||||
@@ -469,7 +456,8 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]):
|
||||
options: OptionsCoT | ChatOptions[None] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
**kwargs: Any,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
) -> Awaitable[ChatResponse[Any]]: ...
|
||||
|
||||
@overload
|
||||
@@ -481,7 +469,8 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]):
|
||||
options: OptionsCoT | ChatOptions[Any] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
**kwargs: Any,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
) -> ResponseStream[ChatResponseUpdate, ChatResponse[Any]]: ...
|
||||
|
||||
def get_response(
|
||||
@@ -492,7 +481,8 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]):
|
||||
options: OptionsCoT | ChatOptions[Any] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
**kwargs: Any,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
) -> Awaitable[ChatResponse[Any]] | ResponseStream[ChatResponseUpdate, ChatResponse[Any]]:
|
||||
"""Get a response from a chat client.
|
||||
|
||||
@@ -504,13 +494,9 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]):
|
||||
When omitted, the client-level default is used.
|
||||
tokenizer: Optional per-call tokenizer override. When omitted, the
|
||||
client-level default is used.
|
||||
**kwargs: Additional compatibility keyword arguments. Lower chat-client layers do not
|
||||
consume ``function_invocation_kwargs`` directly; if present, it is ignored here
|
||||
because function invocation has already been handled by upper layers. If a
|
||||
``client_kwargs`` mapping is present, it is flattened into standard keyword
|
||||
arguments before forwarding to ``_inner_get_response()`` so client implementations
|
||||
can leverage those values, while implementations that ignore
|
||||
extra kwargs remain compatible.
|
||||
function_invocation_kwargs: Keyword arguments forwarded only to tool invocation layers.
|
||||
client_kwargs: Additional client-specific keyword arguments forwarded to
|
||||
``_inner_get_response()``.
|
||||
|
||||
Returns:
|
||||
When streaming a response stream of ChatResponseUpdates, otherwise an Awaitable ChatResponse.
|
||||
@@ -519,14 +505,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]):
|
||||
compaction_strategy=compaction_strategy,
|
||||
tokenizer=tokenizer,
|
||||
)
|
||||
compatibility_client_kwargs = kwargs.pop("client_kwargs", None)
|
||||
kwargs.pop("function_invocation_kwargs", None)
|
||||
merged_client_kwargs = (
|
||||
dict(cast(Mapping[str, Any], compatibility_client_kwargs))
|
||||
if isinstance(compatibility_client_kwargs, Mapping)
|
||||
else {}
|
||||
)
|
||||
merged_client_kwargs.update(kwargs)
|
||||
merged_client_kwargs = dict(client_kwargs) if client_kwargs is not None else {}
|
||||
|
||||
if not compaction_overrides:
|
||||
return self._inner_get_response(
|
||||
|
||||
@@ -768,7 +768,8 @@ class MCPTool:
|
||||
options["stop"] = params.stopSequences
|
||||
|
||||
try:
|
||||
response = await self.client.get_response(
|
||||
chat_client: Any = self.client
|
||||
response: Any = await chat_client.get_response(
|
||||
messages,
|
||||
options=options or None,
|
||||
)
|
||||
|
||||
@@ -39,7 +39,7 @@ if TYPE_CHECKING:
|
||||
from ._clients import SupportsChatGetResponse
|
||||
from ._compaction import CompactionStrategy, TokenizerProtocol
|
||||
from ._sessions import AgentSession
|
||||
from ._tools import FunctionTool
|
||||
from ._tools import FunctionTool, ToolTypes
|
||||
from ._types import ChatOptions, ChatResponse, ChatResponseUpdate
|
||||
|
||||
ResponseModelBoundT = TypeVar("ResponseModelBoundT", bound=BaseModel)
|
||||
@@ -100,6 +100,7 @@ class AgentContext:
|
||||
agent: The agent being invoked.
|
||||
messages: The messages being sent to the agent.
|
||||
session: The agent session for this invocation, if any.
|
||||
tools: Run-level tool overrides for this invocation, if any.
|
||||
options: The options for the agent invocation as a dict.
|
||||
stream: Whether this is a streaming invocation.
|
||||
compaction_strategy: Optional per-run compaction override.
|
||||
@@ -142,6 +143,7 @@ class AgentContext:
|
||||
agent: SupportsAgentRun,
|
||||
messages: list[Message],
|
||||
session: AgentSession | None = None,
|
||||
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
|
||||
options: Mapping[str, Any] | None = None,
|
||||
stream: bool = False,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
@@ -165,6 +167,7 @@ class AgentContext:
|
||||
agent: The agent being invoked.
|
||||
messages: The messages being sent to the agent.
|
||||
session: The agent session for this invocation, if any.
|
||||
tools: Run-level tool overrides for this invocation, if any.
|
||||
options: The options for the agent invocation as a dict.
|
||||
stream: Whether this is a streaming invocation.
|
||||
compaction_strategy: Optional per-run compaction override.
|
||||
@@ -181,6 +184,7 @@ class AgentContext:
|
||||
self.agent = agent
|
||||
self.messages = messages
|
||||
self.session = session
|
||||
self.tools = tools
|
||||
self.options = options
|
||||
self.stream = stream
|
||||
self.compaction_strategy = compaction_strategy
|
||||
@@ -1025,7 +1029,7 @@ class ChatMiddlewareLayer(Generic[OptionsCoT]):
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
) -> Awaitable[ChatResponse[ResponseModelBoundT]]: ...
|
||||
|
||||
@overload
|
||||
@@ -1039,7 +1043,6 @@ class ChatMiddlewareLayer(Generic[OptionsCoT]):
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[ChatResponse[Any]]: ...
|
||||
|
||||
@overload
|
||||
@@ -1053,7 +1056,6 @@ class ChatMiddlewareLayer(Generic[OptionsCoT]):
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ResponseStream[ChatResponseUpdate, ChatResponse[Any]]: ...
|
||||
|
||||
def get_response(
|
||||
@@ -1066,27 +1068,26 @@ class ChatMiddlewareLayer(Generic[OptionsCoT]):
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[ChatResponse[Any]] | ResponseStream[ChatResponseUpdate, ChatResponse[Any]]:
|
||||
"""Execute the chat pipeline if middleware is configured."""
|
||||
super_get_response = super().get_response # type: ignore[misc]
|
||||
|
||||
if compaction_strategy is not None:
|
||||
kwargs["compaction_strategy"] = compaction_strategy
|
||||
if tokenizer is not None:
|
||||
kwargs["tokenizer"] = tokenizer
|
||||
|
||||
effective_client_kwargs = dict(client_kwargs) if client_kwargs is not None else {}
|
||||
call_middleware = effective_client_kwargs.pop("middleware", [])
|
||||
context_kwargs = dict(effective_client_kwargs)
|
||||
if compaction_strategy is not None:
|
||||
context_kwargs["compaction_strategy"] = compaction_strategy
|
||||
if tokenizer is not None:
|
||||
context_kwargs["tokenizer"] = tokenizer
|
||||
pipeline = self._get_chat_middleware_pipeline(call_middleware) # type: ignore[reportUnknownArgumentType]
|
||||
if not pipeline.has_middlewares:
|
||||
return super_get_response( # type: ignore[no-any-return]
|
||||
messages=messages,
|
||||
stream=stream,
|
||||
options=options,
|
||||
compaction_strategy=compaction_strategy,
|
||||
tokenizer=tokenizer,
|
||||
function_invocation_kwargs=function_invocation_kwargs,
|
||||
client_kwargs=effective_client_kwargs,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
context = ChatContext(
|
||||
@@ -1094,7 +1095,7 @@ class ChatMiddlewareLayer(Generic[OptionsCoT]):
|
||||
messages=list(messages),
|
||||
options=options,
|
||||
stream=stream,
|
||||
kwargs={**effective_client_kwargs, **kwargs},
|
||||
kwargs=context_kwargs,
|
||||
function_invocation_kwargs=function_invocation_kwargs,
|
||||
)
|
||||
|
||||
@@ -1180,12 +1181,12 @@ class AgentMiddlewareLayer:
|
||||
stream: Literal[False] = ...,
|
||||
session: AgentSession | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
|
||||
options: ChatOptions[ResponseModelBoundT],
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[AgentResponse[ResponseModelBoundT]]: ...
|
||||
|
||||
@overload
|
||||
@@ -1196,12 +1197,12 @@ class AgentMiddlewareLayer:
|
||||
stream: Literal[False] = ...,
|
||||
session: AgentSession | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
|
||||
options: ChatOptions[None] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[AgentResponse[Any]]: ...
|
||||
|
||||
@overload
|
||||
@@ -1212,12 +1213,12 @@ class AgentMiddlewareLayer:
|
||||
stream: Literal[True],
|
||||
session: AgentSession | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
|
||||
options: ChatOptions[Any] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
|
||||
|
||||
def run(
|
||||
@@ -1227,12 +1228,12 @@ class AgentMiddlewareLayer:
|
||||
stream: bool = False,
|
||||
session: AgentSession | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
|
||||
options: ChatOptions[Any] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
|
||||
"""MiddlewareTypes-enabled unified run method."""
|
||||
# Re-categorize self.middleware at runtime to support dynamic changes
|
||||
@@ -1263,23 +1264,23 @@ class AgentMiddlewareLayer:
|
||||
messages,
|
||||
stream=stream,
|
||||
session=session,
|
||||
tools=tools,
|
||||
options=options,
|
||||
compaction_strategy=compaction_strategy,
|
||||
tokenizer=tokenizer,
|
||||
function_invocation_kwargs=effective_function_invocation_kwargs,
|
||||
client_kwargs=effective_client_kwargs,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
context = AgentContext(
|
||||
agent=self, # type: ignore[arg-type]
|
||||
messages=normalize_messages(messages),
|
||||
session=session,
|
||||
tools=tools,
|
||||
options=options,
|
||||
stream=stream,
|
||||
compaction_strategy=compaction_strategy,
|
||||
tokenizer=tokenizer,
|
||||
kwargs=kwargs,
|
||||
client_kwargs=effective_client_kwargs,
|
||||
function_invocation_kwargs=effective_function_invocation_kwargs,
|
||||
)
|
||||
@@ -1313,22 +1314,16 @@ class AgentMiddlewareLayer:
|
||||
def _middleware_handler(
|
||||
self, context: AgentContext
|
||||
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
|
||||
# TODO(Copilot): Delete once direct ``run(**kwargs)`` compatibility is removed.
|
||||
client_kwargs = {**context.client_kwargs, **context.kwargs}
|
||||
# TODO(Copilot): Delete once direct ``run(**kwargs)`` compatibility is removed.
|
||||
function_invocation_kwargs = {
|
||||
**context.function_invocation_kwargs,
|
||||
**{k: v for k, v in context.kwargs.items() if k != "middleware"},
|
||||
}
|
||||
return super().run( # type: ignore[misc, no-any-return]
|
||||
context.messages,
|
||||
stream=context.stream,
|
||||
session=context.session,
|
||||
tools=context.tools,
|
||||
options=context.options,
|
||||
compaction_strategy=context.compaction_strategy,
|
||||
tokenizer=context.tokenizer,
|
||||
function_invocation_kwargs=function_invocation_kwargs,
|
||||
client_kwargs=client_kwargs,
|
||||
function_invocation_kwargs=context.function_invocation_kwargs,
|
||||
client_kwargs=context.client_kwargs,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import json
|
||||
import logging
|
||||
import sys
|
||||
import typing
|
||||
import warnings
|
||||
from collections.abc import (
|
||||
AsyncIterable,
|
||||
Awaitable,
|
||||
@@ -344,8 +343,6 @@ class FunctionTool(SerializationMixin):
|
||||
self._instance = None # Store the instance for bound methods
|
||||
self._context_parameter_name: str | None = None
|
||||
self._input_model_explicitly_provided = input_model is not None
|
||||
# TODO(Copilot): Delete once legacy ``**kwargs`` runtime injection is removed.
|
||||
self._forward_runtime_kwargs: bool = False
|
||||
if self.func:
|
||||
self._discover_injected_parameters()
|
||||
|
||||
@@ -390,10 +387,6 @@ class FunctionTool(SerializationMixin):
|
||||
for name, param in signature.parameters.items():
|
||||
if name in {"self", "cls"}:
|
||||
continue
|
||||
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
||||
self._forward_runtime_kwargs = True
|
||||
continue
|
||||
|
||||
annotation = type_hints.get(name, param.annotation)
|
||||
if self._is_context_parameter(name, annotation):
|
||||
if self._context_parameter_name is not None:
|
||||
@@ -518,6 +511,7 @@ class FunctionTool(SerializationMixin):
|
||||
*,
|
||||
arguments: BaseModel | Mapping[str, Any] | None = None,
|
||||
context: FunctionInvocationContext | None = None,
|
||||
tool_call_id: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> list[Content]:
|
||||
"""Run the AI function with the provided arguments as a Pydantic model.
|
||||
@@ -530,7 +524,10 @@ class FunctionTool(SerializationMixin):
|
||||
Keyword Args:
|
||||
arguments: A mapping or model instance containing the arguments for the function.
|
||||
context: Explicit function invocation context carrying runtime kwargs.
|
||||
kwargs: Deprecated keyword arguments to pass to the function. Use ``context`` instead.
|
||||
tool_call_id: Optional tool call identifier used for telemetry and tracing.
|
||||
kwargs: Direct function argument values. When provided, every keyword
|
||||
must match a declared tool parameter. Runtime data must be passed
|
||||
via ``context``.
|
||||
|
||||
Returns:
|
||||
A list of Content items representing the tool output.
|
||||
@@ -552,18 +549,13 @@ class FunctionTool(SerializationMixin):
|
||||
{key: value for key, value in kwargs.items() if key in parameter_names} if arguments is None else {}
|
||||
)
|
||||
runtime_kwargs = dict(context.kwargs) if context is not None else {}
|
||||
deprecated_runtime_kwargs = {
|
||||
key: value for key, value in kwargs.items() if key not in direct_argument_kwargs and key != "tool_call_id"
|
||||
}
|
||||
if deprecated_runtime_kwargs:
|
||||
warnings.warn(
|
||||
"Passing runtime keyword arguments directly to FunctionTool.invoke() is deprecated; "
|
||||
"pass them via FunctionInvocationContext instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
unexpected_kwargs = {key: value for key, value in kwargs.items() if key not in direct_argument_kwargs}
|
||||
if unexpected_kwargs:
|
||||
unexpected_names = ", ".join(sorted(unexpected_kwargs))
|
||||
raise TypeError(
|
||||
f"Unexpected keyword argument(s) for tool '{self.name}': {unexpected_names}. "
|
||||
"Pass runtime data via FunctionInvocationContext instead."
|
||||
)
|
||||
runtime_kwargs.update(deprecated_runtime_kwargs)
|
||||
tool_call_id = kwargs.get("tool_call_id", runtime_kwargs.pop("tool_call_id", None))
|
||||
if arguments is None and direct_argument_kwargs:
|
||||
arguments = direct_argument_kwargs
|
||||
if arguments is None and context is not None:
|
||||
@@ -614,17 +606,6 @@ class FunctionTool(SerializationMixin):
|
||||
|
||||
call_kwargs = dict(validated_arguments)
|
||||
observable_kwargs = dict(validated_arguments)
|
||||
|
||||
# Legacy runtime kwargs injection path retained for backwards compatibility with tools
|
||||
# that still declare ``**kwargs``. New tools should consume runtime data via ``ctx``.
|
||||
legacy_runtime_kwargs = dict(runtime_kwargs)
|
||||
if self._forward_runtime_kwargs and legacy_runtime_kwargs:
|
||||
for key, value in legacy_runtime_kwargs.items():
|
||||
if key not in call_kwargs:
|
||||
call_kwargs[key] = value
|
||||
if key not in observable_kwargs:
|
||||
observable_kwargs[key] = value
|
||||
|
||||
if self._context_parameter_name is not None and effective_context is not None:
|
||||
call_kwargs[self._context_parameter_name] = effective_context
|
||||
|
||||
@@ -1420,7 +1401,7 @@ async def _auto_invoke_function(
|
||||
# No middleware - execute directly
|
||||
try:
|
||||
direct_context = None
|
||||
if getattr(tool, "_forward_runtime_kwargs", False) or getattr(tool, "_context_parameter_name", None):
|
||||
if getattr(tool, "_context_parameter_name", None):
|
||||
direct_context = FunctionInvocationContext(
|
||||
function=tool,
|
||||
arguments=args,
|
||||
@@ -2078,7 +2059,6 @@ class FunctionInvocationLayer(Generic[OptionsCoT]):
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[ChatResponse[ResponseModelBoundT]]: ...
|
||||
|
||||
@overload
|
||||
@@ -2093,7 +2073,6 @@ class FunctionInvocationLayer(Generic[OptionsCoT]):
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[ChatResponse[Any]]: ...
|
||||
|
||||
@overload
|
||||
@@ -2108,7 +2087,6 @@ class FunctionInvocationLayer(Generic[OptionsCoT]):
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ResponseStream[ChatResponseUpdate, ChatResponse[Any]]: ...
|
||||
|
||||
def get_response(
|
||||
@@ -2122,7 +2100,6 @@ class FunctionInvocationLayer(Generic[OptionsCoT]):
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[ChatResponse[Any]] | ResponseStream[ChatResponseUpdate, ChatResponse[Any]]:
|
||||
from ._middleware import categorize_middleware
|
||||
from ._types import (
|
||||
@@ -2133,14 +2110,6 @@ class FunctionInvocationLayer(Generic[OptionsCoT]):
|
||||
)
|
||||
|
||||
super_get_response = super().get_response # type: ignore[misc]
|
||||
if kwargs:
|
||||
warnings.warn(
|
||||
"Passing client-specific keyword arguments directly to get_response() is deprecated; "
|
||||
"pass them via client_kwargs instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
effective_client_kwargs = dict(client_kwargs) if client_kwargs is not None else {}
|
||||
if middleware is not None:
|
||||
existing = effective_client_kwargs.get("middleware", [])
|
||||
@@ -2176,19 +2145,23 @@ class FunctionInvocationLayer(Generic[OptionsCoT]):
|
||||
invocation_session=invocation_session,
|
||||
middleware_pipeline=function_middleware_pipeline,
|
||||
)
|
||||
filtered_kwargs = {k: v for k, v in {**effective_client_kwargs, **kwargs}.items() if k != "session"}
|
||||
filtered_kwargs = {k: v for k, v in effective_client_kwargs.items() if k != "session"}
|
||||
|
||||
# Make options mutable so we can update conversation_id during function invocation loop
|
||||
mutable_options: dict[str, Any] = dict(options) if options else {}
|
||||
# Remove additional_function_arguments from options passed to underlying chat client
|
||||
# It's for tool invocation only and not recognized by chat service APIs
|
||||
mutable_options.pop("additional_function_arguments", None)
|
||||
# Support tools passed via kwargs in direct client.get_response(...) calls.
|
||||
if "tools" in filtered_kwargs:
|
||||
if mutable_options.get("tools") is None:
|
||||
mutable_options["tools"] = filtered_kwargs["tools"]
|
||||
filtered_kwargs.pop("tools", None)
|
||||
|
||||
if not self.function_invocation_configuration.get("enabled", True):
|
||||
return super_get_response( # type: ignore[no-any-return]
|
||||
messages=messages,
|
||||
stream=stream,
|
||||
options=mutable_options,
|
||||
compaction_strategy=compaction_strategy,
|
||||
tokenizer=tokenizer,
|
||||
function_invocation_kwargs=function_invocation_kwargs,
|
||||
client_kwargs=filtered_kwargs,
|
||||
)
|
||||
if not stream:
|
||||
|
||||
async def _get_response() -> ChatResponse[Any]:
|
||||
@@ -2235,7 +2208,7 @@ class FunctionInvocationLayer(Generic[OptionsCoT]):
|
||||
aggregated_usage = add_usage_details(aggregated_usage, response.usage_details)
|
||||
|
||||
if response.conversation_id is not None:
|
||||
_update_conversation_id(kwargs, response.conversation_id, mutable_options)
|
||||
_update_conversation_id(filtered_kwargs, response.conversation_id, mutable_options)
|
||||
prepped_messages = []
|
||||
|
||||
result = await _process_function_requests(
|
||||
@@ -2379,7 +2352,7 @@ class FunctionInvocationLayer(Generic[OptionsCoT]):
|
||||
return
|
||||
|
||||
if response.conversation_id is not None:
|
||||
_update_conversation_id(kwargs, response.conversation_id, mutable_options)
|
||||
_update_conversation_id(filtered_kwargs, response.conversation_id, mutable_options)
|
||||
prepped_messages = []
|
||||
|
||||
result = await _process_function_requests(
|
||||
|
||||
@@ -12,7 +12,7 @@ from agent_framework import Content
|
||||
|
||||
from .._agents import SupportsAgentRun
|
||||
from .._sessions import AgentSession
|
||||
from .._types import AgentResponse, AgentResponseUpdate, Message
|
||||
from .._types import AgentResponse, AgentResponseUpdate, Message, ResponseStream
|
||||
from ._agent_utils import resolve_agent_id
|
||||
from ._const import WORKFLOW_RUN_KWARGS_KEY
|
||||
from ._executor import Executor, handler
|
||||
@@ -352,7 +352,8 @@ class AgentExecutor(Executor):
|
||||
"""
|
||||
run_kwargs, options = self._prepare_agent_run_args(ctx.get_state(WORKFLOW_RUN_KWARGS_KEY, {}))
|
||||
|
||||
response = await self._agent.run(
|
||||
run_agent = cast(Callable[..., Awaitable[AgentResponse[Any]]], self._agent.run)
|
||||
response = await run_agent(
|
||||
self._cache,
|
||||
stream=False,
|
||||
session=self._session,
|
||||
@@ -383,7 +384,8 @@ class AgentExecutor(Executor):
|
||||
|
||||
updates: list[AgentResponseUpdate] = []
|
||||
streamed_user_input_requests: list[Content] = []
|
||||
stream = self._agent.run(
|
||||
run_agent_stream = cast(Callable[..., ResponseStream[AgentResponseUpdate, AgentResponse[Any]]], self._agent.run)
|
||||
stream = run_agent_stream(
|
||||
self._cache,
|
||||
stream=True,
|
||||
session=self._session,
|
||||
|
||||
@@ -49,8 +49,9 @@ if TYPE_CHECKING: # pragma: no cover
|
||||
from ._agents import SupportsAgentRun
|
||||
from ._clients import SupportsChatGetResponse
|
||||
from ._compaction import CompactionStrategy, TokenizerProtocol
|
||||
from ._middleware import MiddlewareTypes
|
||||
from ._sessions import AgentSession
|
||||
from ._tools import FunctionTool
|
||||
from ._tools import FunctionTool, ToolTypes
|
||||
from ._types import (
|
||||
AgentResponse,
|
||||
AgentResponseUpdate,
|
||||
@@ -1191,7 +1192,8 @@ class ChatTelemetryLayer(Generic[OptionsCoT]):
|
||||
options: ChatOptions[ResponseModelBoundT],
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
**kwargs: Any,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
) -> Awaitable[ChatResponse[ResponseModelBoundT]]: ...
|
||||
|
||||
@overload
|
||||
@@ -1203,7 +1205,8 @@ class ChatTelemetryLayer(Generic[OptionsCoT]):
|
||||
options: OptionsCoT | ChatOptions[None] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
**kwargs: Any,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
) -> Awaitable[ChatResponse[Any]]: ...
|
||||
|
||||
@overload
|
||||
@@ -1215,7 +1218,8 @@ class ChatTelemetryLayer(Generic[OptionsCoT]):
|
||||
options: OptionsCoT | ChatOptions[Any] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
**kwargs: Any,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
) -> ResponseStream[ChatResponseUpdate, ChatResponse[Any]]: ...
|
||||
|
||||
def get_response(
|
||||
@@ -1226,7 +1230,8 @@ class ChatTelemetryLayer(Generic[OptionsCoT]):
|
||||
options: OptionsCoT | ChatOptions[Any] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
**kwargs: Any,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
) -> Awaitable[ChatResponse[Any]] | ResponseStream[ChatResponseUpdate, ChatResponse[Any]]:
|
||||
"""Trace chat responses with OpenTelemetry spans and metrics.
|
||||
|
||||
@@ -1238,25 +1243,14 @@ class ChatTelemetryLayer(Generic[OptionsCoT]):
|
||||
tokenizer: Optional tokenizer used by token-aware compaction strategies.
|
||||
|
||||
Keyword Args:
|
||||
kwargs: Compatibility keyword arguments from higher client layers. This layer does
|
||||
not consume ``function_invocation_kwargs`` directly; if present, it is ignored
|
||||
because function invocation has already been processed above. If a ``client_kwargs``
|
||||
mapping is present, it is flattened into ordinary keyword arguments for tracing and
|
||||
forwarding so clients that use those values continue to work while clients that
|
||||
ignore extra kwargs remain compatible.
|
||||
function_invocation_kwargs: Keyword arguments forwarded only to tool invocation layers.
|
||||
client_kwargs: Additional client-specific keyword arguments for downstream chat clients.
|
||||
"""
|
||||
from ._types import ChatResponse, ChatResponseUpdate, ResponseStream # type: ignore[reportUnusedImport]
|
||||
|
||||
global OBSERVABILITY_SETTINGS
|
||||
super_get_response = super().get_response # type: ignore[misc]
|
||||
compatibility_client_kwargs = kwargs.pop("client_kwargs", None)
|
||||
kwargs.pop("function_invocation_kwargs", None)
|
||||
merged_client_kwargs = (
|
||||
dict(cast(Mapping[str, Any], compatibility_client_kwargs))
|
||||
if isinstance(compatibility_client_kwargs, Mapping)
|
||||
else {}
|
||||
)
|
||||
merged_client_kwargs.update(kwargs)
|
||||
merged_client_kwargs = dict(client_kwargs) if client_kwargs is not None else {}
|
||||
|
||||
if not OBSERVABILITY_SETTINGS.ENABLED:
|
||||
return super_get_response( # type: ignore[no-any-return]
|
||||
@@ -1265,7 +1259,8 @@ class ChatTelemetryLayer(Generic[OptionsCoT]):
|
||||
options=options,
|
||||
compaction_strategy=compaction_strategy,
|
||||
tokenizer=tokenizer,
|
||||
**merged_client_kwargs,
|
||||
function_invocation_kwargs=function_invocation_kwargs,
|
||||
client_kwargs=merged_client_kwargs,
|
||||
)
|
||||
|
||||
opts: dict[str, Any] = options or {} # type: ignore[assignment]
|
||||
@@ -1292,7 +1287,8 @@ class ChatTelemetryLayer(Generic[OptionsCoT]):
|
||||
options=opts,
|
||||
compaction_strategy=compaction_strategy,
|
||||
tokenizer=tokenizer,
|
||||
**merged_client_kwargs,
|
||||
function_invocation_kwargs=function_invocation_kwargs,
|
||||
client_kwargs=merged_client_kwargs,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1384,7 +1380,8 @@ class ChatTelemetryLayer(Generic[OptionsCoT]):
|
||||
options=opts,
|
||||
compaction_strategy=compaction_strategy,
|
||||
tokenizer=tokenizer,
|
||||
**merged_client_kwargs,
|
||||
function_invocation_kwargs=function_invocation_kwargs,
|
||||
client_kwargs=merged_client_kwargs,
|
||||
),
|
||||
)
|
||||
except Exception as exception:
|
||||
@@ -1512,11 +1509,29 @@ class AgentTelemetryLayer:
|
||||
*,
|
||||
stream: Literal[False] = ...,
|
||||
session: AgentSession | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
|
||||
options: ChatOptions[ResponseModelBoundT],
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
) -> Awaitable[AgentResponse[ResponseModelBoundT]]: ...
|
||||
|
||||
@overload
|
||||
def run(
|
||||
self,
|
||||
messages: AgentRunInputs | None = None,
|
||||
*,
|
||||
stream: Literal[False] = ...,
|
||||
session: AgentSession | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
|
||||
options: ChatOptions[None] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[AgentResponse[Any]]: ...
|
||||
|
||||
@overload
|
||||
@@ -1526,11 +1541,13 @@ class AgentTelemetryLayer:
|
||||
*,
|
||||
stream: Literal[True],
|
||||
session: AgentSession | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
|
||||
options: ChatOptions[Any] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
|
||||
|
||||
def run(
|
||||
@@ -1539,11 +1556,13 @@ class AgentTelemetryLayer:
|
||||
*,
|
||||
stream: bool = False,
|
||||
session: AgentSession | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
|
||||
options: ChatOptions[Any] | None = None,
|
||||
compaction_strategy: CompactionStrategy | None = None,
|
||||
tokenizer: TokenizerProtocol | None = None,
|
||||
function_invocation_kwargs: Mapping[str, Any] | None = None,
|
||||
client_kwargs: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
|
||||
"""Trace agent runs with OpenTelemetry spans and metrics."""
|
||||
global OBSERVABILITY_SETTINGS
|
||||
@@ -1554,23 +1573,27 @@ class AgentTelemetryLayer:
|
||||
super().run, # type: ignore[misc]
|
||||
)
|
||||
provider_name = str(self.otel_provider_name)
|
||||
super_run_kwargs: dict[str, Any] = {
|
||||
"messages": messages,
|
||||
"stream": stream,
|
||||
"session": session,
|
||||
"tools": tools,
|
||||
"options": options,
|
||||
"compaction_strategy": compaction_strategy,
|
||||
"tokenizer": tokenizer,
|
||||
"function_invocation_kwargs": function_invocation_kwargs,
|
||||
"client_kwargs": client_kwargs,
|
||||
}
|
||||
if middleware is not None:
|
||||
super_run_kwargs["middleware"] = middleware
|
||||
if not OBSERVABILITY_SETTINGS.ENABLED:
|
||||
return super_run( # type: ignore[no-any-return]
|
||||
messages=messages,
|
||||
stream=stream,
|
||||
session=session,
|
||||
compaction_strategy=compaction_strategy,
|
||||
tokenizer=tokenizer,
|
||||
function_invocation_kwargs=function_invocation_kwargs,
|
||||
client_kwargs=client_kwargs,
|
||||
**kwargs,
|
||||
)
|
||||
return super_run(**super_run_kwargs) # type: ignore[no-any-return]
|
||||
|
||||
default_options = getattr(self, "default_options", {})
|
||||
options = kwargs.get("options")
|
||||
default_options = dict(getattr(self, "default_options", {}))
|
||||
merged_client_kwargs = dict(client_kwargs) if client_kwargs is not None else {}
|
||||
merged_client_kwargs.update(kwargs)
|
||||
merged_options: dict[str, Any] = merge_chat_options(default_options, options or {})
|
||||
merged_options: dict[str, Any] = merge_chat_options(
|
||||
default_options, dict(options) if options is not None else {}
|
||||
)
|
||||
attributes = _get_span_attributes(
|
||||
operation_name=OtelAttr.AGENT_INVOKE_OPERATION,
|
||||
provider_name=provider_name,
|
||||
@@ -1590,16 +1613,7 @@ class AgentTelemetryLayer:
|
||||
|
||||
if stream:
|
||||
try:
|
||||
run_result: object = super_run(
|
||||
messages=messages,
|
||||
stream=True,
|
||||
session=session,
|
||||
compaction_strategy=compaction_strategy,
|
||||
tokenizer=tokenizer,
|
||||
function_invocation_kwargs=function_invocation_kwargs,
|
||||
client_kwargs=client_kwargs,
|
||||
**kwargs,
|
||||
)
|
||||
run_result: object = super_run(**super_run_kwargs)
|
||||
if isinstance(run_result, ResponseStream):
|
||||
result_stream: ResponseStream[AgentResponseUpdate, AgentResponse[Any]] = run_result # pyright: ignore[reportUnknownVariableType]
|
||||
elif isinstance(run_result, Awaitable):
|
||||
@@ -1693,16 +1707,7 @@ class AgentTelemetryLayer:
|
||||
)
|
||||
start_time_stamp = perf_counter()
|
||||
try:
|
||||
response: AgentResponse[Any] = await super_run(
|
||||
messages=messages,
|
||||
stream=False,
|
||||
session=session,
|
||||
compaction_strategy=compaction_strategy,
|
||||
tokenizer=tokenizer,
|
||||
function_invocation_kwargs=function_invocation_kwargs,
|
||||
client_kwargs=client_kwargs,
|
||||
**kwargs,
|
||||
)
|
||||
response: AgentResponse[Any] = await super_run(**super_run_kwargs)
|
||||
except Exception as exception:
|
||||
capture_exception(span=span, exception=exception, timestamp=time_ns())
|
||||
raise
|
||||
|
||||
@@ -148,11 +148,9 @@ async def test_chat_client_agent_init_with_name(
|
||||
assert agent.description == "Test"
|
||||
|
||||
|
||||
def test_agent_init_warns_for_direct_additional_properties(client: SupportsChatGetResponse) -> None:
|
||||
with pytest.warns(DeprecationWarning, match="additional_properties"):
|
||||
agent = Agent(client=client, legacy_key="legacy-value")
|
||||
|
||||
assert agent.additional_properties["legacy_key"] == "legacy-value"
|
||||
def test_agent_init_rejects_direct_additional_properties(client: SupportsChatGetResponse) -> None:
|
||||
with pytest.raises(TypeError):
|
||||
Agent(client=client, legacy_key="legacy-value")
|
||||
|
||||
|
||||
async def test_chat_client_agent_run(client: SupportsChatGetResponse) -> None:
|
||||
@@ -303,7 +301,6 @@ async def test_prepare_run_context_handles_function_kwargs(
|
||||
},
|
||||
compaction_strategy=None,
|
||||
tokenizer=None,
|
||||
legacy_kwargs={"legacy_key": "legacy-value"},
|
||||
function_invocation_kwargs={"runtime_key": "runtime-value"},
|
||||
client_kwargs={"client_key": "client-value"},
|
||||
)
|
||||
@@ -311,7 +308,6 @@ async def test_prepare_run_context_handles_function_kwargs(
|
||||
assert ctx["chat_options"]["temperature"] == 0.4
|
||||
assert "additional_function_arguments" not in ctx["chat_options"]
|
||||
assert ctx["function_invocation_kwargs"]["from_options"] == "options-value"
|
||||
assert ctx["function_invocation_kwargs"]["legacy_key"] == "legacy-value"
|
||||
assert ctx["function_invocation_kwargs"]["runtime_key"] == "runtime-value"
|
||||
assert "session" not in ctx["function_invocation_kwargs"]
|
||||
assert ctx["client_kwargs"]["client_key"] == "client-value"
|
||||
@@ -1181,8 +1177,8 @@ async def test_agent_run_accepts_prefixed_mcp_tools(chat_client_base: Any) -> No
|
||||
assert tool_names == ["search", "docs_search"]
|
||||
|
||||
|
||||
async def test_agent_tool_receives_session_in_kwargs(chat_client_base: Any) -> None:
|
||||
"""Verify legacy **kwargs tools receive the session when agent.run() is called with one."""
|
||||
async def test_agent_tool_without_context_does_not_receive_session(chat_client_base: Any) -> None:
|
||||
"""Verify tools without FunctionInvocationContext no longer receive injected session kwargs."""
|
||||
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
@@ -1215,8 +1211,8 @@ async def test_agent_tool_receives_session_in_kwargs(chat_client_base: Any) -> N
|
||||
result = await agent.run("hello", session=session)
|
||||
|
||||
assert result.text == "done"
|
||||
assert captured.get("has_session") is True
|
||||
assert captured.get("has_state") is True
|
||||
assert captured.get("has_session") is False
|
||||
assert captured.get("has_state") is False
|
||||
|
||||
|
||||
async def test_agent_tool_receives_explicit_session_via_function_invocation_context_kwargs(
|
||||
@@ -1278,7 +1274,7 @@ async def test_chat_agent_tool_choice_run_level_overrides_agent_level(chat_clien
|
||||
agent = Agent(
|
||||
client=chat_client_base,
|
||||
tools=[tool_tool],
|
||||
options={"tool_choice": "auto"},
|
||||
default_options={"tool_choice": "auto"},
|
||||
)
|
||||
|
||||
# Run with run-level tool_choice="required"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
|
||||
import inspect
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -15,11 +14,6 @@ from agent_framework import (
|
||||
Message,
|
||||
SlidingWindowStrategy,
|
||||
SupportsChatGetResponse,
|
||||
SupportsCodeInterpreterTool,
|
||||
SupportsFileSearchTool,
|
||||
SupportsImageGenerationTool,
|
||||
SupportsMCPTool,
|
||||
SupportsWebSearchTool,
|
||||
TruncationStrategy,
|
||||
)
|
||||
|
||||
@@ -53,11 +47,9 @@ def test_base_client(chat_client_base: SupportsChatGetResponse):
|
||||
assert isinstance(chat_client_base, SupportsChatGetResponse)
|
||||
|
||||
|
||||
def test_base_client_warns_for_direct_additional_properties(chat_client_base: SupportsChatGetResponse) -> None:
|
||||
with pytest.warns(DeprecationWarning, match="additional_properties"):
|
||||
client = type(chat_client_base)(legacy_key="legacy-value")
|
||||
|
||||
assert client.additional_properties["legacy_key"] == "legacy-value"
|
||||
def test_base_client_rejects_direct_additional_properties(chat_client_base: SupportsChatGetResponse) -> None:
|
||||
with pytest.raises(TypeError):
|
||||
type(chat_client_base)(legacy_key="legacy-value")
|
||||
|
||||
|
||||
def test_base_client_as_agent_uses_explicit_additional_properties(chat_client_base: SupportsChatGetResponse) -> None:
|
||||
@@ -66,27 +58,6 @@ def test_base_client_as_agent_uses_explicit_additional_properties(chat_client_ba
|
||||
assert agent.additional_properties == {"team": "core"}
|
||||
|
||||
|
||||
def test_openai_chat_completion_client_get_response_docstring_surfaces_layered_runtime_docs() -> None:
|
||||
from agent_framework.openai import OpenAIChatCompletionClient
|
||||
|
||||
docstring = inspect.getdoc(OpenAIChatCompletionClient.get_response)
|
||||
|
||||
assert docstring is not None
|
||||
assert "Get a response from a chat client." in docstring
|
||||
assert "function_invocation_kwargs" in docstring
|
||||
assert "middleware: Optional per-call chat and function middleware." in docstring
|
||||
assert "function_middleware: Optional per-call function middleware." not in docstring
|
||||
|
||||
|
||||
def test_openai_chat_completion_client_get_response_is_defined_on_openai_class() -> None:
|
||||
from agent_framework.openai import OpenAIChatCompletionClient
|
||||
|
||||
signature = inspect.signature(OpenAIChatCompletionClient.get_response)
|
||||
|
||||
assert OpenAIChatCompletionClient.get_response.__qualname__ == "OpenAIChatCompletionClient.get_response"
|
||||
assert "middleware" in signature.parameters
|
||||
|
||||
|
||||
async def test_base_client_get_response_uses_explicit_client_kwargs(chat_client_base: SupportsChatGetResponse) -> None:
|
||||
async def fake_inner_get_response(**kwargs):
|
||||
assert kwargs["trace_id"] == "trace-123"
|
||||
@@ -333,66 +304,3 @@ async def test_chat_client_instructions_handling(chat_client_base: SupportsChatG
|
||||
assert appended_messages[0].text == "You are a helpful assistant."
|
||||
assert appended_messages[1].role == "user"
|
||||
assert appended_messages[1].text == "hello"
|
||||
|
||||
|
||||
# region Tool Support Protocol Tests
|
||||
|
||||
|
||||
def test_openai_responses_client_supports_all_tool_protocols():
|
||||
"""Test that OpenAIResponsesClient supports all hosted tool protocols."""
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
|
||||
assert isinstance(OpenAIResponsesClient, SupportsCodeInterpreterTool)
|
||||
assert isinstance(OpenAIResponsesClient, SupportsWebSearchTool)
|
||||
assert isinstance(OpenAIResponsesClient, SupportsImageGenerationTool)
|
||||
assert isinstance(OpenAIResponsesClient, SupportsMCPTool)
|
||||
assert isinstance(OpenAIResponsesClient, SupportsFileSearchTool)
|
||||
|
||||
|
||||
def test_openai_chat_completion_client_supports_web_search_only():
|
||||
"""Test that OpenAIChatClient only supports web search tool."""
|
||||
from agent_framework.openai import OpenAIChatCompletionClient
|
||||
|
||||
assert not isinstance(OpenAIChatCompletionClient, SupportsCodeInterpreterTool)
|
||||
assert isinstance(OpenAIChatCompletionClient, SupportsWebSearchTool)
|
||||
assert not isinstance(OpenAIChatCompletionClient, SupportsImageGenerationTool)
|
||||
assert not isinstance(OpenAIChatCompletionClient, SupportsMCPTool)
|
||||
assert not isinstance(OpenAIChatCompletionClient, SupportsFileSearchTool)
|
||||
|
||||
|
||||
def test_openai_assistants_client_supports_code_interpreter_and_file_search():
|
||||
"""Test that OpenAIAssistantsClient supports code interpreter and file search."""
|
||||
from agent_framework.openai import OpenAIAssistantsClient
|
||||
|
||||
assert isinstance(OpenAIAssistantsClient, SupportsCodeInterpreterTool)
|
||||
assert not isinstance(OpenAIAssistantsClient, SupportsWebSearchTool)
|
||||
assert not isinstance(OpenAIAssistantsClient, SupportsImageGenerationTool)
|
||||
assert not isinstance(OpenAIAssistantsClient, SupportsMCPTool)
|
||||
assert isinstance(OpenAIAssistantsClient, SupportsFileSearchTool)
|
||||
|
||||
|
||||
def test_protocol_isinstance_with_client_instance():
|
||||
"""Test that protocol isinstance works with client instances."""
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
|
||||
# Create mock client instance (won't connect to API)
|
||||
client = OpenAIResponsesClient.__new__(OpenAIResponsesClient)
|
||||
|
||||
assert isinstance(client, SupportsCodeInterpreterTool)
|
||||
assert isinstance(client, SupportsWebSearchTool)
|
||||
|
||||
|
||||
def test_protocol_tool_methods_return_dict():
|
||||
"""Test that static tool methods return dict[str, Any]."""
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
|
||||
code_tool = OpenAIResponsesClient.get_code_interpreter_tool()
|
||||
assert isinstance(code_tool, dict)
|
||||
assert code_tool.get("type") == "code_interpreter"
|
||||
|
||||
web_tool = OpenAIResponsesClient.get_web_search_tool()
|
||||
assert isinstance(web_tool, dict)
|
||||
assert web_tool.get("type") == "web_search"
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
@@ -13,6 +13,7 @@ from agent_framework import (
|
||||
Content,
|
||||
Message,
|
||||
SupportsChatGetResponse,
|
||||
chat_middleware,
|
||||
tool,
|
||||
)
|
||||
from agent_framework._compaction import (
|
||||
@@ -74,7 +75,7 @@ async def test_base_client_with_function_calling(chat_client_base: SupportsChatG
|
||||
assert response.messages[2].text == "done"
|
||||
|
||||
|
||||
async def test_base_client_with_function_calling_tools_in_kwargs(chat_client_base: SupportsChatGetResponse):
|
||||
async def test_base_client_with_function_calling_string_input(chat_client_base: SupportsChatGetResponse):
|
||||
exec_counter = 0
|
||||
|
||||
@tool(name="test_function", approval_mode="never_require")
|
||||
@@ -95,7 +96,7 @@ async def test_base_client_with_function_calling_tools_in_kwargs(chat_client_bas
|
||||
ChatResponse(messages=Message(role="assistant", text="done")),
|
||||
]
|
||||
|
||||
response = await chat_client_base.get_response("hello", tools=[ai_func])
|
||||
response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [ai_func]})
|
||||
|
||||
assert exec_counter == 1
|
||||
assert len(response.messages) == 3
|
||||
@@ -1429,6 +1430,36 @@ async def test_function_invocation_config_enabled_false(chat_client_base: Suppor
|
||||
assert len(response.messages) > 0
|
||||
|
||||
|
||||
async def test_function_invocation_config_enabled_false_preserves_invocation_kwargs(
|
||||
chat_client_base: SupportsChatGetResponse,
|
||||
):
|
||||
"""Test disabled function invocation still forwards invocation kwargs downstream."""
|
||||
captured_kwargs: dict[str, Any] = {}
|
||||
|
||||
@tool(name="test_function")
|
||||
def ai_func(arg1: str) -> str:
|
||||
return f"Processed {arg1}"
|
||||
|
||||
@chat_middleware
|
||||
async def capture_middleware(context, call_next):
|
||||
captured_kwargs.update(context.function_invocation_kwargs or {})
|
||||
await call_next()
|
||||
|
||||
chat_client_base.chat_middleware = [capture_middleware]
|
||||
chat_client_base.run_responses = [
|
||||
ChatResponse(messages=Message(role="assistant", text="response without function calling")),
|
||||
]
|
||||
chat_client_base.function_invocation_configuration["enabled"] = False
|
||||
|
||||
await chat_client_base.get_response(
|
||||
[Message(role="user", text="hello")],
|
||||
options={"tool_choice": "auto", "tools": [ai_func]},
|
||||
function_invocation_kwargs={"tool_request_id": "tool-123"},
|
||||
)
|
||||
|
||||
assert captured_kwargs == {"tool_request_id": "tool-123"}
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Error handling and failsafe behavior needs investigation in unified API")
|
||||
async def test_function_invocation_config_max_consecutive_errors(chat_client_base: SupportsChatGetResponse):
|
||||
"""Test that max_consecutive_errors_per_request limits error retries."""
|
||||
@@ -1523,7 +1554,7 @@ async def test_function_invocation_stop_clears_conversation_id_non_stream(chat_c
|
||||
response = await chat_client_base.get_response(
|
||||
[Message(role="user", text="hello")],
|
||||
options={"tool_choice": "auto", "tools": [error_func]},
|
||||
session=session_stub,
|
||||
client_kwargs={"session": session_stub},
|
||||
)
|
||||
|
||||
assert response.conversation_id is None
|
||||
@@ -1881,8 +1912,7 @@ async def test_hosted_tool_approval_response(chat_client_base: SupportsChatGetRe
|
||||
# Send the approval response
|
||||
response = await chat_client_base.get_response(
|
||||
[Message(role="user", contents=[approval_response])],
|
||||
tool_choice="auto",
|
||||
tools=[local_func],
|
||||
options={"tool_choice": "auto", "tools": [local_func]},
|
||||
)
|
||||
|
||||
# The hosted tool approval should be returned as-is (not executed)
|
||||
@@ -1930,8 +1960,7 @@ async def test_hosted_mcp_approval_response_passthrough(chat_client_base: Suppor
|
||||
|
||||
response = await chat_client_base.get_response(
|
||||
messages,
|
||||
tool_choice="auto",
|
||||
tools=[local_func],
|
||||
options={"tool_choice": "auto", "tools": [local_func]},
|
||||
)
|
||||
|
||||
# The response should succeed without errors
|
||||
@@ -2024,8 +2053,7 @@ async def test_mixed_local_and_hosted_approval_flow(chat_client_base: SupportsCh
|
||||
|
||||
response = await chat_client_base.get_response(
|
||||
messages,
|
||||
tool_choice="auto",
|
||||
tools=[local_func],
|
||||
options={"tool_choice": "auto", "tools": [local_func]},
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
@@ -2799,7 +2827,7 @@ async def test_streaming_function_invocation_stop_clears_conversation_id(chat_cl
|
||||
"hello",
|
||||
options={"tool_choice": "auto", "tools": [error_func]},
|
||||
stream=True,
|
||||
session=session_stub,
|
||||
client_kwargs={"session": session_stub},
|
||||
)
|
||||
async for _ in stream:
|
||||
pass
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Tests for kwargs propagation from get_response() to @tool functions."""
|
||||
|
||||
from collections.abc import AsyncIterable, Awaitable, MutableSequence, Sequence
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import (
|
||||
Agent,
|
||||
BaseChatClient,
|
||||
ChatMiddlewareLayer,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
FunctionInvocationContext,
|
||||
FunctionInvocationLayer,
|
||||
Message,
|
||||
ResponseStream,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.observability import ChatTelemetryLayer
|
||||
|
||||
|
||||
class _MockBaseChatClient(BaseChatClient[Any]):
|
||||
"""Mock chat client for testing function invocation."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.run_responses: list[ChatResponse] = []
|
||||
self.streaming_responses: list[list[ChatResponseUpdate]] = []
|
||||
self.call_count: int = 0
|
||||
|
||||
def _inner_get_response(
|
||||
self,
|
||||
*,
|
||||
messages: MutableSequence[Message],
|
||||
stream: bool,
|
||||
options: dict[str, Any],
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]:
|
||||
if stream:
|
||||
return self._get_streaming_response(messages=messages, options=options, **kwargs)
|
||||
|
||||
async def _get() -> ChatResponse:
|
||||
return await self._get_non_streaming_response(messages=messages, options=options, **kwargs)
|
||||
|
||||
return _get()
|
||||
|
||||
async def _get_non_streaming_response(
|
||||
self,
|
||||
*,
|
||||
messages: MutableSequence[Message],
|
||||
options: dict[str, Any],
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
self.call_count += 1
|
||||
if self.run_responses:
|
||||
return self.run_responses.pop(0)
|
||||
return ChatResponse(messages=Message(role="assistant", text="default response"))
|
||||
|
||||
def _get_streaming_response(
|
||||
self,
|
||||
*,
|
||||
messages: MutableSequence[Message],
|
||||
options: dict[str, Any],
|
||||
**kwargs: Any,
|
||||
) -> ResponseStream[ChatResponseUpdate, ChatResponse]:
|
||||
async def _stream() -> AsyncIterable[ChatResponseUpdate]:
|
||||
self.call_count += 1
|
||||
if self.streaming_responses:
|
||||
for update in self.streaming_responses.pop(0):
|
||||
yield update
|
||||
else:
|
||||
yield ChatResponseUpdate(
|
||||
contents=[Content.from_text("default streaming response")], role="assistant", finish_reason="stop"
|
||||
)
|
||||
|
||||
def _finalize(updates: Sequence[ChatResponseUpdate]) -> ChatResponse:
|
||||
response_format = options.get("response_format")
|
||||
output_format_type = response_format if isinstance(response_format, type) else None
|
||||
return ChatResponse.from_updates(updates, output_format_type=output_format_type)
|
||||
|
||||
return ResponseStream(_stream(), finalizer=_finalize)
|
||||
|
||||
|
||||
class FunctionInvokingMockClient(
|
||||
FunctionInvocationLayer[Any],
|
||||
ChatMiddlewareLayer[Any],
|
||||
ChatTelemetryLayer[Any],
|
||||
_MockBaseChatClient,
|
||||
):
|
||||
"""Mock client with function invocation support."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TestKwargsPropagationToFunctionTool:
|
||||
"""Test cases for kwargs flowing from get_response() to @tool functions."""
|
||||
|
||||
async def test_kwargs_propagate_to_tool_with_kwargs(self) -> None:
|
||||
"""Test that kwargs passed to get_response() are available in @tool **kwargs."""
|
||||
# TODO(Copilot): Remove this legacy coverage once runtime ``**kwargs`` tool injection is removed.
|
||||
captured_kwargs: dict[str, Any] = {}
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def capture_kwargs_tool(x: int, **kwargs: Any) -> str:
|
||||
"""A tool that captures kwargs for testing."""
|
||||
captured_kwargs.update(kwargs)
|
||||
return f"result: x={x}"
|
||||
|
||||
client = FunctionInvokingMockClient()
|
||||
client.run_responses = [
|
||||
# First response: function call
|
||||
ChatResponse(
|
||||
messages=[
|
||||
Message(
|
||||
role="assistant",
|
||||
contents=[
|
||||
Content.from_function_call(
|
||||
call_id="call_1", name="capture_kwargs_tool", arguments='{"x": 42}'
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
),
|
||||
# Second response: final answer
|
||||
ChatResponse(messages=[Message(role="assistant", text="Done!")]),
|
||||
]
|
||||
|
||||
result = await client.get_response(
|
||||
messages=[Message(role="user", text="Test")],
|
||||
stream=False,
|
||||
options={
|
||||
"tools": [capture_kwargs_tool],
|
||||
"additional_function_arguments": {
|
||||
"user_id": "user-123",
|
||||
"session_token": "secret-token",
|
||||
"custom_data": {"key": "value"},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
# Verify the tool was called and received the kwargs
|
||||
assert "user_id" in captured_kwargs, f"Expected 'user_id' in captured kwargs: {captured_kwargs}"
|
||||
assert captured_kwargs["user_id"] == "user-123"
|
||||
assert "session_token" in captured_kwargs
|
||||
assert captured_kwargs["session_token"] == "secret-token"
|
||||
assert "custom_data" in captured_kwargs
|
||||
assert captured_kwargs["custom_data"] == {"key": "value"}
|
||||
# Verify result
|
||||
assert result.messages[-1].text == "Done!"
|
||||
|
||||
async def test_kwargs_not_forwarded_to_tool_without_kwargs(self) -> None:
|
||||
"""Test that kwargs are NOT forwarded to @tool that doesn't accept **kwargs."""
|
||||
# TODO(Copilot): Remove this legacy coverage once runtime ``**kwargs`` tool injection is removed.
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def simple_tool(x: int) -> str:
|
||||
"""A simple tool without **kwargs."""
|
||||
return f"result: x={x}"
|
||||
|
||||
client = FunctionInvokingMockClient()
|
||||
client.run_responses = [
|
||||
ChatResponse(
|
||||
messages=[
|
||||
Message(
|
||||
role="assistant",
|
||||
contents=[
|
||||
Content.from_function_call(call_id="call_1", name="simple_tool", arguments='{"x": 99}')
|
||||
],
|
||||
)
|
||||
]
|
||||
),
|
||||
ChatResponse(messages=[Message(role="assistant", text="Completed!")]),
|
||||
]
|
||||
|
||||
# Call with additional_function_arguments - the tool should work but not receive them
|
||||
result = await client.get_response(
|
||||
messages=[Message(role="user", text="Test")],
|
||||
stream=False,
|
||||
options={
|
||||
"tools": [simple_tool],
|
||||
"additional_function_arguments": {"user_id": "user-123"},
|
||||
},
|
||||
)
|
||||
|
||||
# Verify the tool was called successfully (no error from extra kwargs)
|
||||
assert result.messages[-1].text == "Completed!"
|
||||
|
||||
async def test_kwargs_isolated_between_function_calls(self) -> None:
|
||||
"""Test that kwargs are consistent across multiple function call invocations."""
|
||||
# TODO(Copilot): Remove this legacy coverage once runtime ``**kwargs`` tool injection is removed.
|
||||
invocation_kwargs: list[dict[str, Any]] = []
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def tracking_tool(name: str, **kwargs: Any) -> str:
|
||||
"""A tool that tracks kwargs from each invocation."""
|
||||
invocation_kwargs.append(dict(kwargs))
|
||||
return f"called with {name}"
|
||||
|
||||
client = FunctionInvokingMockClient()
|
||||
client.run_responses = [
|
||||
# Two function calls in one response
|
||||
ChatResponse(
|
||||
messages=[
|
||||
Message(
|
||||
role="assistant",
|
||||
contents=[
|
||||
Content.from_function_call(
|
||||
call_id="call_1", name="tracking_tool", arguments='{"name": "first"}'
|
||||
),
|
||||
Content.from_function_call(
|
||||
call_id="call_2", name="tracking_tool", arguments='{"name": "second"}'
|
||||
),
|
||||
],
|
||||
)
|
||||
]
|
||||
),
|
||||
ChatResponse(messages=[Message(role="assistant", text="All done!")]),
|
||||
]
|
||||
|
||||
result = await client.get_response(
|
||||
messages=[Message(role="user", text="Test")],
|
||||
stream=False,
|
||||
options={
|
||||
"tools": [tracking_tool],
|
||||
"additional_function_arguments": {
|
||||
"request_id": "req-001",
|
||||
"trace_context": {"trace_id": "abc"},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
# Both invocations should have received the same kwargs
|
||||
assert len(invocation_kwargs) == 2
|
||||
for kwargs in invocation_kwargs:
|
||||
assert kwargs.get("request_id") == "req-001"
|
||||
assert kwargs.get("trace_context") == {"trace_id": "abc"}
|
||||
assert result.messages[-1].text == "All done!"
|
||||
|
||||
async def test_streaming_response_kwargs_propagation(self) -> None:
|
||||
"""Test that kwargs propagate to @tool in streaming mode."""
|
||||
# TODO(Copilot): Remove this legacy coverage once runtime ``**kwargs`` tool injection is removed.
|
||||
captured_kwargs: dict[str, Any] = {}
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def streaming_capture_tool(value: str, **kwargs: Any) -> str:
|
||||
"""A tool that captures kwargs during streaming."""
|
||||
captured_kwargs.update(kwargs)
|
||||
return f"processed: {value}"
|
||||
|
||||
client = FunctionInvokingMockClient()
|
||||
client.streaming_responses = [
|
||||
# First stream: function call
|
||||
[
|
||||
ChatResponseUpdate(
|
||||
role="assistant",
|
||||
contents=[
|
||||
Content.from_function_call(
|
||||
call_id="stream_call_1",
|
||||
name="streaming_capture_tool",
|
||||
arguments='{"value": "streaming-test"}',
|
||||
)
|
||||
],
|
||||
finish_reason="stop",
|
||||
)
|
||||
],
|
||||
# Second stream: final response
|
||||
[
|
||||
ChatResponseUpdate(
|
||||
contents=[Content.from_text("Stream complete!")], role="assistant", finish_reason="stop"
|
||||
)
|
||||
],
|
||||
]
|
||||
|
||||
# Collect streaming updates
|
||||
updates: list[ChatResponseUpdate] = []
|
||||
stream = client.get_response(
|
||||
messages=[Message(role="user", text="Test")],
|
||||
stream=True,
|
||||
options={
|
||||
"tools": [streaming_capture_tool],
|
||||
"additional_function_arguments": {
|
||||
"streaming_session": "session-xyz",
|
||||
"correlation_id": "corr-123",
|
||||
},
|
||||
},
|
||||
)
|
||||
async for update in stream:
|
||||
updates.append(update)
|
||||
|
||||
# Verify kwargs were captured by the tool
|
||||
assert "streaming_session" in captured_kwargs, f"Expected 'streaming_session' in {captured_kwargs}"
|
||||
assert captured_kwargs["streaming_session"] == "session-xyz"
|
||||
assert captured_kwargs["correlation_id"] == "corr-123"
|
||||
|
||||
async def test_agent_run_injects_function_invocation_context(self) -> None:
|
||||
"""Test that Agent.run injects FunctionInvocationContext for ctx-based tools."""
|
||||
captured_context_kwargs: dict[str, Any] = {}
|
||||
captured_client_kwargs: dict[str, Any] = {}
|
||||
captured_options: dict[str, Any] = {}
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def capture_context_tool(x: int, ctx: FunctionInvocationContext) -> str:
|
||||
captured_context_kwargs.update(ctx.kwargs)
|
||||
return f"result: x={x}"
|
||||
|
||||
class CapturingFunctionInvokingMockClient(FunctionInvokingMockClient):
|
||||
async def _get_non_streaming_response(
|
||||
self,
|
||||
*,
|
||||
messages: MutableSequence[Message],
|
||||
options: dict[str, Any],
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
captured_options.update(options)
|
||||
captured_client_kwargs.update(kwargs)
|
||||
return await super()._get_non_streaming_response(messages=messages, options=options, **kwargs)
|
||||
|
||||
client = CapturingFunctionInvokingMockClient()
|
||||
client.run_responses = [
|
||||
ChatResponse(
|
||||
messages=[
|
||||
Message(
|
||||
role="assistant",
|
||||
contents=[
|
||||
Content.from_function_call(
|
||||
call_id="call_1",
|
||||
name="capture_context_tool",
|
||||
arguments='{"x": 42}',
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
),
|
||||
ChatResponse(messages=[Message(role="assistant", text="Done!")]),
|
||||
]
|
||||
|
||||
agent = Agent(client=client, tools=[capture_context_tool])
|
||||
result = await agent.run(
|
||||
[Message(role="user", text="Test")],
|
||||
function_invocation_kwargs={"tool_request_id": "tool-123"},
|
||||
client_kwargs={"client_request_id": "client-456"},
|
||||
)
|
||||
|
||||
assert captured_context_kwargs["tool_request_id"] == "tool-123"
|
||||
assert "client_request_id" not in captured_context_kwargs
|
||||
assert captured_client_kwargs["client_request_id"] == "client-456"
|
||||
assert "tool_request_id" not in captured_client_kwargs
|
||||
assert "additional_function_arguments" not in captured_options
|
||||
assert result.messages[-1].text == "Done!"
|
||||
@@ -1751,6 +1751,9 @@ async def test_mcp_tool_sampling_callback_no_valid_content():
|
||||
assert isinstance(result, types.ErrorData)
|
||||
assert result.code == types.INTERNAL_ERROR
|
||||
assert "Failed to get right content types from the response." in result.message
|
||||
mock_chat_client.get_response.assert_awaited_once()
|
||||
_, kwargs = mock_chat_client.get_response.await_args
|
||||
assert kwargs["options"] == {"max_tokens": None}
|
||||
|
||||
|
||||
async def test_mcp_tool_sampling_callback_no_response_and_successful_message_creation():
|
||||
@@ -3704,14 +3707,19 @@ async def test_mcp_tool_filters_framework_kwargs():
|
||||
|
||||
# Invoke the tool with framework kwargs that should be filtered out
|
||||
await func.invoke(
|
||||
param="test_value",
|
||||
response_format=MockResponseFormat, # Should be filtered
|
||||
chat_options={"some": "option"}, # Should be filtered
|
||||
tools=[Mock()], # Should be filtered
|
||||
tool_choice="auto", # Should be filtered
|
||||
session=Mock(), # Should be filtered
|
||||
conversation_id="conv-123", # Should be filtered
|
||||
options={"metadata": "value"}, # Should be filtered
|
||||
context=FunctionInvocationContext(
|
||||
function=func,
|
||||
arguments={"param": "test_value"},
|
||||
kwargs={
|
||||
"response_format": MockResponseFormat, # Should be filtered
|
||||
"chat_options": {"some": "option"}, # Should be filtered
|
||||
"tools": [Mock()], # Should be filtered
|
||||
"tool_choice": "auto", # Should be filtered
|
||||
"session": Mock(), # Should be filtered
|
||||
"conversation_id": "conv-123", # Should be filtered
|
||||
"options": {"metadata": "value"}, # Should be filtered
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
# Verify call_tool was called with only the valid argument
|
||||
|
||||
@@ -789,9 +789,10 @@ class TestChatAgentFunctionMiddlewareWithTools:
|
||||
assert modified_kwargs["new_param"] == "added_by_middleware"
|
||||
assert modified_kwargs["custom_param"] == "test_value"
|
||||
|
||||
async def test_run_kwargs_available_in_function_middleware(self, chat_client_base: "MockBaseChatClient") -> None:
|
||||
"""Test that kwargs passed directly to agent.run() appear in FunctionInvocationContext.kwargs,
|
||||
including complex nested values like dicts."""
|
||||
async def test_function_invocation_kwargs_available_in_function_middleware(
|
||||
self, chat_client_base: "MockBaseChatClient"
|
||||
) -> None:
|
||||
"""Test that function_invocation_kwargs appear in FunctionInvocationContext.kwargs."""
|
||||
captured_kwargs: dict[str, Any] = {}
|
||||
|
||||
@function_middleware
|
||||
@@ -822,18 +823,20 @@ class TestChatAgentFunctionMiddlewareWithTools:
|
||||
session_metadata = {"tenant": "acme-corp", "region": "us-west"}
|
||||
await agent.run(
|
||||
[Message(role="user", text="Get weather")],
|
||||
user_id="user-456",
|
||||
session_metadata=session_metadata,
|
||||
function_invocation_kwargs={
|
||||
"user_id": "user-456",
|
||||
"session_metadata": session_metadata,
|
||||
},
|
||||
)
|
||||
|
||||
assert "user_id" in captured_kwargs, f"Expected 'user_id' in kwargs: {captured_kwargs}"
|
||||
assert captured_kwargs["user_id"] == "user-456"
|
||||
assert captured_kwargs["session_metadata"] == {"tenant": "acme-corp", "region": "us-west"}
|
||||
|
||||
async def test_run_kwargs_merged_with_additional_function_arguments(
|
||||
async def test_function_invocation_kwargs_merged_with_additional_function_arguments(
|
||||
self, chat_client_base: "MockBaseChatClient"
|
||||
) -> None:
|
||||
"""Test that explicit additional_function_arguments in options take precedence over run kwargs."""
|
||||
"""Test that explicit additional_function_arguments in options take precedence."""
|
||||
captured_kwargs: dict[str, Any] = {}
|
||||
|
||||
@function_middleware
|
||||
@@ -863,9 +866,10 @@ class TestChatAgentFunctionMiddlewareWithTools:
|
||||
|
||||
await agent.run(
|
||||
[Message(role="user", text="Get weather")],
|
||||
# This kwarg should be overridden by additional_function_arguments
|
||||
user_id="from-kwargs",
|
||||
tenant_id="from-kwargs",
|
||||
function_invocation_kwargs={
|
||||
"user_id": "from-kwargs",
|
||||
"tenant_id": "from-kwargs",
|
||||
},
|
||||
options={
|
||||
"additional_function_arguments": {
|
||||
"user_id": "from-options",
|
||||
@@ -876,15 +880,15 @@ class TestChatAgentFunctionMiddlewareWithTools:
|
||||
|
||||
# additional_function_arguments takes precedence for overlapping keys
|
||||
assert captured_kwargs["user_id"] == "from-options"
|
||||
# Non-overlapping kwargs from run() still come through
|
||||
# Non-overlapping function_invocation_kwargs still come through
|
||||
assert captured_kwargs["tenant_id"] == "from-kwargs"
|
||||
# Keys only in additional_function_arguments are present
|
||||
assert captured_kwargs["extra_key"] == "only-in-options"
|
||||
|
||||
async def test_run_kwargs_consistent_across_multiple_tool_calls(
|
||||
async def test_function_invocation_kwargs_consistent_across_multiple_tool_calls(
|
||||
self, chat_client_base: "MockBaseChatClient"
|
||||
) -> None:
|
||||
"""Test that kwargs are consistent across multiple tool invocations in a single run."""
|
||||
"""Test that function_invocation_kwargs are consistent across tool invocations."""
|
||||
invocation_kwargs: list[dict[str, Any]] = []
|
||||
|
||||
@function_middleware
|
||||
@@ -917,8 +921,10 @@ class TestChatAgentFunctionMiddlewareWithTools:
|
||||
|
||||
await agent.run(
|
||||
[Message(role="user", text="Get weather for both cities")],
|
||||
user_id="user-456",
|
||||
request_id="req-001",
|
||||
function_invocation_kwargs={
|
||||
"user_id": "user-456",
|
||||
"request_id": "req-001",
|
||||
},
|
||||
)
|
||||
|
||||
assert len(invocation_kwargs) == 2
|
||||
@@ -2060,23 +2066,21 @@ class TestChatAgentChatMiddleware:
|
||||
"agent_middleware_after",
|
||||
]
|
||||
|
||||
async def test_agent_middleware_can_access_and_override_custom_kwargs(self) -> None:
|
||||
"""Test that agent middleware can access and override custom parameters like temperature."""
|
||||
captured_kwargs: dict[str, Any] = {}
|
||||
modified_kwargs: dict[str, Any] = {}
|
||||
async def test_agent_middleware_can_access_and_override_options(self) -> None:
|
||||
"""Test that agent middleware can access and override runtime options."""
|
||||
captured_options: dict[str, Any] = {}
|
||||
modified_options: dict[str, Any] = {}
|
||||
|
||||
@agent_middleware
|
||||
async def kwargs_middleware(context: AgentContext, call_next: Callable[[], Awaitable[None]]) -> None:
|
||||
# Capture the original kwargs
|
||||
captured_kwargs.update(context.kwargs)
|
||||
assert isinstance(context.options, dict)
|
||||
captured_options.update(context.options)
|
||||
|
||||
# Modify some kwargs
|
||||
context.kwargs["temperature"] = 0.9
|
||||
context.kwargs["max_tokens"] = 500
|
||||
context.kwargs["new_param"] = "added_by_middleware"
|
||||
context.options["temperature"] = 0.9
|
||||
context.options["max_tokens"] = 500
|
||||
context.options["new_param"] = "added_by_middleware"
|
||||
|
||||
# Store modified kwargs for verification
|
||||
modified_kwargs.update(context.kwargs)
|
||||
modified_options.update(context.options)
|
||||
|
||||
await call_next()
|
||||
|
||||
@@ -2084,24 +2088,25 @@ class TestChatAgentChatMiddleware:
|
||||
client = MockBaseChatClient()
|
||||
agent = Agent(client=client, middleware=[kwargs_middleware])
|
||||
|
||||
# Execute the agent with custom parameters
|
||||
# Execute the agent with runtime options
|
||||
messages = [Message(role="user", text="test message")]
|
||||
response = await agent.run(messages, temperature=0.7, max_tokens=100, custom_param="test_value")
|
||||
response = await agent.run(
|
||||
messages,
|
||||
options={"temperature": 0.7, "max_tokens": 100, "custom_param": "test_value"},
|
||||
)
|
||||
|
||||
# Verify response
|
||||
assert response is not None
|
||||
assert len(response.messages) > 0
|
||||
|
||||
# Verify middleware captured the original kwargs
|
||||
assert captured_kwargs["temperature"] == 0.7
|
||||
assert captured_kwargs["max_tokens"] == 100
|
||||
assert captured_kwargs["custom_param"] == "test_value"
|
||||
assert captured_options["temperature"] == 0.7
|
||||
assert captured_options["max_tokens"] == 100
|
||||
assert captured_options["custom_param"] == "test_value"
|
||||
|
||||
# Verify middleware could modify the kwargs
|
||||
assert modified_kwargs["temperature"] == 0.9
|
||||
assert modified_kwargs["max_tokens"] == 500
|
||||
assert modified_kwargs["new_param"] == "added_by_middleware"
|
||||
assert modified_kwargs["custom_param"] == "test_value" # Should still be there
|
||||
assert modified_options["temperature"] == 0.9
|
||||
assert modified_options["max_tokens"] == 500
|
||||
assert modified_options["new_param"] == "added_by_middleware"
|
||||
assert modified_options["custom_param"] == "test_value"
|
||||
|
||||
|
||||
# class TestMiddlewareWithProtocolOnlyAgent:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
from agent_framework import (
|
||||
Agent,
|
||||
@@ -296,50 +297,77 @@ class TestChatMiddleware:
|
||||
assert response3 is not None
|
||||
assert execution_count["count"] == 2 # Should be 2 now
|
||||
|
||||
async def test_chat_client_middleware_can_access_and_override_custom_kwargs(
|
||||
async def test_run_level_middleware_is_not_forwarded_to_inner_client(
|
||||
self, chat_client_base: "MockBaseChatClient"
|
||||
) -> None:
|
||||
"""Test that chat client middleware can access and override custom parameters like temperature."""
|
||||
captured_kwargs: dict[str, Any] = {}
|
||||
modified_kwargs: dict[str, Any] = {}
|
||||
"""Test that run-level middleware stays in the middleware pipeline only."""
|
||||
observed_context_kwargs: dict[str, Any] = {}
|
||||
|
||||
@chat_middleware
|
||||
async def inspecting_middleware(context: ChatContext, call_next: Callable[[], Awaitable[None]]) -> None:
|
||||
observed_context_kwargs.update(context.kwargs)
|
||||
await call_next()
|
||||
|
||||
async def fake_inner_get_response(**kwargs: Any) -> ChatResponse:
|
||||
assert "middleware" not in kwargs
|
||||
return ChatResponse(messages=[Message(role="assistant", text="ok")])
|
||||
|
||||
with patch.object(
|
||||
chat_client_base,
|
||||
"_inner_get_response",
|
||||
side_effect=fake_inner_get_response,
|
||||
) as mock_inner_get_response:
|
||||
response = await chat_client_base.get_response(
|
||||
[Message(role="user", text="hello")],
|
||||
client_kwargs={"middleware": [inspecting_middleware], "trace_id": "trace-123"},
|
||||
)
|
||||
|
||||
assert response.messages[0].text == "ok"
|
||||
assert observed_context_kwargs == {"trace_id": "trace-123"}
|
||||
mock_inner_get_response.assert_called_once()
|
||||
|
||||
async def test_chat_client_middleware_can_access_and_override_options(
|
||||
self, chat_client_base: "MockBaseChatClient"
|
||||
) -> None:
|
||||
"""Test that chat client middleware can access and override runtime options."""
|
||||
captured_options: dict[str, Any] = {}
|
||||
modified_options: dict[str, Any] = {}
|
||||
|
||||
@chat_middleware
|
||||
async def kwargs_middleware(context: ChatContext, call_next: Callable[[], Awaitable[None]]) -> None:
|
||||
# Capture the original kwargs
|
||||
captured_kwargs.update(context.kwargs)
|
||||
assert isinstance(context.options, dict)
|
||||
captured_options.update(context.options)
|
||||
|
||||
# Modify some kwargs
|
||||
context.kwargs["temperature"] = 0.9
|
||||
context.kwargs["max_tokens"] = 500
|
||||
context.kwargs["new_param"] = "added_by_middleware"
|
||||
context.options["temperature"] = 0.9
|
||||
context.options["max_tokens"] = 500
|
||||
context.options["new_param"] = "added_by_middleware"
|
||||
|
||||
# Store modified kwargs for verification
|
||||
modified_kwargs.update(context.kwargs)
|
||||
modified_options.update(context.options)
|
||||
|
||||
await call_next()
|
||||
|
||||
# Add middleware to chat client
|
||||
chat_client_base.chat_middleware = [kwargs_middleware]
|
||||
|
||||
# Execute chat client with custom parameters
|
||||
# Execute chat client with runtime options
|
||||
messages = [Message(role="user", text="test message")]
|
||||
response = await chat_client_base.get_response(
|
||||
messages, temperature=0.7, max_tokens=100, custom_param="test_value"
|
||||
messages,
|
||||
options={"temperature": 0.7, "max_tokens": 100, "custom_param": "test_value"},
|
||||
)
|
||||
|
||||
# Verify response
|
||||
assert response is not None
|
||||
assert len(response.messages) > 0
|
||||
|
||||
assert captured_kwargs["temperature"] == 0.7
|
||||
assert captured_kwargs["max_tokens"] == 100
|
||||
assert captured_kwargs["custom_param"] == "test_value"
|
||||
assert captured_options["temperature"] == 0.7
|
||||
assert captured_options["max_tokens"] == 100
|
||||
assert captured_options["custom_param"] == "test_value"
|
||||
|
||||
# Verify middleware could modify the kwargs
|
||||
assert modified_kwargs["temperature"] == 0.9
|
||||
assert modified_kwargs["max_tokens"] == 500
|
||||
assert modified_kwargs["new_param"] == "added_by_middleware"
|
||||
assert modified_kwargs["custom_param"] == "test_value" # Should still be there
|
||||
assert modified_options["temperature"] == 0.9
|
||||
assert modified_options["max_tokens"] == 500
|
||||
assert modified_options["new_param"] == "added_by_middleware"
|
||||
assert modified_options["custom_param"] == "test_value"
|
||||
|
||||
def test_chat_middleware_pipeline_cache_reuses_matching_middleware(
|
||||
self,
|
||||
|
||||
@@ -207,7 +207,7 @@ async def test_chat_client_observability(mock_chat_client, span_exporter: InMemo
|
||||
|
||||
messages = [Message(role="user", text="Test message")]
|
||||
span_exporter.clear()
|
||||
response = await client.get_response(messages=messages, model_id="Test")
|
||||
response = await client.get_response(messages=messages, options={"model_id": "Test"})
|
||||
assert response is not None
|
||||
spans = span_exporter.get_finished_spans()
|
||||
assert len(spans) == 1
|
||||
@@ -232,7 +232,7 @@ async def test_chat_client_streaming_observability(
|
||||
span_exporter.clear()
|
||||
# Collect all yielded updates
|
||||
updates = []
|
||||
stream = client.get_response(stream=True, messages=messages, model_id="Test")
|
||||
stream = client.get_response(stream=True, messages=messages, options={"model_id": "Test"})
|
||||
async for update in stream:
|
||||
updates.append(update)
|
||||
await stream.get_final_response()
|
||||
@@ -1540,7 +1540,7 @@ async def test_chat_client_observability_exception(mock_chat_client, span_export
|
||||
|
||||
span_exporter.clear()
|
||||
with pytest.raises(ValueError, match="Test error"):
|
||||
await client.get_response(messages=messages, model_id="Test")
|
||||
await client.get_response(messages=messages, options={"model_id": "Test"})
|
||||
|
||||
spans = span_exporter.get_finished_spans()
|
||||
assert len(spans) == 1
|
||||
@@ -1570,7 +1570,7 @@ async def test_chat_client_streaming_observability_exception(mock_chat_client, s
|
||||
|
||||
span_exporter.clear()
|
||||
with pytest.raises(ValueError, match="Streaming error"):
|
||||
async for _ in client.get_response(messages=messages, stream=True, model_id="Test"):
|
||||
async for _ in client.get_response(messages=messages, stream=True, options={"model_id": "Test"}):
|
||||
pass
|
||||
|
||||
spans = span_exporter.get_finished_spans()
|
||||
@@ -2075,7 +2075,7 @@ async def test_capture_messages_with_finish_reason(mock_chat_client, span_export
|
||||
messages = [Message(role="user", text="Test")]
|
||||
|
||||
span_exporter.clear()
|
||||
response = await client.get_response(messages=messages, model_id="Test")
|
||||
response = await client.get_response(messages=messages, options={"model_id": "Test"})
|
||||
|
||||
assert response is not None
|
||||
assert response.finish_reason == "stop"
|
||||
@@ -2165,7 +2165,7 @@ async def test_chat_client_when_disabled(mock_chat_client, span_exporter: InMemo
|
||||
messages = [Message(role="user", text="Test")]
|
||||
|
||||
span_exporter.clear()
|
||||
response = await client.get_response(messages=messages, model_id="Test")
|
||||
response = await client.get_response(messages=messages, options={"model_id": "Test"})
|
||||
|
||||
assert response is not None
|
||||
spans = span_exporter.get_finished_spans()
|
||||
@@ -2181,7 +2181,7 @@ async def test_chat_client_streaming_when_disabled(mock_chat_client, span_export
|
||||
|
||||
span_exporter.clear()
|
||||
updates = []
|
||||
async for update in client.get_response(messages=messages, stream=True, model_id="Test"):
|
||||
async for update in client.get_response(messages=messages, stream=True, options={"model_id": "Test"}):
|
||||
updates.append(update)
|
||||
|
||||
assert len(updates) == 2 # Still works functionally
|
||||
@@ -2661,7 +2661,7 @@ async def test_capture_messages_preserves_non_ascii_characters(mock_chat_client,
|
||||
messages = [Message(role="user", text=japanese_text)]
|
||||
|
||||
span_exporter.clear()
|
||||
response = await client.get_response(messages=messages, model_id="Test")
|
||||
response = await client.get_response(messages=messages, options={"model_id": "Test"})
|
||||
|
||||
assert response is not None
|
||||
spans = span_exporter.get_finished_spans()
|
||||
|
||||
@@ -594,8 +594,8 @@ async def test_tool_invoke_telemetry_sensitive_disabled(span_exporter: InMemoryS
|
||||
assert attributes[OtelAttr.TOOL_CALL_ID] == "test_call_id"
|
||||
|
||||
|
||||
async def test_tool_invoke_ignores_additional_kwargs() -> None:
|
||||
"""Ensure tools drop unknown kwargs when invoked with validated arguments."""
|
||||
async def test_tool_invoke_rejects_unexpected_runtime_kwargs() -> None:
|
||||
"""Ensure invoke() requires runtime data to flow through FunctionInvocationContext."""
|
||||
|
||||
@tool
|
||||
async def simple_tool(message: str) -> str:
|
||||
@@ -604,15 +604,12 @@ async def test_tool_invoke_ignores_additional_kwargs() -> None:
|
||||
|
||||
args = simple_tool.input_model(message="hello world")
|
||||
|
||||
# These kwargs simulate runtime context passed through function invocation.
|
||||
result = await simple_tool.invoke(
|
||||
arguments=args,
|
||||
api_token="secret-token",
|
||||
options={"model_id": "dummy"},
|
||||
)
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert result[0].text == "HELLO WORLD"
|
||||
with pytest.raises(TypeError, match="Unexpected keyword argument"):
|
||||
await simple_tool.invoke(
|
||||
arguments=args,
|
||||
api_token="secret-token",
|
||||
options={"model_id": "dummy"},
|
||||
)
|
||||
|
||||
|
||||
async def test_tool_invoke_telemetry_with_pydantic_args(span_exporter: InMemorySpanExporter):
|
||||
@@ -917,8 +914,8 @@ def test_parse_inputs_unsupported_type():
|
||||
# endregion
|
||||
|
||||
|
||||
async def test_ai_function_with_kwargs_injection():
|
||||
"""Test that ai_function correctly handles kwargs injection and hides them from schema."""
|
||||
async def test_ai_function_with_kwargs_rejects_runtime_invoke_kwargs():
|
||||
"""Test that runtime kwargs must be passed through FunctionInvocationContext."""
|
||||
|
||||
@tool
|
||||
def tool_with_kwargs(x: int, **kwargs: Any) -> str:
|
||||
@@ -937,13 +934,11 @@ async def test_ai_function_with_kwargs_injection():
|
||||
# Verify direct invocation works
|
||||
assert tool_with_kwargs(1, user_id="user1") == "x=1, user=user1"
|
||||
|
||||
# Verify invoke works with injected args
|
||||
result = await tool_with_kwargs.invoke(
|
||||
arguments=tool_with_kwargs.input_model(x=5),
|
||||
user_id="user2",
|
||||
)
|
||||
assert isinstance(result, list)
|
||||
assert result[0].text == "x=5, user=user2"
|
||||
with pytest.raises(TypeError, match="Unexpected keyword argument"):
|
||||
await tool_with_kwargs.invoke(
|
||||
arguments=tool_with_kwargs.input_model(x=5),
|
||||
user_id="user2",
|
||||
)
|
||||
|
||||
# Verify invoke works without injected args (uses default)
|
||||
result_default = await tool_with_kwargs.invoke(
|
||||
|
||||
Reference in New Issue
Block a user