Skip to content

Commit fc9c81b

Browse files
Python: [BREAKING] Remove FunctionTool[Any] compatibility shim for schema passthrough (#3600) (#3907)
* Fix #3600: Pass JSON schemas through without Pydantic conversion This change optimizes FunctionTool and MCP flows by passing JSON schemas directly to providers without converting them to Pydantic models first. Key changes: - Store JSON schema as-is when supplied to FunctionTool - Skip Pydantic model_validate for schema-supplied tools in invoke() - Return MCP tool schemas directly without conversion - Add comprehensive tests for schema passthrough behavior Performance benefits: - Eliminates expensive Pydantic model creation for supplied schemas - Preserves exact schema structure (additionalProperties, custom fields, etc.) - Reduces memory overhead and initialization time Maintains backward compatibility: - Function signature inference still uses Pydantic models - Explicit Pydantic models passed as input_model work as before - All existing tests pass * Fix schema passthrough validation and remove helper * Simplify FunctionTool without generic model dependency * Fix FunctionTool typing fallout in 3600 * Remove FunctionTool[Any] compatibility shim * Use serializable kwargs in OTEL tool args
1 parent cd1e311 commit fc9c81b

16 files changed

Lines changed: 643 additions & 480 deletions

File tree

python/packages/ag-ui/agent_framework_ag_ui/_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ def _register_server_tool_placeholder(self, tool_name: str) -> None:
267267
if any(getattr(tool, "name", None) == tool_name for tool in additional_tools):
268268
return
269269

270-
placeholder: FunctionTool[Any] = FunctionTool(
270+
placeholder: FunctionTool = FunctionTool(
271271
name=tool_name,
272272
description="Server-managed tool placeholder (AG-UI)",
273273
func=None,

python/packages/ag-ui/agent_framework_ag_ui/_utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ def make_json_safe(obj: Any) -> Any: # noqa: ANN401
162162

163163
def convert_agui_tools_to_agent_framework(
164164
agui_tools: list[dict[str, Any]] | None,
165-
) -> list[FunctionTool[Any]] | None:
165+
) -> list[FunctionTool] | None:
166166
"""Convert AG-UI tool definitions to Agent Framework FunctionTool declarations.
167167
168168
Creates declaration-only FunctionTool instances (no executable implementation).
@@ -181,13 +181,13 @@ def convert_agui_tools_to_agent_framework(
181181
if not agui_tools:
182182
return None
183183

184-
result: list[FunctionTool[Any]] = []
184+
result: list[FunctionTool] = []
185185
for tool_def in agui_tools:
186186
# Create declaration-only FunctionTool (func=None means no implementation)
187187
# When func=None, the declaration_only property returns True,
188188
# which tells the function invocation mixin to return the function call
189189
# without executing it (so it can be sent back to the client)
190-
func: FunctionTool[Any] = FunctionTool(
190+
func: FunctionTool = FunctionTool(
191191
name=tool_def.get("name", ""),
192192
description=tool_def.get("description", ""),
193193
func=None, # CRITICAL: Makes declaration_only=True

python/packages/ag-ui/agent_framework_ag_ui_examples/agents/ui_generator_agent.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from __future__ import annotations
66

77
import sys
8-
from typing import TYPE_CHECKING, Any, TypedDict
8+
from typing import TYPE_CHECKING, TypedDict
99

1010
from agent_framework import Agent, FunctionTool, SupportsChatGetResponse
1111
from agent_framework.ag_ui import AgentFrameworkAgent
@@ -23,7 +23,7 @@
2323
from agent_framework import ChatOptions
2424

2525
# Declaration-only tools (func=None) - actual rendering happens on the client side
26-
generate_haiku = FunctionTool[Any](
26+
generate_haiku = FunctionTool(
2727
name="generate_haiku",
2828
description="""Generate a haiku with image and gradient background (FRONTEND_RENDER).
2929
@@ -71,7 +71,7 @@
7171
},
7272
)
7373

74-
create_chart = FunctionTool[Any](
74+
create_chart = FunctionTool(
7575
name="create_chart",
7676
description="""Create an interactive chart (FRONTEND_RENDER).
7777
@@ -99,7 +99,7 @@
9999
},
100100
)
101101

102-
display_timeline = FunctionTool[Any](
102+
display_timeline = FunctionTool(
103103
name="display_timeline",
104104
description="""Display an interactive timeline (FRONTEND_RENDER).
105105
@@ -127,7 +127,7 @@
127127
},
128128
)
129129

130-
show_comparison_table = FunctionTool[Any](
130+
show_comparison_table = FunctionTool(
131131
name="show_comparison_table",
132132
description="""Show a comparison table (FRONTEND_RENDER).
133133

python/packages/claude/agent_framework_claude/_agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ def _prepare_tools(
484484

485485
return create_sdk_mcp_server(name=TOOLS_MCP_SERVER_NAME, tools=sdk_tools), tool_names
486486

487-
def _function_tool_to_sdk_mcp_tool(self, func_tool: FunctionTool[Any]) -> SdkMcpTool[Any]:
487+
def _function_tool_to_sdk_mcp_tool(self, func_tool: FunctionTool) -> SdkMcpTool[Any]:
488488
"""Convert a FunctionTool to an SDK MCP tool.
489489
490490
Args:

python/packages/core/agent_framework/_agents.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ def as_tool(
439439
stream_callback: Callable[[AgentResponseUpdate], None]
440440
| Callable[[AgentResponseUpdate], Awaitable[None]]
441441
| None = None,
442-
) -> FunctionTool[BaseModel]:
442+
) -> FunctionTool:
443443
"""Create a FunctionTool that wraps this agent.
444444
445445
Keyword Args:
@@ -513,7 +513,7 @@ async def agent_wrapper(**kwargs: Any) -> str:
513513
# Create final text from accumulated updates
514514
return AgentResponse.from_updates(response_updates).text
515515

516-
agent_tool: FunctionTool[BaseModel] = FunctionTool(
516+
agent_tool: FunctionTool = FunctionTool(
517517
name=tool_name,
518518
description=tool_description,
519519
func=agent_wrapper,
@@ -1258,17 +1258,12 @@ async def _log(level: types.LoggingLevel, data: Any) -> None:
12581258
@server.list_tools() # type: ignore
12591259
async def _list_tools() -> list[types.Tool]: # type: ignore
12601260
"""List all tools in the agent."""
1261-
# Get the JSON schema from the Pydantic model
1262-
schema = agent_tool.input_model.model_json_schema()
1261+
schema = agent_tool.parameters()
12631262

12641263
tool = types.Tool(
12651264
name=agent_tool.name,
12661265
description=agent_tool.description,
1267-
inputSchema={
1268-
"type": "object",
1269-
"properties": schema.get("properties", {}),
1270-
"required": schema.get("required", []),
1271-
},
1266+
inputSchema=schema,
12721267
)
12731268

12741269
await _log(level="debug", data=f"Agent tool: {agent_tool}")
@@ -1291,7 +1286,9 @@ async def _call_tool( # type: ignore
12911286

12921287
# Create an instance of the input model with the arguments
12931288
try:
1294-
args_instance = agent_tool.input_model(**arguments)
1289+
args_instance: BaseModel | dict[str, Any] = (
1290+
agent_tool.input_model(**arguments) if agent_tool.input_model is not None else arguments
1291+
)
12951292
result = await agent_tool.invoke(arguments=args_instance)
12961293
except Exception as e:
12971294
raise McpError(

python/packages/core/agent_framework/_mcp.py

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,9 @@
2424
from mcp.shared.context import RequestContext
2525
from mcp.shared.exceptions import McpError
2626
from mcp.shared.session import RequestResponder
27-
from pydantic import BaseModel, create_model
2827

2928
from ._tools import (
3029
FunctionTool,
31-
_build_pydantic_model_from_json_schema,
3230
)
3331
from ._types import (
3432
Content,
@@ -355,11 +353,14 @@ def _prepare_message_for_mcp(
355353
return messages
356354

357355

358-
def _get_input_model_from_mcp_prompt(prompt: types.Prompt) -> type[BaseModel]:
359-
"""Creates a Pydantic model from a prompt's parameters."""
356+
def _get_input_model_from_mcp_prompt(prompt: types.Prompt) -> dict[str, Any]:
357+
"""Get the input model from an MCP prompt.
358+
359+
Returns a JSON schema dictionary for prompt arguments.
360+
"""
360361
# Check if 'arguments' is missing or empty
361362
if not prompt.arguments:
362-
return create_model(f"{prompt.name}_input")
363+
return {"type": "object", "properties": {}}
363364

364365
# Convert prompt arguments to JSON schema format
365366
properties: dict[str, Any] = {}
@@ -374,13 +375,10 @@ def _get_input_model_from_mcp_prompt(prompt: types.Prompt) -> type[BaseModel]:
374375
if prompt_argument.required:
375376
required.append(prompt_argument.name)
376377

377-
schema = {"properties": properties, "required": required}
378-
return _build_pydantic_model_from_json_schema(prompt.name, schema)
379-
380-
381-
def _get_input_model_from_mcp_tool(tool: types.Tool) -> type[BaseModel]:
382-
"""Creates a Pydantic model from a tools parameters."""
383-
return _build_pydantic_model_from_json_schema(tool.name, tool.inputSchema)
378+
schema: dict[str, Any] = {"type": "object", "properties": properties}
379+
if required:
380+
schema["required"] = required
381+
return schema
384382

385383

386384
def _normalize_mcp_name(name: str) -> str:
@@ -467,7 +465,7 @@ def __init__(
467465
self.session = session
468466
self.request_timeout = request_timeout
469467
self.client = client
470-
self._functions: list[FunctionTool[Any]] = []
468+
self._functions: list[FunctionTool] = []
471469
self.is_connected: bool = False
472470
self._tools_loaded: bool = False
473471
self._prompts_loaded: bool = False
@@ -476,7 +474,7 @@ def __str__(self) -> str:
476474
return f"MCPTool(name={self.name}, description={self.description})"
477475

478476
@property
479-
def functions(self) -> list[FunctionTool[Any]]:
477+
def functions(self) -> list[FunctionTool]:
480478
"""Get the list of functions that are allowed."""
481479
if not self.allowed_tools:
482480
return self._functions
@@ -744,7 +742,7 @@ async def load_prompts(self) -> None:
744742

745743
input_model = _get_input_model_from_mcp_prompt(prompt)
746744
approval_mode = self._determine_approval_mode(local_name)
747-
func: FunctionTool[BaseModel] = FunctionTool(
745+
func: FunctionTool = FunctionTool(
748746
func=partial(self.get_prompt, prompt.name),
749747
name=local_name,
750748
description=prompt.description or "",
@@ -785,15 +783,14 @@ async def load_tools(self) -> None:
785783
if local_name in existing_names:
786784
continue
787785

788-
input_model = _get_input_model_from_mcp_tool(tool)
789786
approval_mode = self._determine_approval_mode(local_name)
790787
# Create FunctionTools out of each tool
791-
func: FunctionTool[BaseModel] = FunctionTool(
788+
func: FunctionTool = FunctionTool(
792789
func=partial(self.call_tool, tool.name),
793790
name=local_name,
794791
description=tool.description or "",
795792
approval_mode=approval_mode,
796-
input_model=input_model,
793+
input_model=tool.inputSchema,
797794
)
798795
self._functions.append(func)
799796
existing_names.add(local_name)

python/packages/core/agent_framework/_middleware.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,8 @@ async def process(self, context: FunctionInvocationContext, call_next):
234234

235235
def __init__(
236236
self,
237-
function: FunctionTool[Any],
238-
arguments: BaseModel,
237+
function: FunctionTool,
238+
arguments: BaseModel | Mapping[str, Any],
239239
metadata: Mapping[str, Any] | None = None,
240240
result: Any = None,
241241
kwargs: Mapping[str, Any] | None = None,

0 commit comments

Comments
 (0)