mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [BREAKING] changed AIFunction to FunctionTool and @ai_function to @tool (#3413)
* changed AIFunction to FunctionTool and @ai_function to @tool * test and mypy fixes * mypy fix * switch function tool to always_require * fix noop * fix github copilot imports * test fixes * fix ollama test * fixes for tests * fix tests * reverted change to always_require and extended timeout * fix test
This commit is contained in:
committed by
GitHub
Unverified
parent
15b43f2abe
commit
a7d924a7d2
@@ -12,12 +12,12 @@ from typing import TYPE_CHECKING, Any, Generic, cast
|
||||
|
||||
import httpx
|
||||
from agent_framework import (
|
||||
AIFunction,
|
||||
BaseChatClient,
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
FunctionTool,
|
||||
use_chat_middleware,
|
||||
use_function_invocation,
|
||||
)
|
||||
@@ -239,7 +239,7 @@ class AGUIChatClient(BaseChatClient[TAGUIChatOptions], Generic[TAGUIChatOptions]
|
||||
if any(getattr(tool, "name", None) == tool_name for tool in config.additional_tools):
|
||||
return
|
||||
|
||||
placeholder: AIFunction[Any, Any] = AIFunction(
|
||||
placeholder: FunctionTool[Any, Any] = FunctionTool(
|
||||
name=tool_name,
|
||||
description="Server-managed tool placeholder (AG-UI)",
|
||||
func=None,
|
||||
|
||||
@@ -10,7 +10,7 @@ from dataclasses import asdict, is_dataclass
|
||||
from datetime import date, datetime
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import AgentResponseUpdate, AIFunction, ChatResponseUpdate, Role, ToolProtocol
|
||||
from agent_framework import AgentResponseUpdate, ChatResponseUpdate, FunctionTool, Role, ToolProtocol
|
||||
|
||||
# Role mapping constants
|
||||
AGUI_TO_FRAMEWORK_ROLE: dict[str, Role] = {
|
||||
@@ -160,10 +160,10 @@ def make_json_safe(obj: Any) -> Any: # noqa: ANN401
|
||||
|
||||
def convert_agui_tools_to_agent_framework(
|
||||
agui_tools: list[dict[str, Any]] | None,
|
||||
) -> list[AIFunction[Any, Any]] | None:
|
||||
"""Convert AG-UI tool definitions to Agent Framework AIFunction declarations.
|
||||
) -> list[FunctionTool[Any, Any]] | None:
|
||||
"""Convert AG-UI tool definitions to Agent Framework FunctionTool declarations.
|
||||
|
||||
Creates declaration-only AIFunction instances (no executable implementation).
|
||||
Creates declaration-only FunctionTool instances (no executable implementation).
|
||||
These are used to tell the LLM about available tools. The actual execution
|
||||
happens on the client side via @use_function_invocation.
|
||||
|
||||
@@ -174,18 +174,18 @@ def convert_agui_tools_to_agent_framework(
|
||||
agui_tools: List of AG-UI tool definitions with name, description, parameters
|
||||
|
||||
Returns:
|
||||
List of AIFunction declarations, or None if no tools provided
|
||||
List of FunctionTool declarations, or None if no tools provided
|
||||
"""
|
||||
if not agui_tools:
|
||||
return None
|
||||
|
||||
result: list[AIFunction[Any, Any]] = []
|
||||
result: list[FunctionTool[Any, Any]] = []
|
||||
for tool_def in agui_tools:
|
||||
# Create declaration-only AIFunction (func=None means no implementation)
|
||||
# Create declaration-only FunctionTool (func=None means no implementation)
|
||||
# When func=None, the declaration_only property returns True,
|
||||
# which tells @use_function_invocation to return the function call
|
||||
# without executing it (so it can be sent back to the client)
|
||||
func: AIFunction[Any, Any] = AIFunction(
|
||||
func: FunctionTool[Any, Any] = FunctionTool(
|
||||
name=tool_def.get("name", ""),
|
||||
description=tool_def.get("description", ""),
|
||||
func=None, # CRITICAL: Makes declaration_only=True
|
||||
@@ -229,24 +229,24 @@ def convert_tools_to_agui_format(
|
||||
|
||||
results: list[dict[str, Any]] = []
|
||||
|
||||
for tool in tool_list:
|
||||
if isinstance(tool, dict):
|
||||
for tool_item in tool_list:
|
||||
if isinstance(tool_item, dict):
|
||||
# Already in dict format, pass through
|
||||
results.append(tool) # type: ignore[arg-type]
|
||||
elif isinstance(tool, AIFunction):
|
||||
# Convert AIFunction to AG-UI tool format
|
||||
results.append(tool_item) # type: ignore[arg-type]
|
||||
elif isinstance(tool_item, FunctionTool):
|
||||
# Convert FunctionTool to AG-UI tool format
|
||||
results.append(
|
||||
{
|
||||
"name": tool.name,
|
||||
"description": tool.description,
|
||||
"parameters": tool.parameters(),
|
||||
"name": tool_item.name,
|
||||
"description": tool_item.description,
|
||||
"parameters": tool_item.parameters(),
|
||||
}
|
||||
)
|
||||
elif callable(tool):
|
||||
# Convert callable to AIFunction first, then to AG-UI format
|
||||
from agent_framework import ai_function
|
||||
elif callable(tool_item):
|
||||
# Convert callable to FunctionTool first, then to AG-UI format
|
||||
from agent_framework import tool
|
||||
|
||||
ai_func = ai_function(tool)
|
||||
ai_func = tool(tool_item)
|
||||
results.append(
|
||||
{
|
||||
"name": ai_func.name,
|
||||
@@ -254,11 +254,11 @@ def convert_tools_to_agui_format(
|
||||
"parameters": ai_func.parameters(),
|
||||
}
|
||||
)
|
||||
elif isinstance(tool, ToolProtocol):
|
||||
elif isinstance(tool_item, ToolProtocol):
|
||||
# Handle other ToolProtocol implementations
|
||||
# For now, we'll skip non-AIFunction tools as they may not have
|
||||
# For now, we'll skip non-FunctionTool instances as they may not have
|
||||
# the parameters() method. This matches .NET behavior which only
|
||||
# converts AIFunctionDeclaration instances.
|
||||
# converts FunctionToolDeclaration instances.
|
||||
continue
|
||||
|
||||
return results if results else None
|
||||
|
||||
@@ -187,11 +187,11 @@ The package uses a clean, orchestrator-based architecture:
|
||||
You can create your own agent factories following the same pattern as the examples:
|
||||
|
||||
```python
|
||||
from agent_framework import ChatAgent, ai_function
|
||||
from agent_framework import ChatAgent, tool
|
||||
from agent_framework import ChatClientProtocol
|
||||
from agent_framework.ag_ui import AgentFrameworkAgent
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def my_tool(param: str) -> str:
|
||||
"""My custom tool."""
|
||||
return f"Result: {param}"
|
||||
@@ -294,9 +294,9 @@ wrapped_agent = AgentFrameworkAgent(
|
||||
Human-in-the-loop is automatically handled when tools are marked for approval:
|
||||
|
||||
```python
|
||||
from agent_framework import ai_function
|
||||
from agent_framework import tool
|
||||
|
||||
@ai_function(approval_mode="always_require")
|
||||
@tool(approval_mode="always_require")
|
||||
def sensitive_action(param: str) -> str:
|
||||
"""This action requires user approval."""
|
||||
return f"Executed with {param}"
|
||||
|
||||
+2
-2
@@ -2,11 +2,11 @@
|
||||
|
||||
"""Example agent demonstrating predictive state updates with document writing."""
|
||||
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, ai_function
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, tool
|
||||
from agent_framework.ag_ui import AgentFrameworkAgent
|
||||
|
||||
|
||||
@ai_function(approval_mode="always_require")
|
||||
@tool(approval_mode="always_require")
|
||||
def write_document(document: str) -> str:
|
||||
"""Write a document. Use markdown formatting to format the document.
|
||||
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, ai_function
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, tool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class TaskStep(BaseModel):
|
||||
status: StepStatus = Field(default=StepStatus.ENABLED, description="Whether the step is enabled or disabled")
|
||||
|
||||
|
||||
@ai_function(
|
||||
@tool(
|
||||
name="generate_task_steps",
|
||||
description="Generate execution steps for a task",
|
||||
approval_mode="always_require",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, ai_function
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, tool
|
||||
from agent_framework.ag_ui import AgentFrameworkAgent
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -49,7 +49,7 @@ class Recipe(BaseModel):
|
||||
instructions: list[str] = Field(..., description="Step-by-step cooking instructions")
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def update_recipe(recipe: Recipe) -> str:
|
||||
"""Update the recipe with new or modified content.
|
||||
|
||||
|
||||
+4
-4
@@ -5,11 +5,11 @@
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, ai_function
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, tool
|
||||
from agent_framework.ag_ui import AgentFrameworkAgent
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
async def research_topic(topic: str) -> str:
|
||||
"""Research a topic and generate a comprehensive report.
|
||||
|
||||
@@ -35,7 +35,7 @@ async def research_topic(topic: str) -> str:
|
||||
return f"Research report on '{topic}':\n" + "\n".join(results)
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
async def create_presentation(title: str, num_slides: int) -> str:
|
||||
"""Create a presentation with multiple slides.
|
||||
|
||||
@@ -55,7 +55,7 @@ async def create_presentation(title: str, num_slides: int) -> str:
|
||||
return f"Created presentation '{title}' with {num_slides} slides:\n" + "\n".join(slides)
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
async def analyze_data(dataset: str) -> str:
|
||||
"""Analyze a dataset and produce insights.
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, ai_function
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, tool
|
||||
from agent_framework.ag_ui import AgentFrameworkAgent
|
||||
|
||||
|
||||
@ai_function(approval_mode="always_require")
|
||||
@tool(approval_mode="always_require")
|
||||
def create_calendar_event(title: str, date: str, time: str) -> str:
|
||||
"""Create a calendar event.
|
||||
|
||||
@@ -23,7 +23,7 @@ def create_calendar_event(title: str, date: str, time: str) -> str:
|
||||
return f"Calendar event '{title}' created for {date} at {time}"
|
||||
|
||||
|
||||
@ai_function(approval_mode="always_require")
|
||||
@tool(approval_mode="always_require")
|
||||
def send_email(to: str, subject: str, body: str) -> str:
|
||||
"""Send an email.
|
||||
|
||||
@@ -38,7 +38,7 @@ def send_email(to: str, subject: str, body: str) -> str:
|
||||
return f"Email sent to {to} with subject '{subject}'"
|
||||
|
||||
|
||||
@ai_function(approval_mode="always_require")
|
||||
@tool(approval_mode="always_require")
|
||||
def book_meeting_room(room_name: str, date: str, start_time: str, end_time: str) -> str:
|
||||
"""Book a meeting room.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from ag_ui.core import (
|
||||
TextMessageStartEvent,
|
||||
ToolCallStartEvent,
|
||||
)
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, ChatMessage, Content, ai_function
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, ChatMessage, Content, tool
|
||||
from agent_framework.ag_ui import AgentFrameworkAgent
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -39,7 +39,7 @@ class TaskStep(BaseModel):
|
||||
status: StepStatus = Field(default=StepStatus.PENDING, description="The status of the step")
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def generate_task_steps(steps: list[TaskStep]) -> str:
|
||||
"""Generate a list of task steps for completing a task.
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
"""Example agent demonstrating Tool-based Generative UI (Feature 5)."""
|
||||
|
||||
import sys
|
||||
from typing import Any, TypedDict
|
||||
from typing import TYPE_CHECKING, Any, TypedDict
|
||||
|
||||
from agent_framework import AIFunction, ChatAgent, ChatClientProtocol, ChatOptions
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, FunctionTool
|
||||
from agent_framework.ag_ui import AgentFrameworkAgent
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
@@ -13,8 +13,11 @@ if sys.version_info >= (3, 13):
|
||||
else:
|
||||
from typing_extensions import TypeVar # type: ignore # pragma: no cover
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agent_framework import ChatOptions
|
||||
|
||||
# Declaration-only tools (func=None) - actual rendering happens on the client side
|
||||
generate_haiku = AIFunction[Any, str](
|
||||
generate_haiku = FunctionTool[Any, str](
|
||||
name="generate_haiku",
|
||||
description="""Generate a haiku with image and gradient background (FRONTEND_RENDER).
|
||||
|
||||
@@ -62,7 +65,7 @@ generate_haiku = AIFunction[Any, str](
|
||||
},
|
||||
)
|
||||
|
||||
create_chart = AIFunction[Any, str](
|
||||
create_chart = FunctionTool[Any, str](
|
||||
name="create_chart",
|
||||
description="""Create an interactive chart (FRONTEND_RENDER).
|
||||
|
||||
@@ -90,7 +93,7 @@ create_chart = AIFunction[Any, str](
|
||||
},
|
||||
)
|
||||
|
||||
display_timeline = AIFunction[Any, str](
|
||||
display_timeline = FunctionTool[Any, str](
|
||||
name="display_timeline",
|
||||
description="""Display an interactive timeline (FRONTEND_RENDER).
|
||||
|
||||
@@ -118,7 +121,7 @@ display_timeline = AIFunction[Any, str](
|
||||
},
|
||||
)
|
||||
|
||||
show_comparison_table = AIFunction[Any, str](
|
||||
show_comparison_table = FunctionTool[Any, str](
|
||||
name="show_comparison_table",
|
||||
description="""Show a comparison table (FRONTEND_RENDER).
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, ai_function
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, tool
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def get_weather(location: str) -> dict[str, Any]:
|
||||
"""Get the current weather for a location.
|
||||
|
||||
@@ -39,7 +39,7 @@ def get_weather(location: str) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def get_forecast(location: str, days: int = 3) -> str:
|
||||
"""Get the weather forecast for a location.
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ This example demonstrates advanced AGUIChatClient features including:
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import ai_function
|
||||
from agent_framework import tool
|
||||
from agent_framework.ag_ui import AGUIChatClient
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def get_weather(location: str) -> str:
|
||||
"""Get the current weather for a location.
|
||||
|
||||
@@ -33,7 +33,7 @@ def get_weather(location: str) -> str:
|
||||
return weather_data.get(location.lower(), f"Weather data not available for {location}")
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def calculate(a: float, b: float, operation: str) -> str:
|
||||
"""Perform basic arithmetic operations.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
from agent_framework import ChatAgent, ai_function
|
||||
from agent_framework import ChatAgent, tool
|
||||
from agent_framework.ag_ui import AGUIChatClient
|
||||
|
||||
# Enable debug logging
|
||||
@@ -33,7 +33,7 @@ logging.basicConfig(
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ai_function(description="Get the current weather for a location.")
|
||||
@tool(description="Get the current weather for a location.")
|
||||
def get_weather(location: str) -> str:
|
||||
"""Get the current weather for a location.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from agent_framework import ChatAgent, ai_function
|
||||
from agent_framework import ChatAgent, tool
|
||||
from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from dotenv import load_dotenv
|
||||
@@ -87,7 +87,7 @@ async def verify_api_key(api_key: str | None = Security(API_KEY_HEADER)) -> None
|
||||
|
||||
|
||||
# Server-side tool (executes on server)
|
||||
@ai_function(description="Get the time zone for a location.")
|
||||
@tool(description="Get the time zone for a location.")
|
||||
def get_time_zone(location: str) -> str:
|
||||
"""Get the time zone for a location.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from agent_framework import (
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
Role,
|
||||
ai_function,
|
||||
tool,
|
||||
)
|
||||
from pytest import MonkeyPatch
|
||||
|
||||
@@ -231,9 +231,9 @@ class TestAGUIChatClient:
|
||||
When server requests a client function, @use_function_invocation decorator
|
||||
intercepts and executes it locally. This matches .NET AG-UI implementation.
|
||||
"""
|
||||
from agent_framework import ai_function
|
||||
from agent_framework import tool
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def test_tool(param: str) -> str:
|
||||
"""Test tool."""
|
||||
return "result"
|
||||
@@ -299,7 +299,7 @@ class TestAGUIChatClient:
|
||||
async def test_server_tool_calls_not_executed_locally(self, monkeypatch: MonkeyPatch) -> None:
|
||||
"""Server tools should not trigger local function invocation even when client tools exist."""
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def client_tool() -> str:
|
||||
"""Client tool stub."""
|
||||
return "client"
|
||||
|
||||
@@ -680,12 +680,12 @@ async def test_agent_with_use_service_thread_is_true():
|
||||
|
||||
async def test_function_approval_mode_executes_tool():
|
||||
"""Test that function approval with approval_mode='always_require' sends the correct messages."""
|
||||
from agent_framework import ai_function
|
||||
from agent_framework import tool
|
||||
from agent_framework.ag_ui import AgentFrameworkAgent
|
||||
|
||||
messages_received: list[Any] = []
|
||||
|
||||
@ai_function(
|
||||
@tool(
|
||||
name="get_datetime",
|
||||
description="Get the current date and time",
|
||||
approval_mode="always_require",
|
||||
@@ -771,12 +771,12 @@ async def test_function_approval_mode_executes_tool():
|
||||
|
||||
async def test_function_approval_mode_rejection():
|
||||
"""Test that function approval rejection creates a rejection response."""
|
||||
from agent_framework import ai_function
|
||||
from agent_framework import tool
|
||||
from agent_framework.ag_ui import AgentFrameworkAgent
|
||||
|
||||
messages_received: list[Any] = []
|
||||
|
||||
@ai_function(
|
||||
@tool(
|
||||
name="delete_all_data",
|
||||
description="Delete all user data",
|
||||
approval_mode="always_require",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from agent_framework import ChatAgent, ai_function
|
||||
from agent_framework import ChatAgent, tool
|
||||
|
||||
from agent_framework_ag_ui._orchestration._tooling import (
|
||||
collect_server_tools,
|
||||
@@ -25,7 +25,7 @@ class MockMCPTool:
|
||||
self.is_connected = is_connected
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def regular_tool() -> str:
|
||||
"""Regular tool for testing."""
|
||||
return "result"
|
||||
@@ -35,7 +35,7 @@ def _create_chat_agent_with_tool(tool_name: str = "regular_tool") -> ChatAgent:
|
||||
"""Create a ChatAgent with a mocked chat client and a simple tool.
|
||||
|
||||
Note: tool_name parameter is kept for API compatibility but the tool
|
||||
will always be named 'regular_tool' since ai_function uses the function name.
|
||||
will always be named 'regular_tool' since tool uses the function name.
|
||||
"""
|
||||
mock_chat_client = MagicMock()
|
||||
return ChatAgent(chat_client=mock_chat_client, tools=[regular_tool])
|
||||
|
||||
@@ -252,13 +252,13 @@ def test_make_json_safe_dataclass_with_nested_to_dict_object():
|
||||
assert json_str is not None
|
||||
|
||||
|
||||
def test_convert_tools_to_agui_format_with_ai_function():
|
||||
"""Test converting AIFunction to AG-UI format."""
|
||||
from agent_framework import ai_function
|
||||
def test_convert_tools_to_agui_format_with_tool():
|
||||
"""Test converting FunctionTool to AG-UI format."""
|
||||
from agent_framework import tool
|
||||
|
||||
from agent_framework_ag_ui._utils import convert_tools_to_agui_format
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def test_func(param: str, count: int = 5) -> str:
|
||||
"""Test function."""
|
||||
return f"{param} {count}"
|
||||
@@ -318,11 +318,11 @@ def test_convert_tools_to_agui_format_with_none():
|
||||
|
||||
def test_convert_tools_to_agui_format_with_single_tool():
|
||||
"""Test converting single tool (not in list)."""
|
||||
from agent_framework import ai_function
|
||||
from agent_framework import tool
|
||||
|
||||
from agent_framework_ag_ui._utils import convert_tools_to_agui_format
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def single_tool(arg: str) -> str:
|
||||
"""Single tool."""
|
||||
return arg
|
||||
@@ -336,16 +336,16 @@ def test_convert_tools_to_agui_format_with_single_tool():
|
||||
|
||||
def test_convert_tools_to_agui_format_with_multiple_tools():
|
||||
"""Test converting multiple tools."""
|
||||
from agent_framework import ai_function
|
||||
from agent_framework import tool
|
||||
|
||||
from agent_framework_ag_ui._utils import convert_tools_to_agui_format
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def tool1(x: int) -> int:
|
||||
"""Tool 1."""
|
||||
return x
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def tool2(y: str) -> str:
|
||||
"""Tool 2."""
|
||||
return y
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import Any, ClassVar, Final, Generic, Literal, TypedDict
|
||||
|
||||
from agent_framework import (
|
||||
AGENT_FRAMEWORK_USER_AGENT,
|
||||
AIFunction,
|
||||
Annotation,
|
||||
BaseChatClient,
|
||||
ChatMessage,
|
||||
@@ -15,6 +14,7 @@ from agent_framework import (
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
FinishReason,
|
||||
FunctionTool,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
@@ -583,7 +583,7 @@ class AnthropicClient(BaseChatClient[TAnthropicOptions], Generic[TAnthropicOptio
|
||||
match tool:
|
||||
case MutableMapping():
|
||||
tool_list.append(tool)
|
||||
case AIFunction():
|
||||
case FunctionTool():
|
||||
tool_list.append({
|
||||
"type": "custom",
|
||||
"name": tool.name,
|
||||
|
||||
@@ -16,7 +16,7 @@ from agent_framework import (
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
Role,
|
||||
ai_function,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
from anthropic.types.beta import (
|
||||
@@ -259,11 +259,11 @@ def test_prepare_messages_for_anthropic_without_system(mock_anthropic_client: Ma
|
||||
# Tool Conversion Tests
|
||||
|
||||
|
||||
def test_prepare_tools_for_anthropic_ai_function(mock_anthropic_client: MagicMock) -> None:
|
||||
"""Test converting AIFunction to Anthropic format."""
|
||||
def test_prepare_tools_for_anthropic_tool(mock_anthropic_client: MagicMock) -> None:
|
||||
"""Test converting FunctionTool to Anthropic format."""
|
||||
chat_client = create_test_anthropic_client(mock_anthropic_client)
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def get_weather(location: Annotated[str, Field(description="Location to get weather for")]) -> str:
|
||||
"""Get weather for a location."""
|
||||
return f"Weather for {location}"
|
||||
@@ -443,7 +443,7 @@ async def test_prepare_options_with_tools(mock_anthropic_client: MagicMock) -> N
|
||||
"""Test _prepare_options with tools."""
|
||||
chat_client = create_test_anthropic_client(mock_anthropic_client)
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def get_weather(location: str) -> str:
|
||||
"""Get weather for a location."""
|
||||
return f"Weather for {location}"
|
||||
@@ -709,7 +709,7 @@ async def test_inner_get_streaming_response(mock_anthropic_client: MagicMock) ->
|
||||
# Integration Tests
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
|
||||
@@ -6,9 +6,9 @@ from typing import TYPE_CHECKING, Any, Generic, TypedDict, cast
|
||||
|
||||
from agent_framework import (
|
||||
AGENT_FRAMEWORK_USER_AGENT,
|
||||
AIFunction,
|
||||
ChatAgent,
|
||||
ContextProvider,
|
||||
FunctionTool,
|
||||
Middleware,
|
||||
ToolProtocol,
|
||||
normalize_tools,
|
||||
@@ -445,9 +445,9 @@ class AzureAIAgentsProvider(Generic[TOptions_co]):
|
||||
# Add user-provided function tools and MCP tools
|
||||
if provided_tools:
|
||||
for provided_tool in provided_tools:
|
||||
# AIFunction - has implementation for function calling
|
||||
# FunctionTool - has implementation for function calling
|
||||
# MCPTool - ChatAgent handles MCP connection and tool discovery at runtime
|
||||
if isinstance(provided_tool, (AIFunction, MCPTool)):
|
||||
if isinstance(provided_tool, (FunctionTool, MCPTool)):
|
||||
merged.append(provided_tool) # type: ignore[reportUnknownArgumentType]
|
||||
|
||||
return merged
|
||||
@@ -488,7 +488,7 @@ class AzureAIAgentsProvider(Generic[TOptions_co]):
|
||||
provided_names: set[str] = set()
|
||||
if provided_tools:
|
||||
for tool in provided_tools:
|
||||
if isinstance(tool, AIFunction):
|
||||
if isinstance(tool, FunctionTool):
|
||||
provided_names.add(tool.name)
|
||||
|
||||
# Check for missing implementations
|
||||
|
||||
@@ -10,7 +10,6 @@ from typing import Any, ClassVar, Generic, TypedDict
|
||||
|
||||
from agent_framework import (
|
||||
AGENT_FRAMEWORK_USER_AGENT,
|
||||
AIFunction,
|
||||
Annotation,
|
||||
BaseChatClient,
|
||||
ChatAgent,
|
||||
@@ -21,6 +20,7 @@ from agent_framework import (
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
ContextProvider,
|
||||
FunctionTool,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
HostedMCPTool,
|
||||
@@ -1117,7 +1117,7 @@ class AzureAIAgentClient(BaseChatClient[TAzureAIAgentOptions], Generic[TAzureAIA
|
||||
tool_definitions: list[ToolDefinition | dict[str, Any]] = []
|
||||
for tool in tools:
|
||||
match tool:
|
||||
case AIFunction():
|
||||
case FunctionTool():
|
||||
tool_definitions.append(tool.to_json_schema_spec()) # type: ignore[reportUnknownArgumentType]
|
||||
case HostedWebSearchTool():
|
||||
additional_props = tool.additional_properties or {}
|
||||
|
||||
@@ -6,9 +6,9 @@ from typing import Any, Generic, TypedDict
|
||||
|
||||
from agent_framework import (
|
||||
AGENT_FRAMEWORK_USER_AGENT,
|
||||
AIFunction,
|
||||
ChatAgent,
|
||||
ContextProvider,
|
||||
FunctionTool,
|
||||
Middleware,
|
||||
ToolProtocol,
|
||||
get_logger,
|
||||
@@ -20,10 +20,12 @@ from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.ai.projects.models import (
|
||||
AgentReference,
|
||||
AgentVersionDetails,
|
||||
FunctionTool,
|
||||
PromptAgentDefinition,
|
||||
PromptAgentDefinitionText,
|
||||
)
|
||||
from azure.ai.projects.models import (
|
||||
FunctionTool as AzureFunctionTool,
|
||||
)
|
||||
from azure.core.credentials_async import AsyncTokenCredential
|
||||
from pydantic import ValidationError
|
||||
|
||||
@@ -224,7 +226,7 @@ class AzureAIProjectAgentProvider(Generic[TOptions_co]):
|
||||
|
||||
# Connect MCP tools and discover their functions BEFORE creating the agent
|
||||
# This is required because Azure AI Responses API doesn't accept tools at request time
|
||||
mcp_discovered_functions: list[AIFunction[Any, Any]] = []
|
||||
mcp_discovered_functions: list[FunctionTool] = []
|
||||
for mcp_tool in mcp_tools:
|
||||
if not mcp_tool.is_connected:
|
||||
await mcp_tool.connect()
|
||||
@@ -433,9 +435,9 @@ class AzureAIProjectAgentProvider(Generic[TOptions_co]):
|
||||
# Add user-provided function tools and MCP tools
|
||||
if provided_tools:
|
||||
for provided_tool in provided_tools:
|
||||
# AIFunction - has implementation for function calling
|
||||
# FunctionTool - has implementation for function calling
|
||||
# MCPTool - ChatAgent handles MCP connection and tool discovery at runtime
|
||||
if isinstance(provided_tool, (AIFunction, MCPTool)):
|
||||
if isinstance(provided_tool, (FunctionTool, MCPTool)):
|
||||
merged.append(provided_tool) # type: ignore[reportUnknownArgumentType]
|
||||
|
||||
return merged
|
||||
@@ -452,12 +454,14 @@ class AzureAIProjectAgentProvider(Generic[TOptions_co]):
|
||||
"""Validate that required function tools are provided."""
|
||||
# Normalize and validate function tools
|
||||
normalized_tools = normalize_tools(provided_tools)
|
||||
tool_names = {tool.name for tool in normalized_tools if isinstance(tool, AIFunction)}
|
||||
tool_names = {tool.name for tool in normalized_tools if isinstance(tool, FunctionTool)}
|
||||
|
||||
# If function tools exist in agent definition but were not provided,
|
||||
# we need to raise an error, as it won't be possible to invoke the function.
|
||||
missing_tools = [
|
||||
tool.name for tool in (agent_tools or []) if isinstance(tool, FunctionTool) and tool.name not in tool_names
|
||||
tool.name
|
||||
for tool in (agent_tools or [])
|
||||
if isinstance(tool, AzureFunctionTool) and tool.name not in tool_names
|
||||
]
|
||||
|
||||
if missing_tools:
|
||||
|
||||
@@ -5,8 +5,8 @@ from collections.abc import Mapping, MutableMapping, Sequence
|
||||
from typing import Any, ClassVar, Literal, cast
|
||||
|
||||
from agent_framework import (
|
||||
AIFunction,
|
||||
Content,
|
||||
FunctionTool,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
HostedImageGenerationTool,
|
||||
@@ -29,7 +29,6 @@ from azure.ai.projects.models import (
|
||||
ApproximateLocation,
|
||||
CodeInterpreterTool,
|
||||
CodeInterpreterToolAuto,
|
||||
FunctionTool,
|
||||
ImageGenTool,
|
||||
ImageGenToolInputImageMask,
|
||||
MCPTool,
|
||||
@@ -42,6 +41,9 @@ from azure.ai.projects.models import (
|
||||
from azure.ai.projects.models import (
|
||||
FileSearchTool as ProjectsFileSearchTool,
|
||||
)
|
||||
from azure.ai.projects.models import (
|
||||
FunctionTool as AzureFunctionTool,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = get_logger("agent_framework.azure")
|
||||
@@ -141,7 +143,7 @@ def to_azure_ai_agent_tools(
|
||||
tool_definitions: list[ToolDefinition | dict[str, Any]] = []
|
||||
for tool in tools:
|
||||
match tool:
|
||||
case AIFunction():
|
||||
case FunctionTool():
|
||||
tool_definitions.append(tool.to_json_schema_spec()) # type: ignore[reportUnknownArgumentType]
|
||||
case HostedWebSearchTool():
|
||||
additional_props = tool.additional_properties or {}
|
||||
@@ -439,11 +441,11 @@ def to_azure_ai_tools(
|
||||
container = CodeInterpreterToolAuto(file_ids=file_ids if file_ids else None)
|
||||
ci_tool: CodeInterpreterTool = CodeInterpreterTool(container=container)
|
||||
azure_tools.append(ci_tool)
|
||||
case AIFunction():
|
||||
case FunctionTool():
|
||||
params = tool.parameters()
|
||||
params["additionalProperties"] = False
|
||||
azure_tools.append(
|
||||
FunctionTool(
|
||||
AzureFunctionTool(
|
||||
name=tool.name,
|
||||
parameters=params,
|
||||
strict=False,
|
||||
|
||||
@@ -12,7 +12,7 @@ from agent_framework import (
|
||||
HostedFileSearchTool,
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
ai_function,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
from azure.ai.agents.models import (
|
||||
@@ -222,7 +222,7 @@ async def test_create_agent_with_tools(
|
||||
|
||||
provider = AzureAIAgentsProvider(agents_client=mock_agents_client)
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def get_weather(city: str) -> str:
|
||||
"""Get weather for a city."""
|
||||
return f"Weather in {city}"
|
||||
@@ -366,7 +366,7 @@ async def test_get_agent_with_provided_function_tools(
|
||||
mock_agent.tools = [mock_function_tool]
|
||||
mock_agents_client.get_agent = AsyncMock(return_value=mock_agent)
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def get_weather(city: str) -> str:
|
||||
"""Get weather for a city."""
|
||||
return f"Weather in {city}"
|
||||
@@ -483,9 +483,9 @@ def test_to_azure_ai_agent_tools_empty() -> None:
|
||||
|
||||
|
||||
def test_to_azure_ai_agent_tools_function() -> None:
|
||||
"""Test converting AIFunction to Azure tool definition."""
|
||||
"""Test converting FunctionTool to Azure tool definition."""
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def get_weather(city: str) -> str:
|
||||
"""Get weather for a city."""
|
||||
return f"Weather in {city}"
|
||||
|
||||
@@ -23,6 +23,7 @@ from agent_framework import (
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
Role,
|
||||
tool,
|
||||
)
|
||||
from agent_framework._serialization import SerializationMixin
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
@@ -940,7 +941,7 @@ async def test_azure_ai_chat_client_service_url(mock_agents_client: MagicMock) -
|
||||
assert result == "https://test-endpoint.com/"
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prepare_tool_outputs_for_azure_ai_function_result(
|
||||
async def test_azure_ai_chat_client_prepare_tool_outputs_for_azure_tool_result(
|
||||
mock_agents_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _prepare_tool_outputs_for_azure_ai with FunctionResultContent."""
|
||||
@@ -1336,6 +1337,7 @@ def test_azure_ai_chat_client_extract_file_path_contents_empty_annotations(
|
||||
assert len(file_contents) == 0
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
|
||||
@@ -22,6 +22,7 @@ from agent_framework import (
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
Role,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
@@ -1025,6 +1026,7 @@ def test_from_azure_ai_tools() -> None:
|
||||
# region Integration Tests
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
|
||||
@@ -4,16 +4,18 @@ import os
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from agent_framework import AIFunction, ChatAgent
|
||||
from agent_framework import ChatAgent, FunctionTool
|
||||
from agent_framework._mcp import MCPTool
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.ai.projects.models import (
|
||||
AgentReference,
|
||||
AgentVersionDetails,
|
||||
FunctionTool,
|
||||
PromptAgentDefinition,
|
||||
)
|
||||
from azure.ai.projects.models import (
|
||||
FunctionTool as AzureFunctionTool,
|
||||
)
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
|
||||
from agent_framework_azure_ai import AzureAIProjectAgentProvider
|
||||
@@ -373,7 +375,7 @@ async def test_provider_get_agent_missing_function_tools(mock_project_client: Ma
|
||||
mock_agent_version.description = None
|
||||
mock_agent_version.definition = MagicMock(spec=PromptAgentDefinition)
|
||||
mock_agent_version.definition.tools = [
|
||||
FunctionTool(name="test_tool", parameters=[], strict=True, description="Test tool")
|
||||
AzureFunctionTool(name="test_tool", parameters=[], strict=True, description="Test tool")
|
||||
]
|
||||
|
||||
mock_agent_object = MagicMock()
|
||||
@@ -494,7 +496,7 @@ class MockMCPTool(MCPTool): # pyright: ignore[reportGeneralTypeIssues]
|
||||
unit testing. We only need isinstance(obj, MCPTool) to return True.
|
||||
"""
|
||||
|
||||
def __init__(self, functions: list[AIFunction] | None = None) -> None:
|
||||
def __init__(self, functions: list[FunctionTool] | None = None) -> None:
|
||||
self.name = "MockMCPTool"
|
||||
self.description = "A mock MCP tool for testing"
|
||||
self.is_connected = False
|
||||
@@ -502,7 +504,7 @@ class MockMCPTool(MCPTool): # pyright: ignore[reportGeneralTypeIssues]
|
||||
self._connect_called = False
|
||||
|
||||
@property
|
||||
def functions(self) -> list[AIFunction]:
|
||||
def functions(self) -> list[FunctionTool]:
|
||||
return self._mock_functions
|
||||
|
||||
async def connect(self, *, reset: bool = False) -> None:
|
||||
@@ -520,13 +522,13 @@ def mock_mcp_tool() -> MockMCPTool:
|
||||
return MockMCPTool(functions=mock_functions)
|
||||
|
||||
|
||||
def create_mock_ai_function(name: str, description: str = "A mock function") -> AIFunction:
|
||||
"""Create a real AIFunction for testing."""
|
||||
def create_mock_ai_function(name: str, description: str = "A mock function") -> FunctionTool:
|
||||
"""Create a real FunctionTool for testing."""
|
||||
|
||||
def mock_func(arg: str) -> str:
|
||||
return f"Result from {name}: {arg}"
|
||||
|
||||
return AIFunction(func=mock_func, name=name, description=description)
|
||||
return FunctionTool(func=mock_func, name=name, description=description, approval_mode="never_require")
|
||||
|
||||
|
||||
async def test_provider_create_agent_with_mcp_tool(
|
||||
@@ -593,8 +595,8 @@ async def test_provider_create_agent_with_mcp_and_regular_tools(
|
||||
azure_ai_unit_test_env: dict[str, str],
|
||||
mock_mcp_tool: "MockMCPTool",
|
||||
) -> None:
|
||||
"""Test that create_agent handles both MCP tools and regular AIFunctions."""
|
||||
# Create a regular AIFunction
|
||||
"""Test that create_agent handles both MCP tools and regular FunctionTools."""
|
||||
# Create a regular FunctionTool
|
||||
regular_function = create_mock_ai_function("regular_function", "A regular function")
|
||||
|
||||
# Patch normalize_tools to return tools as-is in a list (avoids callable check)
|
||||
@@ -637,7 +639,7 @@ async def test_provider_create_agent_with_mcp_and_regular_tools(
|
||||
)
|
||||
|
||||
# Verify to_azure_ai_tools was called with:
|
||||
# - The regular AIFunction (1)
|
||||
# - The regular FunctionTool (1)
|
||||
# - The 2 discovered MCP functions
|
||||
mock_to_azure_tools.assert_called_once()
|
||||
tools_passed = mock_to_azure_tools.call_args[0][0]
|
||||
|
||||
@@ -10,7 +10,6 @@ from uuid import uuid4
|
||||
|
||||
from agent_framework import (
|
||||
AGENT_FRAMEWORK_USER_AGENT,
|
||||
AIFunction,
|
||||
BaseChatClient,
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
@@ -18,6 +17,7 @@ from agent_framework import (
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
FinishReason,
|
||||
FunctionTool,
|
||||
Role,
|
||||
ToolProtocol,
|
||||
UsageDetails,
|
||||
@@ -548,7 +548,7 @@ class BedrockChatClient(BaseChatClient[TBedrockChatOptions], Generic[TBedrockCha
|
||||
if isinstance(tool, MutableMapping):
|
||||
converted.append(dict(tool))
|
||||
continue
|
||||
if isinstance(tool, AIFunction):
|
||||
if isinstance(tool, FunctionTool):
|
||||
converted.append({
|
||||
"toolSpec": {
|
||||
"name": tool.name,
|
||||
|
||||
@@ -2,22 +2,13 @@
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
|
||||
from agent_framework import (
|
||||
AgentResponse,
|
||||
ChatAgent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
Role,
|
||||
TextContent,
|
||||
ai_function,
|
||||
)
|
||||
from agent_framework import ChatAgent, tool
|
||||
|
||||
from agent_framework_bedrock import BedrockChatClient
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def get_weather(city: str) -> dict[str, str]:
|
||||
"""Return a mock forecast for the requested city."""
|
||||
normalized = city.strip() or "New York"
|
||||
@@ -36,27 +27,18 @@ async def main() -> None:
|
||||
|
||||
response = await agent.run("Use the weather tool to check the forecast for new york.")
|
||||
logging.info("\nAssistant reply:", response.text or "<no text returned>")
|
||||
_log_response(response)
|
||||
|
||||
|
||||
def _log_response(response: AgentResponse) -> None:
|
||||
logging.info("\nConversation transcript:")
|
||||
for idx, message in enumerate(response.messages, start=1):
|
||||
tag = f"{idx}. {message.role.value if isinstance(message.role, Role) else message.role}"
|
||||
_log_contents(tag, message.contents)
|
||||
|
||||
|
||||
def _log_contents(tag: str, contents: Sequence[object]) -> None:
|
||||
logging.info(f"[{tag}] {len(contents)} content blocks")
|
||||
for idx, content in enumerate(contents, start=1):
|
||||
if isinstance(content, TextContent):
|
||||
logging.info(f" {idx}. text -> {content.text}")
|
||||
elif isinstance(content, FunctionCallContent):
|
||||
logging.info(f" {idx}. tool_call ({content.name}) -> {content.arguments}")
|
||||
elif isinstance(content, FunctionResultContent):
|
||||
logging.info(f" {idx}. tool_result ({content.call_id}) -> {content.result}")
|
||||
else: # pragma: no cover - defensive
|
||||
logging.info(f" {idx}. {content.type}")
|
||||
for message in response.messages:
|
||||
for idx, content in enumerate(message.contents, start=1):
|
||||
match content.type:
|
||||
case "text":
|
||||
logging.info(f" {idx}. text -> {content.text}")
|
||||
case "function_call":
|
||||
logging.info(f" {idx}. function_call ({content.name}) -> {content.arguments}")
|
||||
case "function_result":
|
||||
logging.info(f" {idx}. function_result ({content.call_id}) -> {content.result}")
|
||||
case _:
|
||||
logging.info(f" {idx}. {content.type}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -6,10 +6,10 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from agent_framework import (
|
||||
AIFunction,
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
Content,
|
||||
FunctionTool,
|
||||
Role,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
@@ -42,7 +42,7 @@ def test_settings_load_from_environment(monkeypatch: pytest.MonkeyPatch) -> None
|
||||
def test_build_request_includes_tool_config() -> None:
|
||||
client = _build_client()
|
||||
|
||||
tool = AIFunction(name="get_weather", description="desc", func=_dummy_weather, input_model=_WeatherArgs)
|
||||
tool = FunctionTool(name="get_weather", description="desc", func=_dummy_weather, input_model=_WeatherArgs)
|
||||
options = {
|
||||
"tools": [tool],
|
||||
"tool_choice": {"mode": "required", "required_function_name": "get_weather"},
|
||||
|
||||
@@ -31,7 +31,7 @@ from ._memory import Context, ContextProvider
|
||||
from ._middleware import Middleware, use_agent_middleware
|
||||
from ._serialization import SerializationMixin
|
||||
from ._threads import AgentThread, ChatMessageStoreProtocol
|
||||
from ._tools import FUNCTION_INVOKING_CHAT_CLIENT_MARKER, AIFunction, ToolProtocol
|
||||
from ._tools import FUNCTION_INVOKING_CHAT_CLIENT_MARKER, FunctionTool, ToolProtocol
|
||||
from ._types import (
|
||||
AgentResponse,
|
||||
AgentResponseUpdate,
|
||||
@@ -417,8 +417,8 @@ class BaseAgent(SerializationMixin):
|
||||
stream_callback: Callable[[AgentResponseUpdate], None]
|
||||
| Callable[[AgentResponseUpdate], Awaitable[None]]
|
||||
| None = None,
|
||||
) -> AIFunction[BaseModel, str]:
|
||||
"""Create an AIFunction tool that wraps this agent.
|
||||
) -> FunctionTool[BaseModel, str]:
|
||||
"""Create a FunctionTool that wraps this agent.
|
||||
|
||||
Keyword Args:
|
||||
name: The name for the tool. If None, uses the agent's name.
|
||||
@@ -429,7 +429,7 @@ class BaseAgent(SerializationMixin):
|
||||
stream_callback: Optional callback for streaming responses. If provided, uses run_stream.
|
||||
|
||||
Returns:
|
||||
An AIFunction that can be used as a tool by other agents.
|
||||
A FunctionTool that can be used as a tool by other agents.
|
||||
|
||||
Raises:
|
||||
TypeError: If the agent does not implement AgentProtocol.
|
||||
@@ -491,11 +491,12 @@ class BaseAgent(SerializationMixin):
|
||||
# Create final text from accumulated updates
|
||||
return AgentResponse.from_agent_run_response_updates(response_updates).text
|
||||
|
||||
agent_tool: AIFunction[BaseModel, str] = AIFunction(
|
||||
agent_tool: FunctionTool[BaseModel, str] = FunctionTool(
|
||||
name=tool_name,
|
||||
description=tool_description,
|
||||
func=agent_wrapper,
|
||||
input_model=input_model, # type: ignore
|
||||
approval_mode="never_require",
|
||||
)
|
||||
agent_tool._forward_runtime_kwargs = True # type: ignore
|
||||
return agent_tool
|
||||
@@ -1213,7 +1214,19 @@ class ChatAgent(BaseAgent, Generic[TOptions_co]): # type: ignore[misc]
|
||||
Raises:
|
||||
AgentExecutionException: If the conversation IDs on the thread and agent don't match.
|
||||
"""
|
||||
chat_options = deepcopy(self.default_options) if self.default_options else {}
|
||||
# Create a shallow copy of options and deep copy non-tool values
|
||||
# Tools containing HTTP clients or other non-copyable objects cannot be deep copied
|
||||
if self.default_options:
|
||||
chat_options: dict[str, Any] = {}
|
||||
for key, value in self.default_options.items():
|
||||
if key == "tools":
|
||||
# Keep tool references as-is (don't deep copy)
|
||||
chat_options[key] = list(value) if value else []
|
||||
else:
|
||||
# Deep copy other options to prevent mutation
|
||||
chat_options[key] = deepcopy(value)
|
||||
else:
|
||||
chat_options = {}
|
||||
thread = thread or self.get_new_thread()
|
||||
if thread.service_thread_id and thread.context_provider:
|
||||
await thread.context_provider.thread_created(thread.service_thread_id)
|
||||
@@ -1237,7 +1250,7 @@ class ChatAgent(BaseAgent, Generic[TOptions_co]): # type: ignore[misc]
|
||||
if context.instructions:
|
||||
chat_options["instructions"] = (
|
||||
context.instructions
|
||||
if not chat_options.get("instructions")
|
||||
if "instructions" not in chat_options
|
||||
else f"{chat_options['instructions']}\n{context.instructions}"
|
||||
)
|
||||
thread_messages.extend(input_messages or [])
|
||||
|
||||
@@ -25,7 +25,7 @@ from mcp.shared.session import RequestResponder
|
||||
from pydantic import BaseModel, create_model
|
||||
|
||||
from ._tools import (
|
||||
AIFunction,
|
||||
FunctionTool,
|
||||
HostedMCPSpecificApproval,
|
||||
_build_pydantic_model_from_json_schema,
|
||||
)
|
||||
@@ -356,7 +356,7 @@ class MCPTool:
|
||||
self.session = session
|
||||
self.request_timeout = request_timeout
|
||||
self.chat_client = chat_client
|
||||
self._functions: list[AIFunction[Any, Any]] = []
|
||||
self._functions: list[FunctionTool[Any, Any]] = []
|
||||
self.is_connected: bool = False
|
||||
self._tools_loaded: bool = False
|
||||
self._prompts_loaded: bool = False
|
||||
@@ -365,7 +365,7 @@ class MCPTool:
|
||||
return f"MCPTool(name={self.name}, description={self.description})"
|
||||
|
||||
@property
|
||||
def functions(self) -> list[AIFunction[Any, Any]]:
|
||||
def functions(self) -> list[FunctionTool[Any, Any]]:
|
||||
"""Get the list of functions that are allowed."""
|
||||
if not self.allowed_tools:
|
||||
return self._functions
|
||||
@@ -609,7 +609,7 @@ class MCPTool:
|
||||
"""Load prompts from the MCP server.
|
||||
|
||||
Retrieves available prompts from the connected MCP server and converts
|
||||
them into AIFunction instances. Handles pagination automatically.
|
||||
them into FunctionTool instances. Handles pagination automatically.
|
||||
|
||||
Raises:
|
||||
ToolExecutionException: If the MCP server is not connected.
|
||||
@@ -633,7 +633,7 @@ class MCPTool:
|
||||
|
||||
input_model = _get_input_model_from_mcp_prompt(prompt)
|
||||
approval_mode = self._determine_approval_mode(local_name)
|
||||
func: AIFunction[BaseModel, list[ChatMessage] | Any | types.GetPromptResult] = AIFunction(
|
||||
func: FunctionTool[BaseModel, list[ChatMessage] | Any | types.GetPromptResult] = FunctionTool(
|
||||
func=partial(self.get_prompt, prompt.name),
|
||||
name=local_name,
|
||||
description=prompt.description or "",
|
||||
@@ -652,7 +652,7 @@ class MCPTool:
|
||||
"""Load tools from the MCP server.
|
||||
|
||||
Retrieves available tools from the connected MCP server and converts
|
||||
them into AIFunction instances. Handles pagination automatically.
|
||||
them into FunctionTool instances. Handles pagination automatically.
|
||||
|
||||
Raises:
|
||||
ToolExecutionException: If the MCP server is not connected.
|
||||
@@ -676,8 +676,8 @@ class MCPTool:
|
||||
|
||||
input_model = _get_input_model_from_mcp_tool(tool)
|
||||
approval_mode = self._determine_approval_mode(local_name)
|
||||
# Create AIFunctions out of each tool
|
||||
func: AIFunction[BaseModel, list[Content] | Any | types.CallToolResult] = AIFunction(
|
||||
# Create FunctionTools out of each tool
|
||||
func: FunctionTool[BaseModel, list[Content] | Any | types.CallToolResult] = FunctionTool(
|
||||
func=partial(self.call_tool, tool.name),
|
||||
name=local_name,
|
||||
description=tool.description or "",
|
||||
|
||||
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
|
||||
from ._agents import AgentProtocol
|
||||
from ._clients import ChatClientProtocol
|
||||
from ._threads import AgentThread
|
||||
from ._tools import AIFunction
|
||||
from ._tools import FunctionTool
|
||||
from ._types import ChatResponse, ChatResponseUpdate
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ class FunctionInvocationContext(SerializationMixin):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
function: "AIFunction[Any, Any]",
|
||||
function: "FunctionTool[Any, Any]",
|
||||
arguments: "BaseModel",
|
||||
metadata: dict[str, Any] | None = None,
|
||||
result: Any = None,
|
||||
|
||||
@@ -444,11 +444,11 @@ class SerializationMixin:
|
||||
chat_client = OpenAIChatClient.from_dict(client_data, dependencies=dependencies)
|
||||
# Now ready to make API calls with the injected client
|
||||
|
||||
**Function Injection for Tools** - AIFunction runtime dependency:
|
||||
**Function Injection for Tools** - FunctionTool runtime dependency:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import AIFunction
|
||||
from agent_framework import FunctionTool
|
||||
from typing import Annotated
|
||||
|
||||
|
||||
@@ -458,19 +458,19 @@ class SerializationMixin:
|
||||
return f"Current weather in {location}: 72°F and sunny"
|
||||
|
||||
|
||||
# AIFunction has INJECTABLE = {"func"}
|
||||
# FunctionTool has INJECTABLE = {"func"}
|
||||
function_data = {
|
||||
"type": "ai_function",
|
||||
"type": "function_tool",
|
||||
"name": "get_weather",
|
||||
"description": "Get current weather for a location",
|
||||
# func is excluded from serialization
|
||||
}
|
||||
|
||||
# Inject the actual function implementation during deserialization
|
||||
dependencies = {"ai_function": {"func": get_current_weather}}
|
||||
dependencies = {"function_tool": {"func": get_current_weather}}
|
||||
|
||||
# Reconstruct the AIFunction with the callable injected
|
||||
weather_func = AIFunction.from_dict(function_data, dependencies=dependencies)
|
||||
# Reconstruct the FunctionTool with the callable injected
|
||||
weather_func = FunctionTool.from_dict(function_data, dependencies=dependencies)
|
||||
# The function is now callable and ready for agent use
|
||||
|
||||
**Middleware Context Injection** - Agent execution context:
|
||||
|
||||
@@ -33,7 +33,7 @@ from typing import (
|
||||
runtime_checkable,
|
||||
)
|
||||
|
||||
from opentelemetry.metrics import Histogram
|
||||
from opentelemetry.metrics import Histogram, NoOpHistogram
|
||||
from pydantic import AnyUrl, BaseModel, Field, ValidationError, create_model
|
||||
|
||||
from ._logging import get_logger
|
||||
@@ -64,12 +64,20 @@ if sys.version_info >= (3, 12):
|
||||
else:
|
||||
from typing_extensions import override # type: ignore[import] # pragma: no cover
|
||||
|
||||
# TypeVar with defaults support for Python < 3.13
|
||||
if sys.version_info >= (3, 13):
|
||||
from typing import TypeVar as TypeVarWithDefaults # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import (
|
||||
TypeVar as TypeVarWithDefaults, # type: ignore[import] # pragma: no cover
|
||||
)
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
__all__ = [
|
||||
"FUNCTION_INVOKING_CHAT_CLIENT_MARKER",
|
||||
"AIFunction",
|
||||
"FunctionInvocationConfiguration",
|
||||
"FunctionTool",
|
||||
"HostedCodeInterpreterTool",
|
||||
"HostedFileSearchTool",
|
||||
"HostedImageGenerationTool",
|
||||
@@ -77,7 +85,7 @@ __all__ = [
|
||||
"HostedMCPTool",
|
||||
"HostedWebSearchTool",
|
||||
"ToolProtocol",
|
||||
"ai_function",
|
||||
"tool",
|
||||
"use_function_invocation",
|
||||
]
|
||||
|
||||
@@ -89,16 +97,8 @@ DEFAULT_MAX_CONSECUTIVE_ERRORS_PER_REQUEST: Final[int] = 3
|
||||
TChatClient = TypeVar("TChatClient", bound="ChatClientProtocol[Any]")
|
||||
# region Helpers
|
||||
|
||||
ArgsT = TypeVar("ArgsT", bound=BaseModel)
|
||||
ReturnT = TypeVar("ReturnT")
|
||||
|
||||
|
||||
class _NoOpHistogram:
|
||||
def record(self, *args: Any, **kwargs: Any) -> None: # pragma: no cover - trivial
|
||||
return None
|
||||
|
||||
|
||||
_NOOP_HISTOGRAM = _NoOpHistogram()
|
||||
ArgsT = TypeVarWithDefaults("ArgsT", bound=BaseModel, default=BaseModel)
|
||||
ReturnT = TypeVarWithDefaults("ReturnT", default=Any)
|
||||
|
||||
|
||||
def _parse_inputs(
|
||||
@@ -163,7 +163,7 @@ class ToolProtocol(Protocol):
|
||||
|
||||
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 AIFunction's. A AIFunction is usually created by the `ai_function` decorator.
|
||||
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.
|
||||
@@ -190,7 +190,7 @@ 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 AIFunction.
|
||||
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.
|
||||
@@ -543,7 +543,10 @@ def _default_histogram() -> Histogram:
|
||||
from .observability import OBSERVABILITY_SETTINGS # local import to avoid circulars
|
||||
|
||||
if not OBSERVABILITY_SETTINGS.ENABLED: # type: ignore[name-defined]
|
||||
return _NOOP_HISTOGRAM # type: ignore[return-value]
|
||||
return NoOpHistogram(
|
||||
name=OtelAttr.MEASUREMENT_FUNCTION_INVOCATION_DURATION,
|
||||
unit=OtelAttr.DURATION_UNIT,
|
||||
)
|
||||
meter = get_meter()
|
||||
try:
|
||||
return meter.create_histogram(
|
||||
@@ -567,7 +570,7 @@ class EmptyInputModel(BaseModel):
|
||||
"""An empty input model for functions with no parameters."""
|
||||
|
||||
|
||||
class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
class FunctionTool(BaseTool, 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
|
||||
@@ -578,11 +581,11 @@ class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
|
||||
from typing import Annotated
|
||||
from pydantic import BaseModel, Field
|
||||
from agent_framework import AIFunction, ai_function
|
||||
from agent_framework import FunctionTool, tool
|
||||
|
||||
|
||||
# Using the decorator with string annotations
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def get_weather(
|
||||
location: Annotated[str, "The city name"],
|
||||
unit: Annotated[str, "Temperature unit"] = "celsius",
|
||||
@@ -597,7 +600,7 @@ class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
unit: Annotated[str, Field(description="Temperature unit")] = "celsius"
|
||||
|
||||
|
||||
weather_func = AIFunction(
|
||||
weather_func = FunctionTool(
|
||||
name="get_weather",
|
||||
description="Get the weather for a location",
|
||||
func=lambda location, unit="celsius": f"Weather in {location}: 22°{unit[0].upper()}",
|
||||
@@ -625,13 +628,13 @@ class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
input_model: type[ArgsT] | Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize the AIFunction.
|
||||
"""Initialize the FunctionTool.
|
||||
|
||||
Keyword Args:
|
||||
name: The name of the function.
|
||||
description: A description of the function.
|
||||
approval_mode: Whether or not approval is required to run this tool.
|
||||
Default is that approval is not needed.
|
||||
Default is that approval is required.
|
||||
max_invocations: The maximum number of times this function can be invoked.
|
||||
If None, there is no limit. Should be at least 1.
|
||||
max_invocation_exceptions: The maximum number of exceptions allowed during invocations.
|
||||
@@ -652,7 +655,7 @@ class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
self.func = func
|
||||
self._instance = None # Store the instance for bound methods
|
||||
self.input_model = self._resolve_input_model(input_model)
|
||||
self._cached_parameters: dict[str, Any] | None = None # Cache for model_json_schema()
|
||||
self._cached_parameters: dict[str, Any] | None = None
|
||||
self.approval_mode = approval_mode or "never_require"
|
||||
if max_invocations is not None and max_invocations < 1:
|
||||
raise ValueError("max_invocations must be at least 1 or None.")
|
||||
@@ -663,7 +666,7 @@ class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
self.max_invocation_exceptions = max_invocation_exceptions
|
||||
self.invocation_exception_count = 0
|
||||
self._invocation_duration_histogram = _default_histogram()
|
||||
self.type: Literal["ai_function"] = "ai_function"
|
||||
self.type: Literal["function_tool"] = "function_tool"
|
||||
self._forward_runtime_kwargs: bool = False
|
||||
if self.func:
|
||||
sig = inspect.signature(self.func)
|
||||
@@ -680,10 +683,10 @@ class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
return True
|
||||
return self.func is None
|
||||
|
||||
def __get__(self, obj: Any, objtype: type | None = None) -> "AIFunction[ArgsT, ReturnT]":
|
||||
def __get__(self, obj: Any, objtype: type | None = None) -> "FunctionTool[ArgsT, ReturnT]":
|
||||
"""Implement the descriptor protocol to support bound methods.
|
||||
|
||||
When an AIFunction is accessed as an attribute of a class instance,
|
||||
When a FunctionTool is accessed as an attribute of a class instance,
|
||||
this method is called to bind the instance to the function.
|
||||
|
||||
Args:
|
||||
@@ -691,7 +694,7 @@ class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
objtype: The type that owns the descriptor.
|
||||
|
||||
Returns:
|
||||
A new AIFunction with the instance bound to the wrapped function.
|
||||
A new FunctionTool with the instance bound to the wrapped function.
|
||||
"""
|
||||
if obj is None:
|
||||
# Accessed from the class, not an instance
|
||||
@@ -702,7 +705,7 @@ class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
sig = inspect.signature(self.func)
|
||||
params = list(sig.parameters.keys())
|
||||
if params and params[0] in {"self", "cls"}:
|
||||
# Create a new AIFunction with the bound method
|
||||
# Create a new FunctionTool with the bound method
|
||||
import copy
|
||||
|
||||
bound_func = copy.copy(self)
|
||||
@@ -849,7 +852,7 @@ class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
return self._cached_parameters
|
||||
|
||||
def to_json_schema_spec(self) -> dict[str, Any]:
|
||||
"""Convert a AIFunction to the JSON Schema function specification format.
|
||||
"""Convert a FunctionTool to the JSON Schema function specification format.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the function specification in JSON Schema format.
|
||||
@@ -892,29 +895,29 @@ def _tools_to_dict(
|
||||
if not tools:
|
||||
return None
|
||||
if not isinstance(tools, list):
|
||||
if isinstance(tools, AIFunction):
|
||||
if isinstance(tools, FunctionTool):
|
||||
return [tools.to_json_schema_spec()]
|
||||
if isinstance(tools, SerializationMixin):
|
||||
return [tools.to_dict()]
|
||||
if isinstance(tools, dict):
|
||||
return [tools]
|
||||
if callable(tools):
|
||||
return [ai_function(tools).to_json_schema_spec()]
|
||||
return [tool(tools).to_json_schema_spec()]
|
||||
logger.warning("Can't parse tool.")
|
||||
return None
|
||||
results: list[str | dict[str, Any]] = []
|
||||
for tool in tools:
|
||||
if isinstance(tool, AIFunction):
|
||||
results.append(tool.to_json_schema_spec())
|
||||
for tool_item in tools:
|
||||
if isinstance(tool_item, FunctionTool):
|
||||
results.append(tool_item.to_json_schema_spec())
|
||||
continue
|
||||
if isinstance(tool, SerializationMixin):
|
||||
results.append(tool.to_dict())
|
||||
if isinstance(tool_item, SerializationMixin):
|
||||
results.append(tool_item.to_dict())
|
||||
continue
|
||||
if isinstance(tool, dict):
|
||||
results.append(tool)
|
||||
if isinstance(tool_item, dict):
|
||||
results.append(tool_item)
|
||||
continue
|
||||
if callable(tool):
|
||||
results.append(ai_function(tool).to_json_schema_spec())
|
||||
if callable(tool_item):
|
||||
results.append(tool(tool_item).to_json_schema_spec())
|
||||
continue
|
||||
logger.warning("Can't parse tool.")
|
||||
return results
|
||||
@@ -958,10 +961,8 @@ def _parse_annotation(annotation: Any) -> Any:
|
||||
|
||||
def _create_input_model_from_func(func: Callable[..., Any], name: str) -> type[BaseModel]:
|
||||
"""Create a Pydantic model from a function's signature."""
|
||||
# Unwrap AIFunction objects to get the underlying function
|
||||
from agent_framework._tools import AIFunction
|
||||
|
||||
if isinstance(func, AIFunction):
|
||||
# Unwrap FunctionTool objects to get the underlying function
|
||||
if isinstance(func, FunctionTool):
|
||||
func = func.func # type: ignore[assignment]
|
||||
|
||||
sig = inspect.signature(func)
|
||||
@@ -1212,7 +1213,7 @@ def _create_model_from_json_schema(tool_name: str, schema_json: Mapping[str, Any
|
||||
|
||||
|
||||
@overload
|
||||
def ai_function(
|
||||
def tool(
|
||||
func: Callable[..., ReturnT | Awaitable[ReturnT]],
|
||||
*,
|
||||
name: str | None = None,
|
||||
@@ -1221,11 +1222,11 @@ def ai_function(
|
||||
max_invocations: int | None = None,
|
||||
max_invocation_exceptions: int | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
) -> AIFunction[Any, ReturnT]: ...
|
||||
) -> FunctionTool[Any, ReturnT]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def ai_function(
|
||||
def tool(
|
||||
func: None = None,
|
||||
*,
|
||||
name: str | None = None,
|
||||
@@ -1234,10 +1235,10 @@ def ai_function(
|
||||
max_invocations: int | None = None,
|
||||
max_invocation_exceptions: int | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
) -> Callable[[Callable[..., ReturnT | Awaitable[ReturnT]]], AIFunction[Any, ReturnT]]: ...
|
||||
) -> Callable[[Callable[..., ReturnT | Awaitable[ReturnT]]], FunctionTool[Any, ReturnT]]: ...
|
||||
|
||||
|
||||
def ai_function(
|
||||
def tool(
|
||||
func: Callable[..., ReturnT | Awaitable[ReturnT]] | None = None,
|
||||
*,
|
||||
name: str | None = None,
|
||||
@@ -1246,8 +1247,8 @@ def ai_function(
|
||||
max_invocations: int | None = None,
|
||||
max_invocation_exceptions: int | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
) -> AIFunction[Any, ReturnT] | Callable[[Callable[..., ReturnT | Awaitable[ReturnT]]], AIFunction[Any, ReturnT]]:
|
||||
"""Decorate a function to turn it into a AIFunction that can be passed to models and executed automatically.
|
||||
) -> FunctionTool[Any, ReturnT] | Callable[[Callable[..., ReturnT | Awaitable[ReturnT]]], FunctionTool[Any, ReturnT]]:
|
||||
"""Decorate a function to turn it into a FunctionTool that can be passed to models and executed automatically.
|
||||
|
||||
This decorator creates a Pydantic model from the function's signature,
|
||||
which will be used to validate the arguments passed to the function
|
||||
@@ -1266,7 +1267,7 @@ def ai_function(
|
||||
description: A description of the function. If not provided, the function's
|
||||
docstring will be used.
|
||||
approval_mode: Whether or not approval is required to run this tool.
|
||||
Default is that approval is not needed.
|
||||
Default is that approval is required.
|
||||
max_invocations: The maximum number of times this function can be invoked.
|
||||
If None, there is no limit, should be at least 1.
|
||||
max_invocation_exceptions: The maximum number of exceptions allowed during invocations.
|
||||
@@ -1283,12 +1284,12 @@ def ai_function(
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import ai_function
|
||||
from agent_framework import tool
|
||||
from typing import Annotated
|
||||
|
||||
|
||||
@ai_function
|
||||
def ai_function_example(
|
||||
@tool(approval_mode="never_require")
|
||||
def tool_example(
|
||||
arg1: Annotated[str, "The first argument"],
|
||||
arg2: Annotated[int, "The second argument"],
|
||||
) -> str:
|
||||
@@ -1297,8 +1298,8 @@ def ai_function(
|
||||
|
||||
|
||||
# the same function but with approval required to run
|
||||
@ai_function(approval_mode="always_require")
|
||||
def ai_function_example(
|
||||
@tool(approval_mode="always_require")
|
||||
def tool_example(
|
||||
arg1: Annotated[str, "The first argument"],
|
||||
arg2: Annotated[int, "The second argument"],
|
||||
) -> str:
|
||||
@@ -1307,13 +1308,13 @@ def ai_function(
|
||||
|
||||
|
||||
# With custom name and description
|
||||
@ai_function(name="custom_weather", description="Custom weather function")
|
||||
@tool(name="custom_weather", description="Custom weather function")
|
||||
def another_weather_func(location: str) -> str:
|
||||
return f"Weather in {location}"
|
||||
|
||||
|
||||
# Async functions are also supported
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
async def async_get_weather(location: str) -> str:
|
||||
'''Get weather asynchronously.'''
|
||||
# Simulate async operation
|
||||
@@ -1321,12 +1322,12 @@ def ai_function(
|
||||
|
||||
"""
|
||||
|
||||
def decorator(func: Callable[..., ReturnT | Awaitable[ReturnT]]) -> AIFunction[Any, ReturnT]:
|
||||
def decorator(func: Callable[..., ReturnT | Awaitable[ReturnT]]) -> FunctionTool[Any, ReturnT]:
|
||||
@wraps(func)
|
||||
def wrapper(f: Callable[..., ReturnT | Awaitable[ReturnT]]) -> AIFunction[Any, ReturnT]:
|
||||
def wrapper(f: Callable[..., ReturnT | Awaitable[ReturnT]]) -> FunctionTool[Any, ReturnT]:
|
||||
tool_name: str = name or getattr(f, "__name__", "unknown_function") # type: ignore[assignment]
|
||||
tool_desc: str = description or (f.__doc__ or "")
|
||||
return AIFunction[Any, ReturnT](
|
||||
return FunctionTool[Any, ReturnT](
|
||||
name=tool_name,
|
||||
description=tool_desc,
|
||||
approval_mode=approval_mode,
|
||||
@@ -1490,7 +1491,7 @@ async def _auto_invoke_function(
|
||||
custom_args: dict[str, Any] | None = None,
|
||||
*,
|
||||
config: FunctionInvocationConfiguration,
|
||||
tool_map: dict[str, AIFunction[BaseModel, Any]],
|
||||
tool_map: dict[str, FunctionTool[BaseModel, Any]],
|
||||
sequence_index: int | None = None,
|
||||
request_index: int | None = None,
|
||||
middleware_pipeline: Any = None, # Optional MiddlewarePipeline
|
||||
@@ -1503,7 +1504,7 @@ async def _auto_invoke_function(
|
||||
|
||||
Keyword Args:
|
||||
config: The function invocation configuration.
|
||||
tool_map: A mapping of tool names to AIFunction instances.
|
||||
tool_map: A mapping of tool names to FunctionTool instances.
|
||||
sequence_index: The index of the function call in the sequence.
|
||||
request_index: The index of the request iteration.
|
||||
middleware_pipeline: Optional middleware pipeline to apply during execution.
|
||||
@@ -1522,7 +1523,7 @@ async def _auto_invoke_function(
|
||||
# this function is called. This function only handles the actual execution of approved,
|
||||
# non-declaration-only functions.
|
||||
|
||||
tool: AIFunction[BaseModel, Any] | None = None
|
||||
tool: FunctionTool[BaseModel, Any] | None = None
|
||||
if function_call_content.type == "function_call":
|
||||
tool = tool_map.get(function_call_content.name) # type: ignore[arg-type]
|
||||
# Tool should exist because _try_execute_function_calls validates this
|
||||
@@ -1645,17 +1646,17 @@ def _get_tool_map(
|
||||
| Callable[..., Any] \
|
||||
| MutableMapping[str, Any] \
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]",
|
||||
) -> dict[str, AIFunction[Any, Any]]:
|
||||
ai_function_list: dict[str, AIFunction[Any, Any]] = {}
|
||||
for tool in tools if isinstance(tools, list) else [tools]:
|
||||
if isinstance(tool, AIFunction):
|
||||
ai_function_list[tool.name] = tool
|
||||
) -> dict[str, FunctionTool[Any, Any]]:
|
||||
tool_list: dict[str, FunctionTool[Any, Any]] = {}
|
||||
for tool_item in tools if isinstance(tools, list) else [tools]:
|
||||
if isinstance(tool_item, FunctionTool):
|
||||
tool_list[tool_item.name] = tool_item
|
||||
continue
|
||||
if callable(tool):
|
||||
if callable(tool_item):
|
||||
# Convert to AITool if it's a function or callable
|
||||
ai_tool = ai_function(tool)
|
||||
ai_function_list[ai_tool.name] = ai_tool
|
||||
return ai_function_list
|
||||
ai_tool = tool(tool_item)
|
||||
tool_list[ai_tool.name] = ai_tool
|
||||
return tool_list
|
||||
|
||||
|
||||
async def _try_execute_function_calls(
|
||||
|
||||
@@ -18,7 +18,7 @@ from pydantic import BaseModel, ValidationError
|
||||
|
||||
from ._logging import get_logger
|
||||
from ._serialization import SerializationMixin
|
||||
from ._tools import ToolProtocol, ai_function
|
||||
from ._tools import ToolProtocol, tool
|
||||
from .exceptions import AdditionItemMismatch, ContentError
|
||||
|
||||
__all__ = [
|
||||
@@ -2854,7 +2854,7 @@ def normalize_tools(
|
||||
) -> list[ToolProtocol | MutableMapping[str, Any]]:
|
||||
"""Normalize tools into a list.
|
||||
|
||||
Converts callables to AIFunction objects and ensures all tools are either
|
||||
Converts callables to FunctionTool objects and ensures all tools are either
|
||||
ToolProtocol instances or MutableMappings.
|
||||
|
||||
Args:
|
||||
@@ -2866,10 +2866,10 @@ def normalize_tools(
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import normalize_tools, ai_function
|
||||
from agent_framework import normalize_tools, tool
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def my_tool(x: int) -> int:
|
||||
return x * 2
|
||||
|
||||
@@ -2886,14 +2886,14 @@ def normalize_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)):
|
||||
return [ai_function(tools)]
|
||||
return [tool(tools)]
|
||||
return [tools]
|
||||
for tool in tools:
|
||||
if isinstance(tool, (ToolProtocol, MutableMapping)):
|
||||
final_tools.append(tool)
|
||||
for tool_item in tools:
|
||||
if isinstance(tool_item, (ToolProtocol, MutableMapping)):
|
||||
final_tools.append(tool_item)
|
||||
else:
|
||||
# Convert callable to AIFunction
|
||||
final_tools.append(ai_function(tool))
|
||||
# Convert callable to FunctionTool
|
||||
final_tools.append(tool(tool_item))
|
||||
return final_tools
|
||||
|
||||
|
||||
@@ -2908,7 +2908,7 @@ async def validate_tools(
|
||||
) -> list[ToolProtocol | MutableMapping[str, Any]]:
|
||||
"""Validate and normalize tools into a list.
|
||||
|
||||
Converts callables to AIFunction objects, expands MCP tools to their constituent
|
||||
Converts callables to FunctionTool objects, expands MCP tools to their constituent
|
||||
functions (connecting them if needed), and ensures all tools are either ToolProtocol
|
||||
instances or MutableMappings.
|
||||
|
||||
@@ -2921,10 +2921,10 @@ async def validate_tools(
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import validate_tools, ai_function
|
||||
from agent_framework import validate_tools, tool
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def my_tool(x: int) -> int:
|
||||
return x * 2
|
||||
|
||||
@@ -2935,22 +2935,22 @@ async def validate_tools(
|
||||
# List of tools
|
||||
tools = await validate_tools([my_tool, another_tool])
|
||||
"""
|
||||
# Use normalize_tools for common sync logic (converts callables to AIFunction)
|
||||
# Use normalize_tools for common sync logic (converts callables to FunctionTool)
|
||||
normalized = normalize_tools(tools)
|
||||
|
||||
# Handle MCP tool expansion (async-only)
|
||||
final_tools: list[ToolProtocol | MutableMapping[str, Any]] = []
|
||||
for tool in normalized:
|
||||
for tool_ in normalized:
|
||||
# Import MCPTool here to avoid circular imports
|
||||
from ._mcp import MCPTool
|
||||
|
||||
if isinstance(tool, MCPTool):
|
||||
if isinstance(tool_, MCPTool):
|
||||
# Expand MCP tools to their constituent functions
|
||||
if not tool.is_connected:
|
||||
await tool.connect()
|
||||
final_tools.extend(tool.functions) # type: ignore
|
||||
if not tool_.is_connected:
|
||||
await tool_.connect()
|
||||
final_tools.extend(tool_.functions) # type: ignore
|
||||
else:
|
||||
final_tools.append(tool)
|
||||
final_tools.append(tool_)
|
||||
|
||||
return final_tools
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ class WorkflowAgent(BaseAgent):
|
||||
used to load and restore the checkpoint. When provided without checkpoint_id,
|
||||
enables checkpointing for this run.
|
||||
**kwargs: Additional keyword arguments passed through to underlying workflow
|
||||
and ai_function tools.
|
||||
and tool functions.
|
||||
|
||||
Returns:
|
||||
The final workflow response as an AgentResponse.
|
||||
@@ -185,7 +185,7 @@ class WorkflowAgent(BaseAgent):
|
||||
used to load and restore the checkpoint. When provided without checkpoint_id,
|
||||
enables checkpointing for this run.
|
||||
**kwargs: Additional keyword arguments passed through to underlying workflow
|
||||
and ai_function tools.
|
||||
and tool functions.
|
||||
|
||||
Yields:
|
||||
AgentResponseUpdate objects representing the workflow execution progress.
|
||||
@@ -225,7 +225,7 @@ class WorkflowAgent(BaseAgent):
|
||||
checkpoint_id: ID of checkpoint to restore from.
|
||||
checkpoint_storage: Runtime checkpoint storage.
|
||||
**kwargs: Additional keyword arguments passed through to the underlying
|
||||
workflow and ai_function tools.
|
||||
workflow and tool functions.
|
||||
|
||||
Yields:
|
||||
AgentResponseUpdate objects representing the workflow execution progress.
|
||||
|
||||
@@ -11,7 +11,7 @@ INTERNAL_SOURCE_PREFIX = "internal"
|
||||
|
||||
# SharedState key for storing run kwargs that should be passed to agent invocations.
|
||||
# Used by all orchestration patterns (Sequential, Concurrent, GroupChat, Handoff, Magentic)
|
||||
# to pass kwargs from workflow.run_stream() through to agent.run_stream() and @ai_function tools.
|
||||
# to pass kwargs from workflow.run_stream() through to agent.run_stream() and @tool functions.
|
||||
WORKFLOW_RUN_KWARGS_KEY = "_workflow_run_kwargs"
|
||||
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ from typing_extensions import Never
|
||||
from .._agents import AgentProtocol, ChatAgent
|
||||
from .._middleware import FunctionInvocationContext, FunctionMiddleware
|
||||
from .._threads import AgentThread
|
||||
from .._tools import AIFunction, ai_function
|
||||
from .._tools import FunctionTool, tool
|
||||
from .._types import AgentResponse, ChatMessage, Role
|
||||
from ._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse
|
||||
from ._agent_utils import resolve_agent_id
|
||||
@@ -331,7 +331,7 @@ class HandoffAgentExecutor(AgentExecutor):
|
||||
existing_tools = list(default_options.get("tools") or [])
|
||||
existing_names = {getattr(tool, "name", "") for tool in existing_tools if hasattr(tool, "name")}
|
||||
|
||||
new_tools: list[AIFunction[Any, Any]] = []
|
||||
new_tools: list[FunctionTool[Any, Any]] = []
|
||||
for target in targets:
|
||||
tool = self._create_handoff_tool(target.target_id, target.description)
|
||||
if tool.name in existing_names:
|
||||
@@ -347,17 +347,17 @@ class HandoffAgentExecutor(AgentExecutor):
|
||||
else:
|
||||
default_options["tools"] = existing_tools
|
||||
|
||||
def _create_handoff_tool(self, target_id: str, description: str | None = None) -> AIFunction[Any, Any]:
|
||||
def _create_handoff_tool(self, target_id: str, description: str | None = None) -> FunctionTool[Any, Any]:
|
||||
"""Construct the synthetic handoff tool that signals routing to `target_id`."""
|
||||
tool_name = get_handoff_tool_name(target_id)
|
||||
doc = description or f"Handoff to the {target_id} agent."
|
||||
# Note: approval_mode is intentionally NOT set for handoff tools.
|
||||
# Handoff tools are framework-internal signals that trigger routing logic,
|
||||
# not actual function executions. They are automatically intercepted by
|
||||
# Note: approval_mode is set to "never_require" for handoff tools because
|
||||
# they are framework-internal signals that trigger routing logic, not
|
||||
# actual function executions. They are automatically intercepted by
|
||||
# _AutoHandoffMiddleware which short-circuits execution and provides synthetic
|
||||
# results, so the function body never actually runs in practice.
|
||||
|
||||
@ai_function(name=tool_name, description=doc)
|
||||
@tool(name=tool_name, description=doc, approval_mode="never_require")
|
||||
def _handoff_tool(context: str | None = None) -> str:
|
||||
"""Return a deterministic acknowledgement that encodes the target alias."""
|
||||
return f"Handoff to {target_id}"
|
||||
|
||||
@@ -456,7 +456,7 @@ class Workflow(DictConvertible):
|
||||
- Without checkpoint_id: Enables checkpointing for this run, overriding
|
||||
build-time configuration
|
||||
**kwargs: Additional keyword arguments to pass through to agent invocations.
|
||||
These are stored in SharedState and accessible in @ai_function tools
|
||||
These are stored in SharedState and accessible in @tool functions
|
||||
via the **kwargs parameter.
|
||||
|
||||
Yields:
|
||||
@@ -476,7 +476,7 @@ class Workflow(DictConvertible):
|
||||
async for event in workflow.run_stream("start message"):
|
||||
process(event)
|
||||
|
||||
With custom context for ai_functions:
|
||||
With custom context for tools:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -590,7 +590,7 @@ class Workflow(DictConvertible):
|
||||
build-time configuration
|
||||
include_status_events: Whether to include WorkflowStatusEvent instances in the result list.
|
||||
**kwargs: Additional keyword arguments to pass through to agent invocations.
|
||||
These are stored in SharedState and accessible in @ai_function tools
|
||||
These are stored in SharedState and accessible in @tool functions
|
||||
via the **kwargs parameter.
|
||||
|
||||
Returns:
|
||||
@@ -610,7 +610,7 @@ class Workflow(DictConvertible):
|
||||
result = await workflow.run("start message")
|
||||
outputs = result.get_outputs()
|
||||
|
||||
With custom context for ai_functions:
|
||||
With custom context for tools:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ if TYPE_CHECKING: # pragma: no cover
|
||||
from ._agents import AgentProtocol
|
||||
from ._clients import ChatClientProtocol
|
||||
from ._threads import AgentThread
|
||||
from ._tools import AIFunction
|
||||
from ._tools import FunctionTool
|
||||
from ._types import (
|
||||
AgentResponse,
|
||||
AgentResponseUpdate,
|
||||
@@ -1546,7 +1546,7 @@ def use_agent_instrumentation(
|
||||
# region Otel Helpers
|
||||
|
||||
|
||||
def get_function_span_attributes(function: "AIFunction[Any, Any]", tool_call_id: str | None = None) -> dict[str, str]:
|
||||
def get_function_span_attributes(function: "FunctionTool[Any, Any]", tool_call_id: str | None = None) -> dict[str, str]:
|
||||
"""Get the span attributes for the given function.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -11,7 +11,7 @@ from pydantic import BaseModel, SecretStr, ValidationError
|
||||
from .._agents import ChatAgent
|
||||
from .._memory import ContextProvider
|
||||
from .._middleware import Middleware
|
||||
from .._tools import AIFunction, ToolProtocol
|
||||
from .._tools import FunctionTool, ToolProtocol
|
||||
from .._types import normalize_tools
|
||||
from ..exceptions import ServiceInitializationError
|
||||
from ._assistants_client import OpenAIAssistantsClient
|
||||
@@ -215,7 +215,7 @@ class OpenAIAssistantProvider(Generic[TOptions_co]):
|
||||
instructions: System instructions for the assistant.
|
||||
description: A description of the assistant.
|
||||
tools: Tools available to the assistant. Can include:
|
||||
- AIFunction instances or callables decorated with @ai_function
|
||||
- FunctionTool instances or callables decorated with @tool
|
||||
- HostedCodeInterpreterTool for code execution
|
||||
- HostedFileSearchTool for vector store search
|
||||
- Raw tool dictionaries
|
||||
@@ -467,7 +467,7 @@ class OpenAIAssistantProvider(Generic[TOptions_co]):
|
||||
if provided_tools is not None:
|
||||
normalized = normalize_tools(provided_tools)
|
||||
for tool in normalized:
|
||||
if isinstance(tool, AIFunction):
|
||||
if isinstance(tool, FunctionTool):
|
||||
provided_functions.add(tool.name)
|
||||
elif isinstance(tool, MutableMapping) and "function" in tool:
|
||||
func_spec = tool.get("function", {})
|
||||
|
||||
@@ -36,7 +36,7 @@ from .._memory import ContextProvider
|
||||
from .._middleware import Middleware, use_chat_middleware
|
||||
from .._threads import ChatMessageStoreProtocol
|
||||
from .._tools import (
|
||||
AIFunction,
|
||||
FunctionTool,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
ToolProtocol,
|
||||
@@ -626,7 +626,7 @@ class OpenAIAssistantsClient(
|
||||
tool_definitions: list[MutableMapping[str, Any]] = []
|
||||
if tool_mode["mode"] != "none" and tools is not None:
|
||||
for tool in tools:
|
||||
if isinstance(tool, AIFunction):
|
||||
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"})
|
||||
|
||||
@@ -19,7 +19,7 @@ from pydantic import ValidationError
|
||||
from .._clients import BaseChatClient
|
||||
from .._logging import get_logger
|
||||
from .._middleware import use_chat_middleware
|
||||
from .._tools import AIFunction, HostedWebSearchTool, ToolProtocol, use_function_invocation
|
||||
from .._tools import FunctionTool, HostedWebSearchTool, ToolProtocol, use_function_invocation
|
||||
from .._types import (
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
@@ -198,7 +198,7 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient[TOpenAIChatOptions], Gener
|
||||
for tool in tools:
|
||||
if isinstance(tool, ToolProtocol):
|
||||
match tool:
|
||||
case AIFunction():
|
||||
case FunctionTool():
|
||||
chat_tools.append(tool.to_json_schema_spec())
|
||||
case HostedWebSearchTool():
|
||||
web_search_options = (
|
||||
|
||||
@@ -38,7 +38,7 @@ from .._clients import BaseChatClient
|
||||
from .._logging import get_logger
|
||||
from .._middleware import use_chat_middleware
|
||||
from .._tools import (
|
||||
AIFunction,
|
||||
FunctionTool,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
HostedImageGenerationTool,
|
||||
@@ -384,7 +384,7 @@ class OpenAIBaseResponsesClient(
|
||||
container=tool_args,
|
||||
)
|
||||
)
|
||||
case AIFunction():
|
||||
case FunctionTool():
|
||||
params = tool.parameters()
|
||||
params["additionalProperties"] = False
|
||||
response_tools.append(
|
||||
|
||||
@@ -24,7 +24,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 AIFunction, HostedCodeInterpreterTool, HostedFileSearchTool, ToolProtocol
|
||||
from .._tools import FunctionTool, HostedCodeInterpreterTool, HostedFileSearchTool, ToolProtocol
|
||||
from ..exceptions import ServiceInitializationError
|
||||
|
||||
logger: logging.Logger = get_logger("agent_framework.openai")
|
||||
@@ -295,7 +295,7 @@ def to_assistant_tools(
|
||||
tool_definitions: list[dict[str, Any]] = []
|
||||
|
||||
for tool in tools:
|
||||
if isinstance(tool, AIFunction):
|
||||
if isinstance(tool, FunctionTool):
|
||||
tool_definitions.append(tool.to_json_schema_spec())
|
||||
elif isinstance(tool, HostedCodeInterpreterTool):
|
||||
tool_definitions.append({"type": "code_interpreter"})
|
||||
|
||||
@@ -18,6 +18,7 @@ from agent_framework import (
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
HostedCodeInterpreterTool,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIAssistantsClient
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
@@ -253,6 +254,7 @@ def test_azure_assistants_client_serialize(azure_openai_unit_test_env: dict[str,
|
||||
assert "User-Agent" not in dumped_settings["default_headers"]
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
|
||||
@@ -25,7 +25,7 @@ from agent_framework import (
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
ai_function,
|
||||
tool,
|
||||
)
|
||||
from agent_framework._telemetry import USER_AGENT_KEY
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
@@ -631,7 +631,7 @@ async def test_streaming_with_none_delta(
|
||||
assert any(msg.contents for msg in results)
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def get_story_text() -> str:
|
||||
"""Returns a story about Emily and David."""
|
||||
return (
|
||||
@@ -642,7 +642,7 @@ def get_story_text() -> str:
|
||||
)
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def get_weather(location: str) -> str:
|
||||
"""Get the current weather for a location."""
|
||||
return f"The weather in {location} is sunny and 72°F."
|
||||
|
||||
@@ -20,7 +20,7 @@ from agent_framework import (
|
||||
HostedFileSearchTool,
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
ai_function,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.azure import AzureOpenAIResponsesClient
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
@@ -41,7 +41,7 @@ class OutputStruct(BaseModel):
|
||||
weather: str
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
async def get_weather(location: Annotated[str, "The location as a city name"]) -> str:
|
||||
"""Get the current weather in a given location."""
|
||||
# Implementation of the tool to get weather
|
||||
|
||||
@@ -23,7 +23,7 @@ from agent_framework import (
|
||||
Content,
|
||||
Role,
|
||||
ToolProtocol,
|
||||
ai_function,
|
||||
tool,
|
||||
use_chat_middleware,
|
||||
use_function_invocation,
|
||||
)
|
||||
@@ -65,10 +65,10 @@ def ai_tool() -> ToolProtocol:
|
||||
|
||||
|
||||
@fixture
|
||||
def ai_function_tool() -> ToolProtocol:
|
||||
def tool_tool() -> ToolProtocol:
|
||||
"""Returns a executable ToolProtocol."""
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def simple_function(x: int, y: int) -> int:
|
||||
"""A simple function that adds two numbers."""
|
||||
return x + y
|
||||
|
||||
@@ -23,7 +23,7 @@ from agent_framework import (
|
||||
ContextProvider,
|
||||
HostedCodeInterpreterTool,
|
||||
Role,
|
||||
ai_function,
|
||||
tool,
|
||||
)
|
||||
from agent_framework._mcp import MCPTool
|
||||
from agent_framework.exceptions import AgentExecutionException
|
||||
@@ -423,7 +423,7 @@ async def test_chat_agent_as_tool_no_name(chat_client: ChatClientProtocol) -> No
|
||||
|
||||
|
||||
async def test_chat_agent_as_tool_function_execution(chat_client: ChatClientProtocol) -> None:
|
||||
"""Test that the generated AIFunction can be executed."""
|
||||
"""Test that the generated FunctionTool can be executed."""
|
||||
agent = ChatAgent(chat_client=chat_client, name="TestAgent", description="Test agent")
|
||||
|
||||
tool = agent.as_tool()
|
||||
@@ -564,7 +564,7 @@ async def test_agent_tool_receives_thread_in_kwargs(chat_client_base: Any) -> No
|
||||
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
@ai_function(name="echo_thread_info")
|
||||
@tool(name="echo_thread_info", approval_mode="never_require")
|
||||
def echo_thread_info(text: str, **kwargs: Any) -> str: # type: ignore[reportUnknownParameterType]
|
||||
thread = kwargs.get("thread")
|
||||
captured["has_thread"] = thread is not None
|
||||
@@ -596,9 +596,7 @@ async def test_agent_tool_receives_thread_in_kwargs(chat_client_base: Any) -> No
|
||||
assert captured.get("has_message_store") is True
|
||||
|
||||
|
||||
async def test_chat_agent_tool_choice_run_level_overrides_agent_level(
|
||||
chat_client_base: Any, ai_function_tool: Any
|
||||
) -> None:
|
||||
async def test_chat_agent_tool_choice_run_level_overrides_agent_level(chat_client_base: Any, tool_tool: Any) -> None:
|
||||
"""Verify that tool_choice passed to run() overrides agent-level tool_choice."""
|
||||
|
||||
captured_options: list[dict[str, Any]] = []
|
||||
@@ -617,7 +615,7 @@ async def test_chat_agent_tool_choice_run_level_overrides_agent_level(
|
||||
# Create agent with agent-level tool_choice="auto" and a tool (tools required for tool_choice to be meaningful)
|
||||
agent = ChatAgent(
|
||||
chat_client=chat_client_base,
|
||||
tools=[ai_function_tool],
|
||||
tools=[tool_tool],
|
||||
options={"tool_choice": "auto"},
|
||||
)
|
||||
|
||||
@@ -630,7 +628,7 @@ async def test_chat_agent_tool_choice_run_level_overrides_agent_level(
|
||||
|
||||
|
||||
async def test_chat_agent_tool_choice_agent_level_used_when_run_level_not_specified(
|
||||
chat_client_base: Any, ai_function_tool: Any
|
||||
chat_client_base: Any, tool_tool: Any
|
||||
) -> None:
|
||||
"""Verify that agent-level tool_choice is used when run() doesn't specify one."""
|
||||
from agent_framework import ChatOptions
|
||||
@@ -650,7 +648,7 @@ async def test_chat_agent_tool_choice_agent_level_used_when_run_level_not_specif
|
||||
# Create agent with agent-level tool_choice="required" and a tool
|
||||
agent = ChatAgent(
|
||||
chat_client=chat_client_base,
|
||||
tools=[ai_function_tool],
|
||||
tools=[tool_tool],
|
||||
default_options={"tool_choice": "required"},
|
||||
)
|
||||
|
||||
@@ -664,9 +662,7 @@ async def test_chat_agent_tool_choice_agent_level_used_when_run_level_not_specif
|
||||
assert captured_options[0]["tool_choice"] == "required"
|
||||
|
||||
|
||||
async def test_chat_agent_tool_choice_none_at_run_preserves_agent_level(
|
||||
chat_client_base: Any, ai_function_tool: Any
|
||||
) -> None:
|
||||
async def test_chat_agent_tool_choice_none_at_run_preserves_agent_level(chat_client_base: Any, tool_tool: Any) -> None:
|
||||
"""Verify that tool_choice=None at run() uses agent-level default."""
|
||||
from agent_framework import ChatOptions
|
||||
|
||||
@@ -685,7 +681,7 @@ async def test_chat_agent_tool_choice_none_at_run_preserves_agent_level(
|
||||
# Create agent with agent-level tool_choice="auto" and a tool
|
||||
agent = ChatAgent(
|
||||
chat_client=chat_client_base,
|
||||
tools=[ai_function_tool],
|
||||
tools=[tool_tool],
|
||||
default_options={"tool_choice": "auto"},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,433 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Annotated
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agent_framework import (
|
||||
AgentResponse,
|
||||
AgentResponseUpdate,
|
||||
AgentThread,
|
||||
ChatAgent,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedImageGenerationTool,
|
||||
HostedMCPTool,
|
||||
MCPStreamableHTTPTool,
|
||||
ai_function,
|
||||
)
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
|
||||
skip_if_openai_integration_tests_disabled = pytest.mark.skipif(
|
||||
os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true"
|
||||
or os.getenv("OPENAI_API_KEY", "") in ("", "test-dummy-key"),
|
||||
reason="No real OPENAI_API_KEY provided; skipping integration tests."
|
||||
if os.getenv("RUN_INTEGRATION_TESTS", "false").lower() == "true"
|
||||
else "Integration tests are disabled.",
|
||||
)
|
||||
|
||||
|
||||
@ai_function
|
||||
async def get_weather(location: Annotated[str, "The location as a city name"]) -> str:
|
||||
"""Get the current weather in a given location."""
|
||||
# Implementation of the tool to get weather
|
||||
return f"The current weather in {location} is sunny."
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_openai_integration_tests_disabled
|
||||
async def test_openai_responses_client_agent_basic_run_streaming():
|
||||
"""Test OpenAI Responses Client agent basic streaming functionality with OpenAIResponsesClient."""
|
||||
async with ChatAgent(
|
||||
chat_client=OpenAIResponsesClient(),
|
||||
) as agent:
|
||||
# Test streaming run
|
||||
full_text = ""
|
||||
async for chunk in agent.run_stream("Please respond with exactly: 'This is a streaming response test.'"):
|
||||
assert isinstance(chunk, AgentResponseUpdate)
|
||||
if chunk.text:
|
||||
full_text += chunk.text
|
||||
|
||||
assert len(full_text) > 0
|
||||
assert "streaming response test" in full_text.lower()
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_openai_integration_tests_disabled
|
||||
async def test_openai_responses_client_agent_thread_persistence():
|
||||
"""Test OpenAI Responses Client agent thread persistence across runs with OpenAIResponsesClient."""
|
||||
async with ChatAgent(
|
||||
chat_client=OpenAIResponsesClient(),
|
||||
instructions="You are a helpful assistant with good memory.",
|
||||
) as agent:
|
||||
# Create a new thread that will be reused
|
||||
thread = agent.get_new_thread()
|
||||
|
||||
# First interaction
|
||||
first_response = await agent.run("My favorite programming language is Python. Remember this.", thread=thread)
|
||||
|
||||
assert isinstance(first_response, AgentResponse)
|
||||
assert first_response.text is not None
|
||||
|
||||
# Second interaction - test memory
|
||||
second_response = await agent.run("What is my favorite programming language?", thread=thread)
|
||||
|
||||
assert isinstance(second_response, AgentResponse)
|
||||
assert second_response.text is not None
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_openai_integration_tests_disabled
|
||||
async def test_openai_responses_client_agent_thread_storage_with_store_true():
|
||||
"""Test OpenAI Responses Client agent with store=True to verify service_thread_id is returned."""
|
||||
async with ChatAgent(
|
||||
chat_client=OpenAIResponsesClient(),
|
||||
instructions="You are a helpful assistant.",
|
||||
) as agent:
|
||||
# Create a new thread
|
||||
thread = AgentThread()
|
||||
|
||||
# Initially, service_thread_id should be None
|
||||
assert thread.service_thread_id is None
|
||||
|
||||
# Run with store=True to store messages on OpenAI side
|
||||
response = await agent.run(
|
||||
"Hello! Please remember that my name is Alex.",
|
||||
thread=thread,
|
||||
options={"store": True},
|
||||
)
|
||||
|
||||
# Validate response
|
||||
assert isinstance(response, AgentResponse)
|
||||
assert response.text is not None
|
||||
assert len(response.text) > 0
|
||||
|
||||
# After store=True, service_thread_id should be populated
|
||||
assert thread.service_thread_id is not None
|
||||
assert isinstance(thread.service_thread_id, str)
|
||||
assert len(thread.service_thread_id) > 0
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_openai_integration_tests_disabled
|
||||
async def test_openai_responses_client_agent_existing_thread():
|
||||
"""Test OpenAI Responses Client agent with existing thread to continue conversations across agent instances."""
|
||||
# First conversation - capture the thread
|
||||
preserved_thread = None
|
||||
|
||||
async with ChatAgent(
|
||||
chat_client=OpenAIResponsesClient(),
|
||||
instructions="You are a helpful assistant with good memory.",
|
||||
) as first_agent:
|
||||
# Start a conversation and capture the thread
|
||||
thread = first_agent.get_new_thread()
|
||||
first_response = await first_agent.run("My hobby is photography. Remember this.", thread=thread)
|
||||
|
||||
assert isinstance(first_response, AgentResponse)
|
||||
assert first_response.text is not None
|
||||
|
||||
# Preserve the thread for reuse
|
||||
preserved_thread = thread
|
||||
|
||||
# Second conversation - reuse the thread in a new agent instance
|
||||
if preserved_thread:
|
||||
async with ChatAgent(
|
||||
chat_client=OpenAIResponsesClient(),
|
||||
instructions="You are a helpful assistant with good memory.",
|
||||
) as second_agent:
|
||||
# Reuse the preserved thread
|
||||
second_response = await second_agent.run("What is my hobby?", thread=preserved_thread)
|
||||
|
||||
assert isinstance(second_response, AgentResponse)
|
||||
assert second_response.text is not None
|
||||
assert "photography" in second_response.text.lower()
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_openai_integration_tests_disabled
|
||||
async def test_openai_responses_client_agent_hosted_code_interpreter_tool():
|
||||
"""Test OpenAI Responses Client agent with HostedCodeInterpreterTool through OpenAIResponsesClient."""
|
||||
async with ChatAgent(
|
||||
chat_client=OpenAIResponsesClient(),
|
||||
instructions="You are a helpful assistant that can execute Python code.",
|
||||
tools=[HostedCodeInterpreterTool()],
|
||||
) as agent:
|
||||
# Test code interpreter functionality
|
||||
response = await agent.run("Calculate the sum of numbers from 1 to 10 using Python code.")
|
||||
|
||||
assert isinstance(response, AgentResponse)
|
||||
assert response.text is not None
|
||||
assert len(response.text) > 0
|
||||
# Should contain calculation result (sum of 1-10 = 55) or code execution content
|
||||
contains_relevant_content = any(
|
||||
term in response.text.lower() for term in ["55", "sum", "code", "python", "calculate", "10"]
|
||||
)
|
||||
assert contains_relevant_content or len(response.text.strip()) > 10
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_openai_integration_tests_disabled
|
||||
async def test_openai_responses_client_agent_image_generation_tool():
|
||||
"""Test OpenAI Responses Client agent with raw image_generation tool through OpenAIResponsesClient."""
|
||||
async with ChatAgent(
|
||||
chat_client=OpenAIResponsesClient(),
|
||||
instructions="You are a helpful assistant that can generate images.",
|
||||
tools=HostedImageGenerationTool(options={"image_size": "1024x1024", "media_type": "png"}),
|
||||
) as agent:
|
||||
# Test image generation functionality
|
||||
response = await agent.run("Generate an image of a cute red panda sitting on a tree branch in a forest.")
|
||||
|
||||
assert isinstance(response, AgentResponse)
|
||||
assert response.messages
|
||||
|
||||
# Verify we got image content - look for ImageGenerationToolResultContent
|
||||
image_content_found = False
|
||||
for message in response.messages:
|
||||
for content in message.contents:
|
||||
if content.type == "image_generation_tool_result" and content.outputs:
|
||||
image_content_found = True
|
||||
break
|
||||
if image_content_found:
|
||||
break
|
||||
|
||||
# The test passes if we got image content
|
||||
assert image_content_found, "Expected to find image content in response"
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_openai_integration_tests_disabled
|
||||
async def test_openai_responses_client_agent_level_tool_persistence():
|
||||
"""Test that agent-level tools persist across multiple runs with OpenAI Responses Client."""
|
||||
|
||||
async with ChatAgent(
|
||||
chat_client=OpenAIResponsesClient(),
|
||||
instructions="You are a helpful assistant that uses available tools.",
|
||||
tools=[get_weather], # Agent-level tool
|
||||
) as agent:
|
||||
# First run - agent-level tool should be available
|
||||
first_response = await agent.run("What's the weather like in Chicago?")
|
||||
|
||||
assert isinstance(first_response, AgentResponse)
|
||||
assert first_response.text is not None
|
||||
# Should use the agent-level weather tool
|
||||
assert any(term in first_response.text.lower() for term in ["chicago", "sunny", "72"])
|
||||
|
||||
# Second run - agent-level tool should still be available (persistence test)
|
||||
second_response = await agent.run("What's the weather in Miami?")
|
||||
|
||||
assert isinstance(second_response, AgentResponse)
|
||||
assert second_response.text is not None
|
||||
# Should use the agent-level weather tool again
|
||||
assert any(term in second_response.text.lower() for term in ["miami", "sunny", "72"])
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_openai_integration_tests_disabled
|
||||
async def test_openai_responses_client_run_level_tool_isolation():
|
||||
"""Test that run-level tools are isolated to specific runs and don't persist with OpenAI Responses Client."""
|
||||
# Counter to track how many times the weather tool is called
|
||||
call_count = 0
|
||||
|
||||
@ai_function
|
||||
async def get_weather_with_counter(
|
||||
location: Annotated[str, "The location as a city name"],
|
||||
) -> str:
|
||||
"""Get the current weather in a given location."""
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
return f"The weather in {location} is sunny and 72°F."
|
||||
|
||||
async with ChatAgent(
|
||||
chat_client=OpenAIResponsesClient(),
|
||||
instructions="You are a helpful assistant.",
|
||||
) as agent:
|
||||
# First run - use run-level tool
|
||||
first_response = await agent.run(
|
||||
"What's the weather like in Chicago?",
|
||||
tools=[get_weather_with_counter], # Run-level tool
|
||||
)
|
||||
|
||||
assert isinstance(first_response, AgentResponse)
|
||||
assert first_response.text is not None
|
||||
# Should use the run-level weather tool (call count should be 1)
|
||||
assert call_count == 1
|
||||
assert any(term in first_response.text.lower() for term in ["chicago", "sunny", "72"])
|
||||
|
||||
# Second run - run-level tool should NOT persist (key isolation test)
|
||||
second_response = await agent.run("What's the weather like in Miami?")
|
||||
|
||||
assert isinstance(second_response, AgentResponse)
|
||||
assert second_response.text is not None
|
||||
# Should NOT use the weather tool since it was only run-level in previous call
|
||||
# Call count should still be 1 (no additional calls)
|
||||
assert call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_openai_integration_tests_disabled
|
||||
async def test_openai_responses_client_agent_chat_options_agent_level() -> None:
|
||||
"""Integration test for comprehensive ChatOptions parameter coverage with OpenAI Response Agent."""
|
||||
async with ChatAgent(
|
||||
chat_client=OpenAIResponsesClient(),
|
||||
instructions="You are a helpful assistant.",
|
||||
tools=[get_weather],
|
||||
default_options={
|
||||
"max_tokens": 100,
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.9,
|
||||
"seed": 123,
|
||||
"user": "comprehensive-test-user",
|
||||
"tool_choice": "auto",
|
||||
},
|
||||
) as agent:
|
||||
response = await agent.run(
|
||||
"Provide a brief, helpful response.",
|
||||
)
|
||||
|
||||
assert isinstance(response, AgentResponse)
|
||||
assert response.text is not None
|
||||
assert len(response.text) > 0
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_openai_integration_tests_disabled
|
||||
async def test_openai_responses_client_agent_hosted_mcp_tool() -> None:
|
||||
"""Integration test for HostedMCPTool with OpenAI Response Agent using Microsoft Learn MCP."""
|
||||
|
||||
async with ChatAgent(
|
||||
chat_client=OpenAIResponsesClient(),
|
||||
instructions="You are a helpful assistant that can help with microsoft documentation questions.",
|
||||
tools=HostedMCPTool(
|
||||
name="Microsoft Learn MCP",
|
||||
url="https://learn.microsoft.com/api/mcp",
|
||||
description="A Microsoft Learn MCP server for documentation questions",
|
||||
approval_mode="never_require",
|
||||
),
|
||||
) as agent:
|
||||
response = await agent.run(
|
||||
"How to create an Azure storage account using az cli?",
|
||||
# this needs to be high enough to handle the full MCP tool response.
|
||||
options={"max_tokens": 5000},
|
||||
)
|
||||
|
||||
assert isinstance(response, AgentResponse)
|
||||
assert response.text
|
||||
# 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"])
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_openai_integration_tests_disabled
|
||||
async def test_openai_responses_client_agent_local_mcp_tool() -> None:
|
||||
"""Integration test for MCPStreamableHTTPTool with OpenAI Response Agent using Microsoft Learn MCP."""
|
||||
|
||||
mcp_tool = MCPStreamableHTTPTool(
|
||||
name="Microsoft Learn MCP",
|
||||
url="https://learn.microsoft.com/api/mcp",
|
||||
)
|
||||
|
||||
async with ChatAgent(
|
||||
chat_client=OpenAIResponsesClient(),
|
||||
instructions="You are a helpful assistant that can help with microsoft documentation questions.",
|
||||
tools=[mcp_tool],
|
||||
) as agent:
|
||||
response = await agent.run(
|
||||
"How to create an Azure storage account using az cli?",
|
||||
options={"max_tokens": 200},
|
||||
)
|
||||
|
||||
assert isinstance(response, AgentResponse)
|
||||
assert response.text is not None
|
||||
assert len(response.text) > 0
|
||||
# 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"])
|
||||
|
||||
|
||||
class ReleaseBrief(BaseModel):
|
||||
"""Structured output model for release brief testing."""
|
||||
|
||||
title: str
|
||||
summary: str
|
||||
highlights: list[str]
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_openai_integration_tests_disabled
|
||||
async def test_openai_responses_client_agent_with_response_format_pydantic() -> None:
|
||||
"""Integration test for response_format with Pydantic model using OpenAI Responses Client."""
|
||||
async with ChatAgent(
|
||||
chat_client=OpenAIResponsesClient(),
|
||||
instructions="You are a helpful assistant that returns structured JSON responses.",
|
||||
) as agent:
|
||||
response = await agent.run(
|
||||
"Summarize the following release notes into a ReleaseBrief:\n\n"
|
||||
"Version 2.0 Release Notes:\n"
|
||||
"- Added new streaming API for real-time responses\n"
|
||||
"- Improved error handling with detailed messages\n"
|
||||
"- Performance boost of 50% in batch processing\n"
|
||||
"- Fixed memory leak in connection pooling",
|
||||
options={
|
||||
"response_format": ReleaseBrief,
|
||||
},
|
||||
)
|
||||
|
||||
# Validate response
|
||||
assert isinstance(response, AgentResponse)
|
||||
assert response.value is not None
|
||||
assert isinstance(response.value, ReleaseBrief)
|
||||
|
||||
# Validate structured output fields
|
||||
brief = response.value
|
||||
assert len(brief.title) > 0
|
||||
assert len(brief.summary) > 0
|
||||
assert len(brief.highlights) > 0
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_openai_integration_tests_disabled
|
||||
async def test_openai_responses_client_agent_with_runtime_json_schema() -> None:
|
||||
"""Integration test for response_format with runtime JSON schema using OpenAI Responses Client."""
|
||||
runtime_schema = {
|
||||
"title": "WeatherDigest",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {"type": "string"},
|
||||
"conditions": {"type": "string"},
|
||||
"temperature_c": {"type": "number"},
|
||||
"advisory": {"type": "string"},
|
||||
},
|
||||
"required": ["location", "conditions", "temperature_c", "advisory"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
async with ChatAgent(
|
||||
chat_client=OpenAIResponsesClient(),
|
||||
instructions="Return only JSON that matches the provided schema. Do not add commentary.",
|
||||
) as agent:
|
||||
response = await agent.run(
|
||||
"Give a brief weather digest for Seattle.",
|
||||
options={
|
||||
"response_format": {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": runtime_schema["title"],
|
||||
"strict": True,
|
||||
"schema": runtime_schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
# Validate response
|
||||
assert isinstance(response, AgentResponse)
|
||||
assert response.text is not None
|
||||
|
||||
# Parse JSON and validate structure
|
||||
parsed = json.loads(response.text)
|
||||
assert "location" in parsed
|
||||
assert "conditions" in parsed
|
||||
assert "temperature_c" in parsed
|
||||
assert "advisory" in parsed
|
||||
@@ -1,5 +1,6 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
@@ -13,7 +14,7 @@ from agent_framework import (
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
Role,
|
||||
ai_function,
|
||||
tool,
|
||||
)
|
||||
from agent_framework._middleware import FunctionInvocationContext, FunctionMiddleware
|
||||
|
||||
@@ -21,7 +22,7 @@ from agent_framework._middleware import FunctionInvocationContext, FunctionMiddl
|
||||
async def test_base_client_with_function_calling(chat_client_base: ChatClientProtocol):
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
@tool(name="test_function", approval_mode="never_require")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -57,7 +58,7 @@ async def test_base_client_with_function_calling(chat_client_base: ChatClientPro
|
||||
async def test_base_client_with_function_calling_resets(chat_client_base: ChatClientProtocol):
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
@tool(name="test_function", approval_mode="never_require")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -99,7 +100,7 @@ async def test_base_client_with_function_calling_resets(chat_client_base: ChatCl
|
||||
async def test_base_client_with_streaming_function_calling(chat_client_base: ChatClientProtocol):
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
@tool(name="test_function", approval_mode="never_require")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -142,7 +143,7 @@ async def test_function_invocation_inside_aiohttp_server(chat_client_base: ChatC
|
||||
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="start_todo_investigation")
|
||||
@tool(name="start_todo_investigation", approval_mode="never_require")
|
||||
def ai_func(user_query: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -199,7 +200,7 @@ async def test_function_invocation_in_threaded_aiohttp_app(chat_client_base: Cha
|
||||
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="start_threaded_investigation")
|
||||
@tool(name="start_threaded_investigation", approval_mode="never_require")
|
||||
def ai_func(user_query: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -319,13 +320,13 @@ async def test_function_invocation_scenarios(
|
||||
# Simulate a service-side thread with conversation_id
|
||||
conversation_id = "test-thread-123"
|
||||
|
||||
@ai_function(name="no_approval_func")
|
||||
@tool(name="no_approval_func", approval_mode="never_require")
|
||||
def func_no_approval(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
return f"Processed {arg1}"
|
||||
|
||||
@ai_function(name="approval_func", approval_mode="always_require")
|
||||
@tool(name="approval_func", approval_mode="always_require")
|
||||
def func_with_approval(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -473,13 +474,13 @@ async def test_rejected_approval(chat_client_base: ChatClientProtocol):
|
||||
exec_counter_approved = 0
|
||||
exec_counter_rejected = 0
|
||||
|
||||
@ai_function(name="approved_func", approval_mode="always_require")
|
||||
@tool(name="approved_func", approval_mode="always_require")
|
||||
def func_approved(arg1: str) -> str:
|
||||
nonlocal exec_counter_approved
|
||||
exec_counter_approved += 1
|
||||
return f"Approved {arg1}"
|
||||
|
||||
@ai_function(name="rejected_func", approval_mode="always_require")
|
||||
@tool(name="rejected_func", approval_mode="always_require")
|
||||
def func_rejected(arg1: str) -> str:
|
||||
nonlocal exec_counter_rejected
|
||||
exec_counter_rejected += 1
|
||||
@@ -569,7 +570,7 @@ async def test_approval_requests_in_assistant_message(chat_client_base: ChatClie
|
||||
"""Approval requests should be added to the assistant message that contains the function call."""
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_func", approval_mode="always_require")
|
||||
@tool(name="test_func", approval_mode="always_require")
|
||||
def func_with_approval(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -604,7 +605,7 @@ async def test_persisted_approval_messages_replay_correctly(chat_client_base: Ch
|
||||
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_func", approval_mode="always_require")
|
||||
@tool(name="test_func", approval_mode="always_require")
|
||||
def func_with_approval(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -656,7 +657,7 @@ async def test_persisted_approval_messages_replay_correctly(chat_client_base: Ch
|
||||
async def test_no_duplicate_function_calls_after_approval_processing(chat_client_base: ChatClientProtocol):
|
||||
"""Processing approval should not create duplicate function calls in messages."""
|
||||
|
||||
@ai_function(name="test_func", approval_mode="always_require")
|
||||
@tool(name="test_func", approval_mode="always_require")
|
||||
def func_with_approval(arg1: str) -> str:
|
||||
return f"Result {arg1}"
|
||||
|
||||
@@ -700,7 +701,7 @@ async def test_no_duplicate_function_calls_after_approval_processing(chat_client
|
||||
async def test_rejection_result_uses_function_call_id(chat_client_base: ChatClientProtocol):
|
||||
"""Rejection error result should use the function call's call_id, not the approval's id."""
|
||||
|
||||
@ai_function(name="test_func", approval_mode="always_require")
|
||||
@tool(name="test_func", approval_mode="always_require")
|
||||
def func_with_approval(arg1: str) -> str:
|
||||
return f"Result {arg1}"
|
||||
|
||||
@@ -745,7 +746,7 @@ async def test_max_iterations_limit(chat_client_base: ChatClientProtocol):
|
||||
"""Test that MAX_ITERATIONS in additional_properties limits function call loops."""
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
@tool(name="test_function", approval_mode="never_require")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -790,7 +791,7 @@ async def test_function_invocation_config_enabled_false(chat_client_base: ChatCl
|
||||
"""Test that setting enabled=False disables function invocation."""
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
@tool(name="test_function")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -814,7 +815,7 @@ async def test_function_invocation_config_enabled_false(chat_client_base: ChatCl
|
||||
async def test_function_invocation_config_max_consecutive_errors(chat_client_base: ChatClientProtocol):
|
||||
"""Test that max_consecutive_errors_per_request limits error retries."""
|
||||
|
||||
@ai_function(name="error_function")
|
||||
@tool(name="error_function", approval_mode="never_require")
|
||||
def error_func(arg1: str) -> str:
|
||||
raise ValueError("Function error")
|
||||
|
||||
@@ -882,7 +883,7 @@ async def test_function_invocation_config_terminate_on_unknown_calls_false(chat_
|
||||
"""Test that terminate_on_unknown_calls=False returns error message for unknown functions."""
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="known_function")
|
||||
@tool(name="known_function")
|
||||
def known_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -917,7 +918,7 @@ async def test_function_invocation_config_terminate_on_unknown_calls_true(chat_c
|
||||
"""Test that terminate_on_unknown_calls=True stops execution on unknown functions."""
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="known_function")
|
||||
@tool(name="known_function")
|
||||
def known_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -949,13 +950,13 @@ async def test_function_invocation_config_additional_tools(chat_client_base: Cha
|
||||
exec_counter_visible = 0
|
||||
exec_counter_hidden = 0
|
||||
|
||||
@ai_function(name="visible_function")
|
||||
@tool(name="visible_function")
|
||||
def visible_func(arg1: str) -> str:
|
||||
nonlocal exec_counter_visible
|
||||
exec_counter_visible += 1
|
||||
return f"Visible {arg1}"
|
||||
|
||||
@ai_function(name="hidden_function")
|
||||
@tool(name="hidden_function")
|
||||
def hidden_func(arg1: str) -> str:
|
||||
nonlocal exec_counter_hidden
|
||||
exec_counter_hidden += 1
|
||||
@@ -996,7 +997,7 @@ async def test_function_invocation_config_additional_tools(chat_client_base: Cha
|
||||
async def test_function_invocation_config_include_detailed_errors_false(chat_client_base: ChatClientProtocol):
|
||||
"""Test that include_detailed_errors=False returns generic error messages."""
|
||||
|
||||
@ai_function(name="error_function")
|
||||
@tool(name="error_function", approval_mode="never_require")
|
||||
def error_func(arg1: str) -> str:
|
||||
raise ValueError("Specific error message that should not appear")
|
||||
|
||||
@@ -1030,7 +1031,7 @@ async def test_function_invocation_config_include_detailed_errors_false(chat_cli
|
||||
async def test_function_invocation_config_include_detailed_errors_true(chat_client_base: ChatClientProtocol):
|
||||
"""Test that include_detailed_errors=True returns detailed error information."""
|
||||
|
||||
@ai_function(name="error_function")
|
||||
@tool(name="error_function", approval_mode="never_require")
|
||||
def error_func(arg1: str) -> str:
|
||||
raise ValueError("Specific error message that should appear")
|
||||
|
||||
@@ -1100,7 +1101,7 @@ async def test_function_invocation_config_validation_max_consecutive_errors():
|
||||
async def test_argument_validation_error_with_detailed_errors(chat_client_base: ChatClientProtocol):
|
||||
"""Test that argument validation errors include details when include_detailed_errors=True."""
|
||||
|
||||
@ai_function(name="typed_function")
|
||||
@tool(name="typed_function", approval_mode="never_require")
|
||||
def typed_func(arg1: int) -> str: # Expects int, not str
|
||||
return f"Got {arg1}"
|
||||
|
||||
@@ -1134,7 +1135,7 @@ async def test_argument_validation_error_with_detailed_errors(chat_client_base:
|
||||
async def test_argument_validation_error_without_detailed_errors(chat_client_base: ChatClientProtocol):
|
||||
"""Test that argument validation errors are generic when include_detailed_errors=False."""
|
||||
|
||||
@ai_function(name="typed_function")
|
||||
@tool(name="typed_function", approval_mode="never_require")
|
||||
def typed_func(arg1: int) -> str: # Expects int, not str
|
||||
return f"Got {arg1}"
|
||||
|
||||
@@ -1168,7 +1169,7 @@ async def test_argument_validation_error_without_detailed_errors(chat_client_bas
|
||||
async def test_hosted_tool_approval_response(chat_client_base: ChatClientProtocol):
|
||||
"""Test handling of approval responses for hosted tools (tools not in tool_map)."""
|
||||
|
||||
@ai_function(name="local_function")
|
||||
@tool(name="local_function")
|
||||
def local_func(arg1: str) -> str:
|
||||
return f"Local {arg1}"
|
||||
|
||||
@@ -1201,7 +1202,7 @@ async def test_hosted_tool_approval_response(chat_client_base: ChatClientProtoco
|
||||
async def test_unapproved_tool_execution_raises_exception(chat_client_base: ChatClientProtocol):
|
||||
"""Test that attempting to execute an unapproved tool raises ToolException."""
|
||||
|
||||
@ai_function(name="test_function", approval_mode="always_require")
|
||||
@tool(name="test_function", approval_mode="always_require")
|
||||
def test_func(arg1: str) -> str:
|
||||
return f"Result {arg1}"
|
||||
|
||||
@@ -1256,7 +1257,7 @@ async def test_approved_function_call_with_error_without_detailed_errors(chat_cl
|
||||
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="error_func", approval_mode="always_require")
|
||||
@tool(name="error_func", approval_mode="always_require")
|
||||
def error_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -1319,7 +1320,7 @@ async def test_approved_function_call_with_error_with_detailed_errors(chat_clien
|
||||
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="error_func", approval_mode="always_require")
|
||||
@tool(name="error_func", approval_mode="always_require")
|
||||
def error_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -1380,7 +1381,7 @@ async def test_approved_function_call_with_validation_error(chat_client_base: Ch
|
||||
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="typed_func", approval_mode="always_require")
|
||||
@tool(name="typed_func", approval_mode="always_require")
|
||||
def typed_func(arg1: int) -> str: # Expects int, not str
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -1441,7 +1442,7 @@ async def test_approved_function_call_successful_execution(chat_client_base: Cha
|
||||
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="success_func", approval_mode="always_require")
|
||||
@tool(name="success_func", approval_mode="always_require")
|
||||
def success_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -1493,10 +1494,10 @@ async def test_approved_function_call_successful_execution(chat_client_base: Cha
|
||||
|
||||
async def test_declaration_only_tool(chat_client_base: ChatClientProtocol):
|
||||
"""Test that declaration_only tools without implementation (func=None) are not executed."""
|
||||
from agent_framework import AIFunction
|
||||
from agent_framework import FunctionTool
|
||||
|
||||
# Create a truly declaration-only function with no implementation
|
||||
declaration_func = AIFunction(
|
||||
declaration_func = FunctionTool(
|
||||
name="declaration_func",
|
||||
func=None,
|
||||
description="A declaration-only function for testing",
|
||||
@@ -1547,14 +1548,14 @@ async def test_multiple_function_calls_parallel_execution(chat_client_base: Chat
|
||||
|
||||
exec_order = []
|
||||
|
||||
@ai_function(name="func1")
|
||||
@tool(name="func1", approval_mode="never_require")
|
||||
async def func1(arg1: str) -> str:
|
||||
exec_order.append("func1_start")
|
||||
await asyncio.sleep(0.01) # Small delay
|
||||
exec_order.append("func1_end")
|
||||
return f"Result1 {arg1}"
|
||||
|
||||
@ai_function(name="func2")
|
||||
@tool(name="func2", approval_mode="never_require")
|
||||
async def func2(arg1: str) -> str:
|
||||
exec_order.append("func2_start")
|
||||
await asyncio.sleep(0.01) # Small delay
|
||||
@@ -1587,10 +1588,11 @@ async def test_multiple_function_calls_parallel_execution(chat_client_base: Chat
|
||||
assert len(results) == 2
|
||||
|
||||
|
||||
async def test_callable_function_converted_to_ai_function(chat_client_base: ChatClientProtocol):
|
||||
"""Test that plain callable functions are converted to AIFunction."""
|
||||
async def test_callable_function_converted_to_tool(chat_client_base: ChatClientProtocol):
|
||||
"""Test that plain callable functions are converted to FunctionTool."""
|
||||
exec_counter = 0
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def plain_function(arg1: str) -> str:
|
||||
"""A plain function without decorator."""
|
||||
nonlocal exec_counter
|
||||
@@ -1621,7 +1623,7 @@ async def test_callable_function_converted_to_ai_function(chat_client_base: Chat
|
||||
async def test_conversation_id_handling(chat_client_base: ChatClientProtocol):
|
||||
"""Test that conversation_id is properly handled and messages are cleared."""
|
||||
|
||||
@ai_function(name="test_function")
|
||||
@tool(name="test_function", approval_mode="never_require")
|
||||
def test_func(arg1: str) -> str:
|
||||
return f"Result {arg1}"
|
||||
|
||||
@@ -1653,7 +1655,7 @@ async def test_conversation_id_handling(chat_client_base: ChatClientProtocol):
|
||||
async def test_function_result_appended_to_existing_assistant_message(chat_client_base: ChatClientProtocol):
|
||||
"""Test that function results are appended to existing assistant message when appropriate."""
|
||||
|
||||
@ai_function(name="test_function")
|
||||
@tool(name="test_function", approval_mode="never_require")
|
||||
def test_func(arg1: str) -> str:
|
||||
return f"Result {arg1}"
|
||||
|
||||
@@ -1685,7 +1687,7 @@ async def test_error_recovery_resets_counter(chat_client_base: ChatClientProtoco
|
||||
|
||||
call_count = 0
|
||||
|
||||
@ai_function(name="sometimes_fails")
|
||||
@tool(name="sometimes_fails", approval_mode="never_require")
|
||||
def sometimes_fails(arg1: str) -> str:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
@@ -1741,7 +1743,7 @@ async def test_streaming_approval_request_generated(chat_client_base: ChatClient
|
||||
"""Test that approval requests are generated correctly in streaming mode."""
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_func", approval_mode="always_require")
|
||||
@tool(name="test_func", approval_mode="always_require")
|
||||
def func_with_approval(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -1777,7 +1779,7 @@ async def test_streaming_max_iterations_limit(chat_client_base: ChatClientProtoc
|
||||
"""Test that MAX_ITERATIONS in streaming mode limits function call loops."""
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
@tool(name="test_function", approval_mode="never_require")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -1829,7 +1831,7 @@ async def test_streaming_function_invocation_config_enabled_false(chat_client_ba
|
||||
"""Test that setting enabled=False disables function invocation in streaming mode."""
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
@tool(name="test_function", approval_mode="never_require")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -1857,7 +1859,7 @@ async def test_streaming_function_invocation_config_enabled_false(chat_client_ba
|
||||
async def test_streaming_function_invocation_config_max_consecutive_errors(chat_client_base: ChatClientProtocol):
|
||||
"""Test that max_consecutive_errors_per_request limits error retries in streaming mode."""
|
||||
|
||||
@ai_function(name="error_function")
|
||||
@tool(name="error_function", approval_mode="never_require")
|
||||
def error_func(arg1: str) -> str:
|
||||
raise ValueError("Function error")
|
||||
|
||||
@@ -1920,7 +1922,7 @@ async def test_streaming_function_invocation_config_terminate_on_unknown_calls_f
|
||||
"""Test that terminate_on_unknown_calls=False returns error message for unknown functions in streaming mode."""
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="known_function")
|
||||
@tool(name="known_function", approval_mode="never_require")
|
||||
def known_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -1963,7 +1965,7 @@ async def test_streaming_function_invocation_config_terminate_on_unknown_calls_t
|
||||
"""Test that terminate_on_unknown_calls=True stops execution on unknown functions in streaming mode."""
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="known_function")
|
||||
@tool(name="known_function", approval_mode="never_require")
|
||||
def known_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -1996,7 +1998,7 @@ async def test_streaming_function_invocation_config_terminate_on_unknown_calls_t
|
||||
async def test_streaming_function_invocation_config_include_detailed_errors_true(chat_client_base: ChatClientProtocol):
|
||||
"""Test that include_detailed_errors=True returns detailed error information in streaming mode."""
|
||||
|
||||
@ai_function(name="error_function")
|
||||
@tool(name="error_function", approval_mode="never_require")
|
||||
def error_func(arg1: str) -> str:
|
||||
raise ValueError("Specific error message that should appear")
|
||||
|
||||
@@ -2036,7 +2038,7 @@ async def test_streaming_function_invocation_config_include_detailed_errors_fals
|
||||
):
|
||||
"""Test that include_detailed_errors=False returns generic error messages in streaming mode."""
|
||||
|
||||
@ai_function(name="error_function")
|
||||
@tool(name="error_function", approval_mode="never_require")
|
||||
def error_func(arg1: str) -> str:
|
||||
raise ValueError("Specific error message that should not appear")
|
||||
|
||||
@@ -2074,7 +2076,7 @@ async def test_streaming_function_invocation_config_include_detailed_errors_fals
|
||||
async def test_streaming_argument_validation_error_with_detailed_errors(chat_client_base: ChatClientProtocol):
|
||||
"""Test that argument validation errors include details when include_detailed_errors=True in streaming mode."""
|
||||
|
||||
@ai_function(name="typed_function")
|
||||
@tool(name="typed_function", approval_mode="never_require")
|
||||
def typed_func(arg1: int) -> str: # Expects int, not str
|
||||
return f"Got {arg1}"
|
||||
|
||||
@@ -2112,7 +2114,7 @@ async def test_streaming_argument_validation_error_with_detailed_errors(chat_cli
|
||||
async def test_streaming_argument_validation_error_without_detailed_errors(chat_client_base: ChatClientProtocol):
|
||||
"""Test that argument validation errors are generic when include_detailed_errors=False in streaming mode."""
|
||||
|
||||
@ai_function(name="typed_function")
|
||||
@tool(name="typed_function", approval_mode="never_require")
|
||||
def typed_func(arg1: int) -> str: # Expects int, not str
|
||||
return f"Got {arg1}"
|
||||
|
||||
@@ -2149,18 +2151,17 @@ async def test_streaming_argument_validation_error_without_detailed_errors(chat_
|
||||
|
||||
async def test_streaming_multiple_function_calls_parallel_execution(chat_client_base: ChatClientProtocol):
|
||||
"""Test that multiple function calls are executed in parallel in streaming mode."""
|
||||
import asyncio
|
||||
|
||||
exec_order = []
|
||||
|
||||
@ai_function(name="func1")
|
||||
@tool(name="func1", approval_mode="never_require")
|
||||
async def func1(arg1: str) -> str:
|
||||
exec_order.append("func1_start")
|
||||
await asyncio.sleep(0.01) # Small delay
|
||||
exec_order.append("func1_end")
|
||||
return f"Result1 {arg1}"
|
||||
|
||||
@ai_function(name="func2")
|
||||
@tool(name="func2", approval_mode="never_require")
|
||||
async def func2(arg1: str) -> str:
|
||||
exec_order.append("func2_start")
|
||||
await asyncio.sleep(0.01) # Small delay
|
||||
@@ -2202,7 +2203,7 @@ async def test_streaming_approval_requests_in_assistant_message(chat_client_base
|
||||
"""Approval requests should be added to assistant updates in streaming mode."""
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_func", approval_mode="always_require")
|
||||
@tool(name="test_func", approval_mode="always_require")
|
||||
def func_with_approval(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -2238,7 +2239,7 @@ async def test_streaming_error_recovery_resets_counter(chat_client_base: ChatCli
|
||||
|
||||
call_count = 0
|
||||
|
||||
@ai_function(name="sometimes_fails")
|
||||
@tool(name="sometimes_fails", approval_mode="never_require")
|
||||
def sometimes_fails(arg1: str) -> str:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
@@ -2306,7 +2307,7 @@ async def test_terminate_loop_single_function_call(chat_client_base: ChatClientP
|
||||
"""Test that terminate_loop=True exits the function calling loop after single function call."""
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
@tool(name="test_function", approval_mode="never_require")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
@@ -2367,13 +2368,13 @@ async def test_terminate_loop_multiple_function_calls_one_terminates(chat_client
|
||||
normal_call_count = 0
|
||||
terminating_call_count = 0
|
||||
|
||||
@ai_function(name="normal_function")
|
||||
@tool(name="normal_function", approval_mode="never_require")
|
||||
def normal_func(arg1: str) -> str:
|
||||
nonlocal normal_call_count
|
||||
normal_call_count += 1
|
||||
return f"Normal {arg1}"
|
||||
|
||||
@ai_function(name="terminating_function")
|
||||
@tool(name="terminating_function", approval_mode="never_require")
|
||||
def terminating_func(arg1: str) -> str:
|
||||
nonlocal terminating_call_count
|
||||
terminating_call_count += 1
|
||||
@@ -2423,7 +2424,7 @@ async def test_terminate_loop_streaming_single_function_call(chat_client_base: C
|
||||
"""Test that terminate_loop=True exits the streaming function calling loop."""
|
||||
exec_counter = 0
|
||||
|
||||
@ai_function(name="test_function")
|
||||
@tool(name="test_function", approval_mode="never_require")
|
||||
def ai_func(arg1: str) -> str:
|
||||
nonlocal exec_counter
|
||||
exec_counter += 1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Tests for kwargs propagation from get_response() to @ai_function tools."""
|
||||
"""Tests for kwargs propagation from get_response() to @tool functions."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
@@ -9,19 +9,19 @@ from agent_framework import (
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
ai_function,
|
||||
tool,
|
||||
)
|
||||
from agent_framework._tools import _handle_function_calls_response, _handle_function_calls_streaming_response
|
||||
|
||||
|
||||
class TestKwargsPropagationToAIFunction:
|
||||
"""Test cases for kwargs flowing from get_response() to @ai_function tools."""
|
||||
class TestKwargsPropagationToFunctionTool:
|
||||
"""Test cases for kwargs flowing from get_response() to @tool functions."""
|
||||
|
||||
async def test_kwargs_propagate_to_ai_function_with_kwargs(self) -> None:
|
||||
"""Test that kwargs passed to get_response() are available in @ai_function **kwargs."""
|
||||
async def test_kwargs_propagate_to_tool_with_kwargs(self) -> None:
|
||||
"""Test that kwargs passed to get_response() are available in @tool **kwargs."""
|
||||
captured_kwargs: dict[str, Any] = {}
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def capture_kwargs_tool(x: int, **kwargs: Any) -> str:
|
||||
"""A tool that captures kwargs for testing."""
|
||||
captured_kwargs.update(kwargs)
|
||||
@@ -75,10 +75,10 @@ class TestKwargsPropagationToAIFunction:
|
||||
# Verify result
|
||||
assert result.messages[-1].text == "Done!"
|
||||
|
||||
async def test_kwargs_not_forwarded_to_ai_function_without_kwargs(self) -> None:
|
||||
"""Test that kwargs are NOT forwarded to @ai_function that doesn't accept **kwargs."""
|
||||
async def test_kwargs_not_forwarded_to_tool_without_kwargs(self) -> None:
|
||||
"""Test that kwargs are NOT forwarded to @tool that doesn't accept **kwargs."""
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def simple_tool(x: int) -> str:
|
||||
"""A simple tool without **kwargs."""
|
||||
# This should not receive any extra kwargs
|
||||
@@ -120,7 +120,7 @@ class TestKwargsPropagationToAIFunction:
|
||||
"""Test that kwargs don't leak between different function call invocations."""
|
||||
invocation_kwargs: list[dict[str, Any]] = []
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def tracking_tool(name: str, **kwargs: Any) -> str:
|
||||
"""A tool that tracks kwargs from each invocation."""
|
||||
invocation_kwargs.append(dict(kwargs))
|
||||
@@ -170,10 +170,10 @@ class TestKwargsPropagationToAIFunction:
|
||||
assert result.messages[-1].text == "All done!"
|
||||
|
||||
async def test_streaming_response_kwargs_propagation(self) -> None:
|
||||
"""Test that kwargs propagate to @ai_function in streaming mode."""
|
||||
"""Test that kwargs propagate to @tool in streaming mode."""
|
||||
captured_kwargs: dict[str, Any] = {}
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def streaming_capture_tool(value: str, **kwargs: Any) -> str:
|
||||
"""A tool that captures kwargs during streaming."""
|
||||
captured_kwargs.update(kwargs)
|
||||
|
||||
@@ -1228,7 +1228,7 @@ async def test_streamable_http_integration():
|
||||
if not url.startswith("http"):
|
||||
pytest.skip("LOCAL_MCP_URL is not an HTTP URL")
|
||||
|
||||
tool = MCPStreamableHTTPTool(name="integration_test", url=url)
|
||||
tool = MCPStreamableHTTPTool(name="integration_test", url=url, approval_mode="never_require")
|
||||
|
||||
async with tool:
|
||||
# Test that we can connect and load tools
|
||||
@@ -1260,7 +1260,7 @@ async def test_mcp_connection_reset_integration():
|
||||
"""
|
||||
url = os.environ.get("LOCAL_MCP_URL")
|
||||
|
||||
tool = MCPStreamableHTTPTool(name="integration_test", url=url)
|
||||
tool = MCPStreamableHTTPTool(name="integration_test", url=url, approval_mode="never_require")
|
||||
|
||||
async with tool:
|
||||
# Verify initial connection
|
||||
@@ -1620,7 +1620,7 @@ def test_mcp_websocket_tool_get_mcp_client_with_kwargs():
|
||||
async def test_mcp_tool_deduplication():
|
||||
"""Test that MCP tools are not duplicated in MCPTool"""
|
||||
from agent_framework._mcp import MCPTool
|
||||
from agent_framework._tools import AIFunction
|
||||
from agent_framework._tools import FunctionTool
|
||||
|
||||
# Create MCPStreamableHTTPTool instance
|
||||
tool = MCPTool(name="test_mcp_tool")
|
||||
@@ -1629,12 +1629,12 @@ async def test_mcp_tool_deduplication():
|
||||
tool._functions = []
|
||||
|
||||
# Add initial functions
|
||||
func1 = AIFunction(
|
||||
func1 = FunctionTool(
|
||||
func=lambda x: f"Result: {x}",
|
||||
name="analyze_content",
|
||||
description="Analyzes content",
|
||||
)
|
||||
func2 = AIFunction(
|
||||
func2 = FunctionTool(
|
||||
func=lambda x: f"Extract: {x}",
|
||||
name="extract_info",
|
||||
description="Extracts information",
|
||||
@@ -1662,7 +1662,7 @@ async def test_mcp_tool_deduplication():
|
||||
if tool_name in existing_names:
|
||||
continue # Skip duplicates
|
||||
|
||||
new_func = AIFunction(func=lambda x: f"Process: {x}", name=tool_name, description=description)
|
||||
new_func = FunctionTool(func=lambda x: f"Process: {x}", name=tool_name, description=description)
|
||||
tool._functions.append(new_func)
|
||||
existing_names.add(tool_name)
|
||||
added_count += 1
|
||||
|
||||
@@ -28,7 +28,7 @@ from agent_framework._middleware import (
|
||||
FunctionMiddleware,
|
||||
FunctionMiddlewarePipeline,
|
||||
)
|
||||
from agent_framework._tools import AIFunction
|
||||
from agent_framework._tools import FunctionTool
|
||||
|
||||
|
||||
class TestAgentRunContext:
|
||||
@@ -73,7 +73,7 @@ class TestAgentRunContext:
|
||||
class TestFunctionInvocationContext:
|
||||
"""Test cases for FunctionInvocationContext."""
|
||||
|
||||
def test_init_with_defaults(self, mock_function: AIFunction[Any, Any]) -> None:
|
||||
def test_init_with_defaults(self, mock_function: FunctionTool[Any, Any]) -> None:
|
||||
"""Test FunctionInvocationContext initialization with default values."""
|
||||
arguments = FunctionTestArgs(name="test")
|
||||
context = FunctionInvocationContext(function=mock_function, arguments=arguments)
|
||||
@@ -82,7 +82,7 @@ class TestFunctionInvocationContext:
|
||||
assert context.arguments == arguments
|
||||
assert context.metadata == {}
|
||||
|
||||
def test_init_with_custom_metadata(self, mock_function: AIFunction[Any, Any]) -> None:
|
||||
def test_init_with_custom_metadata(self, mock_function: FunctionTool[Any, Any]) -> None:
|
||||
"""Test FunctionInvocationContext initialization with custom metadata."""
|
||||
arguments = FunctionTestArgs(name="test")
|
||||
metadata = {"key": "value"}
|
||||
@@ -419,7 +419,7 @@ class TestFunctionMiddlewarePipeline:
|
||||
await next(context)
|
||||
context.terminate = True
|
||||
|
||||
async def test_execute_with_pre_next_termination(self, mock_function: AIFunction[Any, Any]) -> None:
|
||||
async def test_execute_with_pre_next_termination(self, mock_function: FunctionTool[Any, Any]) -> None:
|
||||
"""Test pipeline execution with termination before next()."""
|
||||
middleware = self.PreNextTerminateFunctionMiddleware()
|
||||
pipeline = FunctionMiddlewarePipeline([middleware])
|
||||
@@ -438,7 +438,7 @@ class TestFunctionMiddlewarePipeline:
|
||||
# Handler should not be called when terminated before next()
|
||||
assert execution_order == []
|
||||
|
||||
async def test_execute_with_post_next_termination(self, mock_function: AIFunction[Any, Any]) -> None:
|
||||
async def test_execute_with_post_next_termination(self, mock_function: FunctionTool[Any, Any]) -> None:
|
||||
"""Test pipeline execution with termination after next()."""
|
||||
middleware = self.PostNextTerminateFunctionMiddleware()
|
||||
pipeline = FunctionMiddlewarePipeline([middleware])
|
||||
@@ -477,7 +477,7 @@ class TestFunctionMiddlewarePipeline:
|
||||
pipeline = FunctionMiddlewarePipeline([test_middleware])
|
||||
assert pipeline.has_middlewares
|
||||
|
||||
async def test_execute_no_middleware(self, mock_function: AIFunction[Any, Any]) -> None:
|
||||
async def test_execute_no_middleware(self, mock_function: FunctionTool[Any, Any]) -> None:
|
||||
"""Test pipeline execution with no middleware."""
|
||||
pipeline = FunctionMiddlewarePipeline()
|
||||
arguments = FunctionTestArgs(name="test")
|
||||
@@ -491,7 +491,7 @@ class TestFunctionMiddlewarePipeline:
|
||||
result = await pipeline.execute(mock_function, arguments, context, final_handler)
|
||||
assert result == expected_result
|
||||
|
||||
async def test_execute_with_middleware(self, mock_function: AIFunction[Any, Any]) -> None:
|
||||
async def test_execute_with_middleware(self, mock_function: FunctionTool[Any, Any]) -> None:
|
||||
"""Test pipeline execution with middleware."""
|
||||
execution_order: list[str] = []
|
||||
|
||||
@@ -778,7 +778,7 @@ class TestClassBasedMiddleware:
|
||||
assert context.metadata["after"] is True
|
||||
assert metadata_updates == ["before", "handler", "after"]
|
||||
|
||||
async def test_function_middleware_execution(self, mock_function: AIFunction[Any, Any]) -> None:
|
||||
async def test_function_middleware_execution(self, mock_function: FunctionTool[Any, Any]) -> None:
|
||||
"""Test class-based function middleware execution."""
|
||||
metadata_updates: list[str] = []
|
||||
|
||||
@@ -840,7 +840,7 @@ class TestFunctionBasedMiddleware:
|
||||
assert context.metadata["function_middleware"] is True
|
||||
assert execution_order == ["function_before", "handler", "function_after"]
|
||||
|
||||
async def test_function_function_middleware(self, mock_function: AIFunction[Any, Any]) -> None:
|
||||
async def test_function_function_middleware(self, mock_function: FunctionTool[Any, Any]) -> None:
|
||||
"""Test function-based function middleware."""
|
||||
execution_order: list[str] = []
|
||||
|
||||
@@ -902,7 +902,7 @@ class TestMixedMiddleware:
|
||||
assert result is not None
|
||||
assert execution_order == ["class_before", "function_before", "handler", "function_after", "class_after"]
|
||||
|
||||
async def test_mixed_function_middleware(self, mock_function: AIFunction[Any, Any]) -> None:
|
||||
async def test_mixed_function_middleware(self, mock_function: FunctionTool[Any, Any]) -> None:
|
||||
"""Test mixed class and function-based function middleware."""
|
||||
execution_order: list[str] = []
|
||||
|
||||
@@ -1022,7 +1022,7 @@ class TestMultipleMiddlewareOrdering:
|
||||
]
|
||||
assert execution_order == expected_order
|
||||
|
||||
async def test_function_middleware_execution_order(self, mock_function: AIFunction[Any, Any]) -> None:
|
||||
async def test_function_middleware_execution_order(self, mock_function: FunctionTool[Any, Any]) -> None:
|
||||
"""Test that multiple function middleware execute in registration order."""
|
||||
execution_order: list[str] = []
|
||||
|
||||
@@ -1150,7 +1150,7 @@ class TestContextContentValidation:
|
||||
result = await pipeline.execute(mock_agent, messages, context, final_handler)
|
||||
assert result is not None
|
||||
|
||||
async def test_function_context_validation(self, mock_function: AIFunction[Any, Any]) -> None:
|
||||
async def test_function_context_validation(self, mock_function: FunctionTool[Any, Any]) -> None:
|
||||
"""Test that function context contains expected data."""
|
||||
|
||||
class ContextValidationMiddleware(FunctionMiddleware):
|
||||
@@ -1498,7 +1498,7 @@ class TestMiddlewareExecutionControl:
|
||||
assert not handler_called
|
||||
assert context.result is None
|
||||
|
||||
async def test_function_middleware_no_next_no_execution(self, mock_function: AIFunction[Any, Any]) -> None:
|
||||
async def test_function_middleware_no_next_no_execution(self, mock_function: FunctionTool[Any, Any]) -> None:
|
||||
"""Test that when function middleware doesn't call next(), no execution happens."""
|
||||
|
||||
class FunctionTestArgs(BaseModel):
|
||||
@@ -1672,9 +1672,9 @@ def mock_agent() -> AgentProtocol:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_function() -> AIFunction[Any, Any]:
|
||||
def mock_function() -> FunctionTool[Any, Any]:
|
||||
"""Mock function for testing."""
|
||||
function = MagicMock(spec=AIFunction[Any, Any])
|
||||
function = MagicMock(spec=FunctionTool[Any, Any])
|
||||
function.name = "test_function"
|
||||
return function
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ from agent_framework._middleware import (
|
||||
FunctionMiddleware,
|
||||
FunctionMiddlewarePipeline,
|
||||
)
|
||||
from agent_framework._tools import AIFunction
|
||||
from agent_framework._tools import FunctionTool
|
||||
|
||||
from .conftest import MockChatClient
|
||||
|
||||
@@ -103,7 +103,7 @@ class TestResultOverrideMiddleware:
|
||||
assert updates[0].text == "overridden"
|
||||
assert updates[1].text == " stream"
|
||||
|
||||
async def test_function_middleware_result_override(self, mock_function: AIFunction[Any, Any]) -> None:
|
||||
async def test_function_middleware_result_override(self, mock_function: FunctionTool[Any, Any]) -> None:
|
||||
"""Test that function middleware can override result."""
|
||||
override_result = "overridden function result"
|
||||
|
||||
@@ -260,7 +260,7 @@ class TestResultOverrideMiddleware:
|
||||
assert execute_result.messages[0].text == "executed response"
|
||||
assert handler_called
|
||||
|
||||
async def test_function_middleware_conditional_no_next(self, mock_function: AIFunction[Any, Any]) -> None:
|
||||
async def test_function_middleware_conditional_no_next(self, mock_function: FunctionTool[Any, Any]) -> None:
|
||||
"""Test that when function middleware conditionally doesn't call next(), no execution happens."""
|
||||
|
||||
class ConditionalNoNextFunctionMiddleware(FunctionMiddleware):
|
||||
@@ -345,7 +345,7 @@ class TestResultObservability:
|
||||
assert observed_responses[0].messages[0].text == "executed response"
|
||||
assert result == observed_responses[0]
|
||||
|
||||
async def test_function_middleware_result_observability(self, mock_function: AIFunction[Any, Any]) -> None:
|
||||
async def test_function_middleware_result_observability(self, mock_function: FunctionTool[Any, Any]) -> None:
|
||||
"""Test that middleware can observe function result after execution."""
|
||||
observed_results: list[str] = []
|
||||
|
||||
@@ -414,7 +414,7 @@ class TestResultObservability:
|
||||
assert result is not None
|
||||
assert result.messages[0].text == "modified after execution"
|
||||
|
||||
async def test_function_middleware_post_execution_override(self, mock_function: AIFunction[Any, Any]) -> None:
|
||||
async def test_function_middleware_post_execution_override(self, mock_function: FunctionTool[Any, Any]) -> None:
|
||||
"""Test that middleware can override function result after observing execution."""
|
||||
|
||||
class PostExecutionOverrideMiddleware(FunctionMiddleware):
|
||||
@@ -456,8 +456,8 @@ def mock_agent() -> AgentProtocol:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_function() -> AIFunction[Any, Any]:
|
||||
def mock_function() -> FunctionTool[Any, Any]:
|
||||
"""Mock function for testing."""
|
||||
function = MagicMock(spec=AIFunction[Any, Any])
|
||||
function = MagicMock(spec=FunctionTool[Any, Any])
|
||||
function.name = "test_function"
|
||||
return function
|
||||
|
||||
@@ -14,6 +14,7 @@ from agent_framework import (
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
FunctionTool,
|
||||
Role,
|
||||
agent_middleware,
|
||||
chat_middleware,
|
||||
@@ -214,9 +215,13 @@ class TestChatAgentFunctionBasedMiddleware:
|
||||
execution_order.append("function_called")
|
||||
return "test_result"
|
||||
|
||||
test_function_tool = FunctionTool(
|
||||
func=test_function, name="test_function", description="Test function", approval_mode="never_require"
|
||||
)
|
||||
|
||||
# Create ChatAgent with function middleware and test function
|
||||
middleware = PreTerminationFunctionMiddleware()
|
||||
agent = ChatAgent(chat_client=chat_client, middleware=[middleware], tools=[test_function])
|
||||
agent = ChatAgent(chat_client=chat_client, middleware=[middleware], tools=[test_function_tool])
|
||||
|
||||
# Execute the agent
|
||||
await agent.run(messages)
|
||||
@@ -271,9 +276,13 @@ class TestChatAgentFunctionBasedMiddleware:
|
||||
execution_order.append("function_called")
|
||||
return "test_result"
|
||||
|
||||
test_function_tool = FunctionTool(
|
||||
func=test_function, name="test_function", description="Test function", approval_mode="never_require"
|
||||
)
|
||||
|
||||
# Create ChatAgent with function middleware and test function
|
||||
middleware = PostTerminationFunctionMiddleware()
|
||||
agent = ChatAgent(chat_client=chat_client, middleware=[middleware], tools=[test_function])
|
||||
agent = ChatAgent(chat_client=chat_client, middleware=[middleware], tools=[test_function_tool])
|
||||
|
||||
# Execute the agent
|
||||
response = await agent.run(messages)
|
||||
@@ -518,11 +527,19 @@ class TestChatAgentMultipleMiddlewareOrdering:
|
||||
# region Tool Functions for Testing
|
||||
|
||||
|
||||
def sample_tool_function(location: str) -> str:
|
||||
def _sample_tool_function_impl(location: str) -> str:
|
||||
"""A simple tool function for middleware testing."""
|
||||
return f"Weather in {location}: sunny"
|
||||
|
||||
|
||||
sample_tool_function = FunctionTool(
|
||||
func=_sample_tool_function_impl,
|
||||
name="sample_tool_function",
|
||||
description="A simple tool function for middleware testing.",
|
||||
approval_mode="never_require",
|
||||
)
|
||||
|
||||
|
||||
# region ChatAgent Function Middleware Tests with Tools
|
||||
|
||||
|
||||
@@ -1157,6 +1174,10 @@ class TestRunLevelMiddleware:
|
||||
execution_log.append("tool_executed")
|
||||
return f"Tool response: {message}"
|
||||
|
||||
custom_tool_wrapped = FunctionTool(
|
||||
func=custom_tool, name="custom_tool", description="Custom tool", approval_mode="never_require"
|
||||
)
|
||||
|
||||
# Set up mock to return a function call first, then a regular response
|
||||
function_call_response = ChatResponse(
|
||||
messages=[
|
||||
@@ -1179,7 +1200,7 @@ class TestRunLevelMiddleware:
|
||||
agent = ChatAgent(
|
||||
chat_client=chat_client,
|
||||
middleware=[AgentLevelAgentMiddleware(), AgentLevelFunctionMiddleware()],
|
||||
tools=[custom_tool],
|
||||
tools=[custom_tool_wrapped],
|
||||
)
|
||||
|
||||
# Execute with run-level middleware
|
||||
@@ -1246,6 +1267,10 @@ class TestMiddlewareDecoratorLogic:
|
||||
execution_order.append("tool_executed")
|
||||
return f"Tool response: {message}"
|
||||
|
||||
custom_tool_wrapped = FunctionTool(
|
||||
func=custom_tool, name="custom_tool", description="Custom tool", approval_mode="never_require"
|
||||
)
|
||||
|
||||
# Set up mock to return a function call first, then a regular response
|
||||
function_call_response = ChatResponse(
|
||||
messages=[
|
||||
@@ -1268,7 +1293,7 @@ class TestMiddlewareDecoratorLogic:
|
||||
agent = ChatAgent(
|
||||
chat_client=chat_client,
|
||||
middleware=[matching_agent_middleware, matching_function_middleware],
|
||||
tools=[custom_tool],
|
||||
tools=[custom_tool_wrapped],
|
||||
)
|
||||
|
||||
response = await agent.run([ChatMessage(role=Role.USER, text="test")])
|
||||
@@ -1313,6 +1338,10 @@ class TestMiddlewareDecoratorLogic:
|
||||
execution_order.append("tool_executed")
|
||||
return f"Tool response: {message}"
|
||||
|
||||
custom_tool_wrapped = FunctionTool(
|
||||
func=custom_tool, name="custom_tool", description="Custom tool", approval_mode="never_require"
|
||||
)
|
||||
|
||||
# Set up mock to return a function call first, then a regular response
|
||||
function_call_response = ChatResponse(
|
||||
messages=[
|
||||
@@ -1333,7 +1362,9 @@ class TestMiddlewareDecoratorLogic:
|
||||
|
||||
# Should work - relies on decorator
|
||||
agent = ChatAgent(
|
||||
chat_client=chat_client, middleware=[decorator_only_agent, decorator_only_function], tools=[custom_tool]
|
||||
chat_client=chat_client,
|
||||
middleware=[decorator_only_agent, decorator_only_function],
|
||||
tools=[custom_tool_wrapped],
|
||||
)
|
||||
|
||||
response = await agent.run([ChatMessage(role=Role.USER, text="test")])
|
||||
@@ -1363,6 +1394,10 @@ class TestMiddlewareDecoratorLogic:
|
||||
execution_order.append("tool_executed")
|
||||
return f"Tool response: {message}"
|
||||
|
||||
custom_tool_wrapped = FunctionTool(
|
||||
func=custom_tool, name="custom_tool", description="Custom tool", approval_mode="never_require"
|
||||
)
|
||||
|
||||
# Set up mock to return a function call first, then a regular response
|
||||
function_call_response = ChatResponse(
|
||||
messages=[
|
||||
@@ -1383,7 +1418,7 @@ class TestMiddlewareDecoratorLogic:
|
||||
|
||||
# Should work - relies on type annotations
|
||||
agent = ChatAgent(
|
||||
chat_client=chat_client, middleware=[type_only_agent, type_only_function], tools=[custom_tool]
|
||||
chat_client=chat_client, middleware=[type_only_agent, type_only_function], tools=[custom_tool_wrapped]
|
||||
)
|
||||
|
||||
response = await agent.run([ChatMessage(role=Role.USER, text="test")])
|
||||
|
||||
@@ -11,6 +11,7 @@ from agent_framework import (
|
||||
ChatResponse,
|
||||
Content,
|
||||
FunctionInvocationContext,
|
||||
FunctionTool,
|
||||
Role,
|
||||
chat_middleware,
|
||||
function_middleware,
|
||||
@@ -337,6 +338,13 @@ class TestChatMiddleware:
|
||||
"""Get weather for a location."""
|
||||
return f"Weather in {location}: sunny"
|
||||
|
||||
sample_tool_wrapped = FunctionTool(
|
||||
func=sample_tool,
|
||||
name="sample_tool",
|
||||
description="Get weather for a location",
|
||||
approval_mode="never_require",
|
||||
)
|
||||
|
||||
# Create function-invocation enabled chat client
|
||||
chat_client = use_chat_middleware(use_function_invocation(MockBaseChatClient))()
|
||||
|
||||
@@ -366,7 +374,7 @@ class TestChatMiddleware:
|
||||
|
||||
# Execute the chat client directly with tools - this should trigger function invocation and middleware
|
||||
messages = [ChatMessage(role=Role.USER, text="What's the weather in San Francisco?")]
|
||||
response = await chat_client.get_response(messages, options={"tools": [sample_tool]})
|
||||
response = await chat_client.get_response(messages, options={"tools": [sample_tool_wrapped]})
|
||||
|
||||
# Verify response
|
||||
assert response is not None
|
||||
@@ -396,6 +404,13 @@ class TestChatMiddleware:
|
||||
"""Get weather for a location."""
|
||||
return f"Weather in {location}: sunny"
|
||||
|
||||
sample_tool_wrapped = FunctionTool(
|
||||
func=sample_tool,
|
||||
name="sample_tool",
|
||||
description="Get weather for a location",
|
||||
approval_mode="never_require",
|
||||
)
|
||||
|
||||
# Create function-invocation enabled chat client
|
||||
chat_client = use_function_invocation(MockBaseChatClient)()
|
||||
|
||||
@@ -423,7 +438,7 @@ class TestChatMiddleware:
|
||||
# Execute the chat client directly with run-level middleware and tools
|
||||
messages = [ChatMessage(role=Role.USER, text="What's the weather in New York?")]
|
||||
response = await chat_client.get_response(
|
||||
messages, options={"tools": [sample_tool]}, middleware=[run_level_function_middleware]
|
||||
messages, options={"tools": [sample_tool_wrapped]}, middleware=[run_level_function_middleware]
|
||||
)
|
||||
|
||||
# Verify response
|
||||
|
||||
@@ -21,8 +21,8 @@ from agent_framework import (
|
||||
ChatResponseUpdate,
|
||||
Role,
|
||||
UsageDetails,
|
||||
ai_function,
|
||||
prepend_agent_framework_to_user_agent,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.exceptions import AgentInitializationError, ChatClientInitializationError
|
||||
from agent_framework.observability import (
|
||||
@@ -606,7 +606,7 @@ async def test_function_call_with_error_handling(span_exporter: InMemorySpanExpo
|
||||
"""Test that function call errors are properly captured in telemetry."""
|
||||
|
||||
# Create a function that raises an error using the decorator
|
||||
@ai_function(name="failing_function", description="A function that fails")
|
||||
@tool(name="failing_function", description="A function that fails")
|
||||
async def failing_function(param: str) -> str:
|
||||
raise ValueError("Function execution failed")
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanE
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from agent_framework import (
|
||||
AIFunction,
|
||||
Content,
|
||||
FunctionTool,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedImageGenerationTool,
|
||||
HostedMCPTool,
|
||||
ToolProtocol,
|
||||
ai_function,
|
||||
tool,
|
||||
)
|
||||
from agent_framework._tools import (
|
||||
_build_pydantic_model_from_json_schema,
|
||||
@@ -24,19 +24,19 @@ from agent_framework._tools import (
|
||||
from agent_framework.exceptions import ToolException
|
||||
from agent_framework.observability import OtelAttr
|
||||
|
||||
# region AIFunction and ai_function decorator tests
|
||||
# region FunctionTool and tool decorator tests
|
||||
|
||||
|
||||
def test_ai_function_decorator():
|
||||
"""Test the ai_function decorator."""
|
||||
def test_tool_decorator():
|
||||
"""Test the tool decorator."""
|
||||
|
||||
@ai_function(name="test_tool", description="A test tool")
|
||||
@tool(name="test_tool", description="A test tool")
|
||||
def test_tool(x: int, y: int) -> int:
|
||||
"""A simple function that adds two numbers."""
|
||||
return x + y
|
||||
|
||||
assert isinstance(test_tool, ToolProtocol)
|
||||
assert isinstance(test_tool, AIFunction)
|
||||
assert isinstance(test_tool, FunctionTool)
|
||||
assert test_tool.name == "test_tool"
|
||||
assert test_tool.description == "A test tool"
|
||||
assert test_tool.parameters() == {
|
||||
@@ -48,16 +48,16 @@ def test_ai_function_decorator():
|
||||
assert test_tool(1, 2) == 3
|
||||
|
||||
|
||||
def test_ai_function_decorator_without_args():
|
||||
"""Test the ai_function decorator."""
|
||||
def test_tool_decorator_without_args():
|
||||
"""Test the tool decorator."""
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def test_tool(x: int, y: int) -> int:
|
||||
"""A simple function that adds two numbers."""
|
||||
return x + y
|
||||
|
||||
assert isinstance(test_tool, ToolProtocol)
|
||||
assert isinstance(test_tool, AIFunction)
|
||||
assert isinstance(test_tool, FunctionTool)
|
||||
assert test_tool.name == "test_tool"
|
||||
assert test_tool.description == "A simple function that adds two numbers."
|
||||
assert test_tool.parameters() == {
|
||||
@@ -67,18 +67,19 @@ def test_ai_function_decorator_without_args():
|
||||
"type": "object",
|
||||
}
|
||||
assert test_tool(1, 2) == 3
|
||||
assert test_tool.approval_mode == "never_require"
|
||||
|
||||
|
||||
def test_ai_function_without_args():
|
||||
"""Test the ai_function decorator."""
|
||||
def test_tool_without_args():
|
||||
"""Test the tool decorator."""
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def test_tool() -> int:
|
||||
"""A simple function that adds two numbers."""
|
||||
return 1 + 2
|
||||
|
||||
assert isinstance(test_tool, ToolProtocol)
|
||||
assert isinstance(test_tool, AIFunction)
|
||||
assert isinstance(test_tool, FunctionTool)
|
||||
assert test_tool.name == "test_tool"
|
||||
assert test_tool.description == "A simple function that adds two numbers."
|
||||
assert test_tool.parameters() == {
|
||||
@@ -89,16 +90,16 @@ def test_ai_function_without_args():
|
||||
assert test_tool() == 3
|
||||
|
||||
|
||||
async def test_ai_function_decorator_with_async():
|
||||
"""Test the ai_function decorator with an async function."""
|
||||
async def test_tool_decorator_with_async():
|
||||
"""Test the tool decorator with an async function."""
|
||||
|
||||
@ai_function(name="async_test_tool", description="An async test tool")
|
||||
@tool(name="async_test_tool", description="An async test tool")
|
||||
async def async_test_tool(x: int, y: int) -> int:
|
||||
"""An async function that adds two numbers."""
|
||||
return x + y
|
||||
|
||||
assert isinstance(async_test_tool, ToolProtocol)
|
||||
assert isinstance(async_test_tool, AIFunction)
|
||||
assert isinstance(async_test_tool, FunctionTool)
|
||||
assert async_test_tool.name == "async_test_tool"
|
||||
assert async_test_tool.description == "An async test tool"
|
||||
assert async_test_tool.parameters() == {
|
||||
@@ -110,11 +111,11 @@ async def test_ai_function_decorator_with_async():
|
||||
assert (await async_test_tool(1, 2)) == 3
|
||||
|
||||
|
||||
def test_ai_function_decorator_in_class():
|
||||
"""Test the ai_function decorator."""
|
||||
def test_tool_decorator_in_class():
|
||||
"""Test the tool decorator."""
|
||||
|
||||
class my_tools:
|
||||
@ai_function(name="test_tool", description="A test tool")
|
||||
@tool(name="test_tool", description="A test tool")
|
||||
def test_tool(self, x: int, y: int) -> int:
|
||||
"""A simple function that adds two numbers."""
|
||||
return x + y
|
||||
@@ -122,7 +123,7 @@ def test_ai_function_decorator_in_class():
|
||||
test_tool = my_tools().test_tool
|
||||
|
||||
assert isinstance(test_tool, ToolProtocol)
|
||||
assert isinstance(test_tool, AIFunction)
|
||||
assert isinstance(test_tool, FunctionTool)
|
||||
assert test_tool.name == "test_tool"
|
||||
assert test_tool.description == "A test tool"
|
||||
assert test_tool.parameters() == {
|
||||
@@ -134,15 +135,15 @@ def test_ai_function_decorator_in_class():
|
||||
assert test_tool(1, 2) == 3
|
||||
|
||||
|
||||
def test_ai_function_with_literal_type_parameter():
|
||||
"""Test ai_function decorator with Literal type parameter (issue #2891)."""
|
||||
def test_tool_with_literal_type_parameter():
|
||||
"""Test tool decorator with Literal type parameter (issue #2891)."""
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def search_flows(category: Literal["Data", "Security", "Network"], issue: str) -> str:
|
||||
"""Search flows by category."""
|
||||
return f"{category}: {issue}"
|
||||
|
||||
assert isinstance(search_flows, AIFunction)
|
||||
assert isinstance(search_flows, FunctionTool)
|
||||
schema = search_flows.parameters()
|
||||
assert schema == {
|
||||
"properties": {
|
||||
@@ -157,18 +158,18 @@ def test_ai_function_with_literal_type_parameter():
|
||||
assert search_flows("Data", "test issue") == "Data: test issue"
|
||||
|
||||
|
||||
def test_ai_function_with_literal_type_in_class_method():
|
||||
"""Test ai_function decorator with Literal type parameter in a class method (issue #2891)."""
|
||||
def test_tool_with_literal_type_in_class_method():
|
||||
"""Test tool decorator with Literal type parameter in a class method (issue #2891)."""
|
||||
|
||||
class MyTools:
|
||||
@ai_function
|
||||
@tool
|
||||
def search_flows(self, category: Literal["Data", "Security", "Network"], issue: str) -> str:
|
||||
"""Search flows by category."""
|
||||
return f"{category}: {issue}"
|
||||
|
||||
tools = MyTools()
|
||||
search_tool = tools.search_flows
|
||||
assert isinstance(search_tool, AIFunction)
|
||||
assert isinstance(search_tool, FunctionTool)
|
||||
schema = search_tool.parameters()
|
||||
assert schema == {
|
||||
"properties": {
|
||||
@@ -183,15 +184,15 @@ def test_ai_function_with_literal_type_in_class_method():
|
||||
assert search_tool("Security", "test issue") == "Security: test issue"
|
||||
|
||||
|
||||
def test_ai_function_with_literal_int_type():
|
||||
"""Test ai_function decorator with Literal int type parameter."""
|
||||
def test_tool_with_literal_int_type():
|
||||
"""Test tool decorator with Literal int type parameter."""
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def set_priority(priority: Literal[1, 2, 3], task: str) -> str:
|
||||
"""Set priority for a task."""
|
||||
return f"Priority {priority}: {task}"
|
||||
|
||||
assert isinstance(set_priority, AIFunction)
|
||||
assert isinstance(set_priority, FunctionTool)
|
||||
schema = set_priority.parameters()
|
||||
assert schema == {
|
||||
"properties": {
|
||||
@@ -205,10 +206,10 @@ def test_ai_function_with_literal_int_type():
|
||||
assert set_priority(1, "important task") == "Priority 1: important task"
|
||||
|
||||
|
||||
def test_ai_function_with_literal_and_annotated():
|
||||
"""Test ai_function decorator with Literal type combined with Annotated for description."""
|
||||
def test_tool_with_literal_and_annotated():
|
||||
"""Test tool decorator with Literal type combined with Annotated for description."""
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def categorize(
|
||||
category: Annotated[Literal["A", "B", "C"], "The category to assign"],
|
||||
name: str,
|
||||
@@ -216,14 +217,14 @@ def test_ai_function_with_literal_and_annotated():
|
||||
"""Categorize an item."""
|
||||
return f"{category}: {name}"
|
||||
|
||||
assert isinstance(categorize, AIFunction)
|
||||
assert isinstance(categorize, FunctionTool)
|
||||
schema = categorize.parameters()
|
||||
# Literal type inside Annotated should preserve enum values
|
||||
assert schema["properties"]["category"]["enum"] == ["A", "B", "C"]
|
||||
assert categorize("A", "test") == "A: test"
|
||||
|
||||
|
||||
async def test_ai_function_decorator_shared_state():
|
||||
async def test_tool_decorator_shared_state():
|
||||
"""Test that decorated methods maintain shared state across multiple calls and tool usage."""
|
||||
|
||||
class StatefulCounter:
|
||||
@@ -233,20 +234,20 @@ async def test_ai_function_decorator_shared_state():
|
||||
self.counter = initial_value
|
||||
self.operation_log: list[str] = []
|
||||
|
||||
@ai_function(name="increment", description="Increment the counter")
|
||||
@tool(name="increment", description="Increment the counter")
|
||||
def increment(self, amount: int) -> str:
|
||||
"""Increment the counter by the given amount."""
|
||||
self.counter += amount
|
||||
self.operation_log.append(f"increment({amount})")
|
||||
return f"Counter incremented by {amount}. New value: {self.counter}"
|
||||
|
||||
@ai_function(name="get_value", description="Get the current counter value")
|
||||
@tool(name="get_value", description="Get the current counter value")
|
||||
def get_value(self) -> str:
|
||||
"""Get the current counter value."""
|
||||
self.operation_log.append("get_value()")
|
||||
return f"Current counter value: {self.counter}"
|
||||
|
||||
@ai_function(name="multiply", description="Multiply the counter")
|
||||
@tool(name="multiply", description="Multiply the counter")
|
||||
def multiply(self, factor: int) -> str:
|
||||
"""Multiply the counter by the given factor."""
|
||||
self.counter *= factor
|
||||
@@ -261,10 +262,10 @@ async def test_ai_function_decorator_shared_state():
|
||||
get_value_tool = counter_instance.get_value
|
||||
multiply_tool = counter_instance.multiply
|
||||
|
||||
# Verify they are AIFunction instances
|
||||
assert isinstance(increment_tool, AIFunction)
|
||||
assert isinstance(get_value_tool, AIFunction)
|
||||
assert isinstance(multiply_tool, AIFunction)
|
||||
# Verify they are FunctionTool instances
|
||||
assert isinstance(increment_tool, FunctionTool)
|
||||
assert isinstance(get_value_tool, FunctionTool)
|
||||
assert isinstance(multiply_tool, FunctionTool)
|
||||
|
||||
# Tool 1 (increment) is used
|
||||
result1 = increment_tool(5)
|
||||
@@ -329,10 +330,10 @@ async def test_ai_function_decorator_shared_state():
|
||||
assert counter_instance.counter == 60
|
||||
|
||||
|
||||
async def test_ai_function_invoke_telemetry_enabled(span_exporter: InMemorySpanExporter):
|
||||
"""Test the ai_function invoke method with telemetry enabled."""
|
||||
async def test_tool_invoke_telemetry_enabled(span_exporter: InMemorySpanExporter):
|
||||
"""Test the tool invoke method with telemetry enabled."""
|
||||
|
||||
@ai_function(
|
||||
@tool(
|
||||
name="telemetry_test_tool",
|
||||
description="A test tool for telemetry",
|
||||
)
|
||||
@@ -373,10 +374,10 @@ async def test_ai_function_invoke_telemetry_enabled(span_exporter: InMemorySpanE
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enable_sensitive_data", [False], indirect=True)
|
||||
async def test_ai_function_invoke_telemetry_sensitive_disabled(span_exporter: InMemorySpanExporter):
|
||||
"""Test the ai_function invoke method with telemetry enabled."""
|
||||
async def test_tool_invoke_telemetry_sensitive_disabled(span_exporter: InMemorySpanExporter):
|
||||
"""Test the tool invoke method with telemetry enabled."""
|
||||
|
||||
@ai_function(
|
||||
@tool(
|
||||
name="telemetry_test_tool",
|
||||
description="A test tool for telemetry",
|
||||
)
|
||||
@@ -416,10 +417,10 @@ async def test_ai_function_invoke_telemetry_sensitive_disabled(span_exporter: In
|
||||
assert attributes[OtelAttr.TOOL_CALL_ID] == "test_call_id"
|
||||
|
||||
|
||||
async def test_ai_function_invoke_ignores_additional_kwargs() -> None:
|
||||
"""Ensure ai_function tools drop unknown kwargs when invoked with validated arguments."""
|
||||
async def test_tool_invoke_ignores_additional_kwargs() -> None:
|
||||
"""Ensure tools drop unknown kwargs when invoked with validated arguments."""
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
async def simple_tool(message: str) -> str:
|
||||
"""Echo tool."""
|
||||
return message.upper()
|
||||
@@ -436,10 +437,10 @@ async def test_ai_function_invoke_ignores_additional_kwargs() -> None:
|
||||
assert result == "HELLO WORLD"
|
||||
|
||||
|
||||
async def test_ai_function_invoke_telemetry_with_pydantic_args(span_exporter: InMemorySpanExporter):
|
||||
"""Test the ai_function invoke method with Pydantic model arguments."""
|
||||
async def test_tool_invoke_telemetry_with_pydantic_args(span_exporter: InMemorySpanExporter):
|
||||
"""Test the tool invoke method with Pydantic model arguments."""
|
||||
|
||||
@ai_function(
|
||||
@tool(
|
||||
name="pydantic_test_tool",
|
||||
description="A test tool with Pydantic args",
|
||||
)
|
||||
@@ -470,10 +471,10 @@ async def test_ai_function_invoke_telemetry_with_pydantic_args(span_exporter: In
|
||||
assert span.attributes[OtelAttr.TOOL_ARGUMENTS] == '{"x":5,"y":10}'
|
||||
|
||||
|
||||
async def test_ai_function_invoke_telemetry_with_exception(span_exporter: InMemorySpanExporter):
|
||||
"""Test the ai_function invoke method with telemetry when an exception occurs."""
|
||||
async def test_tool_invoke_telemetry_with_exception(span_exporter: InMemorySpanExporter):
|
||||
"""Test the tool invoke method with telemetry when an exception occurs."""
|
||||
|
||||
@ai_function(
|
||||
@tool(
|
||||
name="exception_test_tool",
|
||||
description="A test tool that raises an exception",
|
||||
)
|
||||
@@ -507,10 +508,10 @@ async def test_ai_function_invoke_telemetry_with_exception(span_exporter: InMemo
|
||||
assert attributes[OtelAttr.ERROR_TYPE] == ValueError.__name__
|
||||
|
||||
|
||||
async def test_ai_function_invoke_telemetry_async_function(span_exporter: InMemorySpanExporter):
|
||||
"""Test the ai_function invoke method with telemetry on async function."""
|
||||
async def test_tool_invoke_telemetry_async_function(span_exporter: InMemorySpanExporter):
|
||||
"""Test the tool invoke method with telemetry on async function."""
|
||||
|
||||
@ai_function(
|
||||
@tool(
|
||||
name="async_telemetry_test",
|
||||
description="An async test tool for telemetry",
|
||||
)
|
||||
@@ -544,10 +545,10 @@ async def test_ai_function_invoke_telemetry_async_function(span_exporter: InMemo
|
||||
assert attributes[OtelAttr.MEASUREMENT_FUNCTION_TAG_NAME] == "async_telemetry_test"
|
||||
|
||||
|
||||
async def test_ai_function_invoke_invalid_pydantic_args():
|
||||
"""Test the ai_function invoke method with invalid Pydantic model arguments."""
|
||||
async def test_tool_invoke_invalid_pydantic_args():
|
||||
"""Test the tool invoke method with invalid Pydantic model arguments."""
|
||||
|
||||
@ai_function(name="invalid_args_test", description="A test tool for invalid args")
|
||||
@tool(name="invalid_args_test", description="A test tool for invalid args")
|
||||
def invalid_args_test(x: int, y: int) -> int:
|
||||
"""A function for testing invalid Pydantic args."""
|
||||
return x + y
|
||||
@@ -564,20 +565,18 @@ async def test_ai_function_invoke_invalid_pydantic_args():
|
||||
await invalid_args_test.invoke(arguments=wrong_args)
|
||||
|
||||
|
||||
def test_ai_function_serialization():
|
||||
"""Test AIFunction serialization and deserialization."""
|
||||
def test_tool_serialization():
|
||||
"""Test FunctionTool serialization and deserialization."""
|
||||
|
||||
def serialize_test(x: int, y: int) -> int:
|
||||
"""A function for testing serialization."""
|
||||
return x - y
|
||||
|
||||
serialize_test_ai_function = ai_function(name="serialize_test", description="A test tool for serialization")(
|
||||
serialize_test
|
||||
)
|
||||
serialize_test_tool = tool(name="serialize_test", description="A test tool for serialization")(serialize_test)
|
||||
|
||||
# Serialize to dict
|
||||
tool_dict = serialize_test_ai_function.to_dict()
|
||||
assert tool_dict["type"] == "ai_function"
|
||||
tool_dict = serialize_test_tool.to_dict()
|
||||
assert tool_dict["type"] == "function_tool"
|
||||
assert tool_dict["name"] == "serialize_test"
|
||||
assert tool_dict["description"] == "A test tool for serialization"
|
||||
assert tool_dict["input_model"] == {
|
||||
@@ -588,21 +587,21 @@ def test_ai_function_serialization():
|
||||
}
|
||||
|
||||
# Deserialize from dict
|
||||
restored_tool = AIFunction.from_dict(tool_dict, dependencies={"ai_function": {"func": serialize_test}})
|
||||
assert isinstance(restored_tool, AIFunction)
|
||||
restored_tool = FunctionTool.from_dict(tool_dict, dependencies={"function_tool": {"func": serialize_test}})
|
||||
assert isinstance(restored_tool, FunctionTool)
|
||||
assert restored_tool.name == "serialize_test"
|
||||
assert restored_tool.description == "A test tool for serialization"
|
||||
assert restored_tool.parameters() == serialize_test_ai_function.parameters()
|
||||
assert restored_tool.parameters() == serialize_test_tool.parameters()
|
||||
assert restored_tool(10, 4) == 6
|
||||
|
||||
# Deserialize from dict with instance name
|
||||
restored_tool_2 = AIFunction.from_dict(
|
||||
tool_dict, dependencies={"ai_function": {"name:serialize_test": {"func": serialize_test}}}
|
||||
restored_tool_2 = FunctionTool.from_dict(
|
||||
tool_dict, dependencies={"function_tool": {"name:serialize_test": {"func": serialize_test}}}
|
||||
)
|
||||
assert isinstance(restored_tool_2, AIFunction)
|
||||
assert isinstance(restored_tool_2, FunctionTool)
|
||||
assert restored_tool_2.name == "serialize_test"
|
||||
assert restored_tool_2.description == "A test tool for serialization"
|
||||
assert restored_tool_2.parameters() == serialize_test_ai_function.parameters()
|
||||
assert restored_tool_2.parameters() == serialize_test_tool.parameters()
|
||||
assert restored_tool_2(10, 4) == 6
|
||||
|
||||
|
||||
@@ -979,13 +978,17 @@ def mock_chat_client():
|
||||
return MockChatClient()
|
||||
|
||||
|
||||
@ai_function(name="no_approval_tool", description="Tool that doesn't require approval")
|
||||
@tool(
|
||||
name="no_approval_tool",
|
||||
description="Tool that doesn't require approval",
|
||||
approval_mode="never_require",
|
||||
)
|
||||
def no_approval_tool(x: int) -> int:
|
||||
"""A tool that doesn't require approval."""
|
||||
return x * 2
|
||||
|
||||
|
||||
@ai_function(
|
||||
@tool(
|
||||
name="requires_approval_tool",
|
||||
description="Tool that requires approval",
|
||||
approval_mode="always_require",
|
||||
@@ -1451,10 +1454,10 @@ async def test_streaming_two_functions_mixed_approval():
|
||||
assert all(c.type == "function_approval_request" for c in updates[2].contents)
|
||||
|
||||
|
||||
async def test_ai_function_with_kwargs_injection():
|
||||
"""Test that ai_function correctly handles kwargs injection and hides them from schema."""
|
||||
async def test_tool_with_kwargs_injection():
|
||||
"""Test that tool correctly handles kwargs injection and hides them from schema."""
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def tool_with_kwargs(x: int, **kwargs: Any) -> str:
|
||||
"""A tool that accepts kwargs."""
|
||||
user_id = kwargs.get("user_id", "unknown")
|
||||
@@ -1647,11 +1650,11 @@ def test_build_pydantic_model_from_json_schema_array_of_objects_issue():
|
||||
# CRITICAL: Validate using the same methods that actual chat clients use
|
||||
# This is what would actually be sent to the LLM
|
||||
|
||||
# Create an AIFunction wrapper to access the client-facing APIs
|
||||
# Create a FunctionTool wrapper to access the client-facing APIs
|
||||
def dummy_func(**kwargs):
|
||||
return kwargs
|
||||
|
||||
test_func = AIFunction(
|
||||
test_func = FunctionTool(
|
||||
func=dummy_func,
|
||||
name="create_sales_order",
|
||||
description="Create a sales order",
|
||||
|
||||
@@ -24,10 +24,10 @@ from agent_framework import (
|
||||
ToolMode,
|
||||
ToolProtocol,
|
||||
UsageDetails,
|
||||
ai_function,
|
||||
detect_media_type_from_base64,
|
||||
merge_chat_options,
|
||||
prepare_function_call_results,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.exceptions import ContentError
|
||||
|
||||
@@ -51,10 +51,10 @@ def ai_tool() -> ToolProtocol:
|
||||
|
||||
|
||||
@fixture
|
||||
def ai_function_tool() -> ToolProtocol:
|
||||
def tool_tool() -> ToolProtocol:
|
||||
"""Returns a executable ToolProtocol."""
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def simple_function(x: int, y: int) -> int:
|
||||
"""A simple function that adds two numbers."""
|
||||
return x + y
|
||||
@@ -1017,13 +1017,13 @@ def test_chat_options_tool_choice_validation():
|
||||
validate_tool_mode({"mode": "auto", "required_function_name": "should_not_be_here"})
|
||||
|
||||
|
||||
def test_chat_options_merge(ai_function_tool, ai_tool) -> None:
|
||||
def test_chat_options_merge(tool_tool, ai_tool) -> None:
|
||||
"""Test merge_chat_options utility function."""
|
||||
from agent_framework import merge_chat_options
|
||||
|
||||
options1: ChatOptions = {
|
||||
"model_id": "gpt-4o",
|
||||
"tools": [ai_function_tool],
|
||||
"tools": [tool_tool],
|
||||
"logit_bias": {"x": 1},
|
||||
"metadata": {"a": "b"},
|
||||
}
|
||||
@@ -1034,7 +1034,7 @@ def test_chat_options_merge(ai_function_tool, ai_tool) -> None:
|
||||
options3 = merge_chat_options(options1, options2)
|
||||
|
||||
assert options3.get("model_id") == "gpt-4.1"
|
||||
assert options3.get("tools") == [ai_function_tool, ai_tool] # tools are combined
|
||||
assert options3.get("tools") == [tool_tool, ai_tool] # tools are combined
|
||||
assert options3.get("logit_bias") == {"x": 1} # base value preserved
|
||||
assert options3.get("metadata") == {"a": "b"} # base value preserved
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import pytest
|
||||
from openai.types.beta.assistant import Assistant
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from agent_framework import ChatAgent, HostedCodeInterpreterTool, HostedFileSearchTool, ai_function, normalize_tools
|
||||
from agent_framework import ChatAgent, HostedCodeInterpreterTool, HostedFileSearchTool, normalize_tools, tool
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
from agent_framework.openai import OpenAIAssistantProvider
|
||||
from agent_framework.openai._shared import from_assistant_tools, to_assistant_tools
|
||||
@@ -244,11 +244,11 @@ class TestOpenAIAssistantProviderCreateAgent:
|
||||
assert call_kwargs["tools"][0]["type"] == "function"
|
||||
assert call_kwargs["tools"][0]["function"]["name"] == "get_weather"
|
||||
|
||||
async def test_create_agent_with_ai_function(self, mock_async_openai: MagicMock) -> None:
|
||||
"""Test assistant creation with AIFunction."""
|
||||
async def test_create_agent_with_tool(self, mock_async_openai: MagicMock) -> None:
|
||||
"""Test assistant creation with FunctionTool."""
|
||||
provider = OpenAIAssistantProvider(mock_async_openai)
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def my_function(x: int) -> int:
|
||||
"""Double a number."""
|
||||
return x * 2
|
||||
@@ -537,10 +537,10 @@ class TestOpenAIAssistantProviderAsAgent:
|
||||
class TestToolConversion:
|
||||
"""Tests for tool conversion utilities (shared functions)."""
|
||||
|
||||
def test_to_assistant_tools_ai_function(self) -> None:
|
||||
"""Test AIFunction conversion to API format."""
|
||||
def test_to_assistant_tools_tool(self) -> None:
|
||||
"""Test FunctionTool conversion to API format."""
|
||||
|
||||
@ai_function
|
||||
@tool
|
||||
def test_func(x: int) -> int:
|
||||
"""Test function."""
|
||||
return x
|
||||
@@ -555,7 +555,7 @@ class TestToolConversion:
|
||||
|
||||
def test_to_assistant_tools_callable(self) -> None:
|
||||
"""Test raw callable conversion via normalize_tools."""
|
||||
# normalize_tools converts callables to AIFunction
|
||||
# normalize_tools converts callables to FunctionTool
|
||||
normalized = normalize_tools([get_weather])
|
||||
api_tools = to_assistant_tools(normalized)
|
||||
|
||||
@@ -666,12 +666,12 @@ class TestToolValidation:
|
||||
# Should not raise
|
||||
provider._validate_function_tools(assistant_tools, None) # type: ignore[reportPrivateUsage]
|
||||
|
||||
def test_validate_with_ai_function(self, mock_async_openai: MagicMock) -> None:
|
||||
"""Test validation with AIFunction."""
|
||||
def test_validate_with_tool(self, mock_async_openai: MagicMock) -> None:
|
||||
"""Test validation with FunctionTool."""
|
||||
provider = OpenAIAssistantProvider(mock_async_openai)
|
||||
assistant_tools = [create_function_tool("get_weather")]
|
||||
|
||||
wrapped = ai_function(get_weather)
|
||||
wrapped = tool(get_weather)
|
||||
|
||||
# Should not raise
|
||||
provider._validate_function_tools(assistant_tools, [wrapped]) # type: ignore[reportPrivateUsage]
|
||||
@@ -789,6 +789,7 @@ class TestOpenAIAssistantProviderIntegration:
|
||||
"""Integration test with function tools."""
|
||||
provider = OpenAIAssistantProvider()
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def get_current_time() -> str:
|
||||
"""Get the current time."""
|
||||
from datetime import datetime
|
||||
|
||||
@@ -23,7 +23,7 @@ from agent_framework import (
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
Role,
|
||||
ai_function,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
from agent_framework.openai import OpenAIAssistantsClient
|
||||
@@ -709,13 +709,13 @@ def test_prepare_options_basic(mock_async_openai: MagicMock) -> None:
|
||||
assert tool_results is None
|
||||
|
||||
|
||||
def test_prepare_options_with_ai_function_tool(mock_async_openai: MagicMock) -> None:
|
||||
"""Test _prepare_options with AIFunction tool."""
|
||||
def test_prepare_options_with_tool_tool(mock_async_openai: MagicMock) -> None:
|
||||
"""Test _prepare_options with a FunctionTool."""
|
||||
|
||||
chat_client = create_test_openai_assistants_client(mock_async_openai)
|
||||
|
||||
# Create a simple function for testing and decorate it
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def test_function(query: str) -> str:
|
||||
"""A test function."""
|
||||
return f"Result for {query}"
|
||||
@@ -998,6 +998,7 @@ def test_update_agent_name_and_description_none(mock_async_openai: MagicMock) ->
|
||||
assert chat_client.assistant_name is None
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
|
||||
@@ -19,8 +19,8 @@ from agent_framework import (
|
||||
Content,
|
||||
HostedWebSearchTool,
|
||||
ToolProtocol,
|
||||
ai_function,
|
||||
prepare_function_call_results,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.exceptions import ServiceInitializationError, ServiceResponseException
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
@@ -175,7 +175,7 @@ def test_unsupported_tool_handling(openai_unit_test_env: dict[str, str]) -> None
|
||||
"""Test that unsupported tool types are handled correctly."""
|
||||
client = OpenAIChatClient()
|
||||
|
||||
# Create a mock ToolProtocol that's not an AIFunction
|
||||
# Create a mock ToolProtocol that's not a FunctionTool
|
||||
unsupported_tool = MagicMock(spec=ToolProtocol)
|
||||
unsupported_tool.__class__.__name__ = "UnsupportedAITool"
|
||||
|
||||
@@ -189,7 +189,7 @@ def test_unsupported_tool_handling(openai_unit_test_env: dict[str, str]) -> None
|
||||
assert result["tools"] == [dict_tool]
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def get_story_text() -> str:
|
||||
"""Returns a story about Emily and David."""
|
||||
return (
|
||||
@@ -200,7 +200,7 @@ def get_story_text() -> str:
|
||||
)
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def get_weather(location: str) -> str:
|
||||
"""Get the current weather for a location."""
|
||||
return f"The weather in {location} is sunny and 72°F."
|
||||
|
||||
@@ -40,7 +40,7 @@ from agent_framework import (
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
Role,
|
||||
ai_function,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.exceptions import (
|
||||
ServiceInitializationError,
|
||||
@@ -96,7 +96,7 @@ async def delete_vector_store(client: OpenAIResponsesClient, file_id: str, vecto
|
||||
await client.client.files.delete(file_id=file_id)
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
async def get_weather(location: Annotated[str, "The location as a city name"]) -> str:
|
||||
"""Get the current weather in a given location."""
|
||||
# Implementation of the tool to get weather
|
||||
@@ -642,7 +642,7 @@ def test_response_content_creation_with_function_call() -> None:
|
||||
assert function_call.arguments == '{"location": "Seattle"}'
|
||||
|
||||
|
||||
def test_prepare_content_for_openai_function_approval_response() -> None:
|
||||
def test_prepare_content_for_opentool_approval_response() -> None:
|
||||
"""Test _prepare_content_for_openai with function approval response content."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ from agent_framework import (
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
WorkflowOutputEvent,
|
||||
ai_function,
|
||||
executor,
|
||||
tool,
|
||||
use_function_invocation,
|
||||
)
|
||||
|
||||
@@ -132,7 +132,7 @@ async def test_agent_executor_emits_tool_calls_in_streaming_mode() -> None:
|
||||
assert "sunny" in events[3].data.contents[0].text
|
||||
|
||||
|
||||
@ai_function(approval_mode="always_require")
|
||||
@tool(approval_mode="always_require")
|
||||
def mock_tool_requiring_approval(query: str) -> str:
|
||||
"""Mock tool that requires approval before execution."""
|
||||
return f"Executed tool with query: {query}"
|
||||
|
||||
@@ -20,7 +20,7 @@ from agent_framework import (
|
||||
SequentialBuilder,
|
||||
WorkflowRunState,
|
||||
WorkflowStatusEvent,
|
||||
ai_function,
|
||||
tool,
|
||||
)
|
||||
from agent_framework._workflows._const import WORKFLOW_RUN_KWARGS_KEY
|
||||
|
||||
@@ -28,7 +28,7 @@ from agent_framework._workflows._const import WORKFLOW_RUN_KWARGS_KEY
|
||||
_received_kwargs: list[dict[str, Any]] = []
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def tool_with_kwargs(
|
||||
action: Annotated[str, "The action to perform"],
|
||||
**kwargs: Any,
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import Any, Literal, TypedDict, cast
|
||||
|
||||
import yaml
|
||||
from agent_framework import (
|
||||
AIFunction,
|
||||
ChatAgent,
|
||||
ChatClientProtocol,
|
||||
Content,
|
||||
@@ -17,6 +16,9 @@ from agent_framework import (
|
||||
HostedWebSearchTool,
|
||||
ToolProtocol,
|
||||
)
|
||||
from agent_framework import (
|
||||
FunctionTool as AFFunctionTool,
|
||||
)
|
||||
from agent_framework._tools import _create_model_from_json_schema # type: ignore
|
||||
from agent_framework.exceptions import AgentFrameworkException
|
||||
from dotenv import load_dotenv
|
||||
@@ -719,7 +721,7 @@ class AgentFactory:
|
||||
for binding in tool_resource.bindings:
|
||||
if binding.name and (func := self.bindings.get(binding.name)):
|
||||
break
|
||||
return AIFunction( # type: ignore
|
||||
return AFFunctionTool( # type: ignore
|
||||
name=tool_resource.name, # type: ignore
|
||||
description=tool_resource.description, # type: ignore
|
||||
input_model=tool_resource.parameters.to_json_schema() if tool_resource.parameters else None,
|
||||
|
||||
@@ -19,7 +19,7 @@ from agent_framework import (
|
||||
Role,
|
||||
normalize_messages,
|
||||
)
|
||||
from agent_framework._tools import AIFunction, ToolProtocol
|
||||
from agent_framework._tools import FunctionTool, ToolProtocol
|
||||
from agent_framework._types import normalize_tools
|
||||
from agent_framework.exceptions import ServiceException, ServiceInitializationError
|
||||
from copilot import CopilotClient, CopilotSession
|
||||
@@ -417,8 +417,8 @@ class GithubCopilotAgent(BaseAgent, Generic[TOptions]):
|
||||
for tool in tools:
|
||||
if isinstance(tool, ToolProtocol):
|
||||
match tool:
|
||||
case AIFunction():
|
||||
copilot_tools.append(self._ai_function_to_copilot_tool(tool)) # type: ignore
|
||||
case FunctionTool():
|
||||
copilot_tools.append(self._tool_to_copilot_tool(tool)) # type: ignore
|
||||
case _:
|
||||
logger.debug(f"Unsupported tool type: {type(tool)}")
|
||||
elif isinstance(tool, CopilotTool):
|
||||
@@ -426,8 +426,8 @@ class GithubCopilotAgent(BaseAgent, Generic[TOptions]):
|
||||
|
||||
return copilot_tools
|
||||
|
||||
def _ai_function_to_copilot_tool(self, ai_func: AIFunction[Any, Any]) -> CopilotTool:
|
||||
"""Convert an AIFunction to a Copilot SDK tool."""
|
||||
def _tool_to_copilot_tool(self, ai_func: FunctionTool[Any, Any]) -> CopilotTool:
|
||||
"""Convert an FunctionTool to a Copilot SDK tool."""
|
||||
|
||||
async def handler(invocation: ToolInvocation) -> ToolResult:
|
||||
args = invocation.get("arguments", {})
|
||||
|
||||
@@ -7,7 +7,14 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from agent_framework import AgentResponse, AgentResponseUpdate, AgentThread, ChatMessage, Content, Role
|
||||
from agent_framework import (
|
||||
AgentResponse,
|
||||
AgentResponseUpdate,
|
||||
AgentThread,
|
||||
ChatMessage,
|
||||
Content,
|
||||
Role,
|
||||
)
|
||||
from agent_framework.exceptions import ServiceException
|
||||
from copilot.generated.session_events import Data, SessionEvent, SessionEventType
|
||||
|
||||
@@ -733,10 +740,10 @@ class TestGithubCopilotAgentToolConversion:
|
||||
mock_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test that mixed tool types are handled correctly."""
|
||||
from agent_framework._tools import ai_function
|
||||
from agent_framework import tool
|
||||
from copilot.types import Tool as CopilotTool
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def my_function(arg: str) -> str:
|
||||
"""A function tool."""
|
||||
return arg
|
||||
@@ -754,7 +761,7 @@ class TestGithubCopilotAgentToolConversion:
|
||||
result = agent._prepare_tools([my_function, copilot_tool]) # type: ignore
|
||||
|
||||
assert len(result) == 2
|
||||
# First tool is converted AIFunction
|
||||
# First tool is converted FunctionTool
|
||||
assert result[0].name == "my_function"
|
||||
# Second tool is CopilotTool passthrough
|
||||
assert result[1] == copilot_tool
|
||||
|
||||
@@ -6,7 +6,7 @@ from copy import deepcopy
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from agent_framework._tools import AIFunction
|
||||
from agent_framework._tools import FunctionTool
|
||||
from agent_framework._types import ChatMessage
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
@@ -25,8 +25,8 @@ from tau2.environment.tool import Tool # type: ignore[import-untyped]
|
||||
_original_set_state = Environment.set_state
|
||||
|
||||
|
||||
def convert_tau2_tool_to_ai_function(tau2_tool: Tool) -> AIFunction[Any, Any]:
|
||||
"""Convert a tau2 Tool to an AIFunction for agent framework compatibility.
|
||||
def convert_tau2_tool_to_function_tool(tau2_tool: Tool) -> FunctionTool[Any, Any]:
|
||||
"""Convert a tau2 Tool to a FunctionTool for agent framework compatibility.
|
||||
|
||||
Creates a wrapper that preserves the tool's interface while ensuring
|
||||
results are deep-copied to prevent unintended mutations.
|
||||
@@ -37,7 +37,7 @@ def convert_tau2_tool_to_ai_function(tau2_tool: Tool) -> AIFunction[Any, Any]:
|
||||
# Deep copy to prevent mutations of returned data
|
||||
return result.model_copy(deep=True) if isinstance(result, BaseModel) else deepcopy(result)
|
||||
|
||||
return AIFunction(
|
||||
return FunctionTool(
|
||||
name=tau2_tool.name,
|
||||
description=tau2_tool._get_description(),
|
||||
func=wrapped_func,
|
||||
|
||||
@@ -32,7 +32,7 @@ from tau2.utils.utils import get_now # type: ignore[import-untyped]
|
||||
|
||||
from ._message_utils import flip_messages, log_messages
|
||||
from ._sliding_window import SlidingWindowChatMessageStore
|
||||
from ._tau2_utils import convert_agent_framework_messages_to_tau2_messages, convert_tau2_tool_to_ai_function
|
||||
from ._tau2_utils import convert_agent_framework_messages_to_tau2_messages, convert_tau2_tool_to_function_tool
|
||||
|
||||
__all__ = ["ASSISTANT_AGENT_ID", "ORCHESTRATOR_ID", "USER_SIMULATOR_ID", "TaskRunner"]
|
||||
|
||||
@@ -179,9 +179,9 @@ class TaskRunner:
|
||||
f"Environment has {len(env.get_tools())} tools: {', '.join([tool.name for tool in env.get_tools()])}"
|
||||
)
|
||||
|
||||
# Convert tau2 tools to agent framework AIFunction format
|
||||
# Convert tau2 tools to agent framework FunctionTool format
|
||||
# This bridges the gap between tau2's tool system and agent framework's expectations
|
||||
ai_functions = [convert_tau2_tool_to_ai_function(tool) for tool in tools]
|
||||
tools = [convert_tau2_tool_to_function_tool(tool) for tool in tools]
|
||||
|
||||
# Combines general customer service behavior with specific policy guidelines
|
||||
assistant_system_prompt = f"""<instructions>
|
||||
@@ -198,7 +198,7 @@ class TaskRunner:
|
||||
return ChatAgent(
|
||||
chat_client=assistant_chat_client,
|
||||
instructions=assistant_system_prompt,
|
||||
tools=ai_functions,
|
||||
tools=tools,
|
||||
temperature=self.assistant_sampling_temperature,
|
||||
chat_message_store_factory=lambda: SlidingWindowChatMessageStore(
|
||||
system_message=assistant_system_prompt,
|
||||
|
||||
@@ -6,11 +6,10 @@ import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from agent_framework._tools import AIFunction
|
||||
from agent_framework._types import ChatMessage, Content, Role
|
||||
from agent_framework import ChatMessage, Content, FunctionTool, Role
|
||||
from agent_framework_lab_tau2._tau2_utils import (
|
||||
convert_agent_framework_messages_to_tau2_messages,
|
||||
convert_tau2_tool_to_ai_function,
|
||||
convert_tau2_tool_to_function_tool,
|
||||
)
|
||||
from tau2.data_model.message import AssistantMessage, SystemMessage, ToolCall, ToolMessage, UserMessage
|
||||
from tau2.domains.airline.data_model import FlightDB
|
||||
@@ -51,8 +50,8 @@ def tau2_airline_environment() -> Environment:
|
||||
)
|
||||
|
||||
|
||||
def test_convert_tau2_tool_to_ai_function_basic(tau2_airline_environment):
|
||||
"""Test basic conversion from tau2 tool to AIFunction."""
|
||||
def test_convert_tau2_tool_to_function_tool_basic(tau2_airline_environment):
|
||||
"""Test basic conversion from tau2 tool to FunctionTool."""
|
||||
# Get real tools from tau2 environment
|
||||
tools = tau2_airline_environment.get_tools()
|
||||
|
||||
@@ -61,33 +60,33 @@ def test_convert_tau2_tool_to_ai_function_basic(tau2_airline_environment):
|
||||
tau2_tool = tools[0]
|
||||
|
||||
# Convert the tool
|
||||
ai_function = convert_tau2_tool_to_ai_function(tau2_tool)
|
||||
tool = convert_tau2_tool_to_function_tool(tau2_tool)
|
||||
|
||||
# Verify the conversion
|
||||
assert isinstance(ai_function, AIFunction)
|
||||
assert ai_function.name == tau2_tool.name
|
||||
assert ai_function.description == tau2_tool._get_description()
|
||||
assert ai_function.input_model == tau2_tool.params
|
||||
assert isinstance(tool, FunctionTool)
|
||||
assert tool.name == tau2_tool.name
|
||||
assert tool.description == tau2_tool._get_description()
|
||||
assert tool.input_model == tau2_tool.params
|
||||
|
||||
# Test that the function is callable (we won't call it with real params to avoid side effects)
|
||||
assert callable(ai_function.func)
|
||||
assert callable(tool.func)
|
||||
|
||||
|
||||
def test_convert_tau2_tool_to_ai_function_multiple_tools(tau2_airline_environment):
|
||||
def test_convert_tau2_tool_to_function_tool_multiple_tools(tau2_airline_environment):
|
||||
"""Test conversion with multiple tau2 tools."""
|
||||
# Get real tools from tau2 environment
|
||||
tools = tau2_airline_environment.get_tools()
|
||||
|
||||
# Convert multiple tools
|
||||
ai_functions = [convert_tau2_tool_to_ai_function(tool) for tool in tools[:3]] # Test first 3 tools
|
||||
function_tools = [convert_tau2_tool_to_function_tool(tool) for tool in tools[:3]] # Test first 3 tools
|
||||
|
||||
# Verify all conversions
|
||||
for ai_function, tau2_tool in zip(ai_functions, tools[:3], strict=False):
|
||||
assert isinstance(ai_function, AIFunction)
|
||||
assert ai_function.name == tau2_tool.name
|
||||
assert ai_function.description == tau2_tool._get_description()
|
||||
assert ai_function.input_model == tau2_tool.params
|
||||
assert callable(ai_function.func)
|
||||
for tool, tau2_tool in zip(function_tools, tools[:3], strict=False):
|
||||
assert isinstance(tool, FunctionTool)
|
||||
assert tool.name == tau2_tool.name
|
||||
assert tool.description == tau2_tool._get_description()
|
||||
assert tool.input_model == tau2_tool.params
|
||||
assert callable(tool.func)
|
||||
|
||||
|
||||
def test_convert_agent_framework_messages_to_tau2_messages_system():
|
||||
|
||||
@@ -14,13 +14,13 @@ from itertools import chain
|
||||
from typing import Any, ClassVar, Generic, TypedDict
|
||||
|
||||
from agent_framework import (
|
||||
AIFunction,
|
||||
BaseChatClient,
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
FunctionTool,
|
||||
Role,
|
||||
ToolProtocol,
|
||||
UsageDetails,
|
||||
@@ -545,13 +545,13 @@ class OllamaChatClient(BaseChatClient[TOllamaChatOptions], Generic[TOllamaChatOp
|
||||
for tool in tools:
|
||||
if isinstance(tool, ToolProtocol):
|
||||
match tool:
|
||||
case AIFunction():
|
||||
case FunctionTool():
|
||||
chat_tools.append(tool.to_json_schema_spec())
|
||||
case _:
|
||||
raise ServiceInvalidRequestError(
|
||||
"Unsupported tool type '"
|
||||
f"{type(tool).__name__}"
|
||||
"' for Ollama client. Supported tool types: AIFunction."
|
||||
"' for Ollama client. Supported tool types: FunctionTool."
|
||||
)
|
||||
else:
|
||||
chat_tools.append(tool if isinstance(tool, dict) else dict(tool))
|
||||
|
||||
@@ -11,8 +11,8 @@ from agent_framework import (
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
HostedWebSearchTool,
|
||||
ai_function,
|
||||
chat_middleware,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.exceptions import (
|
||||
ServiceInitializationError,
|
||||
@@ -109,7 +109,7 @@ def mock_chat_completion_tool_call() -> OllamaChatResponse:
|
||||
)
|
||||
|
||||
|
||||
@ai_function
|
||||
@tool(approval_mode="never_require")
|
||||
def hello_world(arg1: str) -> str:
|
||||
return "Hello World"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user