Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion src/google/adk/examples/example_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = ""
Expand Down Expand Up @@ -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"

Expand Down
78 changes: 75 additions & 3 deletions tests/unittests/examples/test_example_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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}"
Expand Down Expand Up @@ -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}"
Expand Down Expand Up @@ -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"}}}'
]
Loading