Skip to content

Commit eb8716f

Browse files
authored
chore(firebaseai): merge same parse logic from developer api and vertexai (#17670)
* first pass * fix and add test * reduce the unnecessary public function expose * make parseCandidate private * make grounding private except parseGroundingMetadata * Add doc * add more test cases for developer/api specific case * fix analyzer * address comments
1 parent e9a6c04 commit eb8716f

File tree

4 files changed

+236
-195
lines changed

4 files changed

+236
-195
lines changed

packages/firebase_ai/firebase_ai/lib/src/api.dart

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -469,8 +469,8 @@ enum BlockReason {
469469

470470
const BlockReason(this._jsonString);
471471

472-
// ignore: unused_element
473-
static BlockReason _parseValue(String jsonObject) {
472+
/// Parse the json to [BlockReason] object.
473+
static BlockReason parseValue(String jsonObject) {
474474
return switch (jsonObject) {
475475
'BLOCK_REASON_UNSPECIFIED' => BlockReason.unknown,
476476
'SAFETY' => BlockReason.safety,
@@ -679,8 +679,8 @@ enum FinishReason {
679679
/// Convert to json format
680680
String toJson() => _jsonString;
681681

682-
// ignore: unused_element
683-
static FinishReason _parseValue(Object jsonObject) {
682+
/// Parse the json to [FinishReason] object.
683+
static FinishReason parseValue(Object jsonObject) {
684684
return switch (jsonObject) {
685685
'UNSPECIFIED' => FinishReason.unknown,
686686
'STOP' => FinishReason.stop,
@@ -1265,12 +1265,12 @@ Candidate _parseCandidate(Object? jsonObject) {
12651265
},
12661266
switch (jsonObject) {
12671267
{'citationMetadata': final Object citationMetadata} =>
1268-
_parseCitationMetadata(citationMetadata),
1268+
parseCitationMetadata(citationMetadata),
12691269
_ => null
12701270
},
12711271
switch (jsonObject) {
12721272
{'finishReason': final Object finishReason} =>
1273-
FinishReason._parseValue(finishReason),
1273+
FinishReason.parseValue(finishReason),
12741274
_ => null
12751275
},
12761276
switch (jsonObject) {
@@ -1279,7 +1279,7 @@ Candidate _parseCandidate(Object? jsonObject) {
12791279
},
12801280
groundingMetadata: switch (jsonObject) {
12811281
{'groundingMetadata': final Object groundingMetadata} =>
1282-
_parseGroundingMetadata(groundingMetadata),
1282+
parseGroundingMetadata(groundingMetadata),
12831283
_ => null
12841284
});
12851285
}
@@ -1292,7 +1292,7 @@ PromptFeedback _parsePromptFeedback(Object jsonObject) {
12921292
PromptFeedback(
12931293
switch (jsonObject) {
12941294
{'blockReason': final String blockReason} =>
1295-
BlockReason._parseValue(blockReason),
1295+
BlockReason.parseValue(blockReason),
12961296
_ => null,
12971297
},
12981298
switch (jsonObject) {
@@ -1379,7 +1379,11 @@ SafetyRating _parseSafetyRating(Object? jsonObject) {
13791379
severityScore: jsonObject['severityScore'] as double?);
13801380
}
13811381

1382-
CitationMetadata _parseCitationMetadata(Object? jsonObject) {
1382+
/// Parses a [CitationMetadata] from a JSON object.
1383+
///
1384+
/// This function is used internally to convert citation metadata from the API
1385+
/// response.
1386+
CitationMetadata parseCitationMetadata(Object? jsonObject) {
13831387
return switch (jsonObject) {
13841388
{'citationSources': final List<Object?> citationSources} =>
13851389
CitationMetadata(citationSources.map(_parseCitationSource).toList()),
@@ -1405,7 +1409,11 @@ Citation _parseCitationSource(Object? jsonObject) {
14051409
);
14061410
}
14071411

1408-
GroundingMetadata _parseGroundingMetadata(Object? jsonObject) {
1412+
/// Parses a [GroundingMetadata] from a JSON object.
1413+
///
1414+
/// This function is used internally to convert grounding metadata from the API
1415+
/// response.
1416+
GroundingMetadata parseGroundingMetadata(Object? jsonObject) {
14091417
if (jsonObject is! Map) {
14101418
throw unhandledFormat('GroundingMetadata', jsonObject);
14111419
}

packages/firebase_ai/firebase_ai/lib/src/developer/api.dart

Lines changed: 37 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -16,64 +16,25 @@ import '../api.dart'
1616
show
1717
BlockReason,
1818
Candidate,
19-
Citation,
20-
CitationMetadata,
2119
CountTokensResponse,
2220
FinishReason,
2321
GenerateContentResponse,
2422
GenerationConfig,
25-
GroundingChunk,
26-
GroundingMetadata,
27-
GroundingSupport,
2823
HarmBlockThreshold,
2924
HarmCategory,
3025
HarmProbability,
3126
PromptFeedback,
3227
SafetyRating,
3328
SafetySetting,
34-
SearchEntryPoint,
35-
Segment,
3629
SerializationStrategy,
37-
WebGroundingChunk,
38-
parseUsageMetadata;
30+
parseUsageMetadata,
31+
parseCitationMetadata,
32+
parseGroundingMetadata;
3933
import '../content.dart' show Content, parseContent;
4034
import '../error.dart';
4135
import '../tool.dart' show Tool, ToolConfig;
4236

43-
HarmProbability _parseHarmProbability(Object jsonObject) =>
44-
switch (jsonObject) {
45-
'UNSPECIFIED' => HarmProbability.unknown,
46-
'NEGLIGIBLE' => HarmProbability.negligible,
47-
'LOW' => HarmProbability.low,
48-
'MEDIUM' => HarmProbability.medium,
49-
'HIGH' => HarmProbability.high,
50-
_ => throw unhandledFormat('HarmProbability', jsonObject),
51-
};
52-
HarmCategory _parseHarmCategory(Object jsonObject) => switch (jsonObject) {
53-
'HARM_CATEGORY_UNSPECIFIED' => HarmCategory.unknown,
54-
'HARM_CATEGORY_HARASSMENT' => HarmCategory.harassment,
55-
'HARM_CATEGORY_HATE_SPEECH' => HarmCategory.hateSpeech,
56-
'HARM_CATEGORY_SEXUALLY_EXPLICIT' => HarmCategory.sexuallyExplicit,
57-
'HARM_CATEGORY_DANGEROUS_CONTENT' => HarmCategory.dangerousContent,
58-
_ => throw unhandledFormat('HarmCategory', jsonObject),
59-
};
60-
61-
FinishReason _parseFinishReason(Object jsonObject) => switch (jsonObject) {
62-
'UNSPECIFIED' => FinishReason.unknown,
63-
'STOP' => FinishReason.stop,
64-
'MAX_TOKENS' => FinishReason.maxTokens,
65-
'SAFETY' => FinishReason.safety,
66-
'RECITATION' => FinishReason.recitation,
67-
'OTHER' => FinishReason.other,
68-
_ => throw unhandledFormat('FinishReason', jsonObject),
69-
};
70-
BlockReason _parseBlockReason(String jsonObject) => switch (jsonObject) {
71-
'BLOCK_REASON_UNSPECIFIED' => BlockReason.unknown,
72-
'SAFETY' => BlockReason.safety,
73-
'OTHER' => BlockReason.other,
74-
_ => throw unhandledFormat('BlockReason', jsonObject),
75-
};
76-
String _harmBlockThresholdtoJson(HarmBlockThreshold? threshold) =>
37+
String _harmBlockThresholdToJson(HarmBlockThreshold? threshold) =>
7738
switch (threshold) {
7839
null => 'HARM_BLOCK_THRESHOLD_UNSPECIFIED',
7940
HarmBlockThreshold.low => 'BLOCK_LOW_AND_ABOVE',
@@ -97,7 +58,7 @@ Object _safetySettingToJson(SafetySetting safetySetting) {
9758
}
9859
return {
9960
'category': _harmCategoryToJson(safetySetting.category),
100-
'threshold': _harmBlockThresholdtoJson(safetySetting.threshold)
61+
'threshold': _harmBlockThresholdToJson(safetySetting.threshold)
10162
};
10263
}
10364

@@ -180,6 +141,7 @@ final class DeveloperSerialization implements SerializationStrategy {
180141
};
181142
}
182143

144+
// Developer API and Vertex AI has different _parseSafetyRating logic.
183145
Candidate _parseCandidate(Object? jsonObject) {
184146
if (jsonObject is! Map) {
185147
throw unhandledFormat('Candidate', jsonObject);
@@ -196,12 +158,12 @@ Candidate _parseCandidate(Object? jsonObject) {
196158
},
197159
switch (jsonObject) {
198160
{'citationMetadata': final Object citationMetadata} =>
199-
_parseCitationMetadata(citationMetadata),
161+
parseCitationMetadata(citationMetadata),
200162
_ => null
201163
},
202164
switch (jsonObject) {
203165
{'finishReason': final Object finishReason} =>
204-
_parseFinishReason(finishReason),
166+
FinishReason.parseValue(finishReason),
205167
_ => null
206168
},
207169
switch (jsonObject) {
@@ -210,12 +172,13 @@ Candidate _parseCandidate(Object? jsonObject) {
210172
},
211173
groundingMetadata: switch (jsonObject) {
212174
{'groundingMetadata': final Object groundingMetadata} =>
213-
_parseGroundingMetadata(groundingMetadata),
175+
parseGroundingMetadata(groundingMetadata),
214176
_ => null
215177
},
216178
);
217179
}
218180

181+
// Developer API and Vertex AI has different _parseSafetyRating logic.
219182
PromptFeedback _parsePromptFeedback(Object jsonObject) {
220183
return switch (jsonObject) {
221184
{
@@ -224,7 +187,7 @@ PromptFeedback _parsePromptFeedback(Object jsonObject) {
224187
PromptFeedback(
225188
switch (jsonObject) {
226189
{'blockReason': final String blockReason} =>
227-
_parseBlockReason(blockReason),
190+
BlockReason.parseValue(blockReason),
228191
_ => null,
229192
},
230193
switch (jsonObject) {
@@ -241,147 +204,36 @@ SafetyRating _parseSafetyRating(Object? jsonObject) {
241204
return switch (jsonObject) {
242205
{
243206
'category': final Object category,
244-
'probability': final Object probability
207+
'probability': final Object probability,
208+
'blocked': final bool? isBlocked,
209+
} =>
210+
SafetyRating(
211+
_parseHarmCategory(category), _parseHarmProbability(probability),
212+
isBlocked: isBlocked),
213+
{
214+
'category': final Object category,
215+
'probability': final Object probability,
245216
} =>
246217
SafetyRating(
247218
_parseHarmCategory(category), _parseHarmProbability(probability)),
248219
_ => throw unhandledFormat('SafetyRating', jsonObject),
249220
};
250221
}
251222

252-
CitationMetadata _parseCitationMetadata(Object? jsonObject) {
253-
return switch (jsonObject) {
254-
{'citationSources': final List<Object?> citationSources} =>
255-
CitationMetadata(citationSources.map(_parseCitationSource).toList()),
256-
// Vertex SDK format uses `citations`
257-
{'citations': final List<Object?> citationSources} =>
258-
CitationMetadata(citationSources.map(_parseCitationSource).toList()),
259-
_ => throw unhandledFormat('CitationMetadata', jsonObject),
260-
};
261-
}
262-
263-
Citation _parseCitationSource(Object? jsonObject) {
264-
if (jsonObject is! Map) {
265-
throw unhandledFormat('CitationSource', jsonObject);
266-
}
267-
268-
final uriString = jsonObject['uri'] as String?;
269-
270-
return Citation(
271-
jsonObject['startIndex'] as int?,
272-
jsonObject['endIndex'] as int?,
273-
uriString != null ? Uri.parse(uriString) : null,
274-
jsonObject['license'] as String?,
275-
);
276-
}
277-
278-
GroundingMetadata _parseGroundingMetadata(Object? jsonObject) {
279-
if (jsonObject is! Map) {
280-
throw unhandledFormat('GroundingMetadata', jsonObject);
281-
}
282-
283-
final searchEntryPoint = switch (jsonObject) {
284-
{'searchEntryPoint': final Object? searchEntryPoint} =>
285-
_parseSearchEntryPoint(searchEntryPoint),
286-
_ => null,
287-
};
288-
final groundingChunks = switch (jsonObject) {
289-
{'groundingChunks': final List<Object?> groundingChunks} =>
290-
groundingChunks.map(_parseGroundingChunk).toList(),
291-
_ => null,
292-
} ??
293-
[];
294-
// Filters out null elements, which are returned from _parseGroundingSupport when
295-
// segment is null.
296-
final groundingSupport = switch (jsonObject) {
297-
{'groundingSupport': final List<Object?> groundingSupport} =>
298-
groundingSupport
299-
.map(_parseGroundingSupport)
300-
.whereType<GroundingSupport>()
301-
.toList(),
302-
_ => null,
303-
} ??
304-
[];
305-
final webSearchQueries = switch (jsonObject) {
306-
{'webSearchQueries': final List<String>? webSearchQueries} =>
307-
webSearchQueries,
308-
_ => null,
309-
} ??
310-
[];
311-
312-
return GroundingMetadata(
313-
searchEntryPoint: searchEntryPoint,
314-
groundingChunks: groundingChunks,
315-
groundingSupport: groundingSupport,
316-
webSearchQueries: webSearchQueries);
317-
}
318-
319-
Segment _parseSegment(Object? jsonObject) {
320-
if (jsonObject is! Map) {
321-
throw unhandledFormat('Segment', jsonObject);
322-
}
323-
324-
return Segment(
325-
partIndex: (jsonObject['partIndex'] as int?) ?? 0,
326-
startIndex: (jsonObject['startIndex'] as int?) ?? 0,
327-
endIndex: (jsonObject['endIndex'] as int?) ?? 0,
328-
text: (jsonObject['text'] as String?) ?? '');
329-
}
330-
331-
WebGroundingChunk _parseWebGroundingChunk(Object? jsonObject) {
332-
if (jsonObject is! Map) {
333-
throw unhandledFormat('WebGroundingChunk', jsonObject);
334-
}
335-
336-
return WebGroundingChunk(
337-
uri: jsonObject['uri'] as String?,
338-
title: jsonObject['title'] as String?,
339-
domain: jsonObject['domain'] as String?,
340-
);
341-
}
342-
343-
GroundingChunk _parseGroundingChunk(Object? jsonObject) {
344-
if (jsonObject is! Map) {
345-
throw unhandledFormat('GroundingChunk', jsonObject);
346-
}
347-
348-
return GroundingChunk(
349-
web: jsonObject['web'] != null
350-
? _parseWebGroundingChunk(jsonObject['web'])
351-
: null,
352-
);
353-
}
354-
355-
GroundingSupport? _parseGroundingSupport(Object? jsonObject) {
356-
if (jsonObject is! Map) {
357-
throw unhandledFormat('GroundingSupport', jsonObject);
358-
}
359-
360-
final segment = switch (jsonObject) {
361-
{'segment': final Object? segment} => _parseSegment(segment),
362-
_ => null,
363-
};
364-
if (segment == null) {
365-
return null;
366-
}
367-
368-
return GroundingSupport(
369-
segment: segment,
370-
groundingChunkIndices:
371-
(jsonObject['groundingChunkIndices'] as List<int>?) ?? []);
372-
}
373-
374-
SearchEntryPoint _parseSearchEntryPoint(Object? jsonObject) {
375-
if (jsonObject is! Map) {
376-
throw unhandledFormat('SearchEntryPoint', jsonObject);
377-
}
378-
379-
final renderedContent = jsonObject['renderedContent'] as String?;
380-
if (renderedContent == null) {
381-
throw unhandledFormat('SearchEntryPoint', jsonObject);
382-
}
383-
384-
return SearchEntryPoint(
385-
renderedContent: renderedContent,
386-
);
387-
}
223+
HarmProbability _parseHarmProbability(Object jsonObject) =>
224+
switch (jsonObject) {
225+
'UNSPECIFIED' => HarmProbability.unknown,
226+
'NEGLIGIBLE' => HarmProbability.negligible,
227+
'LOW' => HarmProbability.low,
228+
'MEDIUM' => HarmProbability.medium,
229+
'HIGH' => HarmProbability.high,
230+
_ => throw unhandledFormat('HarmProbability', jsonObject),
231+
};
232+
HarmCategory _parseHarmCategory(Object jsonObject) => switch (jsonObject) {
233+
'HARM_CATEGORY_UNSPECIFIED' => HarmCategory.unknown,
234+
'HARM_CATEGORY_HARASSMENT' => HarmCategory.harassment,
235+
'HARM_CATEGORY_HATE_SPEECH' => HarmCategory.hateSpeech,
236+
'HARM_CATEGORY_SEXUALLY_EXPLICIT' => HarmCategory.sexuallyExplicit,
237+
'HARM_CATEGORY_DANGEROUS_CONTENT' => HarmCategory.dangerousContent,
238+
_ => throw unhandledFormat('HarmCategory', jsonObject),
239+
};

0 commit comments

Comments
 (0)