Skip to content

Commit

Permalink
Add details to format exceptions (#210)
Browse files Browse the repository at this point in the history
We do not expect format exceptions to occur unless the server API is
changing. Change from a plain `FormatException` to a specific exception
which indicates the two possible resolution strategies - upgrade if not
already on the latest SDK, or file an issue.
  • Loading branch information
natebosch authored Sep 4, 2024
1 parent 6a160fa commit ec5a820
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 24 deletions.
1 change: 1 addition & 0 deletions pkgs/google_generative_ai/lib/google_generative_ai.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export 'src/content.dart'
export 'src/error.dart'
show
GenerativeAIException,
GenerativeAISdkException,
InvalidApiKey,
ServerException,
UnsupportedUserLocation;
Expand Down
31 changes: 14 additions & 17 deletions pkgs/google_generative_ai/lib/src/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ enum BlockReason {
'BLOCK_REASON_UNSPECIFIED' => BlockReason.unspecified,
'SAFETY' => BlockReason.safety,
'OTHER' => BlockReason.other,
_ => throw FormatException('Unhandled BlockReason format', jsonObject),
_ => throw unhandledFormat('BlockReason', jsonObject),
};

@override
Expand Down Expand Up @@ -308,7 +308,7 @@ enum HarmCategory {
'HARM_CATEGORY_HATE_SPEECH' => hateSpeech,
'HARM_CATEGORY_SEXUALLY_EXPLICIT' => sexuallyExplicit,
'HARM_CATEGORY_DANGEROUS_CONTENT' => dangerousContent,
_ => throw FormatException('Unhandled HarmCategory format', jsonObject),
_ => throw unhandledFormat('HarmCategory', jsonObject),
};

String toJson() => switch (this) {
Expand Down Expand Up @@ -346,8 +346,7 @@ enum HarmProbability {
'LOW' => HarmProbability.low,
'MEDIUM' => HarmProbability.medium,
'HIGH' => HarmProbability.high,
_ =>
throw FormatException('Unhandled HarmProbability format', jsonObject),
_ => throw unhandledFormat('HarmProbability', jsonObject),
};
}

Expand Down Expand Up @@ -409,7 +408,7 @@ enum FinishReason {
'SAFETY' => FinishReason.safety,
'RECITATION' => FinishReason.recitation,
'OTHER' => FinishReason.other,
_ => throw FormatException('Unhandled FinishReason format', jsonObject),
_ => throw unhandledFormat('FinishReason', jsonObject),
};

@override
Expand Down Expand Up @@ -612,16 +611,15 @@ CountTokensResponse parseCountTokensResponse(Object jsonObject) {
return CountTokensResponse._(totalTokens,
extraFields.isEmpty ? null : Map.unmodifiable(extraFields));
}
throw FormatException('Unhandled CountTokensResponse format', jsonObject);
throw unhandledFormat('CountTokensResponse', jsonObject);
}

EmbedContentResponse parseEmbedContentResponse(Object jsonObject) {
return switch (jsonObject) {
{'embedding': final Object embedding} =>
EmbedContentResponse(_parseContentEmbedding(embedding)),
{'error': final Object error} => throw parseError(error),
_ =>
throw FormatException('Unhandled EmbedContentResponse format', jsonObject)
_ => throw unhandledFormat('EmbedContentResponse', jsonObject)
};
}

Expand All @@ -631,14 +629,13 @@ BatchEmbedContentsResponse parseBatchEmbedContentsResponse(Object jsonObject) {
BatchEmbedContentsResponse(
embeddings.map(_parseContentEmbedding).toList()),
{'error': final Object error} => throw parseError(error),
_ =>
throw FormatException('Unhandled EmbedContentResponse format', jsonObject)
_ => throw unhandledFormat('EmbedContentResponse', jsonObject)
};
}

Candidate _parseCandidate(Object? jsonObject) {
if (jsonObject is! Map) {
throw FormatException('Unhandled Candidate format', jsonObject);
throw unhandledFormat('Candidate', jsonObject);
}

return Candidate(
Expand Down Expand Up @@ -684,13 +681,13 @@ PromptFeedback _parsePromptFeedback(Object jsonObject) {
_ => null,
},
safetyRatings.map(_parseSafetyRating).toList()),
_ => throw FormatException('Unhandled PromptFeedback format', jsonObject),
_ => throw unhandledFormat('PromptFeedback', jsonObject),
};
}

UsageMetadata _parseUsageMetadata(Object jsonObject) {
if (jsonObject is! Map<String, Object?>) {
throw FormatException('Unhandled UsageMetadata format', jsonObject);
throw unhandledFormat('UsageMetadata', jsonObject);
}
final promptTokenCount = switch (jsonObject) {
{'promptTokenCount': final int promptTokenCount} => promptTokenCount,
Expand Down Expand Up @@ -719,7 +716,7 @@ SafetyRating _parseSafetyRating(Object? jsonObject) {
} =>
SafetyRating(HarmCategory._parseValue(category),
HarmProbability._parseValue(probability)),
_ => throw FormatException('Unhandled SafetyRating format', jsonObject),
_ => throw unhandledFormat('SafetyRating', jsonObject),
};
}

Expand All @@ -728,7 +725,7 @@ ContentEmbedding _parseContentEmbedding(Object? jsonObject) {
{'values': final List<Object?> values} => ContentEmbedding(<double>[
...values.cast<double>(),
]),
_ => throw FormatException('Unhandled ContentEmbedding format', jsonObject),
_ => throw unhandledFormat('ContentEmbedding', jsonObject),
};
}

Expand All @@ -739,13 +736,13 @@ CitationMetadata _parseCitationMetadata(Object? jsonObject) {
// Vertex SDK format uses `citations`
{'citations': final List<Object?> citationSources} =>
CitationMetadata(citationSources.map(_parseCitationSource).toList()),
_ => throw FormatException('Unhandled CitationMetadata format', jsonObject),
_ => throw unhandledFormat('CitationMetadata', jsonObject),
};
}

CitationSource _parseCitationSource(Object? jsonObject) {
if (jsonObject is! Map) {
throw FormatException('Unhandled CitationSource format', jsonObject);
throw unhandledFormat('CitationSource', jsonObject);
}

final uriString = jsonObject['uri'] as String?;
Expand Down
10 changes: 6 additions & 4 deletions pkgs/google_generative_ai/lib/src/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import 'dart:convert';
import 'dart:typed_data';

import 'error.dart';

/// The base structured datatype containing multi-part content of a message.
final class Content {
/// The producer of the content.
Expand Down Expand Up @@ -57,7 +59,7 @@ Content parseContent(Object jsonObject) {
_ => null,
},
parts.map(_parsePart).toList()),
_ => throw FormatException('Unhandled Content format', jsonObject),
_ => throw unhandledFormat('Content', jsonObject),
};
}

Expand Down Expand Up @@ -91,7 +93,7 @@ Part _parsePart(Object? jsonObject) {
}
} =>
CodeExecutionResult(Outcome._parse(outcome), output),
_ => throw FormatException('Unhandled Part format', jsonObject),
_ => throw unhandledFormat('Part', jsonObject),
};
}

Expand Down Expand Up @@ -213,7 +215,7 @@ enum Language {
static Language _parse(Object jsonObject) => switch (jsonObject) {
'LANGUAGE_UNSPECIFIED' => unspecified,
'PYTHON' => python,
_ => throw FormatException('Unhandled Language format', jsonObject),
_ => throw unhandledFormat('Language', jsonObject),
};

String toJson() => switch (this) {
Expand All @@ -234,7 +236,7 @@ enum Outcome {
'OUTCOME_OK' => ok,
'OUTCOME_FAILED' => failed,
'OUTCOME_DEADLINE_EXCEEDED' => deadlineExceeded,
_ => throw FormatException('Unhandled Language format', jsonObject),
_ => throw unhandledFormat('Language', jsonObject),
};

String toJson() => switch (this) {
Expand Down
24 changes: 23 additions & 1 deletion pkgs/google_generative_ai/lib/src/error.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,25 @@ final class ServerException implements GenerativeAIException {
String toString() => message;
}

/// Exception indicating a stale package version or implementation bug.
///
/// This exception indicates a likely problem with the SDK implementation such
/// as an inability to parse a new response format. Resolution paths may include
/// updating to a new version of the SDK, or filing an issue.
final class GenerativeAISdkException implements Exception {
final String message;

GenerativeAISdkException(this.message);

@override
String toString() => '$message\n'
'This indicates a problem with the Google Generative AI SDK. '
'Try updating to the latest version '
'(https://pub.dev/packages/google_generative_ai/versions), '
'or file an issue at '
'https://github.com/google-gemini/generative-ai-dart/issues.';
}

GenerativeAIException parseError(Object jsonObject) {
return switch (jsonObject) {
{
Expand All @@ -62,6 +81,9 @@ GenerativeAIException parseError(Object jsonObject) {
InvalidApiKey(message),
{'message': UnsupportedUserLocation._message} => UnsupportedUserLocation(),
{'message': final String message} => ServerException(message),
_ => throw FormatException('Unhandled Server Error format', jsonObject)
_ => throw unhandledFormat('server error', jsonObject)
};
}

Exception unhandledFormat(String name, Object? jsonObject) =>
GenerativeAISdkException('Unhandled format for $name: $jsonObject');
4 changes: 2 additions & 2 deletions pkgs/google_generative_ai/test/response_parsing_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ void main() {
expect(
() => parseGenerateContentResponse(decoded),
throwsA(
isA<FormatException>().having(
isA<GenerativeAISdkException>().having(
(e) => e.message,
'message',
startsWith('Unhandled Content format'),
startsWith('Unhandled format for Content:'),
),
),
);
Expand Down

0 comments on commit ec5a820

Please sign in to comment.