Skip to content

Commit

Permalink
Add a text getter for Candidate (#170)
Browse files Browse the repository at this point in the history
Closes #99

Call out that candidates may be ignored in the doc for
`GenerateContentResponse.text`.

Add a `text` getter on `Candidate`. This is the same logic that was in
the response text getter, applied to an individual candidate.

Refactor the response text implementation to use the candidate field.
  • Loading branch information
natebosch authored May 23, 2024
1 parent 1f2b428 commit b67b7b3
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 37 deletions.
2 changes: 2 additions & 0 deletions pkgs/google_generative_ai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
`systemInstruction`) in `countTokens` requests. This aligns the token count
with the token count the backend will see in practice for a
`generateContent` request.
- Add a `text` getter on `Candidate` to make it easer to retrieve the text from
candidates other than the first in a response.

## 0.4.0

Expand Down
85 changes: 48 additions & 37 deletions pkgs/google_generative_ai/lib/src/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,43 +68,24 @@ final class GenerateContentResponse {
///
/// If there are no candidates, or if the first candidate does not contain any
/// text parts, this value is `null`.
String? get text {
return switch (candidates) {
[] => switch (promptFeedback) {
PromptFeedback(
:final blockReason,
:final blockReasonMessage,
) =>
// TODO: Add a specific subtype for this exception?
throw GenerativeAIException('Response was blocked'
'${blockReason != null ? ' due to $blockReason' : ''}'
'${blockReasonMessage != null ? ': $blockReasonMessage' : ''}'),
_ => null,
},
[
Candidate(
finishReason: (FinishReason.recitation || FinishReason.safety) &&
final finishReason,
:final finishMessage,
),
...
] =>
throw GenerativeAIException(
// ignore: prefer_interpolation_to_compose_strings
'Candidate was blocked due to $finishReason' +
(finishMessage != null && finishMessage.isNotEmpty
? ': $finishMessage'
: ''),
),
// Special case for a single TextPart to avoid iterable chain.
[Candidate(content: Content(parts: [TextPart(:final text)])), ...] =>
text,
[Candidate(content: Content(:final parts)), ...]
when parts.any((p) => p is TextPart) =>
parts.whereType<TextPart>().map((p) => p.text).join(''),
[Candidate(), ...] => null,
};
}
///
/// If there is more than one candidate, all but the first are ignored. See
/// [Candidate.text] to get the text content of candidates other than the
/// first.
String? get text => switch (candidates) {
[] => switch (promptFeedback) {
PromptFeedback(
:final blockReason,
:final blockReasonMessage,
) =>
// TODO: Add a specific subtype for this exception?
throw GenerativeAIException('Response was blocked'
'${blockReason != null ? ' due to $blockReason' : ''}'
'${blockReasonMessage != null ? ': $blockReasonMessage' : ''}'),
_ => null,
},
[final candidate, ...] => candidate.text,
};

/// The function call parts of the first candidate in [candidates], if any.
///
Expand Down Expand Up @@ -221,6 +202,36 @@ final class Candidate {
// TODO: token count?
Candidate(this.content, this.safetyRatings, this.citationMetadata,
this.finishReason, this.finishMessage);

/// The concatenation of the text parts of [content], if any.
///
/// If this candidate was finished for a reason of [FinishReason.recitation]
/// or [FinishReason.safety], accessing this text will throw a
/// [GenerativeAIException].
///
/// If [content] contains any text parts, this value is the concatenation of
/// the text.
///
/// If [content] does not contain any text parts, this value is `null`.
String? get text {
if (finishReason case FinishReason.recitation || FinishReason.safety) {
final String suffix;
if (finishMessage case final message? when message.isNotEmpty) {
suffix = ': $message';
} else {
suffix = '';
}
throw GenerativeAIException(
'Candidate was blocked due to $finishReason$suffix');
}
return switch (content.parts) {
// Special case for a single TextPart to avoid iterable chain.
[TextPart(:final text)] => text,
final parts when parts.any((p) => p is TextPart) =>
parts.whereType<TextPart>().map((p) => p.text).join(''),
_ => null,
};
}
}

/// Safety rating for a piece of content.
Expand Down
2 changes: 2 additions & 0 deletions pkgs/google_generative_ai/test/response_parsing_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,8 @@ void main() {
final decoded = jsonDecode(response) as Object;
final generateContentResponse = parseGenerateContentResponse(decoded);
expect(generateContentResponse.text, 'Initial text And more text');
expect(generateContentResponse.candidates.single.text,
'Initial text And more text');
});
});

Expand Down

0 comments on commit b67b7b3

Please sign in to comment.