Skip to content

Commit

Permalink
feat: GenAI - Improved the exception messages when candidates, parts …
Browse files Browse the repository at this point in the history
…or text are not available

Improved cases:

* Response has no Candidates
* Response Candidate Content has no Parts
* Response Candidate Content Part has no text

PiperOrigin-RevId: 628236405
  • Loading branch information
Ark-kun authored and Copybara-Service committed Apr 26, 2024
1 parent 3d22a18 commit e82264d
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 5 deletions.
52 changes: 52 additions & 0 deletions tests/unit/vertexai/test_generative_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,58 @@ def test_generate_content_streaming(self, generative_models: generative_models):
for chunk in stream:
assert chunk.text

@mock.patch.object(
target=prediction_service.PredictionServiceClient,
attribute="generate_content",
new=mock_generate_content,
)
@pytest.mark.parametrize(
"generative_models",
[generative_models, preview_generative_models],
)
def test_generate_content_response_accessor_errors(
self, generative_models: generative_models
):
"""Checks that the exception text contains response information."""
model = generative_models.GenerativeModel("gemini-pro")

# Case when response has no candidates
response1 = model.generate_content("Please block with block_reason=OTHER")

assert response1.prompt_feedback.block_reason.name == "OTHER"

with pytest.raises(ValueError) as e:
_ = response1.text
assert e.match("no candidates")
assert e.match("prompt_feedback")

# Case when response candidate content has no parts
response2 = model.generate_content("Please fail!")

with pytest.raises(ValueError) as e:
_ = response2.text
assert e.match("no parts")
assert e.match("finish_reason")

with pytest.raises(ValueError) as e:
_ = response2.candidates[0].text
assert e.match("no parts")
assert e.match("finish_reason")

# Case when response candidate content part has no text
weather_tool = generative_models.Tool(
function_declarations=[
generative_models.FunctionDeclaration.from_func(get_current_weather)
],
)
response3 = model.generate_content(
"What's the weather like in Boston?", tools=[weather_tool]
)
with pytest.raises(ValueError) as e:
print(response3.text)
assert e.match("no text")
assert e.match("function_call")

@mock.patch.object(
target=prediction_service.PredictionServiceClient,
attribute="generate_content",
Expand Down
50 changes: 45 additions & 5 deletions vertexai/generative_models/_generative_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import copy
import io
import json
import pathlib
from typing import (
Any,
Expand Down Expand Up @@ -1658,8 +1659,23 @@ def text(self) -> str:
" Use `response.candidate[i].text` to get text of a particular candidate."
)
if not self.candidates:
raise ValueError("Response has no candidates (and no text).")
return self.candidates[0].text
raise ValueError(
"Response has no candidates (and thus no text)."
" The response is likely blocked by the safety filters.\n"
"Response:\n"
+ _dict_to_pretty_string(self.to_dict())
)
try:
return self.candidates[0].text
except (ValueError, AttributeError) as e:
# Enrich the error message with the whole Response.
# The Candidate object does not have full information.
raise ValueError(
"Cannot get the response text.\n"
f"{e}\n"
"Response:\n"
+ _dict_to_pretty_string(self.to_dict())
) from e

@property
def prompt_feedback(
Expand Down Expand Up @@ -1728,7 +1744,17 @@ def citation_metadata(self) -> gapic_content_types.CitationMetadata:
# GenerationPart properties
@property
def text(self) -> str:
return self.content.text
try:
return self.content.text
except (ValueError, AttributeError) as e:
# Enrich the error message with the whole Candidate.
# The Content object does not have full information.
raise ValueError(
"Cannot get the Candidate text.\n"
f"{e}\n"
"Candidate:\n"
+ _dict_to_pretty_string(self.to_dict())
) from e

@property
def function_calls(self) -> Sequence[gapic_tool_types.FunctionCall]:
Expand Down Expand Up @@ -1799,7 +1825,12 @@ def text(self) -> str:
if len(self.parts) > 1:
raise ValueError("Multiple content parts are not supported.")
if not self.parts:
raise AttributeError("Content has no parts.")
raise ValueError(
"Response candidate content has no parts (and thus no text)."
" The candidate is likely blocked by the safety filters.\n"
"Content:\n"
+ _dict_to_pretty_string(self.to_dict())
)
return self.parts[0].text


Expand Down Expand Up @@ -1886,7 +1917,11 @@ def to_dict(self) -> Dict[str, Any]:
@property
def text(self) -> str:
if "text" not in self._raw_part:
raise AttributeError("Part has no text.")
raise AttributeError(
"Response candidate content part has no text.\n"
"Part:\n"
+ _dict_to_pretty_string(self.to_dict())
)
return self._raw_part.text

@property
Expand Down Expand Up @@ -2188,6 +2223,11 @@ def _append_gapic_part(
base_part._pb = copy.deepcopy(new_part._pb)


def _dict_to_pretty_string(d: dict) -> str:
"""Format dict as a pretty-printed JSON string."""
return json.dumps(d, indent=2)


_FORMAT_TO_MIME_TYPE = {
"png": "image/png",
"jpeg": "image/jpeg",
Expand Down

0 comments on commit e82264d

Please sign in to comment.