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
@@ -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