-
Notifications
You must be signed in to change notification settings - Fork 646
Description
Summary
The agent framework's API suggests that any ToolProtocol implementation сan be used as a tool. However, in practice, only AIFunction instances are actually supported. Custom tool implementations (like those inheriting from BaseTool blocked by #1620, but overall implementing ToolProtocol) are silently dropped.
This is particularly problematic for stateful tools that need to maintain singleton instances (e.g., Azure SDK clients).
Related Issues
Problem Description
The Type Promise
The framework's type hints suggest broad tool support:
# From OpenAIChatClient._chat_to_tool_spec
tools: Sequence[ToolProtocol | MutableMapping[str, Any]]
# From _tools.py _get_tool_map
tools: ToolProtocol | Callable[..., Any] | ...Translation: "I accept anything implementing ToolProtocol"
The Reality
Only AIFunction instances are actually supported:
Location 1: OpenAIChatClient._chat_to_tool_spec
def _chat_to_tool_spec(self, tools: Sequence[ToolProtocol | MutableMapping[str, Any]]):
for tool in tools:
if isinstance(tool, ToolProtocol):
match tool:
case AIFunction():
chat_tools.append(tool.to_json_schema_spec())
case _:
logger.debug("Unsupported tool passed (type: %s), ignoring", type(tool))
# ↑ Silently ignores any ToolProtocol that isn't AIFunctionLocation 2: _tools.py _get_tool_map
def _get_tool_map(tools):
ai_function_list: dict[str, AIFunction[Any, Any]] = {}
for tool in tools:
if isinstance(tool, AIFunction):
ai_function_list[tool.name] = tool
continue
if callable(tool):
ai_tool = ai_function(tool)
ai_function_list[ai_tool.name] = ai_tool
return ai_function_list
# ↑ BaseTool subclasses disappear here tooUse Case: Azure SDK Singleton Pattern
Many Azure SDK clients should be treated as singletons. However, the framework requires a workaround:
from pydantic import BaseModel, Field
from agent_framework import BaseTool, AIFunction
from azure.search.documents import SearchClient
class SearchInput(BaseModel):
query: str = Field(..., description="Search query")
class AzureSearchTool(BaseTool): # assuming issue 1620 is resolved
"""Search tool that reuses a singleton SearchClient."""
def __init__(self, search_client: SearchClient):
super().__init__(
name="search_documents",
description="Search documents using Azure AI Search",
)
self.search_client = search_client
def as_ai_function(self) -> AIFunction:
"""Convert to AIFunction for agent framework."""
return AIFunction(
name=self.name,
description=self.description,
func=self.invoke,
input_model=SearchInput,
)
async def invoke(self, query: str) -> str:
"""Execute search with lifecycle management."""
results = await self.search_client.search(search_text=query)
return str(results)
# Usage requires workaround
search_client = SearchClient(...)
tool = AzureSearchTool(search_client=search_client)
agent.chat(messages, tools=[tool.as_ai_function()]) # ← Conversion requiredWhy other approaches don't work:
- Direct BaseTool usage: Silently rejected as "Unsupported tool"
- Making tool callable (
__call__): Still rejected; not automatically converted - Bound method (e.g.,
tool.invoke): Auto-converted to AIFunction, but losesnameanddescriptionmetadata (generates generic name like "invoke")
Alternatively of course custom tool can inherit from AIFunction, but by design it seems that AIFunction's main purpose is function decoration.
Root Cause
The framework has two codepaths for tools:
- Hosted tools (HostedWebSearchTool, HostedCodeInterpreterTool)
- Custom tools - only AIFunction is actually supported despite ToolProtocol
being the public interface
The ToolProtocol interface suggests extensibility that doesn't actually exist.