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:
Eduard van Valkenburg
2026-01-28 15:53:53 +01:00
committed by GitHub
Unverified
parent 15b43f2abe
commit a7d924a7d2
255 changed files with 1202 additions and 1290 deletions
@@ -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,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.
@@ -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.
@@ -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",
+3 -3
View File
@@ -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])
+9 -9
View File
@@ -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:
+13 -11
View File
@@ -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 [])
+8 -8
View File
@@ -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:
+75 -74
View File
@@ -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(
+20 -20
View File
@@ -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
+3 -3
View File
@@ -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
+9 -13
View File
@@ -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)
+6 -6
View File
@@ -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")
+92 -89
View File
@@ -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"