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
18 changes: 17 additions & 1 deletion src/google/adk/models/google_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,18 +553,34 @@ def _build_request_log(req: LlmRequest) -> str:
"""


def _get_text_from_response(resp: types.GenerateContentResponse) -> str:
"""Extract text from a GenerateContentResponse without triggering warnings.

The resp.text property emits a UserWarning when the response contains
non-text parts (e.g. function_call). This helper iterates over parts
directly to avoid that warning.
"""
if not resp.candidates:
return ''
parts = resp.candidates[0].content.parts if resp.candidates[0].content else []
if not parts:
return ''
return ''.join(part.text for part in parts if part.text)


def _build_response_log(resp: types.GenerateContentResponse) -> str:
function_calls_text = []
if function_calls := resp.function_calls:
for func_call in function_calls:
function_calls_text.append(
f'name: {func_call.name}, args: {func_call.args}'
)
text = _get_text_from_response(resp)
return f"""
LLM Response:
-----------------------------------------------------------
Text:
{resp.text}
{text}
-----------------------------------------------------------
Function calls:
{_NEW_LINE.join(function_calls_text)}
Expand Down
108 changes: 108 additions & 0 deletions tests/unittests/models/test_google_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import os
import sys
import warnings
from typing import Optional
from unittest import mock
from unittest.mock import AsyncMock
Expand All @@ -24,6 +25,8 @@
from google.adk.models.gemini_llm_connection import GeminiLlmConnection
from google.adk.models.google_llm import _build_function_declaration_log
from google.adk.models.google_llm import _build_request_log
from google.adk.models.google_llm import _build_response_log
from google.adk.models.google_llm import _get_text_from_response
from google.adk.models.google_llm import _RESOURCE_EXHAUSTED_POSSIBLE_FIX_MESSAGE
from google.adk.models.google_llm import _ResourceExhaustedError
from google.adk.models.google_llm import Gemini
Expand Down Expand Up @@ -2140,3 +2143,108 @@ async def __aexit__(self, *args):
# Verify the final speech_config is still None
assert config_arg.speech_config is None
assert isinstance(connection, GeminiLlmConnection)


class TestGetTextFromResponse:
"""Tests for _get_text_from_response helper."""

def test_text_only_response(self):
"""Text-only responses should return the text."""
resp = types.GenerateContentResponse(
candidates=[
types.Candidate(
content=Content(
role="model",
parts=[Part.from_text(text="Hello world")],
),
)
]
)
assert _get_text_from_response(resp) == "Hello world"

def test_function_call_response_no_warning(self):
"""Function-call responses must not trigger a UserWarning."""
resp = types.GenerateContentResponse(
candidates=[
types.Candidate(
content=Content(
role="model",
parts=[
Part(
function_call=types.FunctionCall(
name="my_tool", args={"q": "test"}
)
)
],
),
)
]
)
with warnings.catch_warnings():
warnings.simplefilter("error")
text = _get_text_from_response(resp)
assert text == ""

def test_mixed_parts_no_warning(self):
"""Mixed text + function_call responses should extract text without warning."""
resp = types.GenerateContentResponse(
candidates=[
types.Candidate(
content=Content(
role="model",
parts=[
Part.from_text(text="Calling tool..."),
Part(
function_call=types.FunctionCall(
name="my_tool", args={}
)
),
],
),
)
]
)
with warnings.catch_warnings():
warnings.simplefilter("error")
text = _get_text_from_response(resp)
assert text == "Calling tool..."

def test_empty_candidates(self):
"""Empty candidates should return empty string."""
resp = types.GenerateContentResponse(candidates=[])
assert _get_text_from_response(resp) == ""

def test_none_content(self):
"""Candidate with None content should return empty string."""
resp = types.GenerateContentResponse(
candidates=[types.Candidate(content=None)]
)
assert _get_text_from_response(resp) == ""


class TestBuildResponseLogNoWarning:
"""Test that _build_response_log does not trigger warnings on function calls."""

def test_function_call_log_no_warning(self):
"""Regression test for https://github.com/google/adk-python/issues/4685."""
resp = types.GenerateContentResponse(
candidates=[
types.Candidate(
content=Content(
role="model",
parts=[
Part(
function_call=types.FunctionCall(
name="my_tool", args={"question": "2+2"}
)
)
],
),
finish_reason=types.FinishReason.STOP,
)
]
)
with warnings.catch_warnings():
warnings.simplefilter("error")
log = _build_response_log(resp)
assert "my_tool" in log