Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: improve logging for response.parsed (fixes #455) #527

Merged
merged 1 commit into from
Mar 24, 2025
Merged
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
27 changes: 27 additions & 0 deletions google/genai/tests/models/test_generate_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -1823,3 +1823,30 @@ def test_usage_metadata_part_types(client):
[d.modality.name for d in usage_metadata.prompt_tokens_details]
)
assert modalities == ['IMAGE', 'TEXT']


def test_warning_log_includes_parsed_for_multi_candidate_response(client, caplog):
caplog.set_level(logging.DEBUG, logger='google_genai')

class CountryInfo(BaseModel):
name: str
population: int
capital: str
continent: str
major_cities: list[str]
gdp: int
official_language: str
total_area_sq_mi: int

response = client.models.generate_content(
model='gemini-2.0-flash',
contents='Give me information of the United States.',
config={
'response_mime_type': 'application/json',
'response_schema': CountryInfo,
"candidate_count": 2
},
)
assert response.parsed
assert len(response.candidates) == 2
assert 'parsed' in caplog.text
62 changes: 41 additions & 21 deletions google/genai/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2990,12 +2990,14 @@ class GenerateContentResponse(_common.BaseModel):
automatic_function_calling_history: Optional[list[Content]] = None
parsed: Optional[Union[pydantic.BaseModel, dict, Enum]] = Field(
default=None,
description="""Parsed response if response_schema is provided. Not available for streaming.""",
description="""First candidate from the parsed response if response_schema is provided. Not available for streaming.""",
)

@property
def text(self) -> Optional[str]:
"""Returns the concatenation of all text parts in the response."""
def _get_text(self, warn_property: str = 'text') -> Optional[str]:
"""Returns the concatenation of all text parts in the response.

This is an internal method that allows customizing the warning message.
"""
if (
not self.candidates
or not self.candidates[0].content
Expand All @@ -3004,9 +3006,10 @@ def text(self) -> Optional[str]:
return None
if len(self.candidates) > 1:
logger.warning(
f'there are {len(self.candidates)} candidates, returning text from'
' the first candidate.Access response.candidates directly to get'
' text from other candidates.'
f'there are {len(self.candidates)} candidates, returning'
f' {warn_property} result from the first candidate. Access'
' response.candidates directly to get the result from other'
' candidates.'
)
text = ''
any_text_part_text = False
Expand All @@ -3025,12 +3028,18 @@ def text(self) -> Optional[str]:
if non_text_parts:
logger.warning(
'Warning: there are non-text parts in the response:'
f' {non_text_parts},returning concatenated text from text parts,check'
' out the non text parts for full response from model.'
f' {non_text_parts},returning concatenated {warn_property} result'
' from text parts,check out the non text parts for full response'
' from model.'
)
# part.text == '' is different from part.text is None
return text if any_text_part_text else None

@property
def text(self) -> Optional[str]:
"""Returns the concatenation of all text parts in the response."""
return self._get_text(warn_property='text')

@property
def function_calls(self) -> Optional[list[FunctionCall]]:
"""Returns the list of function calls in the response."""
Expand Down Expand Up @@ -3113,16 +3122,23 @@ def _from_response(
):
# Pydantic schema.
try:
if result.text is not None:
result.parsed = response_schema.model_validate_json(result.text)
result_text = result._get_text(warn_property='parsed')
if result_text is not None:
result.parsed = response_schema.model_validate_json(result_text)
# may not be a valid json per stream response
except pydantic.ValidationError:
pass
except json.decoder.JSONDecodeError:
pass
elif isinstance(response_schema, EnumMeta) and result.text is not None:
elif (
isinstance(response_schema, EnumMeta)
and result._get_text(warn_property='parsed') is not None
):
# Enum with "application/json" returns response in double quotes.
enum_value = result.text.replace('"', '')
result_text = result._get_text(warn_property='parsed')
if result_text is None:
raise ValueError('Response is empty.')
enum_value = result_text.replace('"', '')
try:
result.parsed = response_schema(enum_value)
if (
Expand All @@ -3140,8 +3156,9 @@ class Placeholder(pydantic.BaseModel):
placeholder: response_schema # type: ignore[valid-type]

try:
if result.text is not None:
parsed = {'placeholder': json.loads(result.text)}
result_text = result._get_text(warn_property='parsed')
if result_text is not None:
parsed = {'placeholder': json.loads(result_text)}
placeholder = Placeholder.model_validate(parsed)
result.parsed = placeholder.placeholder
except json.decoder.JSONDecodeError:
Expand All @@ -3156,8 +3173,9 @@ class Placeholder(pydantic.BaseModel):
# want the result converted to. So just return json.
# JSON schema.
try:
if result.text is not None:
result.parsed = json.loads(result.text)
result_text = result._get_text(warn_property='parsed')
if result_text is not None:
result.parsed = json.loads(result_text)
# may not be a valid json per stream response
except json.decoder.JSONDecodeError:
pass
Expand All @@ -3167,12 +3185,13 @@ class Placeholder(pydantic.BaseModel):
for union_type in union_types:
if issubclass(union_type, pydantic.BaseModel):
try:
if result.text is not None:
result_text = result._get_text(warn_property='parsed')
if result_text is not None:

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

parsed = {'placeholder': json.loads(result.text)}
parsed = {'placeholder': json.loads(result_text)}
placeholder = Placeholder.model_validate(parsed)
result.parsed = placeholder.placeholder
except json.decoder.JSONDecodeError:
Expand All @@ -3181,8 +3200,9 @@ class Placeholder(pydantic.BaseModel): # type: ignore[no-redef]
pass
else:
try:
if result.text is not None:
result.parsed = json.loads(result.text)
result_text = result._get_text(warn_property='parsed')
if result_text is not None:
result.parsed = json.loads(result_text)
# may not be a valid json per stream response
except json.decoder.JSONDecodeError:
pass
Expand Down