mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [BREAKING] Replace Hosted*Tool classes with tool methods (#3634)
* Replace Hosted*Tool classes with client static factory methods * fixed failing test * mypy fix * mypy fix 2 * declarative mypy fix * addressed comments * ToolProtocol removal * fixed test * agents mypy fix * fix failing tests * mypy fix * addressed comments * fixed tests * addressed comments + added factory method overrides for azureai v2 client * mypy fix * added kwargs to azureai tool methods * fixed in test * _sessions fix * test fix
This commit is contained in:
committed by
GitHub
Unverified
parent
d249473a6d
commit
7a88af0aef
@@ -38,7 +38,6 @@ from ._threads import AgentThread, ChatMessageStoreProtocol
|
||||
from ._tools import (
|
||||
FunctionInvocationLayer,
|
||||
FunctionTool,
|
||||
ToolProtocol,
|
||||
)
|
||||
from ._types import (
|
||||
AgentResponse,
|
||||
@@ -615,10 +614,11 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
id: str | None = None,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
tools: ToolProtocol
|
||||
tools: FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Any
|
||||
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
|
||||
| None = None,
|
||||
default_options: OptionsCoT | None = None,
|
||||
chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] | None = None,
|
||||
@@ -681,10 +681,10 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
# Get tools from options or named parameter (named param takes precedence)
|
||||
tools_ = tools if tools is not None else opts.pop("tools", None)
|
||||
tools_ = cast(
|
||||
ToolProtocol
|
||||
FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None,
|
||||
tools_,
|
||||
)
|
||||
@@ -694,10 +694,10 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
|
||||
# We ignore the MCP Servers here and store them separately,
|
||||
# we add their functions to the tools list at runtime
|
||||
normalized_tools: list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] = ( # type:ignore[reportUnknownVariableType]
|
||||
normalized_tools: list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] = ( # type:ignore[reportUnknownVariableType]
|
||||
[] if tools_ is None else tools_ if isinstance(tools_, list) else [tools_] # type: ignore[list-item]
|
||||
)
|
||||
self.mcp_tools: list[MCPTool] = [tool for tool in normalized_tools if isinstance(tool, MCPTool)]
|
||||
self.mcp_tools: list[MCPTool] = [tool for tool in normalized_tools if isinstance(tool, MCPTool)] # type: ignore[misc]
|
||||
agent_tools = [tool for tool in normalized_tools if not isinstance(tool, MCPTool)]
|
||||
|
||||
# Build chat options dict
|
||||
@@ -780,10 +780,11 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
*,
|
||||
stream: Literal[False] = ...,
|
||||
thread: AgentThread | None = None,
|
||||
tools: ToolProtocol
|
||||
tools: FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Any
|
||||
| list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
|
||||
| None = None,
|
||||
options: ChatOptions[ResponseModelBoundT],
|
||||
**kwargs: Any,
|
||||
@@ -796,10 +797,11 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
*,
|
||||
stream: Literal[False] = ...,
|
||||
thread: AgentThread | None = None,
|
||||
tools: ToolProtocol
|
||||
tools: FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Any
|
||||
| list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
|
||||
| None = None,
|
||||
options: OptionsCoT | ChatOptions[None] | None = None,
|
||||
**kwargs: Any,
|
||||
@@ -812,10 +814,11 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
*,
|
||||
stream: Literal[True],
|
||||
thread: AgentThread | None = None,
|
||||
tools: ToolProtocol
|
||||
tools: FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Any
|
||||
| list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
|
||||
| None = None,
|
||||
options: OptionsCoT | ChatOptions[Any] | None = None,
|
||||
**kwargs: Any,
|
||||
@@ -827,10 +830,11 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
*,
|
||||
stream: bool = False,
|
||||
thread: AgentThread | None = None,
|
||||
tools: ToolProtocol
|
||||
tools: FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Any
|
||||
| list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
|
||||
| None = None,
|
||||
options: OptionsCoT | ChatOptions[Any] | None = None,
|
||||
**kwargs: Any,
|
||||
@@ -981,10 +985,11 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
*,
|
||||
messages: str | Message | Sequence[str | Message] | None,
|
||||
thread: AgentThread | None,
|
||||
tools: ToolProtocol
|
||||
tools: FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Any
|
||||
| list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
|
||||
| None,
|
||||
options: Mapping[str, Any] | None,
|
||||
kwargs: dict[str, Any],
|
||||
@@ -1000,13 +1005,13 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
)
|
||||
|
||||
# Normalize tools
|
||||
normalized_tools: list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] = (
|
||||
normalized_tools: list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any] = (
|
||||
[] if tools_ is None else tools_ if isinstance(tools_, list) else [tools_]
|
||||
)
|
||||
agent_name = self._get_agent_name()
|
||||
|
||||
# Resolve final tool list (runtime provided tools + local MCP server tools)
|
||||
final_tools: list[ToolProtocol | Callable[..., Any] | dict[str, Any]] = []
|
||||
final_tools: list[FunctionTool | Callable[..., Any] | dict[str, Any] | Any] = []
|
||||
for tool in normalized_tools:
|
||||
if isinstance(tool, MCPTool):
|
||||
if not tool.is_connected:
|
||||
@@ -1392,10 +1397,11 @@ class Agent(
|
||||
id: str | None = None,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
tools: ToolProtocol
|
||||
tools: FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Any
|
||||
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
|
||||
| None = None,
|
||||
default_options: OptionsCoT | None = None,
|
||||
chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] | None = None,
|
||||
|
||||
@@ -33,7 +33,7 @@ from ._serialization import SerializationMixin
|
||||
from ._threads import ChatMessageStoreProtocol
|
||||
from ._tools import (
|
||||
FunctionInvocationConfiguration,
|
||||
ToolProtocol,
|
||||
FunctionTool,
|
||||
)
|
||||
from ._types import (
|
||||
ChatResponse,
|
||||
@@ -68,6 +68,11 @@ logger = get_logger()
|
||||
__all__ = [
|
||||
"BaseChatClient",
|
||||
"SupportsChatGetResponse",
|
||||
"SupportsCodeInterpreterTool",
|
||||
"SupportsFileSearchTool",
|
||||
"SupportsImageGenerationTool",
|
||||
"SupportsMCPTool",
|
||||
"SupportsWebSearchTool",
|
||||
]
|
||||
|
||||
|
||||
@@ -437,10 +442,10 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]):
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
instructions: str | None = None,
|
||||
tools: ToolProtocol
|
||||
tools: FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
default_options: OptionsCoT | Mapping[str, Any] | None = None,
|
||||
chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] | None = None,
|
||||
@@ -510,3 +515,163 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]):
|
||||
function_invocation_configuration=function_invocation_configuration,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region Tool Support Protocols
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class SupportsCodeInterpreterTool(Protocol):
|
||||
"""Protocol for clients that support code interpreter tools.
|
||||
|
||||
This protocol enables runtime checking to determine if a client
|
||||
supports code interpreter functionality.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import SupportsCodeInterpreterTool
|
||||
|
||||
if isinstance(client, SupportsCodeInterpreterTool):
|
||||
tool = client.get_code_interpreter_tool()
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_code_interpreter_tool(**kwargs: Any) -> Any:
|
||||
"""Create a code interpreter tool configuration.
|
||||
|
||||
Keyword Args:
|
||||
**kwargs: Provider-specific configuration options.
|
||||
|
||||
Returns:
|
||||
A tool configuration ready to pass to ChatAgent.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class SupportsWebSearchTool(Protocol):
|
||||
"""Protocol for clients that support web search tools.
|
||||
|
||||
This protocol enables runtime checking to determine if a client
|
||||
supports web search functionality.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import SupportsWebSearchTool
|
||||
|
||||
if isinstance(client, SupportsWebSearchTool):
|
||||
tool = client.get_web_search_tool()
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_web_search_tool(**kwargs: Any) -> Any:
|
||||
"""Create a web search tool configuration.
|
||||
|
||||
Keyword Args:
|
||||
**kwargs: Provider-specific configuration options.
|
||||
|
||||
Returns:
|
||||
A tool configuration ready to pass to ChatAgent.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class SupportsImageGenerationTool(Protocol):
|
||||
"""Protocol for clients that support image generation tools.
|
||||
|
||||
This protocol enables runtime checking to determine if a client
|
||||
supports image generation functionality.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import SupportsImageGenerationTool
|
||||
|
||||
if isinstance(client, SupportsImageGenerationTool):
|
||||
tool = client.get_image_generation_tool()
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_image_generation_tool(**kwargs: Any) -> Any:
|
||||
"""Create an image generation tool configuration.
|
||||
|
||||
Keyword Args:
|
||||
**kwargs: Provider-specific configuration options.
|
||||
|
||||
Returns:
|
||||
A tool configuration ready to pass to ChatAgent.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class SupportsMCPTool(Protocol):
|
||||
"""Protocol for clients that support MCP (Model Context Protocol) tools.
|
||||
|
||||
This protocol enables runtime checking to determine if a client
|
||||
supports MCP server connections.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import SupportsMCPTool
|
||||
|
||||
if isinstance(client, SupportsMCPTool):
|
||||
tool = client.get_mcp_tool(name="my_mcp", url="https://...")
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_mcp_tool(**kwargs: Any) -> Any:
|
||||
"""Create an MCP tool configuration.
|
||||
|
||||
Keyword Args:
|
||||
**kwargs: Provider-specific configuration options including
|
||||
name and url for the MCP server.
|
||||
|
||||
Returns:
|
||||
A tool configuration ready to pass to ChatAgent.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class SupportsFileSearchTool(Protocol):
|
||||
"""Protocol for clients that support file search tools.
|
||||
|
||||
This protocol enables runtime checking to determine if a client
|
||||
supports file search functionality with vector stores.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import SupportsFileSearchTool
|
||||
|
||||
if isinstance(client, SupportsFileSearchTool):
|
||||
tool = client.get_file_search_tool(vector_store_ids=["vs_123"])
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_file_search_tool(**kwargs: Any) -> Any:
|
||||
"""Create a file search tool configuration.
|
||||
|
||||
Keyword Args:
|
||||
**kwargs: Provider-specific configuration options.
|
||||
|
||||
Returns:
|
||||
A tool configuration ready to pass to ChatAgent.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
@@ -12,7 +12,7 @@ from collections.abc import Callable, Collection, Sequence
|
||||
from contextlib import AsyncExitStack, _AsyncGeneratorContextManager # type: ignore
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
from typing import TYPE_CHECKING, Any, Literal, TypedDict
|
||||
|
||||
import httpx
|
||||
from anyio import ClosedResourceError
|
||||
@@ -28,7 +28,6 @@ from pydantic import BaseModel, create_model
|
||||
|
||||
from ._tools import (
|
||||
FunctionTool,
|
||||
HostedMCPSpecificApproval,
|
||||
_build_pydantic_model_from_json_schema,
|
||||
)
|
||||
from ._types import (
|
||||
@@ -45,6 +44,21 @@ else:
|
||||
if TYPE_CHECKING:
|
||||
from ._clients import SupportsChatGetResponse
|
||||
|
||||
|
||||
class MCPSpecificApproval(TypedDict, total=False):
|
||||
"""Represents the specific approval mode for an MCP tool.
|
||||
|
||||
When using this mode, the user must specify which tools always or never require approval.
|
||||
|
||||
Attributes:
|
||||
always_require_approval: A sequence of tool names that always require approval.
|
||||
never_require_approval: A sequence of tool names that never require approval.
|
||||
"""
|
||||
|
||||
always_require_approval: Collection[str] | None
|
||||
never_require_approval: Collection[str] | None
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# region: Helpers
|
||||
@@ -327,7 +341,7 @@ class MCPTool:
|
||||
self,
|
||||
name: str,
|
||||
description: str | None = None,
|
||||
approval_mode: (Literal["always_require", "never_require"] | HostedMCPSpecificApproval | None) = None,
|
||||
approval_mode: (Literal["always_require", "never_require"] | MCPSpecificApproval | None) = None,
|
||||
allowed_tools: Collection[str] | None = None,
|
||||
load_tools: bool = True,
|
||||
parse_tool_results: Literal[True] | Callable[[types.CallToolResult], Any] | None = True,
|
||||
@@ -937,7 +951,7 @@ class MCPStdioTool(MCPTool):
|
||||
request_timeout: int | None = None,
|
||||
session: ClientSession | None = None,
|
||||
description: str | None = None,
|
||||
approval_mode: (Literal["always_require", "never_require"] | HostedMCPSpecificApproval | None) = None,
|
||||
approval_mode: (Literal["always_require", "never_require"] | MCPSpecificApproval | None) = None,
|
||||
allowed_tools: Collection[str] | None = None,
|
||||
args: list[str] | None = None,
|
||||
env: dict[str, str] | None = None,
|
||||
@@ -1058,7 +1072,7 @@ class MCPStreamableHTTPTool(MCPTool):
|
||||
request_timeout: int | None = None,
|
||||
session: ClientSession | None = None,
|
||||
description: str | None = None,
|
||||
approval_mode: (Literal["always_require", "never_require"] | HostedMCPSpecificApproval | None) = None,
|
||||
approval_mode: (Literal["always_require", "never_require"] | MCPSpecificApproval | None) = None,
|
||||
allowed_tools: Collection[str] | None = None,
|
||||
terminate_on_close: bool | None = None,
|
||||
client: SupportsChatGetResponse | None = None,
|
||||
@@ -1173,7 +1187,7 @@ class MCPWebsocketTool(MCPTool):
|
||||
request_timeout: int | None = None,
|
||||
session: ClientSession | None = None,
|
||||
description: str | None = None,
|
||||
approval_mode: (Literal["always_require", "never_require"] | HostedMCPSpecificApproval | None) = None,
|
||||
approval_mode: (Literal["always_require", "never_require"] | MCPSpecificApproval | None) = None,
|
||||
allowed_tools: Collection[str] | None = None,
|
||||
client: SupportsChatGetResponse | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
|
||||
@@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, Any, Final
|
||||
from ._types import Message
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._tools import ToolProtocol
|
||||
from ._tools import FunctionTool
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import Self # pragma: no cover
|
||||
@@ -52,7 +52,7 @@ class Context:
|
||||
self,
|
||||
instructions: str | None = None,
|
||||
messages: Sequence[Message] | None = None,
|
||||
tools: Sequence[ToolProtocol] | None = None,
|
||||
tools: Sequence[FunctionTool] | None = None,
|
||||
):
|
||||
"""Create a new Context object.
|
||||
|
||||
@@ -63,7 +63,7 @@ class Context:
|
||||
"""
|
||||
self.instructions = instructions
|
||||
self.messages: Sequence[Message] = messages or []
|
||||
self.tools: Sequence[ToolProtocol] = tools or []
|
||||
self.tools: Sequence[FunctionTool] = tools or []
|
||||
|
||||
|
||||
# region ContextProvider
|
||||
|
||||
@@ -18,7 +18,6 @@ from abc import abstractmethod
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ._tools import ToolProtocol
|
||||
from ._types import AgentResponse, Message
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -110,7 +109,7 @@ class SessionContext:
|
||||
input_messages: list[Message],
|
||||
context_messages: dict[str, list[Message]] | None = None,
|
||||
instructions: list[str] | None = None,
|
||||
tools: list[ToolProtocol] | None = None,
|
||||
tools: list[Any] | None = None,
|
||||
options: dict[str, Any] | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
):
|
||||
@@ -131,7 +130,7 @@ class SessionContext:
|
||||
self.input_messages = input_messages
|
||||
self.context_messages: dict[str, list[Message]] = context_messages or {}
|
||||
self.instructions: list[str] = instructions or []
|
||||
self.tools: list[ToolProtocol] = tools or []
|
||||
self.tools: list[Any] = tools or []
|
||||
self._response: AgentResponse | None = None
|
||||
self.options: dict[str, Any] = options or {}
|
||||
self.metadata: dict[str, Any] = metadata or {}
|
||||
@@ -185,7 +184,7 @@ class SessionContext:
|
||||
instructions = [instructions]
|
||||
self.instructions.extend(instructions)
|
||||
|
||||
def extend_tools(self, source_id: str, tools: Sequence[ToolProtocol]) -> None:
|
||||
def extend_tools(self, source_id: str, tools: Sequence[Any]) -> None:
|
||||
"""Add tools to be available for this invocation.
|
||||
|
||||
Tools are added with source attribution in their metadata.
|
||||
|
||||
@@ -10,7 +10,6 @@ from collections.abc import (
|
||||
AsyncIterable,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Collection,
|
||||
Mapping,
|
||||
MutableMapping,
|
||||
Sequence,
|
||||
@@ -25,18 +24,16 @@ from typing import (
|
||||
Final,
|
||||
Generic,
|
||||
Literal,
|
||||
Protocol,
|
||||
TypedDict,
|
||||
Union,
|
||||
cast,
|
||||
get_args,
|
||||
get_origin,
|
||||
overload,
|
||||
runtime_checkable,
|
||||
)
|
||||
|
||||
from opentelemetry.metrics import Histogram, NoOpHistogram
|
||||
from pydantic import AnyUrl, BaseModel, Field, ValidationError, create_model
|
||||
from pydantic import BaseModel, Field, ValidationError, create_model
|
||||
|
||||
from ._logging import get_logger
|
||||
from ._serialization import SerializationMixin
|
||||
@@ -58,10 +55,6 @@ if sys.version_info >= (3, 12):
|
||||
from typing import override # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import override # type: ignore[import] # pragma: no cover
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import TypedDict # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import TypedDict # type: ignore # pragma: no cover
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -85,13 +78,6 @@ __all__ = [
|
||||
"FunctionInvocationConfiguration",
|
||||
"FunctionInvocationLayer",
|
||||
"FunctionTool",
|
||||
"HostedCodeInterpreterTool",
|
||||
"HostedFileSearchTool",
|
||||
"HostedImageGenerationTool",
|
||||
"HostedMCPSpecificApproval",
|
||||
"HostedMCPTool",
|
||||
"HostedWebSearchTool",
|
||||
"ToolProtocol",
|
||||
"normalize_function_invocation_configuration",
|
||||
"tool",
|
||||
]
|
||||
@@ -163,380 +149,6 @@ def _parse_inputs(
|
||||
|
||||
|
||||
# region Tools
|
||||
@runtime_checkable
|
||||
class ToolProtocol(Protocol):
|
||||
"""Represents a generic tool.
|
||||
|
||||
This protocol defines the interface that all tools must implement to be compatible
|
||||
with the agent framework. It is implemented by various tool classes such as HostedMCPTool,
|
||||
HostedWebSearchTool, and FunctionTool's. A FunctionTool is usually created by the `tool` decorator.
|
||||
|
||||
Since each connector needs to parse tools differently, users can pass a dict to
|
||||
specify a service-specific tool when no abstraction is available.
|
||||
|
||||
Attributes:
|
||||
name: The name of the tool.
|
||||
description: A description of the tool, suitable for use in describing the purpose to a model.
|
||||
additional_properties: Additional properties associated with the tool.
|
||||
"""
|
||||
|
||||
name: str
|
||||
"""The name of the tool."""
|
||||
description: str
|
||||
"""A description of the tool, suitable for use in describing the purpose to a model."""
|
||||
additional_properties: dict[str, Any] | None
|
||||
"""Additional properties associated with the tool."""
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return a string representation of the tool."""
|
||||
...
|
||||
|
||||
|
||||
class BaseTool(SerializationMixin):
|
||||
"""Base class for AI tools, providing common attributes and methods.
|
||||
|
||||
Used as the base class for the various tools in the agent framework, such as HostedMCPTool,
|
||||
HostedWebSearchTool, and FunctionTool.
|
||||
|
||||
Since each connector needs to parse tools differently, this class is not exposed directly to end users.
|
||||
In most cases, users can pass a dict to specify a service-specific tool when no abstraction is available.
|
||||
"""
|
||||
|
||||
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"additional_properties"}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
description: str = "",
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize the BaseTool.
|
||||
|
||||
Keyword Args:
|
||||
name: The name of the tool.
|
||||
description: A description of the tool.
|
||||
additional_properties: Additional properties associated with the tool.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.additional_properties = additional_properties
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return a string representation of the tool."""
|
||||
if self.description:
|
||||
return f"{self.__class__.__name__}(name={self.name}, description={self.description})"
|
||||
return f"{self.__class__.__name__}(name={self.name})"
|
||||
|
||||
|
||||
class HostedCodeInterpreterTool(BaseTool):
|
||||
"""Represents a hosted tool that can be specified to an AI service to enable it to execute generated code.
|
||||
|
||||
This tool does not implement code interpretation itself. It serves as a marker to inform a service
|
||||
that it is allowed to execute generated code if the service is capable of doing so.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import HostedCodeInterpreterTool
|
||||
|
||||
# Create a code interpreter tool
|
||||
code_tool = HostedCodeInterpreterTool()
|
||||
|
||||
# With file inputs
|
||||
code_tool_with_files = HostedCodeInterpreterTool(inputs=[{"file_id": "file-123"}, {"file_id": "file-456"}])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
inputs: Content | dict[str, Any] | str | list[Content | dict[str, Any] | str] | None = None,
|
||||
description: str | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize the HostedCodeInterpreterTool.
|
||||
|
||||
Keyword Args:
|
||||
inputs: A list of contents that the tool can accept as input. Defaults to None.
|
||||
This should mostly be HostedFileContent or HostedVectorStoreContent.
|
||||
Can also be DataContent, depending on the service used.
|
||||
When supplying a list, it can contain:
|
||||
- Content instances
|
||||
- dicts with properties for Content (e.g., {"uri": "http://example.com", "media_type": "text/html"})
|
||||
- strings (which will be converted to UriContent with media_type "text/plain").
|
||||
If None, defaults to an empty list.
|
||||
description: A description of the tool.
|
||||
additional_properties: Additional properties associated with the tool.
|
||||
**kwargs: Additional keyword arguments to pass to the base class.
|
||||
"""
|
||||
if "name" in kwargs:
|
||||
raise ValueError("The 'name' argument is reserved for the HostedCodeInterpreterTool and cannot be set.")
|
||||
|
||||
self.inputs = _parse_inputs(inputs) if inputs else []
|
||||
|
||||
super().__init__(
|
||||
name="code_interpreter",
|
||||
description=description or "",
|
||||
additional_properties=additional_properties,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class HostedWebSearchTool(BaseTool):
|
||||
"""Represents a web search tool that can be specified to an AI service to enable it to perform web searches.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import HostedWebSearchTool
|
||||
|
||||
# Create a basic web search tool
|
||||
search_tool = HostedWebSearchTool()
|
||||
|
||||
# With location context
|
||||
search_tool_with_location = HostedWebSearchTool(
|
||||
description="Search the web for information",
|
||||
additional_properties={"user_location": {"city": "Seattle", "country": "US"}},
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
description: str | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Initialize a HostedWebSearchTool.
|
||||
|
||||
Keyword Args:
|
||||
description: A description of the tool.
|
||||
additional_properties: Additional properties associated with the tool
|
||||
(e.g., {"user_location": {"city": "Seattle", "country": "US"}}).
|
||||
**kwargs: Additional keyword arguments to pass to the base class.
|
||||
if additional_properties is not provided, any kwargs will be added to additional_properties.
|
||||
"""
|
||||
args: dict[str, Any] = {
|
||||
"name": "web_search",
|
||||
}
|
||||
if additional_properties is not None:
|
||||
args["additional_properties"] = additional_properties
|
||||
elif kwargs:
|
||||
args["additional_properties"] = kwargs
|
||||
if description is not None:
|
||||
args["description"] = description
|
||||
super().__init__(**args)
|
||||
|
||||
|
||||
class HostedImageGenerationToolOptions(TypedDict, total=False):
|
||||
"""Options for HostedImageGenerationTool."""
|
||||
|
||||
count: int
|
||||
image_size: str
|
||||
media_type: str
|
||||
model_id: str
|
||||
response_format: Literal["uri", "data", "hosted"]
|
||||
streaming_count: int
|
||||
|
||||
|
||||
class HostedImageGenerationTool(BaseTool):
|
||||
"""Represents a hosted tool that can be specified to an AI service to enable it to perform image generation."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
options: HostedImageGenerationToolOptions | None = None,
|
||||
description: str | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Initialize a HostedImageGenerationTool."""
|
||||
if "name" in kwargs:
|
||||
raise ValueError("The 'name' argument is reserved for the HostedImageGenerationTool and cannot be set.")
|
||||
|
||||
self.options = options
|
||||
super().__init__(
|
||||
name="image_generation",
|
||||
description=description or "",
|
||||
additional_properties=additional_properties,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class HostedMCPSpecificApproval(TypedDict, total=False):
|
||||
"""Represents the specific mode for a hosted tool.
|
||||
|
||||
When using this mode, the user must specify which tools always or never require approval.
|
||||
This is represented as a dictionary with two optional keys:
|
||||
|
||||
Attributes:
|
||||
always_require_approval: A sequence of tool names that always require approval.
|
||||
never_require_approval: A sequence of tool names that never require approval.
|
||||
"""
|
||||
|
||||
always_require_approval: Collection[str] | None
|
||||
never_require_approval: Collection[str] | None
|
||||
|
||||
|
||||
class HostedMCPTool(BaseTool):
|
||||
"""Represents a MCP tool that is managed and executed by the service.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import HostedMCPTool
|
||||
|
||||
# Create a basic MCP tool
|
||||
mcp_tool = HostedMCPTool(
|
||||
name="my_mcp_tool",
|
||||
url="https://example.com/mcp",
|
||||
)
|
||||
|
||||
# With approval mode and allowed tools
|
||||
mcp_tool_with_approval = HostedMCPTool(
|
||||
name="my_mcp_tool",
|
||||
description="My MCP tool",
|
||||
url="https://example.com/mcp",
|
||||
approval_mode="always_require",
|
||||
allowed_tools=["tool1", "tool2"],
|
||||
headers={"Authorization": "Bearer token"},
|
||||
)
|
||||
|
||||
# With specific approval mode
|
||||
mcp_tool_specific = HostedMCPTool(
|
||||
name="my_mcp_tool",
|
||||
url="https://example.com/mcp",
|
||||
approval_mode={
|
||||
"always_require_approval": ["dangerous_tool"],
|
||||
"never_require_approval": ["safe_tool"],
|
||||
},
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
description: str | None = None,
|
||||
url: AnyUrl | str,
|
||||
approval_mode: Literal["always_require", "never_require"] | HostedMCPSpecificApproval | None = None,
|
||||
allowed_tools: Collection[str] | None = None,
|
||||
headers: dict[str, str] | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Create a hosted MCP tool.
|
||||
|
||||
Keyword Args:
|
||||
name: The name of the tool.
|
||||
description: A description of the tool.
|
||||
url: The URL of the tool.
|
||||
approval_mode: The approval mode for the tool. This can be:
|
||||
- "always_require": The tool always requires approval before use.
|
||||
- "never_require": The tool never requires approval before use.
|
||||
- A dict with keys `always_require_approval` or `never_require_approval`,
|
||||
followed by a sequence of strings with the names of the relevant tools.
|
||||
allowed_tools: A list of tools that are allowed to use this tool.
|
||||
headers: Headers to include in requests to the tool.
|
||||
additional_properties: Additional properties to include in the tool definition.
|
||||
**kwargs: Additional keyword arguments to pass to the base class.
|
||||
"""
|
||||
try:
|
||||
# Validate approval_mode
|
||||
if approval_mode is not None:
|
||||
if isinstance(approval_mode, str):
|
||||
if approval_mode not in ("always_require", "never_require"):
|
||||
raise ValueError(
|
||||
f"Invalid approval_mode: {approval_mode}. "
|
||||
"Must be 'always_require', 'never_require', or a dict with 'always_require_approval' "
|
||||
"or 'never_require_approval' keys."
|
||||
)
|
||||
elif isinstance(approval_mode, dict):
|
||||
# Validate that the dict has sets
|
||||
for key, value in approval_mode.items():
|
||||
if not isinstance(value, set):
|
||||
approval_mode[key] = set(value) # type: ignore
|
||||
|
||||
# Validate allowed_tools
|
||||
if allowed_tools is not None and isinstance(allowed_tools, dict):
|
||||
raise TypeError(
|
||||
f"allowed_tools must be a sequence of strings, not a dict. Got: {type(allowed_tools).__name__}"
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
name=name,
|
||||
description=description or "",
|
||||
additional_properties=additional_properties,
|
||||
**kwargs,
|
||||
)
|
||||
self.url = url if isinstance(url, AnyUrl) else AnyUrl(url)
|
||||
self.approval_mode = approval_mode
|
||||
self.allowed_tools = set(allowed_tools) if allowed_tools else None
|
||||
self.headers = headers
|
||||
except (ValidationError, ValueError, TypeError) as err:
|
||||
raise ToolException(f"Error initializing HostedMCPTool: {err}", inner_exception=err) from err
|
||||
|
||||
|
||||
class HostedFileSearchTool(BaseTool):
|
||||
"""Represents a file search tool that can be specified to an AI service to enable it to perform file searches.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import HostedFileSearchTool
|
||||
|
||||
# Create a basic file search tool
|
||||
file_search = HostedFileSearchTool()
|
||||
|
||||
# With vector store inputs and max results
|
||||
file_search_with_inputs = HostedFileSearchTool(
|
||||
inputs=[{"vector_store_id": "vs_123"}],
|
||||
max_results=10,
|
||||
description="Search files in vector store",
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
inputs: Content | dict[str, Any] | str | list[Content | dict[str, Any] | str] | None = None,
|
||||
max_results: int | None = None,
|
||||
description: str | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Initialize a FileSearchTool.
|
||||
|
||||
Keyword Args:
|
||||
inputs: A list of contents that the tool can accept as input. Defaults to None.
|
||||
This should be one or more HostedVectorStoreContents.
|
||||
When supplying a list, it can contain:
|
||||
- Content instances
|
||||
- dicts with properties for Content (e.g., {"uri": "http://example.com", "media_type": "text/html"})
|
||||
- strings (which will be converted to UriContent with media_type "text/plain").
|
||||
If None, defaults to an empty list.
|
||||
max_results: The maximum number of results to return from the file search.
|
||||
If None, max limit is applied.
|
||||
description: A description of the tool.
|
||||
additional_properties: Additional properties associated with the tool.
|
||||
**kwargs: Additional keyword arguments to pass to the base class.
|
||||
"""
|
||||
if "name" in kwargs:
|
||||
raise ValueError("The 'name' argument is reserved for the HostedFileSearchTool and cannot be set.")
|
||||
|
||||
self.inputs = _parse_inputs(inputs) if inputs else None
|
||||
self.max_results = max_results
|
||||
|
||||
super().__init__(
|
||||
name="file_search",
|
||||
description=description or "",
|
||||
additional_properties=additional_properties,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def _default_histogram() -> Histogram:
|
||||
@@ -576,12 +188,17 @@ class EmptyInputModel(BaseModel):
|
||||
"""An empty input model for functions with no parameters."""
|
||||
|
||||
|
||||
class FunctionTool(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
class FunctionTool(SerializationMixin, Generic[ArgsT, ReturnT]):
|
||||
"""A tool that wraps a Python function to make it callable by AI models.
|
||||
|
||||
This class wraps a Python function to make it callable by AI models with automatic
|
||||
parameter validation and JSON schema generation.
|
||||
|
||||
Attributes:
|
||||
name: The name of the tool.
|
||||
description: A description of the tool, suitable for use in describing the purpose to a model.
|
||||
additional_properties: Additional properties associated with the tool.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
@@ -619,7 +236,12 @@ class FunctionTool(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
"""
|
||||
|
||||
INJECTABLE: ClassVar[set[str]] = {"func"}
|
||||
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"input_model", "_invocation_duration_histogram", "_cached_parameters"}
|
||||
DEFAULT_EXCLUDE: ClassVar[set[str]] = {
|
||||
"additional_properties",
|
||||
"input_model",
|
||||
"_invocation_duration_histogram",
|
||||
"_cached_parameters",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -661,12 +283,14 @@ class FunctionTool(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
the expected arguments.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__(
|
||||
name=name,
|
||||
description=description,
|
||||
additional_properties=additional_properties,
|
||||
**kwargs,
|
||||
)
|
||||
# Core attributes (formerly from BaseTool)
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.additional_properties = additional_properties
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
# FunctionTool-specific attributes
|
||||
self.func = func
|
||||
self._instance = None # Store the instance for bound methods
|
||||
self.input_model = self._resolve_input_model(input_model)
|
||||
@@ -690,6 +314,12 @@ class FunctionTool(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
self._forward_runtime_kwargs = True
|
||||
break
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return a string representation of the tool."""
|
||||
if self.description:
|
||||
return f"{self.__class__.__name__}(name={self.name}, description={self.description})"
|
||||
return f"{self.__class__.__name__}(name={self.name})"
|
||||
|
||||
@property
|
||||
def declaration_only(self) -> bool:
|
||||
"""Indicate whether the function is declaration only (i.e., has no implementation)."""
|
||||
@@ -907,10 +537,10 @@ class FunctionTool(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
|
||||
def _tools_to_dict(
|
||||
tools: (
|
||||
ToolProtocol
|
||||
FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None
|
||||
),
|
||||
) -> list[str | dict[str, Any]] | None:
|
||||
@@ -1464,7 +1094,7 @@ class FunctionInvocationConfiguration(TypedDict, total=False):
|
||||
max_iterations: int
|
||||
max_consecutive_errors_per_request: int
|
||||
terminate_on_unknown_calls: bool
|
||||
additional_tools: Sequence[ToolProtocol]
|
||||
additional_tools: Sequence[FunctionTool]
|
||||
include_detailed_errors: bool
|
||||
|
||||
|
||||
@@ -1638,10 +1268,10 @@ async def _auto_invoke_function(
|
||||
|
||||
|
||||
def _get_tool_map(
|
||||
tools: ToolProtocol
|
||||
tools: FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]],
|
||||
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]],
|
||||
) -> dict[str, FunctionTool[Any, Any]]:
|
||||
tool_list: dict[str, FunctionTool[Any, Any]] = {}
|
||||
for tool_item in tools if isinstance(tools, list) else [tools]:
|
||||
@@ -1659,10 +1289,10 @@ async def _try_execute_function_calls(
|
||||
custom_args: dict[str, Any],
|
||||
attempt_idx: int,
|
||||
function_calls: Sequence[Content],
|
||||
tools: ToolProtocol
|
||||
tools: FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]],
|
||||
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]],
|
||||
config: FunctionInvocationConfiguration,
|
||||
middleware_pipeline: Any = None, # Optional MiddlewarePipeline to avoid circular imports
|
||||
) -> tuple[Sequence[Content], bool]:
|
||||
@@ -1848,8 +1478,8 @@ def _extract_tools(options: dict[str, Any] | None) -> Any:
|
||||
options: The options dict containing chat options.
|
||||
|
||||
Returns:
|
||||
ToolProtocol | Callable[..., Any] | MutableMapping[str, Any] |
|
||||
Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] | None
|
||||
FunctionTool | Callable[..., Any] | MutableMapping[str, Any] |
|
||||
Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None
|
||||
"""
|
||||
if options and isinstance(options, dict):
|
||||
return options.get("tools")
|
||||
|
||||
@@ -15,7 +15,7 @@ from pydantic import BaseModel
|
||||
|
||||
from ._logging import get_logger
|
||||
from ._serialization import SerializationMixin
|
||||
from ._tools import ToolProtocol, tool
|
||||
from ._tools import FunctionTool, tool
|
||||
from .exceptions import AdditionItemMismatch, ContentError
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
@@ -2972,10 +2972,10 @@ class _ChatOptionsBase(TypedDict, total=False):
|
||||
|
||||
# Tool configuration (forward reference to avoid circular import)
|
||||
tools: (
|
||||
ToolProtocol
|
||||
FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None
|
||||
)
|
||||
tool_choice: ToolMode | Literal["auto", "required", "none"]
|
||||
@@ -3065,17 +3065,17 @@ async def validate_chat_options(options: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
def normalize_tools(
|
||||
tools: (
|
||||
ToolProtocol
|
||||
FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None
|
||||
),
|
||||
) -> list[ToolProtocol | MutableMapping[str, Any]]:
|
||||
) -> list[FunctionTool | MutableMapping[str, Any]]:
|
||||
"""Normalize tools into a list.
|
||||
|
||||
Converts callables to FunctionTool objects and ensures all tools are either
|
||||
ToolProtocol instances or MutableMappings.
|
||||
FunctionTool instances or MutableMappings.
|
||||
|
||||
Args:
|
||||
tools: Tools to normalize - can be a single tool, callable, or sequence.
|
||||
@@ -3100,16 +3100,16 @@ def normalize_tools(
|
||||
# List of tools
|
||||
tools = normalize_tools([my_tool, another_tool])
|
||||
"""
|
||||
final_tools: list[ToolProtocol | MutableMapping[str, Any]] = []
|
||||
final_tools: list[FunctionTool | MutableMapping[str, Any]] = []
|
||||
if not tools:
|
||||
return final_tools
|
||||
if not isinstance(tools, Sequence) or isinstance(tools, (str, MutableMapping)):
|
||||
# Single tool (not a sequence, or is a mapping which shouldn't be treated as sequence)
|
||||
if not isinstance(tools, (ToolProtocol, MutableMapping)):
|
||||
if not isinstance(tools, (FunctionTool, MutableMapping)):
|
||||
return [tool(tools)]
|
||||
return [tools]
|
||||
for tool_item in tools:
|
||||
if isinstance(tool_item, (ToolProtocol, MutableMapping)):
|
||||
if isinstance(tool_item, (FunctionTool, MutableMapping)):
|
||||
final_tools.append(tool_item)
|
||||
else:
|
||||
# Convert callable to FunctionTool
|
||||
@@ -3119,17 +3119,17 @@ def normalize_tools(
|
||||
|
||||
async def validate_tools(
|
||||
tools: (
|
||||
ToolProtocol
|
||||
FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None
|
||||
),
|
||||
) -> list[ToolProtocol | MutableMapping[str, Any]]:
|
||||
) -> list[FunctionTool | MutableMapping[str, Any]]:
|
||||
"""Validate and normalize tools into a list.
|
||||
|
||||
Converts callables to FunctionTool objects, expands MCP tools to their constituent
|
||||
functions (connecting them if needed), and ensures all tools are either ToolProtocol
|
||||
functions (connecting them if needed), and ensures all tools are either FunctionTool
|
||||
instances or MutableMappings.
|
||||
|
||||
Args:
|
||||
@@ -3159,7 +3159,7 @@ async def validate_tools(
|
||||
normalized = normalize_tools(tools)
|
||||
|
||||
# Handle MCP tool expansion (async-only)
|
||||
final_tools: list[ToolProtocol | MutableMapping[str, Any]] = []
|
||||
final_tools: list[FunctionTool | MutableMapping[str, Any]] = []
|
||||
for tool_ in normalized:
|
||||
# Import MCPTool here to avoid circular imports
|
||||
from ._mcp import MCPTool
|
||||
|
||||
@@ -13,7 +13,7 @@ from pydantic import BaseModel, SecretStr, ValidationError
|
||||
from .._agents import Agent
|
||||
from .._memory import ContextProvider
|
||||
from .._middleware import MiddlewareTypes
|
||||
from .._tools import FunctionTool, ToolProtocol
|
||||
from .._tools import FunctionTool
|
||||
from .._types import normalize_tools
|
||||
from ..exceptions import ServiceInitializationError
|
||||
from ._assistants_client import OpenAIAssistantsClient
|
||||
@@ -43,10 +43,10 @@ OptionsCoT = TypeVar(
|
||||
)
|
||||
|
||||
_ToolsType = (
|
||||
ToolProtocol
|
||||
FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
)
|
||||
|
||||
|
||||
@@ -221,8 +221,8 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]):
|
||||
description: A description of the assistant.
|
||||
tools: Tools available to the assistant. Can include:
|
||||
- FunctionTool instances or callables decorated with @tool
|
||||
- HostedCodeInterpreterTool for code execution
|
||||
- HostedFileSearchTool for vector store search
|
||||
- Dict-based tools from OpenAIAssistantsClient.get_code_interpreter_tool()
|
||||
- Dict-based tools from OpenAIAssistantsClient.get_file_search_tool()
|
||||
- Raw tool dictionaries
|
||||
metadata: Metadata to attach to the assistant (max 16 key-value pairs).
|
||||
default_options: A TypedDict containing default chat options for the agent.
|
||||
@@ -494,7 +494,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]):
|
||||
self,
|
||||
assistant_tools: list[Any],
|
||||
user_tools: _ToolsType | None,
|
||||
) -> list[ToolProtocol | MutableMapping[str, Any]]:
|
||||
) -> list[FunctionTool | MutableMapping[str, Any]]:
|
||||
"""Merge hosted tools from assistant with user-provided function tools.
|
||||
|
||||
Args:
|
||||
@@ -504,7 +504,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]):
|
||||
Returns:
|
||||
A list of all tools (hosted tools + user function implementations).
|
||||
"""
|
||||
merged: list[ToolProtocol | MutableMapping[str, Any]] = []
|
||||
merged: list[FunctionTool | MutableMapping[str, Any]] = []
|
||||
|
||||
# Add hosted tools from assistant using shared conversion
|
||||
hosted_tools = from_assistant_tools(assistant_tools)
|
||||
@@ -520,7 +520,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]):
|
||||
def _create_chat_agent_from_assistant(
|
||||
self,
|
||||
assistant: Assistant,
|
||||
tools: list[ToolProtocol | MutableMapping[str, Any]] | None,
|
||||
tools: list[FunctionTool | MutableMapping[str, Any]] | None,
|
||||
instructions: str | None,
|
||||
middleware: Sequence[MiddlewareTypes] | None,
|
||||
context_provider: ContextProvider | None,
|
||||
|
||||
@@ -35,8 +35,6 @@ from .._tools import (
|
||||
FunctionInvocationConfiguration,
|
||||
FunctionInvocationLayer,
|
||||
FunctionTool,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
)
|
||||
from .._types import (
|
||||
ChatOptions,
|
||||
@@ -214,6 +212,62 @@ class OpenAIAssistantsClient( # type: ignore[misc]
|
||||
):
|
||||
"""OpenAI Assistants client with middleware, telemetry, and function invocation support."""
|
||||
|
||||
# region Hosted Tool Factory Methods
|
||||
|
||||
@staticmethod
|
||||
def get_code_interpreter_tool() -> dict[str, Any]:
|
||||
"""Create a code interpreter tool configuration for the Assistants API.
|
||||
|
||||
Returns:
|
||||
A dict tool configuration ready to pass to ChatAgent.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.openai import OpenAIAssistantsClient
|
||||
|
||||
# Enable code interpreter
|
||||
tool = OpenAIAssistantsClient.get_code_interpreter_tool()
|
||||
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
return {"type": "code_interpreter"}
|
||||
|
||||
@staticmethod
|
||||
def get_file_search_tool(
|
||||
*,
|
||||
max_num_results: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Create a file search tool configuration for the Assistants API.
|
||||
|
||||
Keyword Args:
|
||||
max_num_results: Maximum number of results to return from file search.
|
||||
|
||||
Returns:
|
||||
A dict tool configuration ready to pass to ChatAgent.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.openai import OpenAIAssistantsClient
|
||||
|
||||
# Basic file search
|
||||
tool = OpenAIAssistantsClient.get_file_search_tool()
|
||||
|
||||
# With result limit
|
||||
tool = OpenAIAssistantsClient.get_file_search_tool(max_num_results=10)
|
||||
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
tool: dict[str, Any] = {"type": "file_search"}
|
||||
|
||||
if max_num_results is not None:
|
||||
tool["file_search"] = {"max_num_results": max_num_results}
|
||||
|
||||
return tool
|
||||
|
||||
# endregion
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -643,16 +697,8 @@ class OpenAIAssistantsClient( # type: ignore[misc]
|
||||
for tool in tools:
|
||||
if isinstance(tool, FunctionTool):
|
||||
tool_definitions.append(tool.to_json_schema_spec()) # type: ignore[reportUnknownArgumentType]
|
||||
elif isinstance(tool, HostedCodeInterpreterTool):
|
||||
tool_definitions.append({"type": "code_interpreter"})
|
||||
elif isinstance(tool, HostedFileSearchTool):
|
||||
params: dict[str, Any] = {
|
||||
"type": "file_search",
|
||||
}
|
||||
if tool.max_results is not None:
|
||||
params["max_num_results"] = tool.max_results
|
||||
tool_definitions.append(params)
|
||||
elif isinstance(tool, MutableMapping):
|
||||
# Pass through dict-based tools directly (from static factory methods)
|
||||
tool_definitions.append(tool)
|
||||
|
||||
if len(tool_definitions) > 0:
|
||||
|
||||
@@ -16,6 +16,7 @@ from openai.types.chat.chat_completion import ChatCompletion, Choice
|
||||
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
|
||||
from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice
|
||||
from openai.types.chat.chat_completion_message_custom_tool_call import ChatCompletionMessageCustomToolCall
|
||||
from openai.types.chat.completion_create_params import WebSearchOptions
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from .._clients import BaseChatClient
|
||||
@@ -25,8 +26,6 @@ from .._tools import (
|
||||
FunctionInvocationConfiguration,
|
||||
FunctionInvocationLayer,
|
||||
FunctionTool,
|
||||
HostedWebSearchTool,
|
||||
ToolProtocol,
|
||||
)
|
||||
from .._types import (
|
||||
ChatOptions,
|
||||
@@ -154,6 +153,58 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
Use ``OpenAIChatClient`` instead for a fully-featured client with all layers applied.
|
||||
"""
|
||||
|
||||
# region Hosted Tool Factory Methods
|
||||
|
||||
@staticmethod
|
||||
def get_web_search_tool(
|
||||
*,
|
||||
web_search_options: WebSearchOptions | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Create a web search tool configuration for the Chat Completions API.
|
||||
|
||||
Note: For the Chat Completions API, web search is passed via the `web_search_options`
|
||||
parameter rather than in the `tools` array. This method returns a dict that can be
|
||||
passed as a tool to ChatAgent, which will handle it appropriately.
|
||||
|
||||
Keyword Args:
|
||||
web_search_options: The full WebSearchOptions configuration. This TypedDict includes:
|
||||
- user_location: Location context with "type" and "approximate" containing
|
||||
"city", "country", "region", "timezone".
|
||||
- search_context_size: One of "low", "medium", "high".
|
||||
|
||||
Returns:
|
||||
A dict configuration that enables web search when passed to ChatAgent.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
|
||||
# Basic web search
|
||||
tool = OpenAIChatClient.get_web_search_tool()
|
||||
|
||||
# With location context
|
||||
tool = OpenAIChatClient.get_web_search_tool(
|
||||
web_search_options={
|
||||
"user_location": {
|
||||
"type": "approximate",
|
||||
"approximate": {"city": "Seattle", "country": "US"},
|
||||
},
|
||||
"search_context_size": "medium",
|
||||
}
|
||||
)
|
||||
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
tool: dict[str, Any] = {"type": "web_search"}
|
||||
|
||||
if web_search_options:
|
||||
tool.update(web_search_options)
|
||||
|
||||
return tool
|
||||
|
||||
# endregion
|
||||
|
||||
@override
|
||||
def _inner_get_response(
|
||||
self,
|
||||
@@ -222,35 +273,35 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
|
||||
# region content creation
|
||||
|
||||
def _prepare_tools_for_openai(self, tools: Sequence[ToolProtocol | MutableMapping[str, Any]]) -> dict[str, Any]:
|
||||
chat_tools: list[dict[str, Any]] = []
|
||||
def _prepare_tools_for_openai(self, tools: Sequence[Any]) -> dict[str, Any]:
|
||||
"""Prepare tools for the OpenAI Chat Completions API.
|
||||
|
||||
Converts FunctionTool to JSON schema format. Web search tools are routed
|
||||
to web_search_options parameter. All other tools pass through unchanged.
|
||||
|
||||
Args:
|
||||
tools: Sequence of tools to prepare.
|
||||
|
||||
Returns:
|
||||
Dict containing tools and optionally web_search_options.
|
||||
"""
|
||||
chat_tools: list[Any] = []
|
||||
web_search_options: dict[str, Any] | None = None
|
||||
for tool in tools:
|
||||
if isinstance(tool, ToolProtocol):
|
||||
match tool:
|
||||
case FunctionTool():
|
||||
chat_tools.append(tool.to_json_schema_spec())
|
||||
case HostedWebSearchTool():
|
||||
web_search_options = (
|
||||
{
|
||||
"user_location": {
|
||||
"approximate": tool.additional_properties.get("user_location", None),
|
||||
"type": "approximate",
|
||||
}
|
||||
}
|
||||
if tool.additional_properties and "user_location" in tool.additional_properties
|
||||
else {}
|
||||
)
|
||||
case _:
|
||||
logger.debug("Unsupported tool passed (type: %s), ignoring", type(tool))
|
||||
if isinstance(tool, FunctionTool):
|
||||
chat_tools.append(tool.to_json_schema_spec())
|
||||
elif isinstance(tool, MutableMapping) and tool.get("type") == "web_search":
|
||||
# Web search is handled via web_search_options, not tools array
|
||||
web_search_options = {k: v for k, v in tool.items() if k != "type"}
|
||||
else:
|
||||
chat_tools.append(tool) # type: ignore[arg-type]
|
||||
ret_dict: dict[str, Any] = {}
|
||||
# Pass through all other tools (dicts, SDK types) unchanged
|
||||
chat_tools.append(tool)
|
||||
result: dict[str, Any] = {}
|
||||
if chat_tools:
|
||||
ret_dict["tools"] = chat_tools
|
||||
result["tools"] = chat_tools
|
||||
if web_search_options is not None:
|
||||
ret_dict["web_search_options"] = web_search_options
|
||||
return ret_dict
|
||||
result["web_search_options"] = web_search_options
|
||||
return result
|
||||
|
||||
def _prepare_options(self, messages: Sequence[Message], options: Mapping[str, Any]) -> dict[str, Any]:
|
||||
# Prepend instructions from options if they exist
|
||||
|
||||
@@ -29,8 +29,8 @@ from openai.types.responses.response_usage import ResponseUsage
|
||||
from openai.types.responses.tool_param import (
|
||||
CodeInterpreter,
|
||||
CodeInterpreterContainerCodeInterpreterToolAuto,
|
||||
ImageGeneration,
|
||||
Mcp,
|
||||
ToolParam,
|
||||
)
|
||||
from openai.types.responses.web_search_tool_param import WebSearchToolParam
|
||||
from pydantic import BaseModel, ValidationError
|
||||
@@ -42,12 +42,6 @@ from .._tools import (
|
||||
FunctionInvocationConfiguration,
|
||||
FunctionInvocationLayer,
|
||||
FunctionTool,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
HostedImageGenerationTool,
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
ToolProtocol,
|
||||
)
|
||||
from .._types import (
|
||||
Annotation,
|
||||
@@ -433,138 +427,334 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
|
||||
|
||||
# region Prep methods
|
||||
|
||||
def _prepare_tools_for_openai(
|
||||
self, tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None
|
||||
) -> list[ToolParam | dict[str, Any]]:
|
||||
response_tools: list[ToolParam | dict[str, Any]] = []
|
||||
if not tools:
|
||||
return response_tools
|
||||
for tool in tools:
|
||||
if isinstance(tool, ToolProtocol):
|
||||
match tool:
|
||||
case HostedMCPTool():
|
||||
response_tools.append(self._prepare_mcp_tool(tool))
|
||||
case HostedCodeInterpreterTool():
|
||||
tool_args: CodeInterpreterContainerCodeInterpreterToolAuto = {"type": "auto"}
|
||||
if tool.inputs:
|
||||
tool_args["file_ids"] = []
|
||||
for tool_input in tool.inputs:
|
||||
if tool_input.type == "hosted_file":
|
||||
tool_args["file_ids"].append(tool_input.file_id) # type: ignore[attr-defined]
|
||||
if not tool_args["file_ids"]:
|
||||
tool_args.pop("file_ids")
|
||||
response_tools.append(
|
||||
CodeInterpreter(
|
||||
type="code_interpreter",
|
||||
container=tool_args,
|
||||
)
|
||||
)
|
||||
case FunctionTool():
|
||||
params = tool.parameters()
|
||||
params["additionalProperties"] = False
|
||||
response_tools.append(
|
||||
FunctionToolParam(
|
||||
name=tool.name,
|
||||
parameters=params,
|
||||
strict=False,
|
||||
type="function",
|
||||
description=tool.description,
|
||||
)
|
||||
)
|
||||
case HostedFileSearchTool():
|
||||
if not tool.inputs:
|
||||
raise ValueError("HostedFileSearchTool requires inputs to be specified.")
|
||||
inputs: list[str] = [
|
||||
inp.vector_store_id # type: ignore[misc]
|
||||
for inp in tool.inputs
|
||||
if inp.type == "hosted_vector_store" # type: ignore[attr-defined]
|
||||
]
|
||||
if not inputs:
|
||||
raise ValueError(
|
||||
"HostedFileSearchTool requires inputs to be of type `HostedVectorStoreContent`."
|
||||
)
|
||||
def _prepare_tools_for_openai(self, tools: Sequence[Any] | None) -> list[Any]:
|
||||
"""Prepare tools for the OpenAI Responses API.
|
||||
|
||||
response_tools.append(
|
||||
FileSearchToolParam(
|
||||
type="file_search",
|
||||
vector_store_ids=inputs,
|
||||
max_num_results=tool.max_results
|
||||
or self.FILE_SEARCH_MAX_RESULTS, # default to max results if not specified
|
||||
)
|
||||
)
|
||||
case HostedWebSearchTool():
|
||||
web_search_tool = WebSearchToolParam(type="web_search")
|
||||
if location := (
|
||||
tool.additional_properties.get("user_location", None)
|
||||
if tool.additional_properties
|
||||
else None
|
||||
):
|
||||
web_search_tool["user_location"] = {
|
||||
"type": "approximate",
|
||||
"city": location.get("city", None),
|
||||
"country": location.get("country", None),
|
||||
"region": location.get("region", None),
|
||||
"timezone": location.get("timezone", None),
|
||||
}
|
||||
if filters := (
|
||||
tool.additional_properties.get("filters", None) if tool.additional_properties else None
|
||||
):
|
||||
web_search_tool["filters"] = filters
|
||||
if search_context_size := (
|
||||
tool.additional_properties.get("search_context_size", None)
|
||||
if tool.additional_properties
|
||||
else None
|
||||
):
|
||||
web_search_tool["search_context_size"] = search_context_size
|
||||
response_tools.append(web_search_tool)
|
||||
case HostedImageGenerationTool():
|
||||
mapped_tool: dict[str, Any] = {"type": "image_generation"}
|
||||
if tool.options:
|
||||
option_mapping = {
|
||||
"image_size": "size",
|
||||
"media_type": "output_format",
|
||||
"model_id": "model",
|
||||
"streaming_count": "partial_images",
|
||||
}
|
||||
# count and response_format are not supported by Responses API
|
||||
for key, value in tool.options.items():
|
||||
mapped_key = option_mapping.get(key, key)
|
||||
mapped_tool[mapped_key] = value
|
||||
if tool.additional_properties:
|
||||
mapped_tool.update(tool.additional_properties)
|
||||
response_tools.append(mapped_tool)
|
||||
case _:
|
||||
logger.debug("Unsupported tool passed (type: %s)", type(tool))
|
||||
Converts FunctionTool to Responses API format. All other tools pass through unchanged.
|
||||
|
||||
Args:
|
||||
tools: Sequence of tools to prepare.
|
||||
|
||||
Returns:
|
||||
List of tool parameters ready for the OpenAI API.
|
||||
"""
|
||||
if not tools:
|
||||
return []
|
||||
response_tools: list[Any] = []
|
||||
for tool in tools:
|
||||
if isinstance(tool, FunctionTool):
|
||||
params = tool.parameters()
|
||||
params["additionalProperties"] = False
|
||||
response_tools.append(
|
||||
FunctionToolParam(
|
||||
name=tool.name,
|
||||
parameters=params,
|
||||
strict=False,
|
||||
type="function",
|
||||
description=tool.description,
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Handle raw dictionary tools
|
||||
tool_dict = tool if isinstance(tool, dict) else dict(tool)
|
||||
response_tools.append(tool_dict)
|
||||
# Pass through all other tools (dicts, SDK types) unchanged
|
||||
response_tools.append(tool)
|
||||
return response_tools
|
||||
|
||||
# region Hosted Tool Factory Methods
|
||||
|
||||
@staticmethod
|
||||
def _prepare_mcp_tool(tool: HostedMCPTool) -> Mcp:
|
||||
"""Get MCP tool from HostedMCPTool."""
|
||||
def get_code_interpreter_tool(
|
||||
*,
|
||||
file_ids: list[str] | None = None,
|
||||
container: Literal["auto"] | CodeInterpreterContainerCodeInterpreterToolAuto = "auto",
|
||||
) -> Any:
|
||||
"""Create a code interpreter tool configuration for the Responses API.
|
||||
|
||||
Keyword Args:
|
||||
file_ids: List of file IDs to make available to the code interpreter.
|
||||
container: Container configuration. Use "auto" for automatic container management,
|
||||
or provide a TypedDict with custom container settings.
|
||||
|
||||
Returns:
|
||||
A CodeInterpreter tool parameter ready to pass to ChatAgent.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
|
||||
# Basic code interpreter
|
||||
tool = OpenAIResponsesClient.get_code_interpreter_tool()
|
||||
|
||||
# With file access
|
||||
tool = OpenAIResponsesClient.get_code_interpreter_tool(file_ids=["file-abc123"])
|
||||
|
||||
# Use with agent
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
container_config: CodeInterpreterContainerCodeInterpreterToolAuto = (
|
||||
container if isinstance(container, dict) else {"type": "auto"}
|
||||
)
|
||||
|
||||
if file_ids:
|
||||
container_config["file_ids"] = file_ids
|
||||
|
||||
return CodeInterpreter(type="code_interpreter", container=container_config)
|
||||
|
||||
@staticmethod
|
||||
def get_web_search_tool(
|
||||
*,
|
||||
user_location: dict[str, str] | None = None,
|
||||
search_context_size: Literal["low", "medium", "high"] | None = None,
|
||||
filters: dict[str, Any] | None = None,
|
||||
) -> Any:
|
||||
"""Create a web search tool configuration for the Responses API.
|
||||
|
||||
Keyword Args:
|
||||
user_location: Location context for search results. Dict with keys like
|
||||
"city", "country", "region", "timezone".
|
||||
search_context_size: Amount of context to include from search results.
|
||||
One of "low", "medium", or "high".
|
||||
filters: Additional search filters.
|
||||
|
||||
Returns:
|
||||
A WebSearchToolParam dict ready to pass to ChatAgent.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
|
||||
# Basic web search
|
||||
tool = OpenAIResponsesClient.get_web_search_tool()
|
||||
|
||||
# With location context
|
||||
tool = OpenAIResponsesClient.get_web_search_tool(
|
||||
user_location={"city": "Seattle", "country": "US"},
|
||||
search_context_size="medium",
|
||||
)
|
||||
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
web_search_tool = WebSearchToolParam(type="web_search")
|
||||
|
||||
if user_location:
|
||||
web_search_tool["user_location"] = {
|
||||
"type": "approximate",
|
||||
"city": user_location.get("city"),
|
||||
"country": user_location.get("country"),
|
||||
"region": user_location.get("region"),
|
||||
"timezone": user_location.get("timezone"),
|
||||
}
|
||||
|
||||
if search_context_size:
|
||||
web_search_tool["search_context_size"] = search_context_size
|
||||
|
||||
if filters:
|
||||
web_search_tool["filters"] = filters # type: ignore[typeddict-item]
|
||||
|
||||
return web_search_tool
|
||||
|
||||
@staticmethod
|
||||
def get_image_generation_tool(
|
||||
*,
|
||||
size: Literal["1024x1024", "1024x1536", "1536x1024", "auto"] | None = None,
|
||||
output_format: Literal["png", "jpeg", "webp"] | None = None,
|
||||
model: Literal["gpt-image-1", "gpt-image-1-mini"] | str | None = None,
|
||||
quality: Literal["low", "medium", "high", "auto"] | None = None,
|
||||
partial_images: int | None = None,
|
||||
background: Literal["transparent", "opaque", "auto"] | None = None,
|
||||
moderation: Literal["auto", "low"] | None = None,
|
||||
output_compression: int | None = None,
|
||||
) -> Any:
|
||||
"""Create an image generation tool configuration for the Responses API.
|
||||
|
||||
Keyword Args:
|
||||
size: Image dimensions. One of "1024x1024", "1024x1536", "1536x1024", or "auto".
|
||||
output_format: Output image format. One of "png", "jpeg", or "webp".
|
||||
model: Model to use for image generation. One of "gpt-image-1" or "gpt-image-1-mini".
|
||||
quality: Image quality level. One of "low", "medium", "high", or "auto".
|
||||
partial_images: Number of partial images to stream during generation.
|
||||
background: Background type. One of "transparent", "opaque", or "auto".
|
||||
moderation: Moderation level. One of "auto" or "low".
|
||||
output_compression: Compression level for output (0-100).
|
||||
|
||||
Returns:
|
||||
An ImageGeneration tool parameter dict ready to pass to ChatAgent.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
|
||||
# Basic image generation
|
||||
tool = OpenAIResponsesClient.get_image_generation_tool()
|
||||
|
||||
# High quality large image
|
||||
tool = OpenAIResponsesClient.get_image_generation_tool(
|
||||
size="1536x1024",
|
||||
quality="high",
|
||||
output_format="png",
|
||||
)
|
||||
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
tool: ImageGeneration = {"type": "image_generation"}
|
||||
|
||||
if size:
|
||||
tool["size"] = size
|
||||
if output_format:
|
||||
tool["output_format"] = output_format
|
||||
if model:
|
||||
tool["model"] = model
|
||||
if quality:
|
||||
tool["quality"] = quality
|
||||
if partial_images is not None:
|
||||
tool["partial_images"] = partial_images
|
||||
if background:
|
||||
tool["background"] = background
|
||||
if moderation:
|
||||
tool["moderation"] = moderation
|
||||
if output_compression is not None:
|
||||
tool["output_compression"] = output_compression
|
||||
|
||||
return tool
|
||||
|
||||
@staticmethod
|
||||
def get_mcp_tool(
|
||||
*,
|
||||
name: str,
|
||||
url: str,
|
||||
description: str | None = None,
|
||||
approval_mode: Literal["always_require", "never_require"] | dict[str, list[str]] | None = None,
|
||||
allowed_tools: list[str] | None = None,
|
||||
headers: dict[str, str] | None = None,
|
||||
) -> Any:
|
||||
"""Create a hosted MCP (Model Context Protocol) tool configuration for the Responses API.
|
||||
|
||||
This configures an MCP server that will be called by OpenAI's service.
|
||||
The tools from this MCP server are executed remotely by OpenAI,
|
||||
not locally by your application.
|
||||
|
||||
Note:
|
||||
For local MCP execution where your application calls the MCP server
|
||||
directly, use the MCP client tools instead of this method.
|
||||
|
||||
Keyword Args:
|
||||
name: A label/name for the MCP server.
|
||||
url: The URL of the MCP server.
|
||||
description: A description of what the MCP server provides.
|
||||
approval_mode: Tool approval mode. Use "always_require" or "never_require" for all tools,
|
||||
or provide a dict with "always_require_approval" and/or "never_require_approval"
|
||||
keys mapping to lists of tool names.
|
||||
allowed_tools: List of tool names that are allowed to be used from this MCP server.
|
||||
headers: HTTP headers to include in requests to the MCP server.
|
||||
|
||||
Returns:
|
||||
An Mcp tool parameter dict ready to pass to ChatAgent.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
|
||||
# Basic MCP tool
|
||||
tool = OpenAIResponsesClient.get_mcp_tool(
|
||||
name="my_mcp",
|
||||
url="https://mcp.example.com",
|
||||
)
|
||||
|
||||
# With approval settings
|
||||
tool = OpenAIResponsesClient.get_mcp_tool(
|
||||
name="github_mcp",
|
||||
url="https://mcp.github.com",
|
||||
description="GitHub MCP server",
|
||||
approval_mode="always_require",
|
||||
headers={"Authorization": "Bearer token"},
|
||||
)
|
||||
|
||||
# With specific tool approvals
|
||||
tool = OpenAIResponsesClient.get_mcp_tool(
|
||||
name="tools_mcp",
|
||||
url="https://tools.example.com",
|
||||
approval_mode={
|
||||
"always_require_approval": ["dangerous_tool"],
|
||||
"never_require_approval": ["safe_tool"],
|
||||
},
|
||||
)
|
||||
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
mcp: Mcp = {
|
||||
"type": "mcp",
|
||||
"server_label": tool.name.replace(" ", "_"),
|
||||
"server_url": str(tool.url),
|
||||
"server_description": tool.description,
|
||||
"headers": tool.headers,
|
||||
"server_label": name.replace(" ", "_"),
|
||||
"server_url": url,
|
||||
}
|
||||
if tool.allowed_tools:
|
||||
mcp["allowed_tools"] = list(tool.allowed_tools)
|
||||
if tool.approval_mode:
|
||||
match tool.approval_mode:
|
||||
case str():
|
||||
mcp["require_approval"] = "always" if tool.approval_mode == "always_require" else "never"
|
||||
case _:
|
||||
if always_require_approvals := tool.approval_mode.get("always_require_approval"):
|
||||
mcp["require_approval"] = {"always": {"tool_names": list(always_require_approvals)}}
|
||||
if never_require_approvals := tool.approval_mode.get("never_require_approval"):
|
||||
mcp["require_approval"] = {"never": {"tool_names": list(never_require_approvals)}}
|
||||
|
||||
if description:
|
||||
mcp["server_description"] = description
|
||||
|
||||
if headers:
|
||||
mcp["headers"] = headers
|
||||
|
||||
if allowed_tools:
|
||||
mcp["allowed_tools"] = allowed_tools
|
||||
|
||||
if approval_mode:
|
||||
if isinstance(approval_mode, str):
|
||||
mcp["require_approval"] = "always" if approval_mode == "always_require" else "never"
|
||||
else:
|
||||
if always_require := approval_mode.get("always_require_approval"):
|
||||
mcp["require_approval"] = {"always": {"tool_names": always_require}}
|
||||
if never_require := approval_mode.get("never_require_approval"):
|
||||
mcp["require_approval"] = {"never": {"tool_names": never_require}}
|
||||
|
||||
return mcp
|
||||
|
||||
@staticmethod
|
||||
def get_file_search_tool(
|
||||
*,
|
||||
vector_store_ids: list[str],
|
||||
max_num_results: int | None = None,
|
||||
) -> Any:
|
||||
"""Create a file search tool configuration for the Responses API.
|
||||
|
||||
Keyword Args:
|
||||
vector_store_ids: List of vector store IDs to search within.
|
||||
max_num_results: Maximum number of results to return. Defaults to 50 if not specified.
|
||||
|
||||
Returns:
|
||||
A FileSearchToolParam dict ready to pass to ChatAgent.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
|
||||
# Basic file search
|
||||
tool = OpenAIResponsesClient.get_file_search_tool(
|
||||
vector_store_ids=["vs_abc123"],
|
||||
)
|
||||
|
||||
# With result limit
|
||||
tool = OpenAIResponsesClient.get_file_search_tool(
|
||||
vector_store_ids=["vs_abc123", "vs_def456"],
|
||||
max_num_results=10,
|
||||
)
|
||||
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
tool = FileSearchToolParam(
|
||||
type="file_search",
|
||||
vector_store_ids=vector_store_ids,
|
||||
)
|
||||
|
||||
if max_num_results is not None:
|
||||
tool["max_num_results"] = max_num_results
|
||||
|
||||
return tool
|
||||
|
||||
# endregion
|
||||
|
||||
async def _prepare_options(
|
||||
self,
|
||||
messages: Sequence[Message],
|
||||
@@ -904,7 +1094,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
|
||||
for annotation in message_content.annotations:
|
||||
match annotation.type:
|
||||
case "file_path":
|
||||
text_content.annotations.append(
|
||||
text_content.annotations.append( # pyright: ignore[reportUnknownMemberType]
|
||||
Annotation(
|
||||
type="citation",
|
||||
file_id=annotation.file_id,
|
||||
@@ -915,7 +1105,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
|
||||
)
|
||||
)
|
||||
case "file_citation":
|
||||
text_content.annotations.append(
|
||||
text_content.annotations.append( # pyright: ignore[reportUnknownMemberType]
|
||||
Annotation(
|
||||
type="citation",
|
||||
url=annotation.filename,
|
||||
@@ -927,7 +1117,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
|
||||
)
|
||||
)
|
||||
case "url_citation":
|
||||
text_content.annotations.append(
|
||||
text_content.annotations.append( # pyright: ignore[reportUnknownMemberType]
|
||||
Annotation(
|
||||
type="citation",
|
||||
title=annotation.title,
|
||||
@@ -943,7 +1133,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
|
||||
)
|
||||
)
|
||||
case "container_file_citation":
|
||||
text_content.annotations.append(
|
||||
text_content.annotations.append( # pyright: ignore[reportUnknownMemberType]
|
||||
Annotation(
|
||||
type="citation",
|
||||
file_id=annotation.file_id,
|
||||
@@ -1107,7 +1297,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
|
||||
"raw_representation": response,
|
||||
}
|
||||
|
||||
if conversation_id := self._get_conversation_id(response, options.get("store")):
|
||||
if conversation_id := self._get_conversation_id(response, options.get("store")): # pyright: ignore[reportUnknownArgumentType]
|
||||
args["conversation_id"] = conversation_id
|
||||
if response.usage and (usage_details := self._parse_usage_from_openai(response.usage)):
|
||||
args["usage_details"] = usage_details
|
||||
@@ -1329,13 +1519,13 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
|
||||
)
|
||||
parsed_output: list[Content] | None = None
|
||||
if result_output:
|
||||
normalized = (
|
||||
normalized = ( # pyright: ignore[reportUnknownVariableType]
|
||||
result_output
|
||||
if isinstance(result_output, Sequence)
|
||||
and not isinstance(result_output, (str, bytes, MutableMapping))
|
||||
else [result_output]
|
||||
)
|
||||
parsed_output = [Content.from_dict(output_item) for output_item in normalized]
|
||||
parsed_output = [Content.from_dict(output_item) for output_item in normalized] # pyright: ignore[reportArgumentType,reportUnknownVariableType]
|
||||
contents.append(
|
||||
Content.from_mcp_server_tool_result(
|
||||
call_id=call_id,
|
||||
|
||||
@@ -26,7 +26,7 @@ from .._logging import get_logger
|
||||
from .._pydantic import AFBaseSettings
|
||||
from .._serialization import SerializationMixin
|
||||
from .._telemetry import APP_INFO, USER_AGENT_KEY, prepend_agent_framework_to_user_agent
|
||||
from .._tools import FunctionTool, HostedCodeInterpreterTool, HostedFileSearchTool, ToolProtocol
|
||||
from .._tools import FunctionTool
|
||||
from ..exceptions import ServiceInitializationError
|
||||
|
||||
logger: logging.Logger = get_logger("agent_framework.openai")
|
||||
@@ -284,12 +284,14 @@ class OpenAIConfigMixin(OpenAIBase):
|
||||
|
||||
|
||||
def to_assistant_tools(
|
||||
tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None,
|
||||
tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Convert Agent Framework tools to OpenAI Assistants API format.
|
||||
|
||||
Handles FunctionTool instances and dict-based tools from static factory methods.
|
||||
|
||||
Args:
|
||||
tools: Normalized tools (from ChatOptions.tools).
|
||||
tools: Sequence of Agent Framework tools.
|
||||
|
||||
Returns:
|
||||
List of tool definitions for OpenAI Assistants API.
|
||||
@@ -302,15 +304,8 @@ def to_assistant_tools(
|
||||
for tool in tools:
|
||||
if isinstance(tool, FunctionTool):
|
||||
tool_definitions.append(tool.to_json_schema_spec())
|
||||
elif isinstance(tool, HostedCodeInterpreterTool):
|
||||
tool_definitions.append({"type": "code_interpreter"})
|
||||
elif isinstance(tool, HostedFileSearchTool):
|
||||
params: dict[str, Any] = {"type": "file_search"}
|
||||
if tool.max_results is not None:
|
||||
params["file_search"] = {"max_num_results": tool.max_results}
|
||||
tool_definitions.append(params)
|
||||
elif isinstance(tool, MutableMapping):
|
||||
# Pass through raw dict definitions
|
||||
# Pass through dict-based tools directly (from static factory methods)
|
||||
tool_definitions.append(dict(tool))
|
||||
|
||||
return tool_definitions
|
||||
@@ -318,11 +313,11 @@ def to_assistant_tools(
|
||||
|
||||
def from_assistant_tools(
|
||||
assistant_tools: list[Any] | None,
|
||||
) -> list[ToolProtocol]:
|
||||
"""Convert OpenAI Assistant tools to Agent Framework format.
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Convert OpenAI Assistant tools to dict-based format.
|
||||
|
||||
This converts hosted tools (code_interpreter, file_search) from an OpenAI
|
||||
Assistant definition back to Agent Framework tool instances.
|
||||
Assistant definition back to dict-based tool definitions.
|
||||
|
||||
Note: Function tools are skipped - user must provide implementations separately.
|
||||
|
||||
@@ -330,12 +325,12 @@ def from_assistant_tools(
|
||||
assistant_tools: Tools from OpenAI Assistant object (assistant.tools).
|
||||
|
||||
Returns:
|
||||
List of Agent Framework tool instances for hosted tools.
|
||||
List of dict-based tool definitions for hosted tools.
|
||||
"""
|
||||
if not assistant_tools:
|
||||
return []
|
||||
|
||||
tools: list[ToolProtocol] = []
|
||||
tools: list[dict[str, Any]] = []
|
||||
|
||||
for tool in assistant_tools:
|
||||
if hasattr(tool, "type"):
|
||||
@@ -346,9 +341,9 @@ def from_assistant_tools(
|
||||
tool_type = None
|
||||
|
||||
if tool_type == "code_interpreter":
|
||||
tools.append(HostedCodeInterpreterTool())
|
||||
tools.append({"type": "code_interpreter"})
|
||||
elif tool_type == "file_search":
|
||||
tools.append(HostedFileSearchTool())
|
||||
tools.append({"type": "file_search"})
|
||||
# Skip function tools - user must provide implementations
|
||||
|
||||
return tools
|
||||
|
||||
@@ -15,7 +15,6 @@ from agent_framework import (
|
||||
AgentThread,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
HostedCodeInterpreterTool,
|
||||
Message,
|
||||
SupportsChatGetResponse,
|
||||
tool,
|
||||
@@ -513,7 +512,7 @@ async def test_azure_assistants_agent_code_interpreter():
|
||||
async with Agent(
|
||||
client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()),
|
||||
instructions="You are a helpful assistant that can write and execute Python code.",
|
||||
tools=[HostedCodeInterpreterTool()],
|
||||
tools=[AzureOpenAIAssistantsClient.get_code_interpreter_tool()],
|
||||
) as agent:
|
||||
# Request code execution
|
||||
response = await agent.run("Write Python code to calculate the factorial of 5 and show the result.")
|
||||
|
||||
@@ -14,10 +14,6 @@ from agent_framework import (
|
||||
AgentResponse,
|
||||
ChatResponse,
|
||||
Content,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
Message,
|
||||
SupportsChatGetResponse,
|
||||
tool,
|
||||
@@ -289,7 +285,7 @@ async def test_integration_web_search() -> None:
|
||||
"messages": "Who are the main characters of Kpop Demon Hunters? Do a web search to find the answer.",
|
||||
"options": {
|
||||
"tool_choice": "auto",
|
||||
"tools": [HostedWebSearchTool()],
|
||||
"tools": [AzureOpenAIResponsesClient.get_web_search_tool()],
|
||||
},
|
||||
"stream": streaming,
|
||||
}
|
||||
@@ -305,17 +301,13 @@ async def test_integration_web_search() -> None:
|
||||
assert "Zoey" in response.text
|
||||
|
||||
# Test that the client will use the web search tool with location
|
||||
additional_properties = {
|
||||
"user_location": {
|
||||
"country": "US",
|
||||
"city": "Seattle",
|
||||
}
|
||||
}
|
||||
content = {
|
||||
"messages": "What is the current weather? Do not ask for my current location.",
|
||||
"options": {
|
||||
"tool_choice": "auto",
|
||||
"tools": [HostedWebSearchTool(additional_properties=additional_properties)],
|
||||
"tools": [
|
||||
AzureOpenAIResponsesClient.get_web_search_tool(user_location={"country": "US", "city": "Seattle"})
|
||||
],
|
||||
},
|
||||
"stream": streaming,
|
||||
}
|
||||
@@ -341,7 +333,12 @@ async def test_integration_client_file_search() -> None:
|
||||
text="What is the weather today? Do a file search to find the answer.",
|
||||
)
|
||||
],
|
||||
options={"tools": [HostedFileSearchTool(inputs=vector_store)], "tool_choice": "auto"},
|
||||
options={
|
||||
"tools": [
|
||||
AzureOpenAIResponsesClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id])
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
},
|
||||
)
|
||||
|
||||
assert "sunny" in response.text.lower()
|
||||
@@ -366,7 +363,12 @@ async def test_integration_client_file_search_streaming() -> None:
|
||||
)
|
||||
],
|
||||
stream=True,
|
||||
options={"tools": [HostedFileSearchTool(inputs=vector_store)], "tool_choice": "auto"},
|
||||
options={
|
||||
"tools": [
|
||||
AzureOpenAIResponsesClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id])
|
||||
],
|
||||
"tool_choice": "auto",
|
||||
},
|
||||
)
|
||||
|
||||
full_response = await response_stream.get_final_response()
|
||||
@@ -379,23 +381,23 @@ async def test_integration_client_file_search_streaming() -> None:
|
||||
@pytest.mark.flaky
|
||||
@skip_if_azure_integration_tests_disabled
|
||||
async def test_integration_client_agent_hosted_mcp_tool() -> None:
|
||||
"""Integration test for HostedMCPTool with Azure Response Agent using Microsoft Learn MCP."""
|
||||
"""Integration test for MCP tool with Azure Response Agent using Microsoft Learn MCP."""
|
||||
client = AzureOpenAIResponsesClient(credential=AzureCliCredential())
|
||||
response = await client.get_response(
|
||||
"How to create an Azure storage account using az cli?",
|
||||
options={
|
||||
# this needs to be high enough to handle the full MCP tool response.
|
||||
"max_tokens": 5000,
|
||||
"tools": HostedMCPTool(
|
||||
"tools": AzureOpenAIResponsesClient.get_mcp_tool(
|
||||
name="Microsoft Learn MCP",
|
||||
url="https://learn.microsoft.com/api/mcp",
|
||||
description="A Microsoft Learn MCP server for documentation questions",
|
||||
approval_mode="never_require",
|
||||
),
|
||||
},
|
||||
)
|
||||
assert isinstance(response, ChatResponse)
|
||||
assert response.text
|
||||
# MCP server may return empty response intermittently - skip test rather than fail
|
||||
if not response.text:
|
||||
pytest.skip("MCP server returned empty response - service-side issue")
|
||||
# Should contain Azure-related content since it's asking about Azure CLI
|
||||
assert any(term in response.text.lower() for term in ["azure", "storage", "account", "cli"])
|
||||
|
||||
@@ -403,13 +405,13 @@ async def test_integration_client_agent_hosted_mcp_tool() -> None:
|
||||
@pytest.mark.flaky
|
||||
@skip_if_azure_integration_tests_disabled
|
||||
async def test_integration_client_agent_hosted_code_interpreter_tool():
|
||||
"""Test Azure Responses Client agent with HostedCodeInterpreterTool through AzureOpenAIResponsesClient."""
|
||||
"""Test Azure Responses Client agent with code interpreter tool."""
|
||||
client = AzureOpenAIResponsesClient(credential=AzureCliCredential())
|
||||
|
||||
response = await client.get_response(
|
||||
"Calculate the sum of numbers from 1 to 10 using Python code.",
|
||||
options={
|
||||
"tools": [HostedCodeInterpreterTool()],
|
||||
"tools": [AzureOpenAIResponsesClient.get_code_interpreter_tool()],
|
||||
},
|
||||
)
|
||||
# Should contain calculation result (sum of 1-10 = 55) or code execution content
|
||||
|
||||
@@ -8,7 +8,6 @@ from typing import Any, Generic
|
||||
from unittest.mock import patch
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pytest import fixture
|
||||
|
||||
from agent_framework import (
|
||||
@@ -21,10 +20,10 @@ from agent_framework import (
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
FunctionInvocationLayer,
|
||||
FunctionTool,
|
||||
Message,
|
||||
ResponseStream,
|
||||
SupportsAgentRun,
|
||||
ToolProtocol,
|
||||
tool,
|
||||
)
|
||||
from agent_framework._clients import OptionsCoT
|
||||
@@ -48,26 +47,20 @@ def chat_history() -> list[Message]:
|
||||
|
||||
|
||||
@fixture
|
||||
def ai_tool() -> ToolProtocol:
|
||||
"""Returns a generic ToolProtocol."""
|
||||
def ai_tool() -> FunctionTool:
|
||||
"""Returns a generic FunctionTool."""
|
||||
|
||||
class GenericTool(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
additional_properties: dict[str, Any] | None = None
|
||||
@tool
|
||||
def generic_tool(name: str) -> str:
|
||||
"""A generic tool that echoes the name."""
|
||||
return f"Hello, {name}"
|
||||
|
||||
def parameters(self) -> dict[str, Any]:
|
||||
"""Return the parameters of the tool as a JSON schema."""
|
||||
return {
|
||||
"name": {"type": "string"},
|
||||
}
|
||||
|
||||
return GenericTool(name="generic_tool", description="A generic tool")
|
||||
return generic_tool
|
||||
|
||||
|
||||
@fixture
|
||||
def tool_tool() -> ToolProtocol:
|
||||
"""Returns a executable ToolProtocol."""
|
||||
def tool_tool() -> FunctionTool:
|
||||
"""Returns a executable FunctionTool."""
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def simple_function(x: int, y: int) -> int:
|
||||
|
||||
@@ -20,11 +20,10 @@ from agent_framework import (
|
||||
Content,
|
||||
Context,
|
||||
ContextProvider,
|
||||
HostedCodeInterpreterTool,
|
||||
FunctionTool,
|
||||
Message,
|
||||
SupportsAgentRun,
|
||||
SupportsChatGetResponse,
|
||||
ToolProtocol,
|
||||
tool,
|
||||
)
|
||||
from agent_framework._agents import _merge_options, _sanitize_agent_name
|
||||
@@ -117,7 +116,7 @@ async def test_chat_client_agent_prepare_thread_and_messages(client: SupportsCha
|
||||
|
||||
|
||||
async def test_prepare_thread_does_not_mutate_agent_chat_options(client: SupportsChatGetResponse) -> None:
|
||||
tool = HostedCodeInterpreterTool()
|
||||
tool = {"type": "code_interpreter"}
|
||||
agent = Agent(client=client, tools=[tool])
|
||||
|
||||
assert agent.default_options.get("tools") is not None
|
||||
@@ -132,7 +131,7 @@ async def test_prepare_thread_does_not_mutate_agent_chat_options(client: Support
|
||||
assert prepared_chat_options.get("tools") is not None
|
||||
assert base_tools is not prepared_chat_options["tools"]
|
||||
|
||||
prepared_chat_options["tools"].append(HostedCodeInterpreterTool()) # type: ignore[arg-type]
|
||||
prepared_chat_options["tools"].append({"type": "code_interpreter"}) # type: ignore[arg-type]
|
||||
assert len(agent.default_options["tools"]) == 1
|
||||
|
||||
|
||||
@@ -144,7 +143,7 @@ async def test_chat_client_agent_update_thread_id(chat_client_base: SupportsChat
|
||||
chat_client_base.run_responses = [mock_response]
|
||||
agent = Agent(
|
||||
client=chat_client_base,
|
||||
tools=HostedCodeInterpreterTool(),
|
||||
tools={"type": "code_interpreter"},
|
||||
)
|
||||
thread = agent.get_new_thread()
|
||||
|
||||
@@ -207,7 +206,7 @@ async def test_chat_client_agent_author_name_is_used_from_response(chat_client_b
|
||||
)
|
||||
]
|
||||
|
||||
agent = Agent(client=chat_client_base, tools=HostedCodeInterpreterTool())
|
||||
agent = Agent(client=chat_client_base, tools={"type": "code_interpreter"})
|
||||
|
||||
result = await agent.run("Hello")
|
||||
assert result.text == "test response"
|
||||
@@ -806,7 +805,7 @@ def test_sanitize_agent_name_replaces_invalid_chars():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_get_new_thread(chat_client_base: SupportsChatGetResponse, tool_tool: ToolProtocol):
|
||||
async def test_agent_get_new_thread(chat_client_base: SupportsChatGetResponse, tool_tool: FunctionTool):
|
||||
"""Test that get_new_thread returns a new AgentThread."""
|
||||
agent = Agent(client=chat_client_base, tools=[tool_tool])
|
||||
|
||||
@@ -818,7 +817,7 @@ async def test_agent_get_new_thread(chat_client_base: SupportsChatGetResponse, t
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_get_new_thread_with_context_provider(
|
||||
chat_client_base: SupportsChatGetResponse, tool_tool: ToolProtocol
|
||||
chat_client_base: SupportsChatGetResponse, tool_tool: FunctionTool
|
||||
):
|
||||
"""Test that get_new_thread passes context_provider to the thread."""
|
||||
|
||||
@@ -837,7 +836,7 @@ async def test_agent_get_new_thread_with_context_provider(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_get_new_thread_with_service_thread_id(
|
||||
chat_client_base: SupportsChatGetResponse, tool_tool: ToolProtocol
|
||||
chat_client_base: SupportsChatGetResponse, tool_tool: FunctionTool
|
||||
):
|
||||
"""Test that get_new_thread passes kwargs like service_thread_id to the thread."""
|
||||
agent = Agent(client=chat_client_base, tools=[tool_tool])
|
||||
@@ -849,7 +848,7 @@ async def test_agent_get_new_thread_with_service_thread_id(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_deserialize_thread(chat_client_base: SupportsChatGetResponse, tool_tool: ToolProtocol):
|
||||
async def test_agent_deserialize_thread(chat_client_base: SupportsChatGetResponse, tool_tool: FunctionTool):
|
||||
"""Test deserialize_thread restores a thread from serialized state."""
|
||||
agent = Agent(client=chat_client_base, tools=[tool_tool])
|
||||
|
||||
|
||||
@@ -8,6 +8,11 @@ from agent_framework import (
|
||||
ChatResponse,
|
||||
Message,
|
||||
SupportsChatGetResponse,
|
||||
SupportsCodeInterpreterTool,
|
||||
SupportsFileSearchTool,
|
||||
SupportsImageGenerationTool,
|
||||
SupportsMCPTool,
|
||||
SupportsWebSearchTool,
|
||||
)
|
||||
|
||||
|
||||
@@ -73,3 +78,66 @@ 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_client_supports_web_search_only():
|
||||
"""Test that OpenAIChatClient only supports web search tool."""
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
|
||||
assert not isinstance(OpenAIChatClient, SupportsCodeInterpreterTool)
|
||||
assert isinstance(OpenAIChatClient, SupportsWebSearchTool)
|
||||
assert not isinstance(OpenAIChatClient, SupportsImageGenerationTool)
|
||||
assert not isinstance(OpenAIChatClient, SupportsMCPTool)
|
||||
assert not isinstance(OpenAIChatClient, 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
|
||||
|
||||
@@ -18,7 +18,6 @@ from agent_framework import (
|
||||
MCPStreamableHTTPTool,
|
||||
MCPWebsocketTool,
|
||||
Message,
|
||||
ToolProtocol,
|
||||
)
|
||||
from agent_framework._mcp import (
|
||||
MCPTool,
|
||||
@@ -744,7 +743,10 @@ def test_get_input_model_from_mcp_prompt():
|
||||
async def test_local_mcp_server_initialization():
|
||||
"""Test MCPTool initialization."""
|
||||
server = MCPTool(name="test_server")
|
||||
assert isinstance(server, ToolProtocol)
|
||||
# MCPTool has the same core attributes as FunctionTool
|
||||
assert hasattr(server, "name")
|
||||
assert hasattr(server, "description")
|
||||
assert hasattr(server, "additional_properties")
|
||||
assert server.name == "test_server"
|
||||
assert server.session is None
|
||||
assert server.functions == []
|
||||
@@ -795,7 +797,9 @@ async def test_local_mcp_server_load_functions():
|
||||
return None
|
||||
|
||||
server = TestServer(name="test_server")
|
||||
assert isinstance(server, ToolProtocol)
|
||||
# MCPTool has the same core attributes as FunctionTool
|
||||
assert hasattr(server, "name")
|
||||
assert hasattr(server, "description")
|
||||
async with server:
|
||||
await server.load_tools()
|
||||
assert len(server.functions) == 1
|
||||
|
||||
@@ -10,10 +10,6 @@ from pydantic import BaseModel, ValidationError
|
||||
from agent_framework import (
|
||||
Content,
|
||||
FunctionTool,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedImageGenerationTool,
|
||||
HostedMCPTool,
|
||||
ToolProtocol,
|
||||
tool,
|
||||
)
|
||||
from agent_framework._tools import (
|
||||
@@ -21,7 +17,6 @@ from agent_framework._tools import (
|
||||
_parse_annotation,
|
||||
_parse_inputs,
|
||||
)
|
||||
from agent_framework.exceptions import ToolException
|
||||
from agent_framework.observability import OtelAttr
|
||||
|
||||
# region FunctionTool and tool decorator tests
|
||||
@@ -35,7 +30,6 @@ def test_tool_decorator():
|
||||
"""A simple function that adds two numbers."""
|
||||
return x + y
|
||||
|
||||
assert isinstance(test_tool, ToolProtocol)
|
||||
assert isinstance(test_tool, FunctionTool)
|
||||
assert test_tool.name == "test_tool"
|
||||
assert test_tool.description == "A test tool"
|
||||
@@ -56,7 +50,6 @@ def test_tool_decorator_without_args():
|
||||
"""A simple function that adds two numbers."""
|
||||
return x + y
|
||||
|
||||
assert isinstance(test_tool, ToolProtocol)
|
||||
assert isinstance(test_tool, FunctionTool)
|
||||
assert test_tool.name == "test_tool"
|
||||
assert test_tool.description == "A simple function that adds two numbers."
|
||||
@@ -174,7 +167,7 @@ def test_tool_without_args():
|
||||
"""A simple function that adds two numbers."""
|
||||
return 1 + 2
|
||||
|
||||
assert isinstance(test_tool, ToolProtocol)
|
||||
assert isinstance(test_tool, FunctionTool)
|
||||
assert isinstance(test_tool, FunctionTool)
|
||||
assert test_tool.name == "test_tool"
|
||||
assert test_tool.description == "A simple function that adds two numbers."
|
||||
@@ -194,7 +187,6 @@ async def test_tool_decorator_with_async():
|
||||
"""An async function that adds two numbers."""
|
||||
return x + y
|
||||
|
||||
assert isinstance(async_test_tool, ToolProtocol)
|
||||
assert isinstance(async_test_tool, FunctionTool)
|
||||
assert async_test_tool.name == "async_test_tool"
|
||||
assert async_test_tool.description == "An async test tool"
|
||||
@@ -218,7 +210,6 @@ def test_tool_decorator_in_class():
|
||||
|
||||
test_tool = my_tools().test_tool
|
||||
|
||||
assert isinstance(test_tool, ToolProtocol)
|
||||
assert isinstance(test_tool, FunctionTool)
|
||||
assert test_tool.name == "test_tool"
|
||||
assert test_tool.description == "A test tool"
|
||||
@@ -701,30 +692,7 @@ def test_tool_serialization():
|
||||
assert restored_tool_2(10, 4) == 6
|
||||
|
||||
|
||||
# region HostedCodeInterpreterTool and _parse_inputs
|
||||
|
||||
|
||||
def test_hosted_code_interpreter_tool_default():
|
||||
"""Test HostedCodeInterpreterTool with default parameters."""
|
||||
tool = HostedCodeInterpreterTool()
|
||||
|
||||
assert tool.name == "code_interpreter"
|
||||
assert tool.inputs == []
|
||||
assert tool.description == ""
|
||||
assert tool.additional_properties is None
|
||||
assert str(tool) == "HostedCodeInterpreterTool(name=code_interpreter)"
|
||||
|
||||
|
||||
def test_hosted_code_interpreter_tool_with_description():
|
||||
"""Test HostedCodeInterpreterTool with description and additional properties."""
|
||||
tool = HostedCodeInterpreterTool(
|
||||
description="A test code interpreter",
|
||||
additional_properties={"version": "1.0", "language": "python"},
|
||||
)
|
||||
|
||||
assert tool.name == "code_interpreter"
|
||||
assert tool.description == "A test code interpreter"
|
||||
assert tool.additional_properties == {"version": "1.0", "language": "python"}
|
||||
# region _parse_inputs tests
|
||||
|
||||
|
||||
def test_parse_inputs_none():
|
||||
@@ -853,185 +821,7 @@ def test_parse_inputs_unsupported_type():
|
||||
_parse_inputs(123)
|
||||
|
||||
|
||||
def test_hosted_code_interpreter_tool_with_string_input():
|
||||
"""Test HostedCodeInterpreterTool with string input."""
|
||||
|
||||
tool = HostedCodeInterpreterTool(inputs="http://example.com")
|
||||
|
||||
assert len(tool.inputs) == 1
|
||||
assert tool.inputs[0].type == "uri"
|
||||
assert tool.inputs[0].uri == "http://example.com"
|
||||
|
||||
|
||||
def test_hosted_code_interpreter_tool_with_dict_inputs():
|
||||
"""Test HostedCodeInterpreterTool with dictionary inputs."""
|
||||
|
||||
inputs = [{"uri": "http://example.com", "media_type": "text/html"}, {"file_id": "file-123"}]
|
||||
|
||||
tool = HostedCodeInterpreterTool(inputs=inputs)
|
||||
|
||||
assert len(tool.inputs) == 2
|
||||
assert tool.inputs[0].type == "uri"
|
||||
assert tool.inputs[0].uri == "http://example.com"
|
||||
assert tool.inputs[0].media_type == "text/html"
|
||||
assert tool.inputs[1].type == "hosted_file"
|
||||
assert tool.inputs[1].file_id == "file-123"
|
||||
|
||||
|
||||
def test_hosted_code_interpreter_tool_with_ai_contents():
|
||||
"""Test HostedCodeInterpreterTool with Content instances."""
|
||||
|
||||
inputs = [Content.from_text(text="Hello, world!"), Content.from_data(data=b"test", media_type="text/plain")]
|
||||
|
||||
tool = HostedCodeInterpreterTool(inputs=inputs)
|
||||
|
||||
assert len(tool.inputs) == 2
|
||||
assert tool.inputs[0].type == "text"
|
||||
assert tool.inputs[0].text == "Hello, world!"
|
||||
assert tool.inputs[1].type == "data"
|
||||
assert tool.inputs[1].media_type == "text/plain"
|
||||
|
||||
|
||||
def test_hosted_code_interpreter_tool_with_single_input():
|
||||
"""Test HostedCodeInterpreterTool with single input (not in list)."""
|
||||
|
||||
input_dict = {"file_id": "file-single"}
|
||||
tool = HostedCodeInterpreterTool(inputs=input_dict)
|
||||
|
||||
assert len(tool.inputs) == 1
|
||||
assert tool.inputs[0].type == "hosted_file"
|
||||
assert tool.inputs[0].file_id == "file-single"
|
||||
|
||||
|
||||
def test_hosted_code_interpreter_tool_with_unknown_input():
|
||||
"""Test HostedCodeInterpreterTool with single unknown input."""
|
||||
with pytest.raises(ValueError, match="Unsupported input type"):
|
||||
HostedCodeInterpreterTool(inputs={"hosted_file": "file-single"})
|
||||
|
||||
|
||||
def test_hosted_image_generation_tool_defaults():
|
||||
"""HostedImageGenerationTool should default name and empty description."""
|
||||
tool = HostedImageGenerationTool()
|
||||
|
||||
assert tool.name == "image_generation"
|
||||
assert tool.description == ""
|
||||
assert tool.options is None
|
||||
assert str(tool) == "HostedImageGenerationTool(name=image_generation)"
|
||||
|
||||
|
||||
def test_hosted_image_generation_tool_with_options():
|
||||
"""HostedImageGenerationTool should store options."""
|
||||
tool = HostedImageGenerationTool(
|
||||
description="Generate images",
|
||||
options={"format": "png", "size": "1024x1024"},
|
||||
additional_properties={"quality": "high"},
|
||||
)
|
||||
|
||||
assert tool.name == "image_generation"
|
||||
assert tool.description == "Generate images"
|
||||
assert tool.options == {"format": "png", "size": "1024x1024"}
|
||||
assert tool.additional_properties == {"quality": "high"}
|
||||
|
||||
|
||||
# region HostedMCPTool tests
|
||||
|
||||
|
||||
def test_hosted_mcp_tool_with_other_fields():
|
||||
"""Test creating a HostedMCPTool with a specific approval dict, headers and additional properties."""
|
||||
tool = HostedMCPTool(
|
||||
name="mcp-tool",
|
||||
url="https://mcp.example",
|
||||
description="A test MCP tool",
|
||||
headers={"x": "y"},
|
||||
additional_properties={"p": 1},
|
||||
)
|
||||
|
||||
assert tool.name == "mcp-tool"
|
||||
# pydantic AnyUrl preserves as string-like
|
||||
assert str(tool.url).startswith("https://")
|
||||
assert tool.headers == {"x": "y"}
|
||||
assert tool.additional_properties == {"p": 1}
|
||||
assert tool.description == "A test MCP tool"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"approval_mode",
|
||||
[
|
||||
"always_require",
|
||||
"never_require",
|
||||
{
|
||||
"always_require_approval": {"toolA"},
|
||||
"never_require_approval": {"toolB"},
|
||||
},
|
||||
{
|
||||
"always_require_approval": ["toolA"],
|
||||
"never_require_approval": ("toolB",),
|
||||
},
|
||||
],
|
||||
ids=["always_require", "never_require", "specific", "specific_with_parsing"],
|
||||
)
|
||||
def test_hosted_mcp_tool_with_approval_mode(approval_mode: str | dict[str, Any]):
|
||||
"""Test creating a HostedMCPTool with a specific approval dict, headers and additional properties."""
|
||||
tool = HostedMCPTool(name="mcp-tool", url="https://mcp.example", approval_mode=approval_mode)
|
||||
|
||||
assert tool.name == "mcp-tool"
|
||||
# pydantic AnyUrl preserves as string-like
|
||||
assert str(tool.url).startswith("https://")
|
||||
if not isinstance(approval_mode, dict):
|
||||
assert tool.approval_mode == approval_mode
|
||||
else:
|
||||
# approval_mode parsed to sets
|
||||
assert isinstance(tool.approval_mode["always_require_approval"], set)
|
||||
assert isinstance(tool.approval_mode["never_require_approval"], set)
|
||||
assert "toolA" in tool.approval_mode["always_require_approval"]
|
||||
assert "toolB" in tool.approval_mode["never_require_approval"]
|
||||
|
||||
|
||||
def test_hosted_mcp_tool_invalid_approval_mode_raises():
|
||||
"""Invalid approval_mode string should raise ServiceInitializationError."""
|
||||
with pytest.raises(ToolException):
|
||||
HostedMCPTool(name="bad", url="https://x", approval_mode="invalid_mode")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"tools",
|
||||
[
|
||||
{"toolA", "toolB"},
|
||||
("toolA", "toolB"),
|
||||
["toolA", "toolB"],
|
||||
["toolA", "toolB", "toolA"],
|
||||
],
|
||||
ids=[
|
||||
"set",
|
||||
"tuple",
|
||||
"list",
|
||||
"list_with_duplicates",
|
||||
],
|
||||
)
|
||||
def test_hosted_mcp_tool_with_allowed_tools(tools: list[str] | tuple[str, ...] | set[str]):
|
||||
"""Test creating a HostedMCPTool with a list of allowed tools."""
|
||||
tool = HostedMCPTool(
|
||||
name="mcp-tool",
|
||||
url="https://mcp.example",
|
||||
allowed_tools=tools,
|
||||
)
|
||||
|
||||
assert tool.name == "mcp-tool"
|
||||
# pydantic AnyUrl preserves as string-like
|
||||
assert str(tool.url).startswith("https://")
|
||||
# approval_mode parsed to set
|
||||
assert isinstance(tool.allowed_tools, set)
|
||||
assert tool.allowed_tools == {"toolA", "toolB"}
|
||||
|
||||
|
||||
def test_hosted_mcp_tool_with_dict_of_allowed_tools():
|
||||
"""Test creating a HostedMCPTool with a dict of allowed tools."""
|
||||
with pytest.raises(ToolException):
|
||||
HostedMCPTool(
|
||||
name="mcp-tool",
|
||||
url="https://mcp.example",
|
||||
allowed_tools={"toolA": "Tool A", "toolC": "Tool C"},
|
||||
)
|
||||
# endregion
|
||||
|
||||
|
||||
async def test_ai_function_with_kwargs_injection():
|
||||
|
||||
@@ -18,11 +18,11 @@ from agent_framework import (
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
FunctionTool,
|
||||
Message,
|
||||
ResponseStream,
|
||||
TextSpanRegion,
|
||||
ToolMode,
|
||||
ToolProtocol,
|
||||
UsageDetails,
|
||||
detect_media_type_from_base64,
|
||||
merge_chat_options,
|
||||
@@ -41,26 +41,20 @@ from agent_framework.exceptions import ContentError
|
||||
|
||||
|
||||
@fixture
|
||||
def ai_tool() -> ToolProtocol:
|
||||
"""Returns a generic ToolProtocol."""
|
||||
def ai_tool() -> FunctionTool:
|
||||
"""Returns a generic FunctionTool."""
|
||||
|
||||
class GenericTool(BaseModel):
|
||||
name: str
|
||||
description: str | None = None
|
||||
additional_properties: dict[str, Any] | None = None
|
||||
@tool
|
||||
def generic_tool(name: str) -> str:
|
||||
"""A generic tool that echoes the name."""
|
||||
return f"Hello, {name}"
|
||||
|
||||
def parameters(self) -> dict[str, Any]:
|
||||
"""Return the parameters of the tool as a JSON schema."""
|
||||
return {
|
||||
"name": {"type": "string"},
|
||||
}
|
||||
|
||||
return GenericTool(name="generic_tool", description="A generic tool")
|
||||
return generic_tool
|
||||
|
||||
|
||||
@fixture
|
||||
def tool_tool() -> ToolProtocol:
|
||||
"""Returns a executable ToolProtocol."""
|
||||
def tool_tool() -> FunctionTool:
|
||||
"""Returns a executable FunctionTool."""
|
||||
|
||||
@tool
|
||||
def simple_function(x: int, y: int) -> int:
|
||||
|
||||
@@ -8,9 +8,9 @@ import pytest
|
||||
from openai.types.beta.assistant import Assistant
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from agent_framework import Agent, HostedCodeInterpreterTool, HostedFileSearchTool, normalize_tools, tool
|
||||
from agent_framework import Agent, normalize_tools, tool
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
from agent_framework.openai import OpenAIAssistantProvider
|
||||
from agent_framework.openai import OpenAIAssistantProvider, OpenAIAssistantsClient
|
||||
from agent_framework.openai._shared import from_assistant_tools, to_assistant_tools
|
||||
|
||||
# region Test Helpers
|
||||
@@ -269,7 +269,7 @@ class TestOpenAIAssistantProviderCreateAgent:
|
||||
await provider.create_agent(
|
||||
name="CodeAgent",
|
||||
model="gpt-4",
|
||||
tools=[HostedCodeInterpreterTool()],
|
||||
tools=[OpenAIAssistantsClient.get_code_interpreter_tool()],
|
||||
)
|
||||
|
||||
call_kwargs = mock_async_openai.beta.assistants.create.call_args.kwargs
|
||||
@@ -282,7 +282,7 @@ class TestOpenAIAssistantProviderCreateAgent:
|
||||
await provider.create_agent(
|
||||
name="SearchAgent",
|
||||
model="gpt-4",
|
||||
tools=[HostedFileSearchTool()],
|
||||
tools=[OpenAIAssistantsClient.get_file_search_tool()],
|
||||
)
|
||||
|
||||
call_kwargs = mock_async_openai.beta.assistants.create.call_args.kwargs
|
||||
@@ -295,7 +295,7 @@ class TestOpenAIAssistantProviderCreateAgent:
|
||||
await provider.create_agent(
|
||||
name="SearchAgent",
|
||||
model="gpt-4",
|
||||
tools=[HostedFileSearchTool(max_results=10)],
|
||||
tools=[OpenAIAssistantsClient.get_file_search_tool(max_num_results=10)],
|
||||
)
|
||||
|
||||
call_kwargs = mock_async_openai.beta.assistants.create.call_args.kwargs
|
||||
@@ -309,7 +309,11 @@ class TestOpenAIAssistantProviderCreateAgent:
|
||||
await provider.create_agent(
|
||||
name="MultiToolAgent",
|
||||
model="gpt-4",
|
||||
tools=[get_weather, HostedCodeInterpreterTool(), HostedFileSearchTool()],
|
||||
tools=[
|
||||
get_weather,
|
||||
OpenAIAssistantsClient.get_code_interpreter_tool(),
|
||||
OpenAIAssistantsClient.get_file_search_tool(),
|
||||
],
|
||||
)
|
||||
|
||||
call_kwargs = mock_async_openai.beta.assistants.create.call_args.kwargs
|
||||
@@ -564,22 +568,22 @@ class TestToolConversion:
|
||||
assert api_tools[0]["function"]["name"] == "get_weather"
|
||||
|
||||
def test_to_assistant_tools_code_interpreter(self) -> None:
|
||||
"""Test HostedCodeInterpreterTool conversion."""
|
||||
api_tools = to_assistant_tools([HostedCodeInterpreterTool()])
|
||||
"""Test code_interpreter tool dict conversion."""
|
||||
api_tools = to_assistant_tools([OpenAIAssistantsClient.get_code_interpreter_tool()])
|
||||
|
||||
assert len(api_tools) == 1
|
||||
assert api_tools[0] == {"type": "code_interpreter"}
|
||||
|
||||
def test_to_assistant_tools_file_search(self) -> None:
|
||||
"""Test HostedFileSearchTool conversion."""
|
||||
api_tools = to_assistant_tools([HostedFileSearchTool()])
|
||||
"""Test file_search tool dict conversion."""
|
||||
api_tools = to_assistant_tools([OpenAIAssistantsClient.get_file_search_tool()])
|
||||
|
||||
assert len(api_tools) == 1
|
||||
assert api_tools[0]["type"] == "file_search"
|
||||
|
||||
def test_to_assistant_tools_file_search_with_max_results(self) -> None:
|
||||
"""Test HostedFileSearchTool with max_results conversion."""
|
||||
api_tools = to_assistant_tools([HostedFileSearchTool(max_results=5)])
|
||||
"""Test file_search tool with max_results conversion."""
|
||||
api_tools = to_assistant_tools([OpenAIAssistantsClient.get_file_search_tool(max_num_results=5)])
|
||||
|
||||
assert api_tools[0]["file_search"]["max_num_results"] == 5
|
||||
|
||||
@@ -605,7 +609,7 @@ class TestToolConversion:
|
||||
tools = from_assistant_tools(assistant_tools)
|
||||
|
||||
assert len(tools) == 1
|
||||
assert isinstance(tools[0], HostedCodeInterpreterTool)
|
||||
assert tools[0] == {"type": "code_interpreter"}
|
||||
|
||||
def test_from_assistant_tools_file_search(self) -> None:
|
||||
"""Test converting file_search tool from OpenAI format."""
|
||||
@@ -614,7 +618,7 @@ class TestToolConversion:
|
||||
tools = from_assistant_tools(assistant_tools)
|
||||
|
||||
assert len(tools) == 1
|
||||
assert isinstance(tools[0], HostedFileSearchTool)
|
||||
assert tools[0] == {"type": "file_search"}
|
||||
|
||||
def test_from_assistant_tools_function_skipped(self) -> None:
|
||||
"""Test that function tools are skipped (no implementations)."""
|
||||
@@ -707,7 +711,7 @@ class TestToolMerging:
|
||||
merged = provider._merge_tools(assistant_tools, None) # type: ignore[reportPrivateUsage]
|
||||
|
||||
assert len(merged) == 1
|
||||
assert isinstance(merged[0], HostedCodeInterpreterTool)
|
||||
assert merged[0] == {"type": "code_interpreter"}
|
||||
|
||||
def test_merge_file_search(self, mock_async_openai: MagicMock) -> None:
|
||||
"""Test merging file search tool."""
|
||||
@@ -717,7 +721,7 @@ class TestToolMerging:
|
||||
merged = provider._merge_tools(assistant_tools, None) # type: ignore[reportPrivateUsage]
|
||||
|
||||
assert len(merged) == 1
|
||||
assert isinstance(merged[0], HostedFileSearchTool)
|
||||
assert merged[0] == {"type": "file_search"}
|
||||
|
||||
def test_merge_with_user_tools(self, mock_async_openai: MagicMock) -> None:
|
||||
"""Test merging hosted and user tools."""
|
||||
@@ -727,7 +731,7 @@ class TestToolMerging:
|
||||
merged = provider._merge_tools(assistant_tools, [get_weather]) # type: ignore[reportPrivateUsage]
|
||||
|
||||
assert len(merged) == 2
|
||||
assert isinstance(merged[0], HostedCodeInterpreterTool)
|
||||
assert merged[0] == {"type": "code_interpreter"}
|
||||
|
||||
def test_merge_multiple_hosted_tools(self, mock_async_openai: MagicMock) -> None:
|
||||
"""Test merging multiple hosted tools."""
|
||||
|
||||
@@ -18,8 +18,6 @@ from agent_framework import (
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
Message,
|
||||
SupportsChatGetResponse,
|
||||
tool,
|
||||
@@ -736,11 +734,11 @@ def test_prepare_options_with_tool_tool(mock_async_openai: MagicMock) -> None:
|
||||
|
||||
|
||||
def test_prepare_options_with_code_interpreter(mock_async_openai: MagicMock) -> None:
|
||||
"""Test _prepare_options with HostedCodeInterpreterTool."""
|
||||
"""Test _prepare_options with code interpreter tool."""
|
||||
client = create_test_openai_assistants_client(mock_async_openai)
|
||||
|
||||
# Create a real HostedCodeInterpreterTool
|
||||
code_tool = HostedCodeInterpreterTool()
|
||||
# Create a code interpreter tool dict
|
||||
code_tool = OpenAIAssistantsClient.get_code_interpreter_tool()
|
||||
|
||||
options = {
|
||||
"tools": [code_tool],
|
||||
@@ -831,12 +829,12 @@ def test_prepare_options_required_function(mock_async_openai: MagicMock) -> None
|
||||
|
||||
|
||||
def test_prepare_options_with_file_search_tool(mock_async_openai: MagicMock) -> None:
|
||||
"""Test _prepare_options with HostedFileSearchTool."""
|
||||
"""Test _prepare_options with file_search tool."""
|
||||
|
||||
client = create_test_openai_assistants_client(mock_async_openai)
|
||||
|
||||
# Create a HostedFileSearchTool with max_results
|
||||
file_search_tool = HostedFileSearchTool(max_results=10)
|
||||
# Create a file_search tool with max_results
|
||||
file_search_tool = OpenAIAssistantsClient.get_file_search_tool(max_num_results=10)
|
||||
|
||||
options = {
|
||||
"tools": [file_search_tool],
|
||||
@@ -851,7 +849,7 @@ def test_prepare_options_with_file_search_tool(mock_async_openai: MagicMock) ->
|
||||
# Check file search tool was set correctly
|
||||
assert "tools" in run_options
|
||||
assert len(run_options["tools"]) == 1
|
||||
expected_tool = {"type": "file_search", "max_num_results": 10}
|
||||
expected_tool = {"type": "file_search", "file_search": {"max_num_results": 10}}
|
||||
assert run_options["tools"][0] == expected_tool
|
||||
assert run_options["tool_choice"] == "auto"
|
||||
|
||||
@@ -1182,7 +1180,7 @@ async def test_file_search() -> None:
|
||||
response = await openai_assistants_client.get_response(
|
||||
messages=messages,
|
||||
options={
|
||||
"tools": [HostedFileSearchTool()],
|
||||
"tools": [OpenAIAssistantsClient.get_file_search_tool()],
|
||||
"tool_resources": {"file_search": {"vector_store_ids": [vector_store.vector_store_id]}},
|
||||
},
|
||||
)
|
||||
@@ -1209,7 +1207,7 @@ async def test_file_search_streaming() -> None:
|
||||
stream=True,
|
||||
messages=messages,
|
||||
options={
|
||||
"tools": [HostedFileSearchTool()],
|
||||
"tools": [OpenAIAssistantsClient.get_file_search_tool()],
|
||||
"tool_resources": {"file_search": {"vector_store_ids": [vector_store.vector_store_id]}},
|
||||
},
|
||||
)
|
||||
@@ -1346,7 +1344,7 @@ async def test_openai_assistants_agent_code_interpreter():
|
||||
async with Agent(
|
||||
client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL),
|
||||
instructions="You are a helpful assistant that can write and execute Python code.",
|
||||
tools=[HostedCodeInterpreterTool()],
|
||||
tools=[OpenAIAssistantsClient.get_code_interpreter_tool()],
|
||||
) as agent:
|
||||
# Request code execution
|
||||
response = await agent.run("Write Python code to calculate the factorial of 5 and show the result.")
|
||||
|
||||
@@ -15,10 +15,8 @@ from pytest import param
|
||||
from agent_framework import (
|
||||
ChatResponse,
|
||||
Content,
|
||||
HostedWebSearchTool,
|
||||
Message,
|
||||
SupportsChatGetResponse,
|
||||
ToolProtocol,
|
||||
prepare_function_call_results,
|
||||
tool,
|
||||
)
|
||||
@@ -172,18 +170,22 @@ async def test_content_filter_exception_handling(openai_unit_test_env: dict[str,
|
||||
|
||||
|
||||
def test_unsupported_tool_handling(openai_unit_test_env: dict[str, str]) -> None:
|
||||
"""Test that unsupported tool types are handled correctly."""
|
||||
"""Test that unsupported tool types are passed through unchanged."""
|
||||
client = OpenAIChatClient()
|
||||
|
||||
# Create a mock ToolProtocol that's not a FunctionTool
|
||||
unsupported_tool = MagicMock(spec=ToolProtocol)
|
||||
unsupported_tool.__class__.__name__ = "UnsupportedAITool"
|
||||
# Create a random object that's not a FunctionTool, dict, or callable
|
||||
# This simulates an unsupported tool type that gets passed through
|
||||
class UnsupportedTool:
|
||||
pass
|
||||
|
||||
# This should ignore the unsupported ToolProtocol and return empty list
|
||||
unsupported_tool = UnsupportedTool()
|
||||
|
||||
# Unsupported tools are passed through for the API to handle/reject
|
||||
result = client._prepare_tools_for_openai([unsupported_tool]) # type: ignore
|
||||
assert result == {}
|
||||
assert "tools" in result
|
||||
assert len(result["tools"]) == 1
|
||||
|
||||
# Also test with a non-ToolProtocol that should be converted to dict
|
||||
# Also test with a dict-based tool that should be passed through
|
||||
dict_tool = {"type": "function", "name": "test"}
|
||||
result = client._prepare_tools_for_openai([dict_tool]) # type: ignore
|
||||
assert result["tools"] == [dict_tool]
|
||||
@@ -770,8 +772,8 @@ def test_prepare_tools_with_web_search_no_location(openai_unit_test_env: dict[st
|
||||
"""Test preparing web search tool without user location."""
|
||||
client = OpenAIChatClient()
|
||||
|
||||
# Web search tool without additional_properties
|
||||
web_search_tool = HostedWebSearchTool()
|
||||
# Web search tool using static method
|
||||
web_search_tool = OpenAIChatClient.get_web_search_tool()
|
||||
|
||||
result = client._prepare_tools_for_openai([web_search_tool])
|
||||
|
||||
@@ -1071,11 +1073,13 @@ async def test_integration_web_search() -> None:
|
||||
client = OpenAIChatClient(model_id="gpt-4o-search-preview")
|
||||
|
||||
for streaming in [False, True]:
|
||||
# Use static method for web search tool
|
||||
web_search_tool = OpenAIChatClient.get_web_search_tool()
|
||||
content = {
|
||||
"messages": "Who are the main characters of Kpop Demon Hunters? Do a web search to find the answer.",
|
||||
"options": {
|
||||
"tool_choice": "auto",
|
||||
"tools": [HostedWebSearchTool()],
|
||||
"tools": [web_search_tool],
|
||||
},
|
||||
}
|
||||
if streaming:
|
||||
@@ -1090,17 +1094,19 @@ async def test_integration_web_search() -> None:
|
||||
assert "Zoey" in response.text
|
||||
|
||||
# Test that the client will use the web search tool with location
|
||||
additional_properties = {
|
||||
"user_location": {
|
||||
"country": "US",
|
||||
"city": "Seattle",
|
||||
web_search_tool_with_location = OpenAIChatClient.get_web_search_tool(
|
||||
web_search_options={
|
||||
"user_location": {
|
||||
"type": "approximate",
|
||||
"approximate": {"country": "US", "city": "Seattle"},
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
content = {
|
||||
"messages": "What is the current weather? Do not ask for my current location.",
|
||||
"options": {
|
||||
"tool_choice": "auto",
|
||||
"tools": [HostedWebSearchTool(additional_properties=additional_properties)],
|
||||
"tools": [web_search_tool_with_location],
|
||||
},
|
||||
}
|
||||
if streaming:
|
||||
|
||||
@@ -31,11 +31,6 @@ from agent_framework import (
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
HostedImageGenerationTool,
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
Message,
|
||||
SupportsChatGetResponse,
|
||||
tool,
|
||||
@@ -236,19 +231,18 @@ async def test_get_response_with_all_parameters() -> None:
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_web_search_tool_with_location() -> None:
|
||||
"""Test HostedWebSearchTool with location parameters."""
|
||||
"""Test web search tool with location parameters."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
# Test web search tool with location
|
||||
web_search_tool = HostedWebSearchTool(
|
||||
additional_properties={
|
||||
"user_location": {
|
||||
"country": "US",
|
||||
"city": "Seattle",
|
||||
"region": "WA",
|
||||
"timezone": "America/Los_Angeles",
|
||||
}
|
||||
# Test web search tool with location using static method
|
||||
web_search_tool = OpenAIResponsesClient.get_web_search_tool(
|
||||
user_location={
|
||||
"city": "Seattle",
|
||||
"country": "US",
|
||||
"region": "WA",
|
||||
"timezone": "America/Los_Angeles",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -260,38 +254,21 @@ async def test_web_search_tool_with_location() -> None:
|
||||
)
|
||||
|
||||
|
||||
async def test_file_search_tool_with_invalid_inputs() -> None:
|
||||
"""Test HostedFileSearchTool with invalid vector store inputs."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
# Test with invalid inputs type (should trigger ValueError)
|
||||
file_search_tool = HostedFileSearchTool(inputs=[Content.from_hosted_file(file_id="invalid")])
|
||||
|
||||
# Should raise an error due to invalid inputs
|
||||
with pytest.raises(ValueError, match="HostedFileSearchTool requires inputs to be of type"):
|
||||
await client.get_response(
|
||||
messages=[Message(role="user", text="Search files")],
|
||||
options={"tools": [file_search_tool]},
|
||||
)
|
||||
|
||||
|
||||
async def test_code_interpreter_tool_variations() -> None:
|
||||
"""Test HostedCodeInterpreterTool with and without file inputs."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
# Test code interpreter without files
|
||||
code_tool_empty = HostedCodeInterpreterTool()
|
||||
# Test code interpreter using static method
|
||||
code_tool = OpenAIResponsesClient.get_code_interpreter_tool()
|
||||
|
||||
with pytest.raises(ServiceResponseException):
|
||||
await client.get_response(
|
||||
messages=[Message(role="user", text="Run some code")],
|
||||
options={"tools": [code_tool_empty]},
|
||||
messages=[Message("user", ["Run some code"])],
|
||||
options={"tools": [code_tool]},
|
||||
)
|
||||
|
||||
# Test code interpreter with files
|
||||
code_tool_with_files = HostedCodeInterpreterTool(
|
||||
inputs=[Content.from_hosted_file(file_id="file1"), Content.from_hosted_file(file_id="file2")]
|
||||
)
|
||||
# Test code interpreter with files using static method
|
||||
code_tool_with_files = OpenAIResponsesClient.get_code_interpreter_tool(file_ids=["file1", "file2"])
|
||||
|
||||
with pytest.raises(ServiceResponseException):
|
||||
await client.get_response(
|
||||
@@ -319,18 +296,20 @@ async def test_content_filter_exception() -> None:
|
||||
assert "content error" in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hosted_file_search_tool_validation() -> None:
|
||||
"""Test get_response HostedFileSearchTool validation."""
|
||||
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
# Test HostedFileSearchTool without inputs (should raise ValueError)
|
||||
empty_file_search_tool = HostedFileSearchTool()
|
||||
# Test file search tool with vector store IDs
|
||||
file_search_tool = OpenAIResponsesClient.get_file_search_tool(vector_store_ids=["vs_123"])
|
||||
|
||||
with pytest.raises((ValueError, ServiceInvalidRequestError)):
|
||||
# Test using file search tool - may raise various exceptions depending on API response
|
||||
with pytest.raises((ValueError, ServiceInvalidRequestError, ServiceResponseException)):
|
||||
await client.get_response(
|
||||
messages=[Message(role="user", text="Test")],
|
||||
options={"tools": [empty_file_search_tool]},
|
||||
messages=[Message("user", ["Test"])],
|
||||
options={"tools": [file_search_tool]},
|
||||
)
|
||||
|
||||
|
||||
@@ -1074,18 +1053,17 @@ def test_streaming_chunk_with_usage_only() -> None:
|
||||
assert update.contents[0].usage_details["total_token_count"] == 75
|
||||
|
||||
|
||||
def test_prepare_tools_for_openai_with_hosted_mcp() -> None:
|
||||
"""Test that HostedMCPTool is converted to the correct response tool dict."""
|
||||
def test_prepare_tools_for_openai_with_mcp() -> None:
|
||||
"""Test that MCP tool dict is converted to the correct response tool dict."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
tool = HostedMCPTool(
|
||||
name="My MCP",
|
||||
# Use static method to create MCP tool
|
||||
tool = OpenAIResponsesClient.get_mcp_tool(
|
||||
name="My_MCP",
|
||||
url="https://mcp.example",
|
||||
description="An MCP server",
|
||||
approval_mode={"always_require_approval": ["tool_a", "tool_b"]},
|
||||
allowed_tools={"tool_a", "tool_b"},
|
||||
allowed_tools=["tool_a", "tool_b"],
|
||||
headers={"X-Test": "yes"},
|
||||
additional_properties={"custom": "value"},
|
||||
approval_mode={"always_require_approval": ["tool_a", "tool_b"]},
|
||||
)
|
||||
|
||||
resp_tools = client._prepare_tools_for_openai([tool])
|
||||
@@ -1097,7 +1075,6 @@ def test_prepare_tools_for_openai_with_hosted_mcp() -> None:
|
||||
assert mcp["server_label"] == "My_MCP"
|
||||
# server_url may be normalized to include a trailing slash by the client
|
||||
assert str(mcp["server_url"]).rstrip("/") == "https://mcp.example"
|
||||
assert mcp["server_description"] == "An MCP server"
|
||||
assert mcp["headers"]["X-Test"] == "yes"
|
||||
assert set(mcp["allowed_tools"]) == {"tool_a", "tool_b"}
|
||||
# approval mapping created from approval_mode dict
|
||||
@@ -1258,13 +1235,15 @@ def test_prepare_tools_for_openai_with_raw_image_generation_minimal() -> None:
|
||||
assert len(image_tool) == 1
|
||||
|
||||
|
||||
def test_prepare_tools_for_openai_with_hosted_image_generation() -> None:
|
||||
"""Test HostedImageGenerationTool conversion."""
|
||||
def test_prepare_tools_for_openai_with_image_generation_options() -> None:
|
||||
"""Test image generation tool conversion with options."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
tool = HostedImageGenerationTool(
|
||||
description="Generate images",
|
||||
options={"output_format": "png", "size": "512x512"},
|
||||
additional_properties={"quality": "high"},
|
||||
|
||||
# Use static method to create image generation tool
|
||||
tool = OpenAIResponsesClient.get_image_generation_tool(
|
||||
output_format="png",
|
||||
size="512x512",
|
||||
quality="high",
|
||||
)
|
||||
|
||||
resp_tools = client._prepare_tools_for_openai([tool])
|
||||
@@ -2324,11 +2303,13 @@ async def test_integration_web_search() -> None:
|
||||
client = OpenAIResponsesClient(model_id="gpt-5")
|
||||
|
||||
for streaming in [False, True]:
|
||||
# Use static method for web search tool
|
||||
web_search_tool = OpenAIResponsesClient.get_web_search_tool()
|
||||
content = {
|
||||
"messages": "Who are the main characters of Kpop Demon Hunters? Do a web search to find the answer.",
|
||||
"options": {
|
||||
"tool_choice": "auto",
|
||||
"tools": [HostedWebSearchTool()],
|
||||
"tools": [web_search_tool],
|
||||
},
|
||||
}
|
||||
if streaming:
|
||||
@@ -2343,17 +2324,14 @@ async def test_integration_web_search() -> None:
|
||||
assert "Zoey" in response.text
|
||||
|
||||
# Test that the client will use the web search tool with location
|
||||
additional_properties = {
|
||||
"user_location": {
|
||||
"country": "US",
|
||||
"city": "Seattle",
|
||||
}
|
||||
}
|
||||
web_search_tool_with_location = OpenAIResponsesClient.get_web_search_tool(
|
||||
user_location={"country": "US", "city": "Seattle"},
|
||||
)
|
||||
content = {
|
||||
"messages": "What is the current weather? Do not ask for my current location.",
|
||||
"options": {
|
||||
"tool_choice": "auto",
|
||||
"tools": [HostedWebSearchTool(additional_properties=additional_properties)],
|
||||
"tools": [web_search_tool_with_location],
|
||||
},
|
||||
}
|
||||
if streaming:
|
||||
@@ -2375,7 +2353,9 @@ async def test_integration_file_search() -> None:
|
||||
assert isinstance(openai_responses_client, SupportsChatGetResponse)
|
||||
|
||||
file_id, vector_store = await create_vector_store(openai_responses_client)
|
||||
# Test that the client will use the web search tool
|
||||
# Use static method for file search tool
|
||||
file_search_tool = OpenAIResponsesClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id])
|
||||
# Test that the client will use the file search tool
|
||||
response = await openai_responses_client.get_response(
|
||||
messages=[
|
||||
Message(
|
||||
@@ -2385,7 +2365,7 @@ async def test_integration_file_search() -> None:
|
||||
],
|
||||
options={
|
||||
"tool_choice": "auto",
|
||||
"tools": [HostedFileSearchTool(inputs=vector_store)],
|
||||
"tools": [file_search_tool],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2406,9 +2386,10 @@ async def test_integration_streaming_file_search() -> None:
|
||||
assert isinstance(openai_responses_client, SupportsChatGetResponse)
|
||||
|
||||
file_id, vector_store = await create_vector_store(openai_responses_client)
|
||||
# Use static method for file search tool
|
||||
file_search_tool = OpenAIResponsesClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id])
|
||||
# Test that the client will use the web search tool
|
||||
response = openai_responses_client.get_response(
|
||||
stream=True,
|
||||
response = openai_responses_client.get_streaming_response(
|
||||
messages=[
|
||||
Message(
|
||||
role="user",
|
||||
@@ -2417,7 +2398,7 @@ async def test_integration_streaming_file_search() -> None:
|
||||
],
|
||||
options={
|
||||
"tool_choice": "auto",
|
||||
"tools": [HostedFileSearchTool(inputs=vector_store)],
|
||||
"tools": [file_search_tool],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user