Skip to content

Commit 1dd97f5

Browse files
xuanyang15copybara-github
authored andcommitted
fix: Add experimental feature to use parameters_json_schema and response_json_schema for McpTool
Co-authored-by: Xuan Yang <xygoogle@google.com> PiperOrigin-RevId: 833144947
1 parent f7f6837 commit 1dd97f5

File tree

10 files changed

+582
-37
lines changed

10 files changed

+582
-37
lines changed

src/google/adk/models/anthropic_llm.py

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import os
2323
from typing import Any
2424
from typing import AsyncGenerator
25-
from typing import Generator
2625
from typing import Iterable
2726
from typing import Literal
2827
from typing import Optional
@@ -98,14 +97,29 @@ def part_to_message_block(
9897
)
9998
elif part.function_response:
10099
content = ""
101-
if (
102-
"result" in part.function_response.response
103-
and part.function_response.response["result"]
104-
):
100+
response_data = part.function_response.response
101+
102+
# Handle response with content array
103+
if "content" in response_data and response_data["content"]:
104+
content_items = []
105+
for item in response_data["content"]:
106+
if isinstance(item, dict):
107+
# Handle text content blocks
108+
if item.get("type") == "text" and "text" in item:
109+
content_items.append(item["text"])
110+
else:
111+
# Handle other structured content
112+
content_items.append(str(item))
113+
else:
114+
content_items.append(str(item))
115+
content = "\n".join(content_items) if content_items else ""
116+
# Handle traditional result format
117+
elif "result" in response_data and response_data["result"]:
105118
# Transformation is required because the content is a list of dict.
106119
# ToolResultBlockParam content doesn't support list of dict. Converting
107120
# to str to prevent anthropic.BadRequestError from being thrown.
108-
content = str(part.function_response.response["result"])
121+
content = str(response_data["result"])
122+
109123
return anthropic_types.ToolResultBlockParam(
110124
tool_use_id=part.function_response.id or "",
111125
type="tool_result",
@@ -219,23 +233,27 @@ def function_declaration_to_tool_param(
219233
"""Converts a function declaration to an Anthropic tool param."""
220234
assert function_declaration.name
221235

222-
properties = {}
223-
required_params = []
224-
if function_declaration.parameters:
225-
if function_declaration.parameters.properties:
226-
for key, value in function_declaration.parameters.properties.items():
227-
value_dict = value.model_dump(exclude_none=True)
228-
_update_type_string(value_dict)
229-
properties[key] = value_dict
230-
if function_declaration.parameters.required:
231-
required_params = function_declaration.parameters.required
232-
233-
input_schema = {
234-
"type": "object",
235-
"properties": properties,
236-
}
237-
if required_params:
238-
input_schema["required"] = required_params
236+
# Use parameters_json_schema if available, otherwise convert from parameters
237+
if function_declaration.parameters_json_schema:
238+
input_schema = function_declaration.parameters_json_schema
239+
else:
240+
properties = {}
241+
required_params = []
242+
if function_declaration.parameters:
243+
if function_declaration.parameters.properties:
244+
for key, value in function_declaration.parameters.properties.items():
245+
value_dict = value.model_dump(exclude_none=True)
246+
_update_type_string(value_dict)
247+
properties[key] = value_dict
248+
if function_declaration.parameters.required:
249+
required_params = function_declaration.parameters.required
250+
251+
input_schema = {
252+
"type": "object",
253+
"properties": properties,
254+
}
255+
if required_params:
256+
input_schema["required"] = required_params
239257

240258
return anthropic_types.ToolParam(
241259
name=function_declaration.name,

src/google/adk/models/google_llm.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,9 +364,15 @@ def _build_function_declaration_log(
364364
k: v.model_dump(exclude_none=True)
365365
for k, v in func_decl.parameters.properties.items()
366366
})
367+
elif func_decl.parameters_json_schema:
368+
param_str = str(func_decl.parameters_json_schema)
369+
367370
return_str = ''
368371
if func_decl.response:
369372
return_str = '-> ' + str(func_decl.response.model_dump(exclude_none=True))
373+
elif func_decl.response_json_schema:
374+
return_str = '-> ' + str(func_decl.response_json_schema)
375+
370376
return f'{func_decl.name}: {param_str} {return_str}'
371377

372378

src/google/adk/models/lite_llm.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,13 +660,16 @@ def _function_declaration_to_tool_param(
660660
},
661661
}
662662

663+
# Handle required field from parameters
663664
required_fields = (
664665
getattr(function_declaration.parameters, "required", None)
665666
if function_declaration.parameters
666667
else None
667668
)
668669
if required_fields:
669670
tool_params["function"]["parameters"]["required"] = required_fields
671+
# parameters_json_schema already has required field in the json schema,
672+
# no need to add it separately
670673

671674
return tool_params
672675

@@ -969,9 +972,15 @@ def _build_function_declaration_log(
969972
k: v.model_dump(exclude_none=True)
970973
for k, v in func_decl.parameters.properties.items()
971974
})
975+
elif func_decl.parameters_json_schema:
976+
param_str = str(func_decl.parameters_json_schema)
977+
972978
return_str = "None"
973979
if func_decl.response:
974980
return_str = str(func_decl.response.model_dump(exclude_none=True))
981+
elif func_decl.response_json_schema:
982+
return_str = str(func_decl.response_json_schema)
983+
975984
return f"{func_decl.name}: {param_str} -> {return_str}"
976985

977986

src/google/adk/tools/mcp_tool/conversion_utils.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,16 @@ def adk_to_mcp_tool_type(tool: BaseTool) -> mcp_types.Tool:
4343
print(mcp_tool)
4444
"""
4545
tool_declaration = tool._get_declaration()
46-
if not tool_declaration or not tool_declaration.parameters:
46+
if not tool_declaration:
4747
input_schema = {}
48-
else:
48+
elif tool_declaration.parameters_json_schema:
49+
# Use JSON schema directly if available
50+
input_schema = tool_declaration.parameters_json_schema
51+
elif tool_declaration.parameters:
52+
# Convert from Schema object
4953
input_schema = gemini_to_json_schema(tool_declaration.parameters)
54+
else:
55+
input_schema = {}
5056
return mcp_types.Tool(
5157
name=tool.name,
5258
description=tool.description,

src/google/adk/tools/mcp_tool/mcp_tool.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
from typing_extensions import override
3131

3232
from ...agents.readonly_context import ReadonlyContext
33+
from ...features import FeatureName
34+
from ...features import is_feature_enabled
3335
from .._gemini_schema_util import _to_gemini_schema
3436
from .mcp_session_manager import MCPSessionManager
3537
from .mcp_session_manager import retry_on_closed_resource
@@ -120,12 +122,21 @@ def _get_declaration(self) -> FunctionDeclaration:
120122
FunctionDeclaration: The Gemini function declaration for the tool.
121123
"""
122124
input_schema = self._mcp_tool.inputSchema
123-
parameters = _to_gemini_schema(input_schema)
124-
function_decl = FunctionDeclaration(
125-
name=self.name,
126-
description=self.description,
127-
parameters=parameters,
128-
)
125+
output_schema = self._mcp_tool.outputSchema
126+
if is_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL):
127+
function_decl = FunctionDeclaration(
128+
name=self.name,
129+
description=self.description,
130+
parameters_json_schema=input_schema,
131+
response_json_schema=output_schema,
132+
)
133+
else:
134+
parameters = _to_gemini_schema(input_schema)
135+
function_decl = FunctionDeclaration(
136+
name=self.name,
137+
description=self.description,
138+
parameters=parameters,
139+
)
129140
return function_decl
130141

131142
@property

tests/unittests/models/test_anthropic_llm.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,45 @@ def test_supported_models():
273273
},
274274
),
275275
),
276+
(
277+
"function_with_parameters_json_schema",
278+
types.FunctionDeclaration(
279+
name="search_database",
280+
description="Searches a database with given criteria.",
281+
parameters_json_schema={
282+
"type": "object",
283+
"properties": {
284+
"query": {
285+
"type": "string",
286+
"description": "The search query",
287+
},
288+
"limit": {
289+
"type": "integer",
290+
"description": "Maximum number of results",
291+
},
292+
},
293+
"required": ["query"],
294+
},
295+
),
296+
anthropic_types.ToolParam(
297+
name="search_database",
298+
description="Searches a database with given criteria.",
299+
input_schema={
300+
"type": "object",
301+
"properties": {
302+
"query": {
303+
"type": "string",
304+
"description": "The search query",
305+
},
306+
"limit": {
307+
"type": "integer",
308+
"description": "Maximum number of results",
309+
},
310+
},
311+
"required": ["query"],
312+
},
313+
),
314+
),
276315
]
277316

278317

@@ -346,3 +385,80 @@ async def mock_coro():
346385
mock_client.messages.create.assert_called_once()
347386
_, kwargs = mock_client.messages.create.call_args
348387
assert kwargs["max_tokens"] == 4096
388+
389+
390+
def test_part_to_message_block_with_content():
391+
"""Test that part_to_message_block handles content format."""
392+
from google.adk.models.anthropic_llm import part_to_message_block
393+
394+
# Create a function response part with content array.
395+
mcp_response_part = types.Part.from_function_response(
396+
name="generate_sample_filesystem",
397+
response={
398+
"content": [{
399+
"type": "text",
400+
"text": '{"name":"root","node_type":"folder","children":[]}',
401+
}]
402+
},
403+
)
404+
mcp_response_part.function_response.id = "test_id_123"
405+
406+
result = part_to_message_block(mcp_response_part)
407+
408+
# ToolResultBlockParam is a TypedDict.
409+
assert isinstance(result, dict)
410+
assert result["tool_use_id"] == "test_id_123"
411+
assert result["type"] == "tool_result"
412+
assert not result["is_error"]
413+
# Verify the content was extracted from the content format.
414+
assert (
415+
'{"name":"root","node_type":"folder","children":[]}' in result["content"]
416+
)
417+
418+
419+
def test_part_to_message_block_with_traditional_result():
420+
"""Test that part_to_message_block handles traditional result format."""
421+
from google.adk.models.anthropic_llm import part_to_message_block
422+
423+
# Create a function response part with traditional result format
424+
traditional_response_part = types.Part.from_function_response(
425+
name="some_tool",
426+
response={
427+
"result": "This is the result from the tool",
428+
},
429+
)
430+
traditional_response_part.function_response.id = "test_id_456"
431+
432+
result = part_to_message_block(traditional_response_part)
433+
434+
# ToolResultBlockParam is a TypedDict.
435+
assert isinstance(result, dict)
436+
assert result["tool_use_id"] == "test_id_456"
437+
assert result["type"] == "tool_result"
438+
assert not result["is_error"]
439+
# Verify the content was extracted from the traditional format
440+
assert "This is the result from the tool" in result["content"]
441+
442+
443+
def test_part_to_message_block_with_multiple_content_items():
444+
"""Test content with multiple items."""
445+
from google.adk.models.anthropic_llm import part_to_message_block
446+
447+
# Create a function response with multiple content items
448+
multi_content_part = types.Part.from_function_response(
449+
name="multi_response_tool",
450+
response={
451+
"content": [
452+
{"type": "text", "text": "First part"},
453+
{"type": "text", "text": "Second part"},
454+
]
455+
},
456+
)
457+
multi_content_part.function_response.id = "test_id_789"
458+
459+
result = part_to_message_block(multi_content_part)
460+
461+
# ToolResultBlockParam is a TypedDict.
462+
assert isinstance(result, dict)
463+
# Multiple text items should be joined with newlines
464+
assert result["content"] == "First part\nSecond part"

0 commit comments

Comments
 (0)