Skip to content

Commit 2aaf5af

Browse files
authored
feat(firebase_ai): Add support for Grounding with Google Search (#17468)
1 parent 376bb6e commit 2aaf5af

File tree

7 files changed

+647
-36
lines changed

7 files changed

+647
-36
lines changed

packages/firebase_ai/firebase_ai/lib/firebase_ai.dart

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,6 @@ export 'src/error.dart'
5151
ServerException,
5252
UnsupportedUserLocation;
5353
export 'src/firebase_ai.dart' show FirebaseAI;
54-
export 'src/function_calling.dart'
55-
show
56-
FunctionCallingConfig,
57-
FunctionCallingMode,
58-
FunctionDeclaration,
59-
Tool,
60-
ToolConfig;
6154
export 'src/imagen_api.dart'
6255
show
6356
ImagenSafetySettings,
@@ -78,3 +71,10 @@ export 'src/live_api.dart'
7871
LiveServerResponse;
7972
export 'src/live_session.dart' show LiveSession;
8073
export 'src/schema.dart' show Schema, SchemaType;
74+
export 'src/tool.dart'
75+
show
76+
FunctionCallingConfig,
77+
FunctionCallingMode,
78+
FunctionDeclaration,
79+
Tool,
80+
ToolConfig;

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

Lines changed: 288 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414

1515
import 'content.dart';
1616
import 'error.dart';
17-
import 'function_calling.dart' show Tool, ToolConfig;
1817
import 'schema.dart';
18+
import 'tool.dart' show Tool, ToolConfig;
1919

2020
/// Response for Count Tokens
2121
final class CountTokensResponse {
@@ -196,7 +196,8 @@ final class Candidate {
196196
// TODO: token count?
197197
// ignore: public_member_api_docs
198198
Candidate(this.content, this.safetyRatings, this.citationMetadata,
199-
this.finishReason, this.finishMessage);
199+
this.finishReason, this.finishMessage,
200+
{this.groundingMetadata});
200201

201202
/// Generated content returned from the model.
202203
final Content content;
@@ -221,6 +222,9 @@ final class Candidate {
221222
/// Message for finish reason.
222223
final String? finishMessage;
223224

225+
/// Metadata returned to the client when grounding is enabled.
226+
final GroundingMetadata? groundingMetadata;
227+
224228
/// The concatenation of the text parts of [content], if any.
225229
///
226230
/// If this candidate was finished for a reason of [FinishReason.recitation]
@@ -252,6 +256,150 @@ final class Candidate {
252256
}
253257
}
254258

259+
/// Represents a specific segment within a [Content], often used to pinpoint
260+
/// the exact location of text or data that grounding information refers to.
261+
final class Segment {
262+
// ignore: public_member_api_docs
263+
Segment(
264+
{required this.partIndex,
265+
required this.startIndex,
266+
required this.endIndex,
267+
required this.text});
268+
269+
/// The zero-based index of the [Part] object within the `parts` array of its
270+
/// parent [Content] object.
271+
///
272+
/// This identifies which part of the content the segment belongs to.
273+
final int partIndex;
274+
275+
/// The zero-based start index of the segment within the specified [Part],
276+
/// measured in UTF-8 bytes.
277+
///
278+
/// This offset is inclusive, starting from 0 at the beginning of the
279+
/// part's content.
280+
final int startIndex;
281+
282+
/// The zero-based end index of the segment within the specified [Part],
283+
/// measured in UTF-8 bytes.
284+
///
285+
/// This offset is exclusive, meaning the character at this index is not
286+
/// included in the segment.
287+
final int endIndex;
288+
289+
/// The text corresponding to the segment from the response.
290+
final String text;
291+
}
292+
293+
/// A grounding chunk sourced from the web.
294+
final class WebGroundingChunk {
295+
// ignore: public_member_api_docs
296+
WebGroundingChunk({this.uri, this.title, this.domain});
297+
298+
/// The URI of the retrieved web page.
299+
final String? uri;
300+
301+
/// The title of the retrieved web page.
302+
final String? title;
303+
304+
/// The domain of the original URI from which the content was retrieved.
305+
///
306+
/// This field is only populated when using the Vertex AI Gemini API.
307+
final String? domain;
308+
}
309+
310+
/// Represents a chunk of retrieved data that supports a claim in the model's
311+
/// response.
312+
///
313+
/// This is part of the grounding information provided when grounding is
314+
/// enabled.
315+
final class GroundingChunk {
316+
// ignore: public_member_api_docs
317+
GroundingChunk({this.web});
318+
319+
/// Contains details if the grounding chunk is from a web source.
320+
final WebGroundingChunk? web;
321+
}
322+
323+
/// Provides information about how a specific segment of the model's response
324+
/// is supported by the retrieved grounding chunks.
325+
final class GroundingSupport {
326+
// ignore: public_member_api_docs
327+
GroundingSupport(
328+
{required this.segment, required this.groundingChunkIndices});
329+
330+
/// Specifies the segment of the model's response content that this
331+
/// grounding support pertains to.
332+
final Segment segment;
333+
334+
/// A list of indices that refer to specific [GroundingChunk]s within the
335+
/// [GroundingMetadata.groundingChunks] array.
336+
///
337+
/// These referenced chunks are the sources that
338+
/// support the claim made in the associated `segment` of the response.
339+
/// For example, an array `[1, 3, 4]`
340+
/// means that `groundingChunks[1]`, `groundingChunks[3]`, and
341+
/// `groundingChunks[4]` are the
342+
/// retrieved content supporting this part of the response.
343+
final List<int> groundingChunkIndices;
344+
}
345+
346+
/// Google Search entry point for web searches.
347+
final class SearchEntryPoint {
348+
// ignore: public_member_api_docs
349+
SearchEntryPoint({required this.renderedContent});
350+
351+
/// An HTML/CSS snippet that **must** be embedded in an app to display a
352+
/// Google Search entry point for follow-up web searches related to the
353+
/// model's "Grounded Response".
354+
///
355+
/// To ensure proper rendering, it's recommended to display this content
356+
/// within a `WebView`.
357+
final String renderedContent;
358+
}
359+
360+
/// Metadata returned to the client when grounding is enabled.
361+
///
362+
/// > Important: If using Grounding with Google Search, you are required to
363+
/// comply with the "Grounding with Google Search" usage requirements for your
364+
/// chosen API provider:
365+
/// [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search)
366+
/// or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms)
367+
/// section within the Service Specific Terms).
368+
final class GroundingMetadata {
369+
// ignore: public_member_api_docs
370+
GroundingMetadata(
371+
{this.searchEntryPoint,
372+
required this.groundingChunks,
373+
required this.groundingSupport,
374+
required this.webSearchQueries});
375+
376+
/// Google Search entry point for web searches.
377+
///
378+
/// This contains an HTML/CSS snippet that **must** be embedded in an app to
379+
// display a Google Search entry point for follow-up web searches related to
380+
// the model's "Grounded Response".
381+
final SearchEntryPoint? searchEntryPoint;
382+
383+
/// A list of [GroundingChunk]s.
384+
///
385+
/// Each chunk represents a piece of retrieved content (e.g., from a web
386+
/// page) that the model used to ground its response.
387+
final List<GroundingChunk> groundingChunks;
388+
389+
/// A list of [GroundingSupport]s.
390+
///
391+
/// Each object details how specific segments of the
392+
/// model's response are supported by the `groundingChunks`.
393+
final List<GroundingSupport> groundingSupport;
394+
395+
/// A list of web search queries that the model performed to gather the
396+
/// grounding information.
397+
///
398+
/// These can be used to allow users to explore the search results
399+
/// themselves.
400+
final List<String> webSearchQueries;
401+
}
402+
255403
/// Safety rating for a piece of content.
256404
///
257405
/// The safety rating contains the category of harm and the harm probability
@@ -1060,29 +1208,33 @@ Candidate _parseCandidate(Object? jsonObject) {
10601208
}
10611209

10621210
return Candidate(
1063-
jsonObject.containsKey('content')
1064-
? parseContent(jsonObject['content'] as Object)
1065-
: Content(null, []),
1066-
switch (jsonObject) {
1067-
{'safetyRatings': final List<Object?> safetyRatings} =>
1068-
safetyRatings.map(_parseSafetyRating).toList(),
1069-
_ => null
1070-
},
1071-
switch (jsonObject) {
1072-
{'citationMetadata': final Object citationMetadata} =>
1073-
_parseCitationMetadata(citationMetadata),
1074-
_ => null
1075-
},
1076-
switch (jsonObject) {
1077-
{'finishReason': final Object finishReason} =>
1078-
FinishReason._parseValue(finishReason),
1079-
_ => null
1080-
},
1081-
switch (jsonObject) {
1082-
{'finishMessage': final String finishMessage} => finishMessage,
1083-
_ => null
1084-
},
1085-
);
1211+
jsonObject.containsKey('content')
1212+
? parseContent(jsonObject['content'] as Object)
1213+
: Content(null, []),
1214+
switch (jsonObject) {
1215+
{'safetyRatings': final List<Object?> safetyRatings} =>
1216+
safetyRatings.map(_parseSafetyRating).toList(),
1217+
_ => null
1218+
},
1219+
switch (jsonObject) {
1220+
{'citationMetadata': final Object citationMetadata} =>
1221+
_parseCitationMetadata(citationMetadata),
1222+
_ => null
1223+
},
1224+
switch (jsonObject) {
1225+
{'finishReason': final Object finishReason} =>
1226+
FinishReason._parseValue(finishReason),
1227+
_ => null
1228+
},
1229+
switch (jsonObject) {
1230+
{'finishMessage': final String finishMessage} => finishMessage,
1231+
_ => null
1232+
},
1233+
groundingMetadata: switch (jsonObject) {
1234+
{'groundingMetadata': final Object groundingMetadata} =>
1235+
_parseGroundingMetadata(groundingMetadata),
1236+
_ => null
1237+
});
10861238
}
10871239

10881240
PromptFeedback _parsePromptFeedback(Object jsonObject) {
@@ -1196,3 +1348,114 @@ Citation _parseCitationSource(Object? jsonObject) {
11961348
jsonObject['license'] as String?,
11971349
);
11981350
}
1351+
1352+
GroundingMetadata _parseGroundingMetadata(Object? jsonObject) {
1353+
if (jsonObject is! Map) {
1354+
throw unhandledFormat('GroundingMetadata', jsonObject);
1355+
}
1356+
1357+
final searchEntryPoint = switch (jsonObject) {
1358+
{'searchEntryPoint': final Object? searchEntryPoint} =>
1359+
_parseSearchEntryPoint(searchEntryPoint),
1360+
_ => null,
1361+
};
1362+
final groundingChunks = switch (jsonObject) {
1363+
{'groundingChunks': final List<Object?> groundingChunks} =>
1364+
groundingChunks.map(_parseGroundingChunk).toList(),
1365+
_ => null,
1366+
} ??
1367+
[];
1368+
// Filters out null elements, which are returned from _parseGroundingSupport when
1369+
// segment is null.
1370+
final groundingSupport = switch (jsonObject) {
1371+
{'groundingSupport': final List<Object?> groundingSupport} =>
1372+
groundingSupport
1373+
.map(_parseGroundingSupport)
1374+
.whereType<GroundingSupport>()
1375+
.toList(),
1376+
_ => null,
1377+
} ??
1378+
[];
1379+
final webSearchQueries = switch (jsonObject) {
1380+
{'webSearchQueries': final List<String>? webSearchQueries} =>
1381+
webSearchQueries,
1382+
_ => null,
1383+
} ??
1384+
[];
1385+
1386+
return GroundingMetadata(
1387+
searchEntryPoint: searchEntryPoint,
1388+
groundingChunks: groundingChunks,
1389+
groundingSupport: groundingSupport,
1390+
webSearchQueries: webSearchQueries);
1391+
}
1392+
1393+
Segment _parseSegment(Object? jsonObject) {
1394+
if (jsonObject is! Map) {
1395+
throw unhandledFormat('Segment', jsonObject);
1396+
}
1397+
1398+
return Segment(
1399+
partIndex: (jsonObject['partIndex'] as int?) ?? 0,
1400+
startIndex: (jsonObject['startIndex'] as int?) ?? 0,
1401+
endIndex: (jsonObject['endIndex'] as int?) ?? 0,
1402+
text: (jsonObject['text'] as String?) ?? '');
1403+
}
1404+
1405+
WebGroundingChunk _parseWebGroundingChunk(Object? jsonObject) {
1406+
if (jsonObject is! Map) {
1407+
throw unhandledFormat('WebGroundingChunk', jsonObject);
1408+
}
1409+
1410+
return WebGroundingChunk(
1411+
uri: jsonObject['uri'] as String?,
1412+
title: jsonObject['title'] as String?,
1413+
domain: jsonObject['domain'] as String?,
1414+
);
1415+
}
1416+
1417+
GroundingChunk _parseGroundingChunk(Object? jsonObject) {
1418+
if (jsonObject is! Map) {
1419+
throw unhandledFormat('GroundingChunk', jsonObject);
1420+
}
1421+
1422+
return GroundingChunk(
1423+
web: jsonObject['web'] != null
1424+
? _parseWebGroundingChunk(jsonObject['web'])
1425+
: null,
1426+
);
1427+
}
1428+
1429+
GroundingSupport? _parseGroundingSupport(Object? jsonObject) {
1430+
if (jsonObject is! Map) {
1431+
throw unhandledFormat('GroundingSupport', jsonObject);
1432+
}
1433+
1434+
final segment = switch (jsonObject) {
1435+
{'segment': final Object? segment} => _parseSegment(segment),
1436+
_ => null,
1437+
};
1438+
if (segment == null) {
1439+
return null;
1440+
}
1441+
1442+
return GroundingSupport(
1443+
segment: segment,
1444+
groundingChunkIndices:
1445+
(jsonObject['groundingChunkIndices'] as List<int>?) ?? []);
1446+
}
1447+
1448+
SearchEntryPoint _parseSearchEntryPoint(Object? jsonObject) {
1449+
if (jsonObject is! Map) {
1450+
throw unhandledFormat('SearchEntryPoint', jsonObject);
1451+
}
1452+
1453+
final renderedContent = jsonObject['renderedContent'] as String?;
1454+
if (renderedContent == null) {
1455+
throw unhandledFormat('SearchEntryPoint', jsonObject);
1456+
}
1457+
1458+
return SearchEntryPoint(
1459+
renderedContent: renderedContent,
1460+
);
1461+
}

packages/firebase_ai/firebase_ai/lib/src/base_model.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ import 'api.dart';
2828
import 'client.dart';
2929
import 'content.dart';
3030
import 'developer/api.dart';
31-
import 'function_calling.dart';
3231
import 'imagen_api.dart';
3332
import 'imagen_content.dart';
3433
import 'live_api.dart';
3534
import 'live_session.dart';
35+
import 'tool.dart';
3636
import 'vertex_version.dart';
3737

3838
part 'generative_model.dart';

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import '../api.dart'
3333
createUsageMetadata;
3434
import '../content.dart' show Content, FunctionCall, Part, TextPart;
3535
import '../error.dart';
36-
import '../function_calling.dart' show Tool, ToolConfig;
36+
import '../tool.dart' show Tool, ToolConfig;
3737

3838
HarmProbability _parseHarmProbability(Object jsonObject) =>
3939
switch (jsonObject) {

0 commit comments

Comments
 (0)