Skip to content

Commit 500206e

Browse files
committed
[FirebaseAI] Add support for Grounding with Google Search
1 parent bf0d25f commit 500206e

File tree

7 files changed

+634
-30
lines changed

7 files changed

+634
-30
lines changed

packages/firebase_ai/firebase_ai/lib/firebase_ai.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export 'src/error.dart'
5151
ServerException,
5252
UnsupportedUserLocation;
5353
export 'src/firebase_ai.dart' show FirebaseAI;
54-
export 'src/function_calling.dart'
54+
export 'src/tool.dart'
5555
show
5656
FunctionCallingConfig,
5757
FunctionCallingMode,

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

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

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

2020
/// Response for Count Tokens
@@ -187,7 +187,8 @@ final class Candidate {
187187
// TODO: token count?
188188
// ignore: public_member_api_docs
189189
Candidate(this.content, this.safetyRatings, this.citationMetadata,
190-
this.finishReason, this.finishMessage);
190+
this.finishReason, this.finishMessage,
191+
{this.groundingMetadata});
191192

192193
/// Generated content returned from the model.
193194
final Content content;
@@ -212,6 +213,9 @@ final class Candidate {
212213
/// Message for finish reason.
213214
final String? finishMessage;
214215

216+
/// Metadata returned to the client when grounding is enabled.
217+
final GroundingMetadata? groundingMetadata;
218+
215219
/// The concatenation of the text parts of [content], if any.
216220
///
217221
/// If this candidate was finished for a reason of [FinishReason.recitation]
@@ -243,6 +247,144 @@ final class Candidate {
243247
}
244248
}
245249

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

10291171
return Candidate(
1030-
jsonObject.containsKey('content')
1031-
? parseContent(jsonObject['content'] as Object)
1032-
: Content(null, []),
1033-
switch (jsonObject) {
1034-
{'safetyRatings': final List<Object?> safetyRatings} =>
1035-
safetyRatings.map(_parseSafetyRating).toList(),
1036-
_ => null
1037-
},
1038-
switch (jsonObject) {
1039-
{'citationMetadata': final Object citationMetadata} =>
1040-
_parseCitationMetadata(citationMetadata),
1041-
_ => null
1042-
},
1043-
switch (jsonObject) {
1044-
{'finishReason': final Object finishReason} =>
1045-
FinishReason._parseValue(finishReason),
1046-
_ => null
1047-
},
1048-
switch (jsonObject) {
1049-
{'finishMessage': final String finishMessage} => finishMessage,
1050-
_ => null
1051-
},
1052-
);
1172+
jsonObject.containsKey('content')
1173+
? parseContent(jsonObject['content'] as Object)
1174+
: Content(null, []),
1175+
switch (jsonObject) {
1176+
{'safetyRatings': final List<Object?> safetyRatings} =>
1177+
safetyRatings.map(_parseSafetyRating).toList(),
1178+
_ => null
1179+
},
1180+
switch (jsonObject) {
1181+
{'citationMetadata': final Object citationMetadata} =>
1182+
_parseCitationMetadata(citationMetadata),
1183+
_ => null
1184+
},
1185+
switch (jsonObject) {
1186+
{'finishReason': final Object finishReason} =>
1187+
FinishReason._parseValue(finishReason),
1188+
_ => null
1189+
},
1190+
switch (jsonObject) {
1191+
{'finishMessage': final String finishMessage} => finishMessage,
1192+
_ => null
1193+
},
1194+
groundingMetadata: switch (jsonObject) {
1195+
{'groundingMetadata': final Object groundingMetadata} =>
1196+
_parseGroundingMetadata(groundingMetadata),
1197+
_ => null
1198+
});
10531199
}
10541200

10551201
PromptFeedback _parsePromptFeedback(Object jsonObject) {
@@ -1163,3 +1309,114 @@ Citation _parseCitationSource(Object? jsonObject) {
11631309
jsonObject['license'] as String?,
11641310
);
11651311
}
1312+
1313+
GroundingMetadata _parseGroundingMetadata(Object? jsonObject) {
1314+
if (jsonObject is! Map) {
1315+
throw unhandledFormat('GroundingMetadata', jsonObject);
1316+
}
1317+
1318+
final searchEntryPoint = switch (jsonObject) {
1319+
{'searchEntryPoint': final Object? searchEntryPoint} =>
1320+
_parseSearchEntryPoint(searchEntryPoint),
1321+
_ => null,
1322+
};
1323+
final groundingChunks = switch (jsonObject) {
1324+
{'groundingChunks': final List<Object?> groundingChunks} =>
1325+
groundingChunks.map(_parseGroundingChunk).toList(),
1326+
_ => null,
1327+
} ??
1328+
[];
1329+
// Filters out null elements, which are returned from _parseGroundingSupport when
1330+
// segment is null.
1331+
final groundingSupport = switch (jsonObject) {
1332+
{'groundingSupport': final List<Object?> groundingSupport} =>
1333+
groundingSupport
1334+
.map(_parseGroundingSupport)
1335+
.whereType<GroundingSupport>()
1336+
.toList(),
1337+
_ => null,
1338+
} ??
1339+
[];
1340+
final webSearchQueries = switch (jsonObject) {
1341+
{'webSearchQueries': final List<String>? webSearchQueries} =>
1342+
webSearchQueries,
1343+
_ => null,
1344+
} ??
1345+
[];
1346+
1347+
return GroundingMetadata(
1348+
searchEntryPoint: searchEntryPoint,
1349+
groundingChunks: groundingChunks,
1350+
groundingSupport: groundingSupport,
1351+
webSearchQueries: webSearchQueries);
1352+
}
1353+
1354+
Segment _parseSegment(Object? jsonObject) {
1355+
if (jsonObject is! Map) {
1356+
throw unhandledFormat('Segment', jsonObject);
1357+
}
1358+
1359+
return Segment(
1360+
partIndex: (jsonObject['partIndex'] as int?) ?? 0,
1361+
startIndex: (jsonObject['startIndex'] as int?) ?? 0,
1362+
endIndex: (jsonObject['endIndex'] as int?) ?? 0,
1363+
text: (jsonObject['text'] as String?) ?? '');
1364+
}
1365+
1366+
WebGroundingChunk _parseWebGroundingChunk(Object? jsonObject) {
1367+
if (jsonObject is! Map) {
1368+
throw unhandledFormat('WebGroundingChunk', jsonObject);
1369+
}
1370+
1371+
return WebGroundingChunk(
1372+
uri: jsonObject['uri'] as String?,
1373+
title: jsonObject['title'] as String?,
1374+
domain: jsonObject['domain'] as String?,
1375+
);
1376+
}
1377+
1378+
GroundingChunk _parseGroundingChunk(Object? jsonObject) {
1379+
if (jsonObject is! Map) {
1380+
throw unhandledFormat('GroundingChunk', jsonObject);
1381+
}
1382+
1383+
return GroundingChunk(
1384+
web: jsonObject['web'] != null
1385+
? _parseWebGroundingChunk(jsonObject['web'])
1386+
: null,
1387+
);
1388+
}
1389+
1390+
GroundingSupport? _parseGroundingSupport(Object? jsonObject) {
1391+
if (jsonObject is! Map) {
1392+
throw unhandledFormat('GroundingSupport', jsonObject);
1393+
}
1394+
1395+
final segment = switch (jsonObject) {
1396+
{'segment': final Object? segment} => _parseSegment(segment),
1397+
_ => null,
1398+
};
1399+
if (segment == null) {
1400+
return null;
1401+
}
1402+
1403+
return GroundingSupport(
1404+
segment: segment,
1405+
groundingChunkIndices:
1406+
(jsonObject['groundingChunkIndices'] as List<int>?) ?? []);
1407+
}
1408+
1409+
SearchEntryPoint _parseSearchEntryPoint(Object? jsonObject) {
1410+
if (jsonObject is! Map) {
1411+
throw unhandledFormat('SearchEntryPoint', jsonObject);
1412+
}
1413+
1414+
final renderedContent = jsonObject['renderedContent'] as String?;
1415+
if (renderedContent == null) {
1416+
throw unhandledFormat('SearchEntryPoint', jsonObject);
1417+
}
1418+
1419+
return SearchEntryPoint(
1420+
renderedContent: renderedContent,
1421+
);
1422+
}

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,7 +28,7 @@ import 'api.dart';
2828
import 'client.dart';
2929
import 'content.dart';
3030
import 'developer/api.dart';
31-
import 'function_calling.dart';
31+
import 'tool.dart';
3232
import 'imagen_api.dart';
3333
import 'imagen_content.dart';
3434
import 'live_api.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)