diff --git a/packages/firebase_ai/firebase_ai/example/lib/main.dart b/packages/firebase_ai/firebase_ai/example/lib/main.dart index 436b909f1159..db1344210deb 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/main.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/main.dart @@ -167,7 +167,10 @@ class _HomeScreenState extends State { ) { switch (index) { case 0: - return ChatPage(title: 'Chat', model: currentModel); + return ChatPage( + title: 'Chat', + useVertexBackend: useVertexBackend, + ); case 1: return AudioPage(title: 'Audio', model: currentModel); case 2: @@ -199,7 +202,10 @@ class _HomeScreenState extends State { default: // Fallback to the first page in case of an unexpected index - return ChatPage(title: 'Chat', model: currentModel); + return ChatPage( + title: 'Chat', + useVertexBackend: useVertexBackend, + ); } } diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/audio_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/audio_page.dart index be04d6a2db30..4af259693bac 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/pages/audio_page.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/audio_page.dart @@ -99,7 +99,7 @@ class _AudioPageState extends State { Future _submitAudioToModel(audioPart) async { try { String textPrompt = 'What is in the audio recording?'; - final prompt = TextPart('What is in the audio recording?'); + const prompt = TextPart('What is in the audio recording?'); setState(() { _messages.add(MessageData(text: textPrompt, fromUser: true)); @@ -137,11 +137,6 @@ class _AudioPageState extends State { itemBuilder: (context, idx) { return MessageWidget( text: _messages[idx].text, - image: Image.memory( - _messages[idx].imageBytes!, - cacheWidth: 400, - cacheHeight: 400, - ), isFromUser: _messages[idx].fromUser ?? false, ); }, diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/chat_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/chat_page.dart index eb8e6128f2fc..388cc76572d1 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/pages/chat_page.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/chat_page.dart @@ -12,15 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:firebase_ai/firebase_ai.dart'; import '../widgets/message_widget.dart'; class ChatPage extends StatefulWidget { - const ChatPage({super.key, required this.title, required this.model}); + const ChatPage({ + super.key, + required this.title, + required this.useVertexBackend, + }); final String title; - final GenerativeModel model; + final bool useVertexBackend; @override State createState() => _ChatPageState(); @@ -28,16 +33,37 @@ class ChatPage extends StatefulWidget { class _ChatPageState extends State { ChatSession? _chat; + GenerativeModel? _model; final ScrollController _scrollController = ScrollController(); final TextEditingController _textController = TextEditingController(); final FocusNode _textFieldFocus = FocusNode(); final List _messages = []; bool _loading = false; + bool _enableThinking = false; @override void initState() { super.initState(); - _chat = widget.model.startChat(); + _initializeChat(); + } + + void _initializeChat() { + final generationConfig = GenerationConfig( + thinkingConfig: + _enableThinking ? ThinkingConfig(includeThoughts: true) : null, + ); + if (widget.useVertexBackend) { + _model = FirebaseAI.vertexAI(auth: FirebaseAuth.instance).generativeModel( + model: 'gemini-2.5-flash', + generationConfig: generationConfig, + ); + } else { + _model = FirebaseAI.googleAI(auth: FirebaseAuth.instance).generativeModel( + model: 'gemini-2.5-flash', + generationConfig: generationConfig, + ); + } + _chat = _model?.startChat(); } void _scrollDown() { @@ -64,18 +90,31 @@ class _ChatPageState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ + SwitchListTile( + title: const Text('Enable Thinking'), + value: _enableThinking, + onChanged: (bool value) { + setState(() { + _enableThinking = value; + _initializeChat(); + }); + }, + ), Expanded( child: ListView.builder( controller: _scrollController, itemBuilder: (context, idx) { + final message = _messages[idx]; return MessageWidget( - text: _messages[idx].text, - image: Image.memory( - _messages[idx].imageBytes!, - cacheWidth: 400, - cacheHeight: 400, - ), - isFromUser: _messages[idx].fromUser ?? false, + text: message.text, + image: message.imageBytes != null + ? Image.memory( + message.imageBytes!, + cacheWidth: 400, + cacheHeight: 400, + ) + : null, + isFromUser: message.fromUser ?? false, ); }, itemCount: _messages.length, @@ -130,6 +169,11 @@ class _ChatPageState extends State { var response = await _chat?.sendMessage( Content.text(message), ); + final thought = response?.thoughtSummary; + if (thought != null) { + _messages + .add(MessageData(text: thought, fromUser: false, isThought: true)); + } var text = response?.text; _messages.add(MessageData(text: text, fromUser: false)); diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/document.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/document.dart index ec5114e8b13a..db2715c402e0 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/pages/document.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/document.dart @@ -47,7 +47,7 @@ class _DocumentPageState extends State { const _prompt = 'Write me a summary in one sentence what this document is about.'; - final prompt = TextPart(_prompt); + const prompt = TextPart(_prompt); setState(() { _messages.add(MessageData(text: _prompt, fromUser: true)); diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart index cf79b61a7104..ec443f53c16c 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart @@ -39,28 +39,39 @@ class Location { } class _FunctionCallingPageState extends State { - late final GenerativeModel _functionCallModel; + late GenerativeModel _functionCallModel; final List _messages = []; bool _loading = false; + bool _enableThinking = false; @override void initState() { super.initState(); + _initializeModel(); + } + + void _initializeModel() { + final generationConfig = GenerationConfig( + thinkingConfig: + _enableThinking ? ThinkingConfig(includeThoughts: true) : null, + ); if (widget.useVertexBackend) { var vertexAI = FirebaseAI.vertexAI(auth: FirebaseAuth.instance); _functionCallModel = vertexAI.generativeModel( - model: 'gemini-2.0-flash', + model: 'gemini-2.5-flash', tools: [ Tool.functionDeclarations([fetchWeatherTool]), ], + generationConfig: generationConfig, ); } else { var googleAI = FirebaseAI.googleAI(auth: FirebaseAuth.instance); _functionCallModel = googleAI.generativeModel( - model: 'gemini-2.0-flash', + model: 'gemini-2.5-flash', tools: [ Tool.functionDeclarations([fetchWeatherTool]), ], + generationConfig: generationConfig, ); } } @@ -118,12 +129,24 @@ class _FunctionCallingPageState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ + SwitchListTile( + title: const Text('Enable Thinking'), + value: _enableThinking, + onChanged: (bool value) { + setState(() { + _enableThinking = value; + _initializeModel(); + }); + }, + ), Expanded( child: ListView.builder( itemBuilder: (context, idx) { + final message = _messages[idx]; return MessageWidget( - text: _messages[idx].text, - isFromUser: _messages[idx].fromUser ?? false, + text: message.text, + isFromUser: message.fromUser ?? false, + isThought: message.isThought, ); }, itemCount: _messages.length, @@ -158,43 +181,87 @@ class _FunctionCallingPageState extends State { Future _testFunctionCalling() async { setState(() { _loading = true; + _messages.clear(); }); - final functionCallChat = _functionCallModel.startChat(); - const prompt = 'What is the weather like in Boston on 10/02 in year 2024?'; + try { + final functionCallChat = _functionCallModel.startChat(); + const prompt = + 'What is the weather like in Boston on 10/02 in year 2024?'; - // Send the message to the generative model. - var response = await functionCallChat.sendMessage( - Content.text(prompt), - ); + _messages.add(MessageData(text: prompt, fromUser: true)); - final functionCalls = response.functionCalls.toList(); - // When the model response with a function call, invoke the function. - if (functionCalls.isNotEmpty) { - final functionCall = functionCalls.first; - if (functionCall.name == 'fetchWeather') { - Map location = - functionCall.args['location']! as Map; - var date = functionCall.args['date']! as String; - var city = location['city'] as String; - var state = location['state'] as String; - final functionResult = await fetchWeather(Location(city, state), date); - // Send the response to the model so that it can use the result to - // generate text for the user. - response = await functionCallChat.sendMessage( - Content.functionResponse(functionCall.name, functionResult), - ); - } else { - throw UnimplementedError( - 'Function not declared to the model: ${functionCall.name}', - ); + // Send the message to the generative model. + var response = await functionCallChat.sendMessage( + Content.text(prompt), + ); + + final thought = response.thoughtSummary; + if (thought != null) { + _messages + .add(MessageData(text: thought, fromUser: false, isThought: true)); } - } - // When the model responds with non-null text content, print it. - if (response.text case final text?) { - _messages.add(MessageData(text: text)); + + final functionCalls = response.functionCalls.toList(); + // When the model response with a function call, invoke the function. + if (functionCalls.isNotEmpty) { + final functionCall = functionCalls.first; + if (functionCall.name == 'fetchWeather') { + Map location = + functionCall.args['location']! as Map; + var date = functionCall.args['date']! as String; + var city = location['city'] as String; + var state = location['state'] as String; + final functionResult = + await fetchWeather(Location(city, state), date); + // Send the response to the model so that it can use the result to + // generate text for the user. + response = await functionCallChat.sendMessage( + Content.functionResponse(functionCall.name, functionResult), + ); + } else { + throw UnimplementedError( + 'Function not declared to the model: ${functionCall.name}', + ); + } + } + // When the model responds with non-null text content, print it. + if (response.text case final text?) { + _messages.add(MessageData(text: text)); + setState(() { + _loading = false; + }); + } + } catch (e) { + _showError(e.toString()); + setState(() { + _loading = false; + }); + } finally { setState(() { _loading = false; }); } } + + void _showError(String message) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Something went wrong'), + content: SingleChildScrollView( + child: SelectableText(message), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('OK'), + ), + ], + ); + }, + ); + } } diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/image_prompt_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/image_prompt_page.dart index 5409c264450b..48fc8667af59 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/pages/image_prompt_page.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/image_prompt_page.dart @@ -188,7 +188,7 @@ class _ImagePromptPageState extends State { final content = [ Content.multi([ TextPart(message), - FileData( + const FileData( 'image/jpeg', 'gs://vertex-ai-example-ef5a2.appspot.com/foodpic.jpg', ), diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/video_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/video_page.dart index 0a98c9a82486..565555e19cd6 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/pages/video_page.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/video_page.dart @@ -46,8 +46,6 @@ class _VideoPageState extends State { const _prompt = 'Can you tell me what is in the video?'; - final prompt = TextPart(_prompt); - setState(() { _messages.add(MessageData(text: _prompt, fromUser: true)); }); @@ -56,7 +54,7 @@ class _VideoPageState extends State { InlineDataPart('video/mp4', videoBytes.buffer.asUint8List()); final response = await widget.model.generateContent([ - Content.multi([prompt, videoPart]), + Content.multi([const TextPart(_prompt), videoPart]), ]); setState(() { diff --git a/packages/firebase_ai/firebase_ai/example/lib/widgets/message_widget.dart b/packages/firebase_ai/firebase_ai/example/lib/widgets/message_widget.dart index 368dfc1fea88..7ea588557ce3 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/widgets/message_widget.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/widgets/message_widget.dart @@ -16,22 +16,30 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; class MessageData { - MessageData({this.imageBytes, this.text, this.fromUser}); + MessageData({ + this.imageBytes, + this.text, + this.fromUser, + this.isThought = false, + }); final Uint8List? imageBytes; final String? text; final bool? fromUser; + final bool isThought; } class MessageWidget extends StatelessWidget { final Image? image; final String? text; final bool isFromUser; + final bool isThought; const MessageWidget({ super.key, this.image, this.text, required this.isFromUser, + this.isThought = false, }); @override @@ -44,9 +52,11 @@ class MessageWidget extends StatelessWidget { child: Container( constraints: const BoxConstraints(maxWidth: 600), decoration: BoxDecoration( - color: isFromUser - ? Theme.of(context).colorScheme.primaryContainer - : Theme.of(context).colorScheme.surfaceContainerHighest, + color: isThought + ? Theme.of(context).colorScheme.secondaryContainer + : isFromUser + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(18), ), padding: const EdgeInsets.symmetric( diff --git a/packages/firebase_ai/firebase_ai/lib/src/api.dart b/packages/firebase_ai/firebase_ai/lib/src/api.dart index 7e4e8fc96d87..09948dc52023 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/api.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/api.dart @@ -95,11 +95,23 @@ final class GenerateContentResponse { : ''), ), // Special case for a single TextPart to avoid iterable chain. - [Candidate(content: Content(parts: [TextPart(:final text)])), ...] => + [ + Candidate( + content: Content( + parts: [TextPart(isThought: final isThought, :final text)] + ) + ), + ... + ] + when isThought != true => text, [Candidate(content: Content(:final parts)), ...] - when parts.any((p) => p is TextPart) => - parts.whereType().map((p) => p.text).join(), + when parts.any((p) => p is TextPart && p.isThought != true) => + parts + .whereType() + .where((p) => p.isThought != true) + .map((p) => p.text) + .join(), [Candidate(), ...] => null, }; } @@ -110,7 +122,9 @@ final class GenerateContentResponse { /// candidate has no [FunctionCall] parts. There is no error thrown if the /// prompt or response were blocked. Iterable get functionCalls => - candidates.firstOrNull?.content.parts.whereType() ?? + candidates.firstOrNull?.content.parts + .whereType() + .where((p) => p.isThought != true) ?? const []; /// The inline data parts of the first candidate in [candidates], if any. @@ -119,8 +133,31 @@ final class GenerateContentResponse { /// candidate has no [InlineDataPart] parts. There is no error thrown if the /// prompt or response were blocked. Iterable get inlineDataParts => - candidates.firstOrNull?.content.parts.whereType() ?? + candidates.firstOrNull?.content.parts + .whereType() + .where((p) => p.isThought != true) ?? const []; + + /// The thought summary of the first candidate in [candidates], if any. + /// + /// If the first candidate's content contains any thought parts, this value is + /// the concatenation of their text. + /// + /// If there are no candidates, or if the first candidate does not contain any + /// thought parts, this value is `null`. + /// + /// Important: Thought summaries are only available when `includeThoughts` is + /// enabled in the ``ThinkingConfig``. For more information, see the + /// [Thinking](https://firebase.google.com/docs/ai-logic/thinking) + String? get thoughtSummary { + final thoughtParts = candidates.firstOrNull?.content.parts + .where((p) => p.isThought == true) + .whereType(); + if (thoughtParts == null || thoughtParts.isEmpty) { + return null; + } + return thoughtParts.map((p) => p.text).join(); + } } /// Feedback metadata of a prompt specified in a [GenerativeModel] request. @@ -861,15 +898,20 @@ enum ResponseModalities { /// Config for thinking features. class ThinkingConfig { // ignore: public_member_api_docs - ThinkingConfig({this.thinkingBudget}); + ThinkingConfig({this.thinkingBudget, this.includeThoughts}); /// The number of thoughts tokens that the model should generate. final int? thinkingBudget; + /// Whether to include thoughts in the response. + final bool? includeThoughts; + // ignore: public_member_api_docs Map toJson() => { if (thinkingBudget case final thinkingBudget?) 'thinkingBudget': thinkingBudget, + if (includeThoughts case final includeThoughts?) + 'includeThoughts': includeThoughts, }; } diff --git a/packages/firebase_ai/firebase_ai/lib/src/content.dart b/packages/firebase_ai/firebase_ai/lib/src/content.dart index fb627a9871c1..bb260b94f27d 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/content.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/content.dart @@ -91,31 +91,45 @@ Part parsePart(Object? jsonObject) { }); } + final isThought = + jsonObject.containsKey('thought') && jsonObject['thought']! as bool; + + final thoughtSignature = jsonObject.containsKey('thoughtSignature') + ? jsonObject['thoughtSignature']! as String + : null; + if (jsonObject.containsKey('functionCall')) { final functionCall = jsonObject['functionCall']; if (functionCall is Map && functionCall.containsKey('name') && functionCall.containsKey('args')) { - return FunctionCall( + return FunctionCall._( functionCall['name'] as String, functionCall['args'] as Map, id: functionCall['id'] as String?, + isThought: isThought, + thoughtSignature: thoughtSignature, ); } else { throw unhandledFormat('functionCall', functionCall); } } return switch (jsonObject) { - {'text': final String text} => TextPart(text), + {'text': final String text} => TextPart._(text, + isThought: isThought, thoughtSignature: thoughtSignature), { 'file_data': { 'file_uri': final String fileUri, 'mime_type': final String mimeType } } => - FileData(mimeType, fileUri), + FileData._(mimeType, fileUri, + isThought: isThought, thoughtSignature: thoughtSignature), {'inlineData': {'mimeType': String mimeType, 'data': String bytes}} => - InlineDataPart(mimeType, base64Decode(bytes)), + InlineDataPart._(mimeType, base64Decode(bytes), + willContinue: false, + isThought: isThought, + thoughtSignature: thoughtSignature), _ => () { log('unhandled part format: $jsonObject'); return UnknownPart(jsonObject); @@ -125,14 +139,23 @@ Part parsePart(Object? jsonObject) { /// A datatype containing media that is part of a multi-part [Content] message. sealed class Part { + // ignore: public_member_api_docs + const Part({this.isThought, String? thoughtSignature}) + : _thoughtSignature = thoughtSignature; + // ignore: public_member_api_docs + final bool? isThought; + + // ignore: unused_field + final String? _thoughtSignature; + /// Convert the [Part] content to json format. Object toJson(); } /// A [Part] that contains unparsable data. -final class UnknownPart implements Part { +final class UnknownPart extends Part { // ignore: public_member_api_docs - UnknownPart(this.data); + UnknownPart(this.data) : super(isThought: false, thoughtSignature: null); /// The unparsed data. final Map data; @@ -142,9 +165,21 @@ final class UnknownPart implements Part { } /// A [Part] with the text content. -final class TextPart implements Part { +final class TextPart extends Part { // ignore: public_member_api_docs - TextPart(this.text); + const TextPart(this.text, {bool? isThought}) + : super( + isThought: isThought, + thoughtSignature: null, + ); + const TextPart._( + this.text, { + bool? isThought, + String? thoughtSignature, + }) : super( + isThought: isThought, + thoughtSignature: thoughtSignature, + ); /// The text content of the [Part] final String text; @@ -153,9 +188,27 @@ final class TextPart implements Part { } /// A [Part] with the byte content of a file. -final class InlineDataPart implements Part { +final class InlineDataPart extends Part { // ignore: public_member_api_docs - InlineDataPart(this.mimeType, this.bytes, {this.willContinue}); + const InlineDataPart( + this.mimeType, + this.bytes, { + this.willContinue, + bool? isThought, + }) : super( + isThought: isThought, + thoughtSignature: null, + ); + const InlineDataPart._( + this.mimeType, + this.bytes, { + this.willContinue, + bool? isThought, + String? thoughtSignature, + }) : super( + isThought: isThought, + thoughtSignature: thoughtSignature, + ); /// File type of the [InlineDataPart]. /// https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/send-multimodal-prompts#media_requirements @@ -186,9 +239,27 @@ final class InlineDataPart implements Part { /// A predicted `FunctionCall` returned from the model that contains /// a string representing the `FunctionDeclaration.name` with the /// arguments and their values. -final class FunctionCall implements Part { +final class FunctionCall extends Part { // ignore: public_member_api_docs - FunctionCall(this.name, this.args, {this.id}); + const FunctionCall( + this.name, + this.args, { + this.id, + bool? isThought, + }) : super( + isThought: isThought, + thoughtSignature: null, + ); + const FunctionCall._( + this.name, + this.args, { + this.id, + bool? isThought, + String? thoughtSignature, + }) : super( + isThought: isThought, + thoughtSignature: thoughtSignature, + ); /// The name of the function to call. final String name; @@ -213,9 +284,17 @@ final class FunctionCall implements Part { } /// The response class for [FunctionCall] -final class FunctionResponse implements Part { +final class FunctionResponse extends Part { // ignore: public_member_api_docs - FunctionResponse(this.name, this.response, {this.id}); + const FunctionResponse( + this.name, + this.response, { + this.id, + bool? isThought, + }) : super( + isThought: isThought, + thoughtSignature: null, + ); /// The name of the function that was called. final String name; @@ -242,9 +321,25 @@ final class FunctionResponse implements Part { } /// A [Part] with Firebase Storage uri as prompt content -final class FileData implements Part { +final class FileData extends Part { // ignore: public_member_api_docs - FileData(this.mimeType, this.fileUri); + const FileData( + this.mimeType, + this.fileUri, { + bool? isThought, + }) : super( + isThought: isThought, + thoughtSignature: null, + ); + const FileData._( + this.mimeType, + this.fileUri, { + bool? isThought, + String? thoughtSignature, + }) : super( + isThought: isThought, + thoughtSignature: thoughtSignature, + ); /// File type of the [FileData]. /// https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/send-multimodal-prompts#media_requirements diff --git a/packages/firebase_ai/firebase_ai/lib/src/developer/api.dart b/packages/firebase_ai/firebase_ai/lib/src/developer/api.dart index 59d587fae559..d0357e2fa07c 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/developer/api.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/developer/api.dart @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'dart:convert'; - import '../api.dart' show BlockReason, @@ -39,8 +37,7 @@ import '../api.dart' UsageMetadata, WebGroundingChunk, createUsageMetadata; -import '../content.dart' - show Content, FunctionCall, InlineDataPart, Part, TextPart; +import '../content.dart' show Content, parseContent; import '../error.dart'; import '../tool.dart' show Tool, ToolConfig; @@ -191,7 +188,7 @@ Candidate _parseCandidate(Object? jsonObject) { return Candidate( jsonObject.containsKey('content') - ? _parseGoogleAIContent(jsonObject['content'] as Object) + ? parseContent(jsonObject['content'] as Object) : Content(null, []), switch (jsonObject) { {'safetyRatings': final List safetyRatings} => @@ -420,35 +417,3 @@ SearchEntryPoint _parseSearchEntryPoint(Object? jsonObject) { renderedContent: renderedContent, ); } - -Content _parseGoogleAIContent(Object jsonObject) { - return switch (jsonObject) { - {'parts': final List parts} => Content( - switch (jsonObject) { - {'role': final String role} => role, - _ => null, - }, - parts.map(_parsePart).toList()), - _ => throw unhandledFormat('Content', jsonObject), - }; -} - -Part _parsePart(Object? jsonObject) { - return switch (jsonObject) { - {'text': final String text} => TextPart(text), - { - 'functionCall': { - 'name': final String name, - 'args': final Map args - } - } => - FunctionCall(name, args), - { - 'functionResponse': {'name': String _, 'response': Map _} - } => - throw UnimplementedError('FunctionResponse part not yet supported'), - {'inlineData': {'mimeType': String mimeType, 'data': String bytes}} => - InlineDataPart(mimeType, base64Decode(bytes)), - _ => throw unhandledFormat('Part', jsonObject), - }; -} diff --git a/packages/firebase_ai/firebase_ai/test/api_test.dart b/packages/firebase_ai/firebase_ai/test/api_test.dart index a1c05dcd751d..c59bf4f03933 100644 --- a/packages/firebase_ai/firebase_ai/test/api_test.dart +++ b/packages/firebase_ai/firebase_ai/test/api_test.dart @@ -48,7 +48,7 @@ void main() { final candidateWithText = Candidate(textContent, null, null, FinishReason.stop, null); final candidateWithMultipleTextParts = Candidate( - Content('model', [TextPart('Hello'), TextPart(' World')]), + Content('model', [const TextPart('Hello'), const TextPart(' World')]), null, null, FinishReason.stop, @@ -223,8 +223,8 @@ void main() { }); test('concatenates text from multiple TextParts', () { - final multiPartContent = - Content('model', [TextPart('Part 1'), TextPart('. Part 2')]); + final multiPartContent = Content( + 'model', [const TextPart('Part 1'), const TextPart('. Part 2')]); final candidate = Candidate(multiPartContent, null, null, FinishReason.stop, null); expect(candidate.text, 'Part 1. Part 2'); diff --git a/packages/firebase_ai/firebase_ai/test/chat_test.dart b/packages/firebase_ai/firebase_ai/test/chat_test.dart index ab5819f0f12a..f2104aeadc4f 100644 --- a/packages/firebase_ai/firebase_ai/test/chat_test.dart +++ b/packages/firebase_ai/firebase_ai/test/chat_test.dart @@ -50,7 +50,7 @@ void main() { final (client, model) = createModel('models/$defaultModelName'); final chat = model.startChat(history: [ Content.text('Hi!'), - Content.model([TextPart('Hello, how can I help you today?')]), + Content.model([const TextPart('Hello, how can I help you today?')]), ]); const prompt = 'Some prompt'; final response = await client.checkRequest( diff --git a/packages/firebase_ai/firebase_ai/test/content_test.dart b/packages/firebase_ai/firebase_ai/test/content_test.dart index 59a68bd6a198..7b949f2fc429 100644 --- a/packages/firebase_ai/firebase_ai/test/content_test.dart +++ b/packages/firebase_ai/firebase_ai/test/content_test.dart @@ -25,7 +25,7 @@ void main() { group('Content tests', () { test('constructor', () { final content = Content('user', - [TextPart('Test'), InlineDataPart('image/png', Uint8List(0))]); + [const TextPart('Test'), InlineDataPart('image/png', Uint8List(0))]); expect(content.role, 'user'); expect(content.parts[0], isA()); expect((content.parts[0] as TextPart).text, 'Test'); @@ -35,7 +35,7 @@ void main() { }); test('text()', () { - final content = Content('user', [TextPart('Test')]); + final content = Content('user', [const TextPart('Test')]); expect(content.role, 'user'); expect(content.parts[0], isA()); }); @@ -48,7 +48,7 @@ void main() { test('multi()', () { final content = Content('user', - [TextPart('Test'), InlineDataPart('image/png', Uint8List(0))]); + [const TextPart('Test'), InlineDataPart('image/png', Uint8List(0))]); expect(content.parts.length, 2); expect(content.parts[0], isA()); expect(content.parts[1], isA()); @@ -56,7 +56,7 @@ void main() { test('toJson', () { final content = Content('user', - [TextPart('Test'), InlineDataPart('image/png', Uint8List(0))]); + [const TextPart('Test'), InlineDataPart('image/png', Uint8List(0))]); final json = content.toJson(); expect(json['role'], 'user'); expect((json['parts']! as List).length, 2); @@ -83,7 +83,7 @@ void main() { group('Part tests', () { test('TextPart toJson', () { - final part = TextPart('Test'); + const part = TextPart('Test'); final json = part.toJson(); expect((json as Map)['text'], 'Test'); }); @@ -96,7 +96,7 @@ void main() { }); test('FunctionCall toJson', () { - final part = FunctionCall( + const part = FunctionCall( 'myFunction', { 'arguments': [ @@ -132,7 +132,7 @@ void main() { }); test('FileData toJson', () { - final part = FileData('image/png', 'gs://bucket-name/path'); + const part = FileData('image/png', 'gs://bucket-name/path'); final json = part.toJson(); expect((json as Map)['file_data']['mime_type'], 'image/png'); expect(json['file_data']['file_uri'], 'gs://bucket-name/path'); diff --git a/packages/firebase_ai/firebase_ai/test/google_ai_generative_model_test.dart b/packages/firebase_ai/firebase_ai/test/google_ai_generative_model_test.dart index 1f0f4c902e79..9b5c960b2a9b 100644 --- a/packages/firebase_ai/firebase_ai/test/google_ai_generative_model_test.dart +++ b/packages/firebase_ai/firebase_ai/test/google_ai_generative_model_test.dart @@ -138,7 +138,7 @@ void main() { matchesGenerateContentResponse( GenerateContentResponse([ Candidate( - Content('model', [TextPart(result)]), + Content('model', [const TextPart(result)]), null, null, null, diff --git a/packages/firebase_ai/firebase_ai/test/google_ai_response_parsing_test.dart b/packages/firebase_ai/firebase_ai/test/google_ai_response_parsing_test.dart index eec1b98938b4..27527ff2dc44 100644 --- a/packages/firebase_ai/firebase_ai/test/google_ai_response_parsing_test.dart +++ b/packages/firebase_ai/firebase_ai/test/google_ai_response_parsing_test.dart @@ -196,7 +196,7 @@ void main() { [ Candidate( Content.model([ - TextPart('Mountain View, California, United States'), + const TextPart('Mountain View, California, United States'), ]), [ SafetyRating( @@ -329,7 +329,7 @@ void main() { GenerateContentResponse( [ Candidate( - Content.model([TextPart('placeholder')]), + Content.model([const TextPart('placeholder')]), [ SafetyRating( HarmCategory.sexuallyExplicit, @@ -464,7 +464,7 @@ void main() { GenerateContentResponse( [ Candidate( - Content.model([TextPart('placeholder')]), + Content.model([const TextPart('placeholder')]), [ SafetyRating( HarmCategory.sexuallyExplicit, @@ -551,7 +551,7 @@ void main() { Content.model([ // ExecutableCode(Language.python, 'print(\'hello world\')'), // CodeExecutionResult(Outcome.ok, 'hello world'), - TextPart('hello world') + const TextPart('hello world') ]), [], null, diff --git a/packages/firebase_ai/firebase_ai/test/live_test.dart b/packages/firebase_ai/firebase_ai/test/live_test.dart index 388d112a3537..6f013c1dc486 100644 --- a/packages/firebase_ai/firebase_ai/test/live_test.dart +++ b/packages/firebase_ai/firebase_ai/test/live_test.dart @@ -84,7 +84,7 @@ void main() { }); test('LiveServerToolCall constructor and properties', () { - final functionCall = FunctionCall('test', {}); + const functionCall = FunctionCall('test', {}); final message = LiveServerToolCall(functionCalls: [functionCall]); expect(message.functionCalls, [functionCall]); @@ -149,7 +149,7 @@ void main() { }); test('LiveClientToolResponse toJson() returns correct JSON', () { - final response = FunctionResponse('test', {}); + const response = FunctionResponse('test', {}); final message = LiveClientToolResponse(functionResponses: [response]); expect(message.toJson(), { 'toolResponse': { diff --git a/packages/firebase_ai/firebase_ai/test/model_test.dart b/packages/firebase_ai/firebase_ai/test/model_test.dart index 860b8e19ba7c..b41a32f7a9cd 100644 --- a/packages/firebase_ai/firebase_ai/test/model_test.dart +++ b/packages/firebase_ai/firebase_ai/test/model_test.dart @@ -123,7 +123,7 @@ void main() { matchesGenerateContentResponse( GenerateContentResponse([ Candidate( - Content('model', [TextPart(result)]), + Content('model', [const TextPart(result)]), null, null, null, diff --git a/packages/firebase_ai/firebase_ai/test/response_parsing_test.dart b/packages/firebase_ai/firebase_ai/test/response_parsing_test.dart index 1414a5e3c50a..0d37e79c1f02 100644 --- a/packages/firebase_ai/firebase_ai/test/response_parsing_test.dart +++ b/packages/firebase_ai/firebase_ai/test/response_parsing_test.dart @@ -287,7 +287,7 @@ void main() { [ Candidate( Content.model([ - TextPart('Mountain View, California, United States'), + const TextPart('Mountain View, California, United States'), ]), [ SafetyRating( @@ -420,7 +420,7 @@ void main() { GenerateContentResponse( [ Candidate( - Content.model([TextPart('placeholder')]), + Content.model([const TextPart('placeholder')]), [ SafetyRating( HarmCategory.sexuallyExplicit, @@ -555,7 +555,7 @@ void main() { GenerateContentResponse( [ Candidate( - Content.model([TextPart('placeholder')]), + Content.model([const TextPart('placeholder')]), [ SafetyRating( HarmCategory.sexuallyExplicit, diff --git a/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/audio_page.dart b/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/audio_page.dart index 63862e18f0e3..235022bf1338 100644 --- a/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/audio_page.dart +++ b/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/audio_page.dart @@ -98,15 +98,14 @@ class _AudioPageState extends State { Future _submitAudioToModel(audioPart) async { try { - String textPrompt = 'What is in the audio recording?'; - final prompt = TextPart('What is in the audio recording?'); + const textPrompt = 'What is in the audio recording?'; setState(() { _messages.add(MessageData(text: textPrompt, fromUser: true)); }); final response = await widget.model.generateContent([ - Content.multi([prompt, audioPart]), + Content.multi([const TextPart(textPrompt), audioPart]), ]); setState(() { diff --git a/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/document.dart b/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/document.dart index ff98680f429d..65eceb62ed2e 100644 --- a/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/document.dart +++ b/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/document.dart @@ -47,8 +47,6 @@ class _DocumentPageState extends State { const _prompt = 'Write me a summary in one sentence what this document is about.'; - final prompt = TextPart(_prompt); - setState(() { _messages.add(MessageData(text: _prompt, fromUser: true)); }); @@ -57,7 +55,7 @@ class _DocumentPageState extends State { InlineDataPart('application/pdf', docBytes.buffer.asUint8List()); final response = await widget.model.generateContent([ - Content.multi([prompt, pdfPart]), + Content.multi([const TextPart(_prompt), pdfPart]), ]); setState(() { diff --git a/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/image_prompt_page.dart b/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/image_prompt_page.dart index 0d84c5941c03..a34d55887553 100644 --- a/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/image_prompt_page.dart +++ b/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/image_prompt_page.dart @@ -184,7 +184,7 @@ class _ImagePromptPageState extends State { final content = [ Content.multi([ TextPart(message), - FileData( + const FileData( 'image/jpeg', 'gs://vertex-ai-example-ef5a2.appspot.com/foodpic.jpg', ), diff --git a/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/video_page.dart b/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/video_page.dart index 532b981385f7..e214114d7ebc 100644 --- a/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/video_page.dart +++ b/packages/firebase_vertexai/firebase_vertexai/example/lib/pages/video_page.dart @@ -46,8 +46,6 @@ class _VideoPageState extends State { const _prompt = 'Can you tell me what is in the video?'; - final prompt = TextPart(_prompt); - setState(() { _messages.add(MessageData(text: _prompt, fromUser: true)); }); @@ -56,7 +54,7 @@ class _VideoPageState extends State { InlineDataPart('video/mp4', videoBytes.buffer.asUint8List()); final response = await widget.model.generateContent([ - Content.multi([prompt, videoPart]), + Content.multi([const TextPart(_prompt), videoPart]), ]); setState(() { diff --git a/packages/firebase_vertexai/firebase_vertexai/test/api_test.dart b/packages/firebase_vertexai/firebase_vertexai/test/api_test.dart index 0da8522fd67e..e11927ef3497 100644 --- a/packages/firebase_vertexai/firebase_vertexai/test/api_test.dart +++ b/packages/firebase_vertexai/firebase_vertexai/test/api_test.dart @@ -45,7 +45,7 @@ void main() { final candidateWithText = Candidate(textContent, null, null, FinishReason.stop, null); final candidateWithMultipleTextParts = Candidate( - Content('model', [TextPart('Hello'), TextPart(' World')]), + Content('model', [const TextPart('Hello'), const TextPart(' World')]), null, null, FinishReason.stop, @@ -220,8 +220,13 @@ void main() { }); test('concatenates text from multiple TextParts', () { - final multiPartContent = - Content('model', [TextPart('Part 1'), TextPart('. Part 2')]); + final multiPartContent = Content( + 'model', + [ + const TextPart('Part 1'), + const TextPart('. Part 2'), + ], + ); final candidate = Candidate(multiPartContent, null, null, FinishReason.stop, null); expect(candidate.text, 'Part 1. Part 2'); diff --git a/packages/firebase_vertexai/firebase_vertexai/test/chat_test.dart b/packages/firebase_vertexai/firebase_vertexai/test/chat_test.dart index 026d14dca580..1222c079a8ac 100644 --- a/packages/firebase_vertexai/firebase_vertexai/test/chat_test.dart +++ b/packages/firebase_vertexai/firebase_vertexai/test/chat_test.dart @@ -50,7 +50,7 @@ void main() { final (client, model) = createModel('models/$defaultModelName'); final chat = model.startChat(history: [ Content.text('Hi!'), - Content.model([TextPart('Hello, how can I help you today?')]), + Content.model([const TextPart('Hello, how can I help you today?')]), ]); const prompt = 'Some prompt'; final response = await client.checkRequest( diff --git a/packages/firebase_vertexai/firebase_vertexai/test/content_test.dart b/packages/firebase_vertexai/firebase_vertexai/test/content_test.dart index 59a68bd6a198..38c421a38ccd 100644 --- a/packages/firebase_vertexai/firebase_vertexai/test/content_test.dart +++ b/packages/firebase_vertexai/firebase_vertexai/test/content_test.dart @@ -25,7 +25,7 @@ void main() { group('Content tests', () { test('constructor', () { final content = Content('user', - [TextPart('Test'), InlineDataPart('image/png', Uint8List(0))]); + [const TextPart('Test'), InlineDataPart('image/png', Uint8List(0))]); expect(content.role, 'user'); expect(content.parts[0], isA()); expect((content.parts[0] as TextPart).text, 'Test'); @@ -35,7 +35,7 @@ void main() { }); test('text()', () { - final content = Content('user', [TextPart('Test')]); + final content = Content('user', [const TextPart('Test')]); expect(content.role, 'user'); expect(content.parts[0], isA()); }); @@ -48,7 +48,7 @@ void main() { test('multi()', () { final content = Content('user', - [TextPart('Test'), InlineDataPart('image/png', Uint8List(0))]); + [const TextPart('Test'), InlineDataPart('image/png', Uint8List(0))]); expect(content.parts.length, 2); expect(content.parts[0], isA()); expect(content.parts[1], isA()); @@ -56,7 +56,7 @@ void main() { test('toJson', () { final content = Content('user', - [TextPart('Test'), InlineDataPart('image/png', Uint8List(0))]); + [const TextPart('Test'), InlineDataPart('image/png', Uint8List(0))]); final json = content.toJson(); expect(json['role'], 'user'); expect((json['parts']! as List).length, 2); @@ -83,7 +83,7 @@ void main() { group('Part tests', () { test('TextPart toJson', () { - final part = TextPart('Test'); + const part = TextPart('Test'); final json = part.toJson(); expect((json as Map)['text'], 'Test'); }); @@ -96,7 +96,7 @@ void main() { }); test('FunctionCall toJson', () { - final part = FunctionCall( + const part = FunctionCall( 'myFunction', { 'arguments': [ @@ -132,7 +132,7 @@ void main() { }); test('FileData toJson', () { - final part = FileData('image/png', 'gs://bucket-name/path'); + const part = FileData('image/png', 'gs://bucket-name/path'); final json = part.toJson(); expect((json as Map)['file_data']['mime_type'], 'image/png'); expect(json['file_data']['file_uri'], 'gs://bucket-name/path'); @@ -188,7 +188,7 @@ void main() { expect(result, isA()); final inlineData = result as InlineDataPart; expect(inlineData.mimeType, 'image/png'); - expect(inlineData.bytes, [1, 2, 3]); + expect(inlineData.bytes, const [1, 2, 3]); }); test('returns UnknownPart for functionResponse', () { diff --git a/packages/firebase_vertexai/firebase_vertexai/test/google_ai_generative_model_test.dart b/packages/firebase_vertexai/firebase_vertexai/test/google_ai_generative_model_test.dart index 9be8a316a11d..b2e1968cea81 100644 --- a/packages/firebase_vertexai/firebase_vertexai/test/google_ai_generative_model_test.dart +++ b/packages/firebase_vertexai/firebase_vertexai/test/google_ai_generative_model_test.dart @@ -138,7 +138,7 @@ void main() { matchesGenerateContentResponse( GenerateContentResponse([ Candidate( - Content('model', [TextPart(result)]), + Content('model', [const TextPart(result)]), null, null, null, @@ -334,7 +334,7 @@ void main() { }, response: arbitraryGenerateContentResponse, ); - }, skip: 'No support for code executation'); + }, skip: 'No support for code execution'); test('can override code execution', () async { final (client, model) = createModel(); diff --git a/packages/firebase_vertexai/firebase_vertexai/test/google_ai_response_parsing_test.dart b/packages/firebase_vertexai/firebase_vertexai/test/google_ai_response_parsing_test.dart index 76dc0adebf65..ba6277444ea0 100644 --- a/packages/firebase_vertexai/firebase_vertexai/test/google_ai_response_parsing_test.dart +++ b/packages/firebase_vertexai/firebase_vertexai/test/google_ai_response_parsing_test.dart @@ -196,7 +196,7 @@ void main() { [ Candidate( Content.model([ - TextPart('Mountain View, California, United States'), + const TextPart('Mountain View, California, United States'), ]), [ SafetyRating( @@ -329,7 +329,7 @@ void main() { GenerateContentResponse( [ Candidate( - Content.model([TextPart('placeholder')]), + Content.model([const TextPart('placeholder')]), [ SafetyRating( HarmCategory.sexuallyExplicit, @@ -464,7 +464,7 @@ void main() { GenerateContentResponse( [ Candidate( - Content.model([TextPart('placeholder')]), + Content.model([const TextPart('placeholder')]), [ SafetyRating( HarmCategory.sexuallyExplicit, @@ -551,7 +551,7 @@ void main() { Content.model([ // ExecutableCode(Language.python, 'print(\'hello world\')'), // CodeExecutionResult(Outcome.ok, 'hello world'), - TextPart('hello world') + const TextPart('hello world') ]), [], null, diff --git a/packages/firebase_vertexai/firebase_vertexai/test/live_test.dart b/packages/firebase_vertexai/firebase_vertexai/test/live_test.dart index beb7bfe3fb3c..526cb19a77de 100644 --- a/packages/firebase_vertexai/firebase_vertexai/test/live_test.dart +++ b/packages/firebase_vertexai/firebase_vertexai/test/live_test.dart @@ -85,7 +85,7 @@ void main() { }); test('LiveServerToolCall constructor and properties', () { - final functionCall = FunctionCall('test', {}); + const functionCall = FunctionCall('test', {}); final message = LiveServerToolCall(functionCalls: [functionCall]); expect(message.functionCalls, [functionCall]); @@ -150,7 +150,7 @@ void main() { }); test('LiveClientToolResponse toJson() returns correct JSON', () { - final response = FunctionResponse('test', {}); + const response = FunctionResponse('test', {}); final message = LiveClientToolResponse(functionResponses: [response]); expect(message.toJson(), { 'toolResponse': { diff --git a/packages/firebase_vertexai/firebase_vertexai/test/model_test.dart b/packages/firebase_vertexai/firebase_vertexai/test/model_test.dart index a0727913a68b..1079e00cbbed 100644 --- a/packages/firebase_vertexai/firebase_vertexai/test/model_test.dart +++ b/packages/firebase_vertexai/firebase_vertexai/test/model_test.dart @@ -123,7 +123,7 @@ void main() { matchesGenerateContentResponse( GenerateContentResponse([ Candidate( - Content('model', [TextPart(result)]), + Content('model', [const TextPart(result)]), null, null, null, diff --git a/packages/firebase_vertexai/firebase_vertexai/test/response_parsing_test.dart b/packages/firebase_vertexai/firebase_vertexai/test/response_parsing_test.dart index 97b2d580877c..7af47214b881 100644 --- a/packages/firebase_vertexai/firebase_vertexai/test/response_parsing_test.dart +++ b/packages/firebase_vertexai/firebase_vertexai/test/response_parsing_test.dart @@ -287,7 +287,7 @@ void main() { [ Candidate( Content.model([ - TextPart('Mountain View, California, United States'), + const TextPart('Mountain View, California, United States'), ]), [ SafetyRating( @@ -420,7 +420,7 @@ void main() { GenerateContentResponse( [ Candidate( - Content.model([TextPart('placeholder')]), + Content.model([const TextPart('placeholder')]), [ SafetyRating( HarmCategory.sexuallyExplicit, @@ -555,7 +555,7 @@ void main() { GenerateContentResponse( [ Candidate( - Content.model([TextPart('placeholder')]), + Content.model([const TextPart('placeholder')]), [ SafetyRating( HarmCategory.sexuallyExplicit,