Skip to content

fix(examples): render function responses in examples as JSON - #7964

Open
1828703450qqcom-ux wants to merge 1 commit into
google:mainfrom
1828703450qqcom-ux:fix/examples-function-response-json
Open

1828703450qqcom-ux wants to merge 1 commit into
google:mainfrom
1828703450qqcom-ux:fix/examples-function-response-json

Conversation

@1828703450qqcom-ux

Copy link
Copy Markdown
Contributor

fix(examples): render function responses in examples as JSON

Summary

  • convert_examples_to_text is documented to "Convert function response part
    to 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,
    so the model is taught a tool response looks like:

    {'will_continue': None, 'scheduling': None, 'parts': None, 'id': 'call_1',
     'name': 'search', 'response': {'result': 42}}
    
  • It is also a Python repr, not JSON: None/True/single quotes, and any
    nested pydantic value inside response renders through its repr, which
    spans several lines and breaks the fenced block that wraps the example.

  • Fix: serialize the response fields as JSON with the unset optional fields
    dropped, so the example shows the response shape only.

Reproduction

Public API, on the current tree:

fr = types.FunctionResponse(
    name="search", id="call_1",
    response={"result": 42, "note": "it's fine"},
)
ex = Example(
    input=types.Content(role="user", parts=[types.Part(text="hi")]),
    output=[types.Content(role="model", parts=[types.Part(function_response=fr)])],
)
print(convert_examples_to_text([ex], "gemini-2.5-flash"))

Before, inside the fence:

{'will_continue': None, 'scheduling': None, 'parts': None, 'id': 'call_1',
 'name': 'search', 'response': {'result': 42, 'note': "it's fine"}}

After:

{"id": "call_1", "name": "search", "response": {"result": 42, "note": "it's fine"}}

The nested-value case is worse before — a types.Part in the response dict
renders as Part(\n text='nested_value'\n), so the fenced block contains raw
newlines and the example no longer reads as one tool-output payload. After the
change the same input renders {"nested": {"text": "nested_value"}} on one
line.

Fix

src/google/adk/examples/example_util.py — the function-response branch now
calls a small _function_response_json helper that emits
json.dumps(response.model_dump(exclude_none=True), default=str).
model_dump also recurses into nested pydantic values, so they become JSON
objects instead of multi-line reprs. A non-JSON-serializable value falls back
to its string form rather than failing the whole system-instruction build.

The function-call branch is unchanged: it is documented to render a
python-like call and its existing tests pin that shape.

Validation

Two new regression tests in tests/unittests/examples/test_example_util.py:

case before after
function response renders JSON without unset fields will_continue/scheduling/parts leak, Python repr clean JSON
nested value in the response dict Part(\n text=...) breaks the fence one-line JSON object

Both fail on the unmodified tree (verified by stashing only the source file)
and pass after. The exact before-output is visible in the assertion diff of
test_function_response_nested_values_render_as_json.

The three existing tests that pinned f"{test_function_response.__dict__}"
in their expected string now expect the JSON form; no other expectations
changed. Those tests had echoed the implementation's __dict__ rather than
specifying an output format.

  • tests/unittests/examples/: 76 passed.
  • tests/unittests/examples/ + tests/unittests/tools/: 3580 passed,
    2 skipped, 4 failed. The 4 failures are
    tests/unittests/tools/test_skill_toolset.py POSIX-environment cases
    (shell metacharacters, FIFO, symlink) and reproduce identically on
    unmodified main; this change does not touch that module.
  • mypy 2.4.1 on the changed file: Success: no issues found.
  • Pre-commit: ruff, isort, pyink, addlicense, codespell,
    fix end of files, trim trailing whitespace all pass. The three
    native-Windows shebang hooks (check-new-py-prefix, compliance-checks,
    check-docs) cannot start /bin/bash on this host and were run directly
    through Python instead: scripts/check_new_py_files.py (git-detection
    mode), scripts/compliance_checks.py and scripts/check_docs.py all exit
    0. This change adds no new Python files, so the prefix/unit-guide rule is
    not engaged.

Scope

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants