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
31 changes: 30 additions & 1 deletion src/google/adk/models/gemma_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from pydantic import BaseModel
from pydantic import Field
from pydantic import ValidationError
from pydantic_core import to_jsonable_python
from typing_extensions import override

logger = logging.getLogger('google_adk.' + __name__)
Expand Down Expand Up @@ -286,7 +287,7 @@ def _convert_content_parts_for_gemma(
has_function_response_part = True
response_text = (
f'Invoking tool `{func_response.name}` produced:'
f' `{json.dumps(func_response.response)}`.'
f' `{_function_response_to_json(func_response.response)}`.'
)
new_parts.append(Part.from_text(text=response_text))
elif func_call := part.function_call:
Expand All @@ -299,6 +300,34 @@ def _convert_content_parts_for_gemma(
return new_parts, has_function_response_part, has_function_call_part


def _function_response_to_json(response: dict[str, Any] | None) -> str:
"""Serializes a function response payload as the JSON text Gemma 3 reads.

The payload holds whatever the tool returned, which can include values the
json module cannot encode, such as datetimes, bytes or Pydantic models. They
are converted the way a Gemini request carries a function response: Pydantic
model fields that are None are dropped (None values in dicts and lists stay
null), bytes are base64-encoded, and values Pydantic cannot serialize fall
back to their str(). Non-ASCII text is kept as is, so the model reads the
characters rather than escape sequences.

Args:
response: The payload of the function response.

Returns:
The payload as a JSON string.
"""
return json.dumps(
to_jsonable_python(
response,
exclude_none=True,
bytes_mode='base64',
serialize_unknown=True,
),
ensure_ascii=False,
)


def _build_gemma_function_system_instruction(
function_declarations: list[FunctionDeclaration],
) -> str:
Expand Down
78 changes: 78 additions & 0 deletions tests/unittests/models/test_gemma_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import datetime

from google.adk import models
from google.adk.memory.memory_entry import MemoryEntry
from google.adk.models.gemma_llm import Gemma
from google.adk.models.google_llm import Gemini
from google.adk.models.llm_request import LlmRequest
from google.adk.models.llm_response import LlmResponse
from google.adk.tools.load_memory_tool import LoadMemoryResponse
from google.genai import types
from google.genai.types import Content
from google.genai.types import Part
Expand Down Expand Up @@ -190,6 +194,80 @@ async def test_preprocess_request_with_function_response():
assert llm_request.contents[0].parts[0].function_call is None


@pytest.mark.asyncio
@pytest.mark.parametrize(
"response,want_json",
[
pytest.param(
{"city": "München", "forecast": "東京は晴れ"},
'{"city": "München", "forecast": "東京は晴れ"}',
id="non_ascii_text",
),
pytest.param(
{"checked_in_at": datetime.datetime(2026, 10, 22, 9, 30)},
'{"checked_in_at": "2026-10-22T09:30:00"}',
id="datetime",
),
pytest.param(
{"thumbnail": b"\xff\xd8"},
'{"thumbnail": "_9g="}',
id="bytes",
),
pytest.param(
{"cancelled_at": None, "guests": [{"email": None}]},
'{"cancelled_at": null, "guests": [{"email": null}]}',
id="none_values",
),
pytest.param(
{
"result": LoadMemoryResponse(
memories=[
MemoryEntry(
content=types.Content(
role="user",
parts=[types.Part(text="I live in Paris.")],
),
author="user",
)
]
)
},
'{"result": {"memories": [{"content": {"parts": [{"text": "I live'
' in Paris."}], "role": "user"}, "custom_metadata": {}, "author":'
' "user"}]}}',
id="pydantic_model",
),
],
)
async def test_preprocess_request_with_function_response_converts_values(
response, want_json
):
"""Tool results reach Gemma as readable JSON, whatever values they hold."""
llm_request = LlmRequest(
model="gemma-3-1b-it",
contents=[
types.Content(
role="user",
parts=[
types.Part(
function_response=types.FunctionResponse(
name="get_booking", response=response
)
)
],
)
],
config=types.GenerateContentConfig(),
)

await Gemma()._preprocess_request(llm_request)

assert (
llm_request.contents[0].parts[0].text
== f"Invoking tool `get_booking` produced: `{want_json}`."
)


@pytest.mark.asyncio
async def test_preprocess_request_with_function_call():
func_call_data = types.FunctionCall(name="get_current_time", args={})
Expand Down
Loading