Skip to content

Python: ToolProtocol is not fully supported - only AIFunction works #1622

@anguzo

Description

@anguzo

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 AIFunction

Location 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 too

Use 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 required

Why 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 loses name and description metadata (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:

  1. Hosted tools (HostedWebSearchTool, HostedCodeInterpreterTool)
  2. Custom tools - only AIFunction is actually supported despite ToolProtocol
    being the public interface

The ToolProtocol interface suggests extensibility that doesn't actually exist.

Metadata

Metadata

Labels

model clientsIssues related to the model client implementationspython

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions