From e1c5d82df2336396c5c869d0f7812a444b7efb34 Mon Sep 17 00:00:00 2001 From: 1828703450qqcom-ux <265310764+1828703450qqcom-ux@users.noreply.github.com> Date: Tue, 3 Nov 2026 17:28:18 +0800 Subject: [PATCH] fix(examples): render function responses in examples as JSON convert_examples_to_text is documented to convert a function response part to a json string, but it rendered part.function_response.__dict__, the pydantic model's internal storage. That leaks every unset optional field as None into the few-shot example and renders nested values through their Python repr, which spans several lines and breaks the fenced block around the example. Serialize the response fields as JSON with the unset optional fields dropped, so the example shows the response shape the model is being taught. Non-JSON-serializable values fall back to their string form rather than failing the system-instruction build. --- src/google/adk/examples/example_util.py | 24 +++++- tests/unittests/examples/test_example_util.py | 78 ++++++++++++++++++- 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/src/google/adk/examples/example_util.py b/src/google/adk/examples/example_util.py index 2a94c7e05dd..54f9b596f75 100644 --- a/src/google/adk/examples/example_util.py +++ b/src/google/adk/examples/example_util.py @@ -16,7 +16,9 @@ """Utility functions for converting examples to a string that can be used in system instructions in the prompt.""" +import json import logging +from typing import Any from .base_example_provider import BaseExampleProvider from .example import Example @@ -40,6 +42,23 @@ _FUNCTION_RESPONSE_SUFFIX = "\n```\n" +def _function_response_json(response: Any) -> str: + """Renders a function response as the JSON string the example promises. + + ``FunctionResponse.__dict__`` is the model's internal storage, so it leaks + every unset optional field as ``None`` and renders nested values as their + Python repr, which for a pydantic model spans several lines and breaks the + fenced block around the example. Serializing the fields instead keeps the + example to the response shape the model is being shown. + """ + try: + return json.dumps(response.model_dump(exclude_none=True), default=str) + except (TypeError, ValueError): + # Values the JSON encoder cannot represent fall back to their string form + # rather than failing the whole system-instruction build. + return str(response.model_dump(exclude_none=True)) + + def convert_examples_to_text(examples: list[Example], model: str | None) -> str: """Converts a list of examples to a string that can be used in a system instruction.""" examples_str = "" @@ -78,7 +97,10 @@ def convert_examples_to_text(examples: list[Example], model: str | None) -> str: # Convert function response part to json string elif part.function_response: prefix = _FUNCTION_PREFIX if is_gemini else _FUNCTION_RESPONSE_PREFIX - output += f"{prefix}{part.function_response.__dict__}{_FUNCTION_RESPONSE_SUFFIX}" + output += ( + f"{prefix}{_function_response_json(part.function_response)}" + f"{_FUNCTION_RESPONSE_SUFFIX}" + ) elif part.text: output += f"{part.text}\n" diff --git a/tests/unittests/examples/test_example_util.py b/tests/unittests/examples/test_example_util.py index 7db0aed4704..2bf584a547a 100644 --- a/tests/unittests/examples/test_example_util.py +++ b/tests/unittests/examples/test_example_util.py @@ -339,7 +339,8 @@ def test_example_conversion_with_function_response(model): f"{example_util._EXAMPLE_START.format(1)}" f"{example_util._USER_PREFIX}test_input\n" f"{example_util._MODEL_PREFIX}{prefix}" - f"{test_function_response.__dict__}" + '{"name": "test_function", "response": {"test_string_argument":' + ' "test_value", "test_int_argument": 1}}' f"{example_util._FUNCTION_RESPONSE_SUFFIX}" f"{example_util._EXAMPLE_END}" f"{example_util._EXAMPLES_END}" @@ -403,7 +404,8 @@ def test_example_conversion_with_function_call_response(model): "test_function(test_string_argument='test_value', test_int_argument=1)" f"{example_util._FUNCTION_CALL_SUFFIX}" f"{response_prefix}" - f"{test_function_response.__dict__}" + '{"name": "test_function", "response": {"test_string_argument":' + ' "test_value", "test_int_argument": 1}}' f"{example_util._FUNCTION_RESPONSE_SUFFIX}" f"{example_util._EXAMPLE_END}" f"{example_util._EXAMPLES_END}" @@ -469,7 +471,8 @@ def test_example_conversion_with_text_and_function_call_response(model): "test_function(test_string_argument='test_value', test_int_argument=1)" f"{example_util._FUNCTION_CALL_SUFFIX}" f"{response_prefix}" - f"{test_function_response.__dict__}" + '{"name": "test_function", "response": {"test_string_argument":' + ' "test_value", "test_int_argument": 1}}' f"{example_util._FUNCTION_RESPONSE_SUFFIX}" f"{example_util._EXAMPLE_END}" f"{example_util._EXAMPLES_END}" @@ -543,3 +546,72 @@ def test_building_si_from_base_example_provider(model): ) == expected_output ) + + +def test_function_response_renders_json_without_unset_fields(): + """A function response example is JSON and carries no unset optional fields. + + ``FunctionResponse.__dict__`` used to leak ``will_continue``, ``scheduling`` + and ``parts`` as ``None`` into every example and render nested values as a + multi-line Python repr. The example now shows the response shape only. + """ + test_function_response = types.FunctionResponse( + name="test_function", + id="call_1", + response={"test_string_argument": "test_value", "test_int_argument": 1}, + ) + output_content = [ + types.Content( + role="model", + parts=[types.Part(function_response=test_function_response)], + ) + ] + test_example = example.Example(input=BASIC_INPUT, output=output_content) + + rendered = example_util.convert_examples_to_text([test_example], "gemini") + + assert ( + '{"id": "call_1", "name": "test_function", "response":' + ' {"test_string_argument": "test_value", "test_int_argument": 1}}' + in rendered + ) + # Unset optional fields must not reach the example. + assert "will_continue" not in rendered + assert "scheduling" not in rendered + assert "'parts': None" not in rendered + # It is JSON, not a Python repr. + assert "None" not in rendered + + +def test_function_response_nested_values_render_as_json(): + """Nested values inside a response do not introduce raw newlines. + + A pydantic model in the response dict used to be rendered through its repr, + which spans several lines and breaks the fenced block around the example. + """ + test_function_response = types.FunctionResponse( + name="test_function", + response={"nested": types.Part(text="nested_value")}, + ) + output_content = [ + types.Content( + role="model", + parts=[types.Part(function_response=test_function_response)], + ) + ] + test_example = example.Example(input=BASIC_INPUT, output=output_content) + + rendered = example_util.convert_examples_to_text([test_example], "gemini") + + assert '{"nested": {"text": "nested_value"}}' in rendered + # The rendered example must stay on one line inside the fence. + assert "Part(" not in rendered + payload_lines = [ + line + for line in rendered.split("```")[1].splitlines() + if line.startswith("{") + ] + assert payload_lines == [ + '{"name": "test_function", "response": {"nested": {"text":' + ' "nested_value"}}}' + ]