Skip to content

Commit e02b59f

Browse files
committed
fix(models): handle arbitrary dict responses in part_to_message_block
AnthropicLlm.part_to_message_block() only serialized FunctionResponse dicts containing a "content" or "result" key, silently returning an empty string for any other structure. Tools such as SkillToolset's load_skill (which returns {"skill_name", "instructions", "frontmatter"}) and run_skill_script (which returns {"stdout", "stderr", "status"}) were completely invisible to Claude models as a result. Add an else-branch that JSON-serializes the full response dict when neither known key is present, ensuring all tool responses reach the model regardless of key names.
1 parent ffe97ec commit e02b59f

File tree

2 files changed

+87
-0
lines changed

2 files changed

+87
-0
lines changed

src/google/adk/models/anthropic_llm.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,12 @@ def part_to_message_block(
145145
content = json.dumps(result)
146146
else:
147147
content = str(result)
148+
elif response_data:
149+
# Fallback: serialize the entire response dict as JSON so that tools
150+
# returning arbitrary key structures (e.g. load_skill returning
151+
# {"skill_name", "instructions", "frontmatter"}) are not silently
152+
# dropped.
153+
content = json.dumps(response_data)
148154

149155
return anthropic_types.ToolResultBlockParam(
150156
tool_use_id=part.function_response.id or "",

tests/unittests/models/test_anthropic_llm.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,87 @@ def test_part_to_message_block_nested_dict_result():
765765
assert parsed["results"][0]["tags"] == ["a", "b"]
766766

767767

768+
# --- Tests for arbitrary dict fallback (e.g. SkillToolset load_skill) ---
769+
770+
771+
def test_part_to_message_block_arbitrary_dict_serialized_as_json():
772+
"""Dicts with keys other than 'content'/'result' should be JSON-serialized.
773+
774+
This covers tools like load_skill that return arbitrary key structures
775+
such as {"skill_name": ..., "instructions": ..., "frontmatter": ...}.
776+
"""
777+
response_part = types.Part.from_function_response(
778+
name="load_skill",
779+
response={
780+
"skill_name": "my_skill",
781+
"instructions": "Step 1: do this. Step 2: do that.",
782+
"frontmatter": {"version": "1.0", "tags": ["a", "b"]},
783+
},
784+
)
785+
response_part.function_response.id = "test_id"
786+
787+
result = part_to_message_block(response_part)
788+
789+
assert result["type"] == "tool_result"
790+
assert result["tool_use_id"] == "test_id"
791+
assert not result["is_error"]
792+
parsed = json.loads(result["content"])
793+
assert parsed["skill_name"] == "my_skill"
794+
assert parsed["instructions"] == "Step 1: do this. Step 2: do that."
795+
assert parsed["frontmatter"]["version"] == "1.0"
796+
797+
798+
def test_part_to_message_block_run_skill_script_response():
799+
"""run_skill_script response keys (stdout/stderr/status) should not be dropped."""
800+
response_part = types.Part.from_function_response(
801+
name="run_skill_script",
802+
response={
803+
"skill_name": "my_skill",
804+
"script_path": "scripts/setup.py",
805+
"stdout": "Done.",
806+
"stderr": "",
807+
"status": "success",
808+
},
809+
)
810+
response_part.function_response.id = "test_id_2"
811+
812+
result = part_to_message_block(response_part)
813+
814+
parsed = json.loads(result["content"])
815+
assert parsed["status"] == "success"
816+
assert parsed["stdout"] == "Done."
817+
818+
819+
def test_part_to_message_block_error_response_not_dropped():
820+
"""Error dicts like {"error": ..., "error_code": ...} should be serialized."""
821+
response_part = types.Part.from_function_response(
822+
name="load_skill",
823+
response={
824+
"error": "Skill 'missing' not found.",
825+
"error_code": "SKILL_NOT_FOUND",
826+
},
827+
)
828+
response_part.function_response.id = "test_id_3"
829+
830+
result = part_to_message_block(response_part)
831+
832+
parsed = json.loads(result["content"])
833+
assert parsed["error_code"] == "SKILL_NOT_FOUND"
834+
835+
836+
def test_part_to_message_block_empty_response_stays_empty():
837+
"""An empty response dict should still produce an empty content string."""
838+
response_part = types.Part.from_function_response(
839+
name="some_tool",
840+
response={},
841+
)
842+
response_part.function_response.id = "test_id_4"
843+
844+
result = part_to_message_block(response_part)
845+
846+
assert result["content"] == ""
847+
848+
768849
# --- Tests for Bug #1: Streaming support ---
769850

770851

0 commit comments

Comments
 (0)