Skip to content

Commit a931305

Browse files
committed
fix: serialize non-standard FunctionResponse dicts in AnthropicLlm
Rebased onto latest main, resolved conflicts with upstream json.dumps improvement. Keeps both upstream's json.dumps serialization for standard results AND the fallback path for non-standard response dicts (e.g. SkillToolset).
1 parent f8270c8 commit a931305

2 files changed

Lines changed: 38 additions & 0 deletions

File tree

src/google/adk/models/anthropic_llm.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ def part_to_message_block(
146146
content = json.dumps(result)
147147
else:
148148
content = str(result)
149+
elif response_data:
150+
# Fallback: serialize any non-standard response dict (e.g. SkillToolset
151+
# returns {"skill_name": ..., "instructions": ..., "frontmatter": ...})
152+
content = json.dumps(response_data)
149153

150154
return anthropic_types.ToolResultBlockParam(
151155
tool_use_id=part.function_response.id or "",

tests/unittests/models/test_anthropic_llm.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,30 @@ def test_part_to_message_block_with_multiple_content_items():
741741
assert result["content"] == "First part\nSecond part"
742742

743743

744+
def test_part_to_message_block_with_non_standard_response():
745+
"""Test that part_to_message_block serializes non-standard response dicts.
746+
747+
Regression test for https://github.com/google/adk-python/issues/4779.
748+
SkillToolset returns dicts like {"skill_name": ..., "instructions": ...}
749+
that don't contain "content" or "result" keys. These were silently
750+
dropped (empty string), causing Claude to never see the tool output.
751+
"""
752+
import json
753+
754+
from google.adk.models.anthropic_llm import part_to_message_block
755+
756+
skill_response = {
757+
"skill_name": "search_docs",
758+
"instructions": "Use the search API to find documents.",
759+
"frontmatter": {"version": "1.0"},
760+
}
761+
part = types.Part.from_function_response(
762+
name="load_skill",
763+
response=skill_response,
764+
)
765+
part.function_response.id = "test_skill_id"
766+
767+
744768
def test_part_to_message_block_with_pdf_document():
745769
"""Test that part_to_message_block handles PDF document parts."""
746770
pdf_data = b"%PDF-1.4 fake pdf content"
@@ -751,6 +775,16 @@ def test_part_to_message_block_with_pdf_document():
751775
result = part_to_message_block(part)
752776

753777
assert isinstance(result, dict)
778+
assert result["tool_use_id"] == "test_skill_id"
779+
assert result["type"] == "tool_result"
780+
assert not result["is_error"]
781+
# Content must be non-empty and contain the original data
782+
parsed = json.loads(result["content"])
783+
assert parsed["skill_name"] == "search_docs"
784+
assert parsed["instructions"] == "Use the search API to find documents."
785+
assert parsed["frontmatter"] == {"version": "1.0"}
786+
787+
754788
assert result["type"] == "document"
755789
assert result["source"]["type"] == "base64"
756790
assert result["source"]["media_type"] == "application/pdf"

0 commit comments

Comments
 (0)