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

Add a text getter for Candidate #170

Merged
merged 2 commits into from
May 23, 2024
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
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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do other language sdks have a similar affordance? It's a nice shorthand for getting results from a response, but may be easy for people to mis-use (to not understand that there could be more than one candidate response).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this API has a coordinated designed across the SDKs. I don't think the text getter on Candidate is as consistent, but every language does have a utility to get the text of the first candidate from a GenerateContentResponse.

[] => 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