Skip to content

Commit aac7184

Browse files
sararobcopybara-github
authored andcommitted
fix: improve logging for response.parsed (fixes #455)
PiperOrigin-RevId: 737992402
1 parent 2efd6f2 commit aac7184

File tree

2 files changed

+68
-21
lines changed

2 files changed

+68
-21
lines changed

google/genai/tests/models/test_generate_content.py

+27
Original file line numberDiff line numberDiff line change
@@ -1780,3 +1780,30 @@ def test_multiple_function_calls(client):
17801780
assert 'sunny' in response.text
17811781
assert '100 degrees' in response.text
17821782
assert '$100' in response.text
1783+
1784+
1785+
def test_warning_log_includes_parsed_for_multi_candidate_response(client, caplog):
1786+
caplog.set_level(logging.DEBUG, logger='google_genai')
1787+
1788+
class CountryInfo(BaseModel):
1789+
name: str
1790+
population: int
1791+
capital: str
1792+
continent: str
1793+
major_cities: list[str]
1794+
gdp: int
1795+
official_language: str
1796+
total_area_sq_mi: int
1797+
1798+
response = client.models.generate_content(
1799+
model='gemini-2.0-flash',
1800+
contents='Give me information of the United States.',
1801+
config={
1802+
'response_mime_type': 'application/json',
1803+
'response_schema': CountryInfo,
1804+
"candidate_count": 2
1805+
},
1806+
)
1807+
assert response.parsed
1808+
assert len(response.candidates) == 2
1809+
assert 'parsed' in caplog.text

google/genai/types.py

+41-21
Original file line numberDiff line numberDiff line change
@@ -2914,12 +2914,14 @@ class GenerateContentResponse(_common.BaseModel):
29142914
automatic_function_calling_history: Optional[list[Content]] = None
29152915
parsed: Optional[Union[pydantic.BaseModel, dict, Enum]] = Field(
29162916
default=None,
2917-
description="""Parsed response if response_schema is provided. Not available for streaming.""",
2917+
description="""First candidate from the parsed response if response_schema is provided. Not available for streaming.""",
29182918
)
29192919

2920-
@property
2921-
def text(self) -> Optional[str]:
2922-
"""Returns the concatenation of all text parts in the response."""
2920+
def _get_text(self, warn_property: str = 'text') -> Optional[str]:
2921+
"""Returns the concatenation of all text parts in the response.
2922+
2923+
This is an internal method that allows customizing the warning message.
2924+
"""
29232925
if (
29242926
not self.candidates
29252927
or not self.candidates[0].content
@@ -2928,9 +2930,10 @@ def text(self) -> Optional[str]:
29282930
return None
29292931
if len(self.candidates) > 1:
29302932
logger.warning(
2931-
f'there are {len(self.candidates)} candidates, returning text from'
2932-
' the first candidate.Access response.candidates directly to get'
2933-
' text from other candidates.'
2933+
f'there are {len(self.candidates)} candidates, returning'
2934+
f' {warn_property} result from the first candidate. Access'
2935+
' response.candidates directly to get the result from other'
2936+
' candidates.'
29342937
)
29352938
text = ''
29362939
any_text_part_text = False
@@ -2949,12 +2952,18 @@ def text(self) -> Optional[str]:
29492952
if non_text_parts:
29502953
logger.warning(
29512954
'Warning: there are non-text parts in the response:'
2952-
f' {non_text_parts},returning concatenated text from text parts,check'
2953-
' out the non text parts for full response from model.'
2955+
f' {non_text_parts},returning concatenated {warn_property} result'
2956+
' from text parts,check out the non text parts for full response'
2957+
' from model.'
29542958
)
29552959
# part.text == '' is different from part.text is None
29562960
return text if any_text_part_text else None
29572961

2962+
@property
2963+
def text(self) -> Optional[str]:
2964+
"""Returns the concatenation of all text parts in the response."""
2965+
return self._get_text(warn_property='text')
2966+
29582967
@property
29592968
def function_calls(self) -> Optional[list[FunctionCall]]:
29602969
"""Returns the list of function calls in the response."""
@@ -3037,16 +3046,23 @@ def _from_response(
30373046
):
30383047
# Pydantic schema.
30393048
try:
3040-
if result.text is not None:
3041-
result.parsed = response_schema.model_validate_json(result.text)
3049+
result_text = result._get_text(warn_property='parsed')
3050+
if result_text is not None:
3051+
result.parsed = response_schema.model_validate_json(result_text)
30423052
# may not be a valid json per stream response
30433053
except pydantic.ValidationError:
30443054
pass
30453055
except json.decoder.JSONDecodeError:
30463056
pass
3047-
elif isinstance(response_schema, EnumMeta) and result.text is not None:
3057+
elif (
3058+
isinstance(response_schema, EnumMeta)
3059+
and result._get_text(warn_property='parsed') is not None
3060+
):
30483061
# Enum with "application/json" returns response in double quotes.
3049-
enum_value = result.text.replace('"', '')
3062+
result_text = result._get_text(warn_property='parsed')
3063+
if result_text is None:
3064+
raise ValueError('Response is empty.')
3065+
enum_value = result_text.replace('"', '')
30503066
try:
30513067
result.parsed = response_schema(enum_value)
30523068
if (
@@ -3064,8 +3080,9 @@ class Placeholder(pydantic.BaseModel):
30643080
placeholder: response_schema # type: ignore[valid-type]
30653081

30663082
try:
3067-
if result.text is not None:
3068-
parsed = {'placeholder': json.loads(result.text)}
3083+
result_text = result._get_text(warn_property='parsed')
3084+
if result_text is not None:
3085+
parsed = {'placeholder': json.loads(result_text)}
30693086
placeholder = Placeholder.model_validate(parsed)
30703087
result.parsed = placeholder.placeholder
30713088
except json.decoder.JSONDecodeError:
@@ -3080,8 +3097,9 @@ class Placeholder(pydantic.BaseModel):
30803097
# want the result converted to. So just return json.
30813098
# JSON schema.
30823099
try:
3083-
if result.text is not None:
3084-
result.parsed = json.loads(result.text)
3100+
result_text = result._get_text(warn_property='parsed')
3101+
if result_text is not None:
3102+
result.parsed = json.loads(result_text)
30853103
# may not be a valid json per stream response
30863104
except json.decoder.JSONDecodeError:
30873105
pass
@@ -3091,12 +3109,13 @@ class Placeholder(pydantic.BaseModel):
30913109
for union_type in union_types:
30923110
if issubclass(union_type, pydantic.BaseModel):
30933111
try:
3094-
if result.text is not None:
3112+
result_text = result._get_text(warn_property='parsed')
3113+
if result_text is not None:
30953114

30963115
class Placeholder(pydantic.BaseModel): # type: ignore[no-redef]
30973116
placeholder: response_schema # type: ignore[valid-type]
30983117

3099-
parsed = {'placeholder': json.loads(result.text)}
3118+
parsed = {'placeholder': json.loads(result_text)}
31003119
placeholder = Placeholder.model_validate(parsed)
31013120
result.parsed = placeholder.placeholder
31023121
except json.decoder.JSONDecodeError:
@@ -3105,8 +3124,9 @@ class Placeholder(pydantic.BaseModel): # type: ignore[no-redef]
31053124
pass
31063125
else:
31073126
try:
3108-
if result.text is not None:
3109-
result.parsed = json.loads(result.text)
3127+
result_text = result._get_text(warn_property='parsed')
3128+
if result_text is not None:
3129+
result.parsed = json.loads(result_text)
31103130
# may not be a valid json per stream response
31113131
except json.decoder.JSONDecodeError:
31123132
pass

0 commit comments

Comments
 (0)