Python: fix(claude): preserve $defs in JSON schema for nested Pydantic models (#3655)

* fix(claude): preserve $defs in JSON schema for nested Pydantic models

- Preserve $defs section from Pydantic JSON schema when converting FunctionTool to SDK MCP tool
- This fixes tools with nested Pydantic models that use $ref references
- Add test for nested type schema preservation

Fixes #3654

* Adjust shared state import

* Fix MCP tool kwargs serialization bug

---------

Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
This commit is contained in:
Dineshsuriya D
2026-02-04 12:04:26 +05:30
committed by GitHub
Unverified
parent 06d43ee130
commit 5c6cf4fc92
6 changed files with 50 additions and 5 deletions
@@ -511,6 +511,9 @@ class ClaudeAgent(BaseAgent, Generic[TOptions]):
"properties": schema.get("properties", {}),
"required": schema.get("required", []),
}
# Preserve $defs for nested type references (Pydantic uses $defs for nested models)
if "$defs" in schema:
input_schema["$defs"] = schema["$defs"]
return SdkMcpTool(
name=func_tool.name,
@@ -499,6 +499,33 @@ class TestClaudeAgentToolConversion:
assert sdk_tool.input_schema is not None
assert "properties" in sdk_tool.input_schema # type: ignore[operator]
def test_function_tool_to_sdk_mcp_tool_preserves_defs_for_nested_types(self) -> None:
"""Test that $defs is preserved for tools with nested Pydantic models."""
from pydantic import BaseModel
class Address(BaseModel):
street: str
city: str
class Person(BaseModel):
name: str
address: Address
@tool
def create_person(person: Person) -> str:
"""Create a person with address."""
return f"{person.name} lives at {person.address.street}, {person.address.city}"
agent = ClaudeAgent()
sdk_tool = agent._function_tool_to_sdk_mcp_tool(create_person) # type: ignore[reportPrivateUsage]
# Verify $defs is preserved in the schema
assert sdk_tool.input_schema is not None
assert "$defs" in sdk_tool.input_schema # type: ignore[operator]
assert "Address" in sdk_tool.input_schema["$defs"] # type: ignore[index]
# Verify the nested reference exists in properties
assert "person" in sdk_tool.input_schema["properties"] # type: ignore[index]
async def test_tool_handler_success(self) -> None:
"""Test tool handler executes successfully."""
+17 -2
View File
@@ -796,11 +796,26 @@ class FunctionTool(BaseTool, Generic[ArgsT, ReturnT]):
attributes = get_function_span_attributes(self, tool_call_id=tool_call_id)
if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED: # type: ignore[name-defined]
# Filter out framework kwargs that are not JSON serializable
serializable_kwargs = {
k: v
for k, v in kwargs.items()
if k
not in {
"chat_options",
"tools",
"tool_choice",
"thread",
"conversation_id",
"options",
"response_format",
}
}
attributes.update({
OtelAttr.TOOL_ARGUMENTS: arguments.model_dump_json()
if arguments
else json.dumps(kwargs)
if kwargs
else json.dumps(serializable_kwargs, default=str)
if serializable_kwargs
else "None"
})
with get_function_span(attributes=attributes) as span:
@@ -10,7 +10,6 @@ from agent_framework import (
Executor,
InProcRunnerContext,
Message,
SharedState,
WorkflowContext,
handler,
)
@@ -24,6 +23,7 @@ from agent_framework._workflows._edge import (
SwitchCaseEdgeGroupDefault,
)
from agent_framework._workflows._edge_runner import create_edge_runner
from agent_framework._workflows._shared_state import SharedState
from agent_framework.observability import EdgeGroupDeliveryStatus
# Add for test
@@ -8,7 +8,6 @@ from agent_framework import (
ExecutorFailedEvent,
InProcRunnerContext,
RequestInfoEvent,
SharedState,
Workflow,
WorkflowBuilder,
WorkflowContext,
@@ -20,6 +19,7 @@ from agent_framework import (
WorkflowStatusEvent,
handler,
)
from agent_framework._workflows._shared_state import SharedState
class FailingExecutor(Executor):
@@ -32,9 +32,9 @@ from typing import Any, Literal, cast
from agent_framework._workflows import (
Executor,
SharedState,
WorkflowContext,
)
from agent_framework._workflows._shared_state import SharedState
from powerfx import Engine
if sys.version_info >= (3, 11):