From 14ebdce80749e78319a9ee7983ba40f448ec41cd Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Fri, 29 Aug 2025 14:13:55 -0700 Subject: [PATCH 01/11] init setup --- .../firebase_ai/lib/src/base_model.dart | 64 ++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart index 9c28e62736be..59386aacf5cb 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart @@ -57,10 +57,16 @@ enum Task { predict, } +enum TemplateTask { + /// Request type for server template generate content. + templateGenerateContent, +} + abstract interface class _ModelUri { String get baseAuthority; String get apiVersion; Uri taskUri(Task task); + Uri templateTaskUri(TemplateTask task, String templateId); ({String prefix, String name}) get model; } @@ -70,7 +76,8 @@ final class _VertexUri implements _ModelUri { required String location, required FirebaseApp app}) : model = _normalizeModelName(model), - _projectUri = _vertexUri(app, location); + _projectUri = _vertexUri(app, location), + _templateUri = _vertexTemplateUri(app, location); static const _baseAuthority = 'firebasevertexai.googleapis.com'; static const _apiVersion = 'v1beta'; @@ -93,7 +100,24 @@ final class _VertexUri implements _ModelUri { ); } + static String _vertexTemplateId( + FirebaseApp app, String location, String templateId) { + var projectId = app.options.projectId; + return 'projects/$projectId/locations/$location/templates/$templateId'; + } + + static Uri _vertexTemplateUri(FirebaseApp app, String location) { + var projectId = app.options.projectId; + return Uri.https( + _baseAuthority, + '/$_apiVersion/projects/$projectId/locations/$location', + ); + } + final Uri _projectUri; + + final Uri _templateUri; + @override final ({String prefix, String name}) model; @@ -109,6 +133,13 @@ final class _VertexUri implements _ModelUri { pathSegments: _projectUri.pathSegments .followedBy([model.prefix, '${model.name}:${task.name}'])); } + + @override + Uri templateTaskUri(TemplateTask task, String templateId) { + return _projectUri.replace( + pathSegments: _projectUri.pathSegments + .followedBy([model.prefix, '${model.name}:${task.name}'])); + } } final class _GoogleAIUri implements _ModelUri { @@ -116,7 +147,8 @@ final class _GoogleAIUri implements _ModelUri { required String model, required FirebaseApp app, }) : model = _normalizeModelName(model), - _baseUri = _googleAIBaseUri(app: app); + _baseUri = _googleAIBaseUri(app: app), + _baseTemplateUri = _googleAIBaseTemplateUri(app: app); /// Returns the model code for a user friendly model name. /// @@ -130,12 +162,21 @@ final class _GoogleAIUri implements _ModelUri { static const _apiVersion = 'v1beta'; static const _baseAuthority = 'firebasevertexai.googleapis.com'; + static Uri _googleAIBaseUri( {String apiVersion = _apiVersion, required FirebaseApp app}) => Uri.https( _baseAuthority, '$apiVersion/projects/${app.options.projectId}'); + + static Uri _googleAIBaseTemplateUri( + {String apiVersion = _apiVersion, required FirebaseApp app}) => + Uri.https( + _baseAuthority, '$apiVersion/projects/${app.options.projectId}'); + final Uri _baseUri; + final Uri _baseTemplateUri; + @override final ({String prefix, String name}) model; @@ -149,6 +190,13 @@ final class _GoogleAIUri implements _ModelUri { Uri taskUri(Task task) => _baseUri.replace( pathSegments: _baseUri.pathSegments .followedBy([model.prefix, '${model.name}:${task.name}'])); + + @override + Uri templateTaskUri(TemplateTask task, String templateId) { + return _baseTemplateUri.replace( + pathSegments: _baseTemplateUri.pathSegments + .followedBy([model.prefix, '${model.name}:${task.name}'])); + } } /// Base class for models. @@ -202,6 +250,9 @@ abstract class BaseModel { /// Returns a URI for the given [task]. Uri taskUri(Task task) => _modelUri.taskUri(task); + + Uri templateTaskUri(TemplateTask task, String templateId) => + _modelUri.templateTaskUri(task, templateId); } /// An abstract base class for models that interact with an API using an [ApiClient]. @@ -230,4 +281,13 @@ abstract class BaseApiClientModel extends BaseModel { Future makeRequest(Task task, Map params, T Function(Map) parse) => _client.makeRequest(taskUri(task), params).then(parse); + + Future makeTemplateRequest( + TemplateTask task, + String templateId, + Map params, + T Function(Map) parse) => + _client + .makeRequest(templateTaskUri(task, templateId), params) + .then(parse); } From b05f94751aab5cd124167243a0bbdad0a047482b Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Tue, 2 Sep 2025 16:53:28 -0700 Subject: [PATCH 02/11] add the api --- .../firebase_ai/lib/src/base_model.dart | 50 +++++++++++-------- .../firebase_ai/lib/src/generative_model.dart | 7 +++ 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart index 59386aacf5cb..1ade10a4a5be 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart @@ -67,6 +67,7 @@ abstract interface class _ModelUri { String get apiVersion; Uri taskUri(Task task); Uri templateTaskUri(TemplateTask task, String templateId); + String templateName(String templateId); ({String prefix, String name}) get model; } @@ -100,12 +101,6 @@ final class _VertexUri implements _ModelUri { ); } - static String _vertexTemplateId( - FirebaseApp app, String location, String templateId) { - var projectId = app.options.projectId; - return 'projects/$projectId/locations/$location/templates/$templateId'; - } - static Uri _vertexTemplateUri(FirebaseApp app, String location) { var projectId = app.options.projectId; return Uri.https( @@ -136,10 +131,17 @@ final class _VertexUri implements _ModelUri { @override Uri templateTaskUri(TemplateTask task, String templateId) { - return _projectUri.replace( - pathSegments: _projectUri.pathSegments - .followedBy([model.prefix, '${model.name}:${task.name}'])); + return _templateUri.replace( + pathSegments: _templateUri.pathSegments + .followedBy(['templates', '$templateId:${task.name}'])); } + + @override + String templateName(String templateId) => _templateUri + .replace( + pathSegments: + _templateUri.pathSegments.followedBy(['templates', templateId])) + .toString(); } final class _GoogleAIUri implements _ModelUri { @@ -193,10 +195,17 @@ final class _GoogleAIUri implements _ModelUri { @override Uri templateTaskUri(TemplateTask task, String templateId) { - return _baseTemplateUri.replace( - pathSegments: _baseTemplateUri.pathSegments - .followedBy([model.prefix, '${model.name}:${task.name}'])); + return _baseUri.replace( + pathSegments: _baseUri.pathSegments + .followedBy(['templates', '$templateId:${task.name}'])); } + + @override + String templateName(String templateId) => _baseTemplateUri + .replace( + pathSegments: _baseTemplateUri.pathSegments + .followedBy(['templates', templateId])) + .toString(); } /// Base class for models. @@ -253,6 +262,8 @@ abstract class BaseModel { Uri templateTaskUri(TemplateTask task, String templateId) => _modelUri.templateTaskUri(task, templateId); + + String templateName(String templateId) => _modelUri.templateName(templateId); } /// An abstract base class for models that interact with an API using an [ApiClient]. @@ -282,12 +293,11 @@ abstract class BaseApiClientModel extends BaseModel { T Function(Map) parse) => _client.makeRequest(taskUri(task), params).then(parse); - Future makeTemplateRequest( - TemplateTask task, - String templateId, - Map params, - T Function(Map) parse) => - _client - .makeRequest(templateTaskUri(task, templateId), params) - .then(parse); + Future makeTemplateRequest(TemplateTask task, String templateId, + Map params, T Function(Map) parse) { + params['name'] = templateName(templateId); + return _client + .makeRequest(templateTaskUri(task, templateId), params) + .then(parse); + } } diff --git a/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart b/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart index 4f570e9446d6..68f19d473dfd 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart @@ -194,6 +194,13 @@ final class GenerativeModel extends BaseApiClientModel { return makeRequest(Task.countTokens, parameters, _serializationStrategy.parseCountTokensResponse); } + + Future templateGenerateContent( + String templateId, + Map params, + ) => + makeTemplateRequest(TemplateTask.templateGenerateContent, templateId, + params, _serializationStrategy.parseGenerateContentResponse); } /// Returns a [GenerativeModel] using it's private constructor. From 46762abc8c312908f3b257ca7a985b78f1e5d18c Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Tue, 2 Sep 2025 22:36:24 -0700 Subject: [PATCH 03/11] Add chat page test case --- .../example/lib/pages/chat_page.dart | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) 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 388cc76572d1..b13e4d5e0759 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 @@ -138,6 +138,18 @@ class _ChatPageState extends State { const SizedBox.square( dimension: 15, ), + if (!_loading) + IconButton( + onPressed: () async { + await _sendServerTemplateMessage(_textController.text); + }, + icon: Icon( + Icons.ten_mp, + color: Theme.of(context).colorScheme.primary, + ), + ) + else + const CircularProgressIndicator(), if (!_loading) IconButton( onPressed: () async { @@ -200,6 +212,51 @@ class _ChatPageState extends State { } } + Future _sendServerTemplateMessage(String templatePrompt) async { + setState(() { + _loading = true; + }); + + try { + var response = await _model?.templateGenerateContent( + 'greeting.prompt', + { + 'inputs': { + 'name': 'lily', + 'language': 'Chinese', + }, + }, + ); + + var text = response?.text; + var image = response?.inlineDataParts.first; + _messages.add( + MessageData(text: text, imageBytes: image?.bytes, fromUser: false), + ); + + if (text == null && image == null) { + _showError('No response from API.'); + return; + } else { + setState(() { + _loading = false; + _scrollDown(); + }); + } + } catch (e) { + _showError(e.toString()); + setState(() { + _loading = false; + }); + } finally { + _textController.clear(); + setState(() { + _loading = false; + }); + _textFieldFocus.requestFocus(); + } + } + void _showError(String message) { showDialog( context: context, From e632bb8bc3735232a3ae202ff2ac14aa69f90521 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Thu, 4 Sep 2025 22:13:32 -0700 Subject: [PATCH 04/11] add template imagen generate --- .../example/lib/pages/chat_page.dart | 9 ++--- .../firebase_ai/lib/src/base_model.dart | 39 ++++++++++++------- .../firebase_ai/lib/src/generative_model.dart | 3 ++ .../lib/src/imagen/imagen_model.dart | 14 +++++++ 4 files changed, 47 insertions(+), 18 deletions(-) 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 b13e4d5e0759..724c9c341046 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 @@ -222,19 +222,18 @@ class _ChatPageState extends State { 'greeting.prompt', { 'inputs': { - 'name': 'lily', - 'language': 'Chinese', + 'name': templatePrompt, }, }, ); var text = response?.text; - var image = response?.inlineDataParts.first; + _messages.add( - MessageData(text: text, imageBytes: image?.bytes, fromUser: false), + MessageData(text: text, fromUser: false), ); - if (text == null && image == null) { + if (text == null) { _showError('No response from API.'); return; } else { diff --git a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart index 1ade10a4a5be..5e70a948e3e6 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart @@ -60,6 +60,9 @@ enum Task { enum TemplateTask { /// Request type for server template generate content. templateGenerateContent, + + /// Request type for server template for Prediction Services like Imagen. + templatePredict, } abstract interface class _ModelUri { @@ -78,7 +81,8 @@ final class _VertexUri implements _ModelUri { required FirebaseApp app}) : model = _normalizeModelName(model), _projectUri = _vertexUri(app, location), - _templateUri = _vertexTemplateUri(app, location); + _templateUri = _vertexTemplateUri(app, location), + _templateName = _vertexTemplateName(app, location); static const _baseAuthority = 'firebasevertexai.googleapis.com'; static const _apiVersion = 'v1beta'; @@ -109,10 +113,17 @@ final class _VertexUri implements _ModelUri { ); } + static String _vertexTemplateName(FirebaseApp app, String location) { + var projectId = app.options.projectId; + return 'projects/$projectId/locations/$location'; + } + final Uri _projectUri; final Uri _templateUri; + final String _templateName; + @override final ({String prefix, String name}) model; @@ -137,11 +148,8 @@ final class _VertexUri implements _ModelUri { } @override - String templateName(String templateId) => _templateUri - .replace( - pathSegments: - _templateUri.pathSegments.followedBy(['templates', templateId])) - .toString(); + String templateName(String templateId) => + '$_templateName/templates/$templateId'; } final class _GoogleAIUri implements _ModelUri { @@ -150,7 +158,8 @@ final class _GoogleAIUri implements _ModelUri { required FirebaseApp app, }) : model = _normalizeModelName(model), _baseUri = _googleAIBaseUri(app: app), - _baseTemplateUri = _googleAIBaseTemplateUri(app: app); + _baseTemplateUri = _googleAIBaseTemplateUri(app: app), + _baseTemplateName = _googleAIBaseTemplateName(app: app); /// Returns the model code for a user friendly model name. /// @@ -175,10 +184,15 @@ final class _GoogleAIUri implements _ModelUri { Uri.https( _baseAuthority, '$apiVersion/projects/${app.options.projectId}'); + static String _googleAIBaseTemplateName({required FirebaseApp app}) => + 'projects/${app.options.projectId}'; + final Uri _baseUri; final Uri _baseTemplateUri; + final String _baseTemplateName; + @override final ({String prefix, String name}) model; @@ -201,11 +215,8 @@ final class _GoogleAIUri implements _ModelUri { } @override - String templateName(String templateId) => _baseTemplateUri - .replace( - pathSegments: _baseTemplateUri.pathSegments - .followedBy(['templates', templateId])) - .toString(); + String templateName(String templateId) => + '$_baseTemplateName/templates/$templateId'; } /// Base class for models. @@ -293,9 +304,11 @@ abstract class BaseApiClientModel extends BaseModel { T Function(Map) parse) => _client.makeRequest(taskUri(task), params).then(parse); + /// Make a unary request for [task] with [templateId] and JSON encodable + /// [params]. Future makeTemplateRequest(TemplateTask task, String templateId, Map params, T Function(Map) parse) { - params['name'] = templateName(templateId); + //params['name'] = templateName(templateId); return _client .makeRequest(templateTaskUri(task, templateId), params) .then(parse); diff --git a/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart b/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart index 68f19d473dfd..be53751a28a4 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart @@ -195,6 +195,9 @@ final class GenerativeModel extends BaseApiClientModel { _serializationStrategy.parseCountTokensResponse); } + /// Generates content from a template with the given [templateId] and [params]. + /// + /// Sends a "templateGenerateContent" API request for the configured model. Future templateGenerateContent( String templateId, Map params, diff --git a/packages/firebase_ai/firebase_ai/lib/src/imagen/imagen_model.dart b/packages/firebase_ai/firebase_ai/lib/src/imagen/imagen_model.dart index 2957c056522c..3cc3282ad550 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/imagen/imagen_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/imagen/imagen_model.dart @@ -97,6 +97,20 @@ final class ImagenModel extends BaseApiClientModel { parseImagenGenerationResponse(jsonObject), ); + /// Generates images from a template with the given [templateId] and [params]. + @experimental + Future> templateGenerateImages( + String templateId, + Map params, + ) => + makeTemplateRequest( + TemplateTask.templatePredict, + templateId, + params, + (jsonObject) => + parseImagenGenerationResponse(jsonObject), + ); + /// Generates images with format of [ImagenGCSImage] based on the given /// prompt. /// Note: Keep this API private until future release. From 242c4ff8b1510be769815f126a5ea2cedb8b91cc Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Thu, 18 Sep 2025 13:23:16 -0700 Subject: [PATCH 05/11] add history placeholder --- .../firebase_ai/lib/src/base_model.dart | 8 +++- .../firebase_ai/firebase_ai/lib/src/chat.dart | 37 +++++++++++++++++++ .../firebase_ai/lib/src/generative_model.dart | 10 ++++- .../lib/src/imagen/imagen_model.dart | 1 + 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart index 5e70a948e3e6..123032b030a7 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart @@ -306,8 +306,12 @@ abstract class BaseApiClientModel extends BaseModel { /// Make a unary request for [task] with [templateId] and JSON encodable /// [params]. - Future makeTemplateRequest(TemplateTask task, String templateId, - Map params, T Function(Map) parse) { + Future makeTemplateRequest( + TemplateTask task, + String templateId, + Map params, + Iterable? history, + T Function(Map) parse) { //params['name'] = templateName(templateId); return _client .makeRequest(templateTaskUri(task, templateId), params) diff --git a/packages/firebase_ai/firebase_ai/lib/src/chat.dart b/packages/firebase_ai/firebase_ai/lib/src/chat.dart index fdcbd3cb2920..2c1debab50a8 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/chat.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/chat.dart @@ -166,6 +166,37 @@ final class ChatSession { } } +final class TemplateChatSession { + TemplateChatSession._(this._templateGenerateContent, this._templateId, + this._params, this._history); + + final Future Function(Iterable content, + String templateId, Map params) _templateGenerateContent; + final String _templateId; + final Map _params; + final List _history; + + final _mutex = Mutex(); + + Future sendMessage(Content message) async { + final lock = await _mutex.acquire(); + try { + final response = await _templateGenerateContent( + _history.followedBy([message]), _templateId, _params); + if (response.candidates case [final candidate, ...]) { + _history.add(message); + final normalizedContent = candidate.content.role == null + ? Content('model', candidate.content.parts) + : candidate.content; + _history.add(normalizedContent); + } + return response; + } finally { + lock.release(); + } + } +} + /// [StartChatExtension] on [GenerativeModel] extension StartChatExtension on GenerativeModel { /// Starts a [ChatSession] that will use this model to respond to messages. @@ -181,4 +212,10 @@ extension StartChatExtension on GenerativeModel { GenerationConfig? generationConfig}) => ChatSession._(generateContent, generateContentStream, history ?? [], safetySettings, generationConfig); + + TemplateChatSession startTemplateChat( + String templateId, Map params, + {List? history}) => + TemplateChatSession._(templateGenerateContentWithHistory, templateId, + params, history ?? []); } diff --git a/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart b/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart index be53751a28a4..b0cae90e071d 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart @@ -203,7 +203,15 @@ final class GenerativeModel extends BaseApiClientModel { Map params, ) => makeTemplateRequest(TemplateTask.templateGenerateContent, templateId, - params, _serializationStrategy.parseGenerateContentResponse); + params, null, _serializationStrategy.parseGenerateContentResponse); + + Future templateGenerateContentWithHistory( + Iterable history, + String templateId, + Map params, + ) => + makeTemplateRequest(TemplateTask.templateGenerateContent, templateId, + params, history, _serializationStrategy.parseGenerateContentResponse); } /// Returns a [GenerativeModel] using it's private constructor. diff --git a/packages/firebase_ai/firebase_ai/lib/src/imagen/imagen_model.dart b/packages/firebase_ai/firebase_ai/lib/src/imagen/imagen_model.dart index a849880d23e5..26e7d14e8028 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/imagen/imagen_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/imagen/imagen_model.dart @@ -107,6 +107,7 @@ final class ImagenModel extends BaseApiClientModel { TemplateTask.templatePredict, templateId, params, + null, (jsonObject) => parseImagenGenerationResponse(jsonObject), ); From b663a16398a065cf5f095e7eb51ec3cdb340482c Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Sun, 28 Sep 2025 22:15:33 -0700 Subject: [PATCH 06/11] more examples for server template --- .../example/lib/pages/chat_page.dart | 16 +++- .../example/lib/pages/image_prompt_page.dart | 83 +++++++++++++++++-- .../example/lib/pages/imagen_page.dart | 53 ++++++++++++ .../firebase_ai/lib/firebase_ai.dart | 3 +- .../firebase_ai/lib/src/base_model.dart | 12 ++- .../firebase_ai/firebase_ai/lib/src/chat.dart | 56 ++++++++++--- 6 files changed, 197 insertions(+), 26 deletions(-) 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 724c9c341046..93b6aad704e9 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 @@ -33,6 +33,7 @@ class ChatPage extends StatefulWidget { class _ChatPageState extends State { ChatSession? _chat; + TemplateChatSession? _templateChat; GenerativeModel? _model; final ScrollController _scrollController = ScrollController(); final TextEditingController _textController = TextEditingController(); @@ -64,6 +65,9 @@ class _ChatPageState extends State { ); } _chat = _model?.startChat(); + _templateChat = _model?.startTemplateChat( + 'chat_history.prompt', + ); } void _scrollDown() { @@ -221,12 +225,18 @@ class _ChatPageState extends State { var response = await _model?.templateGenerateContent( 'greeting.prompt', { - 'inputs': { - 'name': templatePrompt, - }, + 'name': templatePrompt, + 'language': 'Chinese', }, ); + // var response = await _templateChat?.sendMessage( + // Content.text(templatePrompt), + // { + // 'message': templatePrompt, + // }, + // ); + var text = response?.text; _messages.add( 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 48fc8667af59..4b32dabbfd14 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 @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:firebase_ai/firebase_ai.dart'; import 'package:flutter/services.dart'; @@ -65,11 +65,13 @@ class _ImagePromptPageState extends State { var content = _generatedContent[idx]; return MessageWidget( text: content.text, - image: Image.memory( - content.imageBytes!, - cacheWidth: 400, - cacheHeight: 400, - ), + image: content.imageBytes == null + ? null + : Image.memory( + content.imageBytes!, + cacheWidth: 400, + cacheHeight: 400, + ), isFromUser: content.fromUser ?? false, ); }, @@ -93,6 +95,18 @@ class _ImagePromptPageState extends State { const SizedBox.square( dimension: 15, ), + if (!_loading) + IconButton( + onPressed: () async { + await _sendTemplateStorageUriPrompt( + _textController.text, + ); + }, + icon: Icon( + Icons.ten_mp, + color: Theme.of(context).colorScheme.primary, + ), + ), if (!_loading) IconButton( onPressed: () async { @@ -223,6 +237,63 @@ class _ImagePromptPageState extends State { } } + Future _sendTemplateStorageUriPrompt(String message) async { + setState(() { + _loading = true; + }); + try { + // final content = [ + // Content.multi([ + // TextPart(message), + // const FileData( + // 'image/jpeg', + // 'gs://vertex-ai-example-ef5a2.appspot.com/foodpic.jpg', + // ), + // ]), + // ]; + // _generatedContent.add(MessageData(text: message, fromUser: true)); + + ByteData catBytes = await rootBundle.load('assets/images/cat.jpg'); + _generatedContent.add(MessageData( + text: message, + imageBytes: catBytes.buffer.asUint8List(), + fromUser: true)); + var response = await widget.model.templateGenerateContent( + 'media.prompt', + { + 'imageData': { + 'isInline': true, + 'mimeType': 'image/jpeg', + 'contents': base64Encode(catBytes.buffer.asUint8List()), + }, + }, + ); + var text = response.text; + _generatedContent.add(MessageData(text: text, fromUser: false)); + + if (text == null) { + _showError('No response from API.'); + return; + } else { + setState(() { + _loading = false; + _scrollDown(); + }); + } + } catch (e) { + _showError(e.toString()); + setState(() { + _loading = false; + }); + } finally { + _textController.clear(); + setState(() { + _loading = false; + }); + _textFieldFocus.requestFocus(); + } + } + void _showError(String message) { showDialog( context: context, diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/imagen_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/imagen_page.dart index 518c4bdce40f..cad086545afc 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/pages/imagen_page.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/imagen_page.dart @@ -169,6 +169,20 @@ class _ImagenPageState extends State { ), tooltip: 'Inpaint', ), + if (!_loading) + IconButton( + onPressed: () async { + await _generateServerTemplateImage( + _textController.text, + ); + }, + icon: Icon( + Icons.ten_mp, + color: Theme.of(context).colorScheme.primary, + ), + ) + else + const CircularProgressIndicator(), if (!_loading) IconButton( onPressed: () async { @@ -444,6 +458,45 @@ class _ImagenPageState extends State { }); } + Future _generateServerTemplateImage(String prompt) async { + setState(() { + _loading = true; + }); + MessageData? resultMessage; + try { + var response = await widget.model.templateGenerateImages( + 'generate_images.prompt', + { + 'prompt': prompt, + }, + ); + + if (response.images.isNotEmpty) { + var imagenImage = response.images[0]; + + resultMessage = MessageData( + imageBytes: imagenImage.bytesBase64Encoded, + text: prompt, + fromUser: false, + ); + } else { + // Handle the case where no images were generated + _showError('Error: No images were generated.'); + } + } catch (e) { + _showError(e.toString()); + } + + setState(() { + if (resultMessage != null) { + _generatedContent.add(resultMessage); + } + + _loading = false; + _scrollDown(); + }); + } + Future _generateImageFromPrompt(String prompt) async { setState(() { _loading = true; diff --git a/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart b/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart index fddab7bbfc41..4fb35679af43 100644 --- a/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart +++ b/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart @@ -34,7 +34,8 @@ export 'src/api.dart' UsageMetadata; export 'src/base_model.dart' show GenerativeModel, ImagenModel, LiveGenerativeModel; -export 'src/chat.dart' show ChatSession, StartChatExtension; +export 'src/chat.dart' + show ChatSession, StartChatExtension, TemplateChatSession; export 'src/content.dart' show Content, diff --git a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart index 123032b030a7..c522b7d27d53 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart @@ -309,12 +309,18 @@ abstract class BaseApiClientModel extends BaseModel { Future makeTemplateRequest( TemplateTask task, String templateId, - Map params, + Map? params, Iterable? history, T Function(Map) parse) { - //params['name'] = templateName(templateId); + Map body = {}; + if (params != null) { + body['inputs'] = params; + } + if (history != null) { + body['history'] = history.map((c) => c.toJson()).toList(); + } return _client - .makeRequest(templateTaskUri(task, templateId), params) + .makeRequest(templateTaskUri(task, templateId), body) .then(parse); } } diff --git a/packages/firebase_ai/firebase_ai/lib/src/chat.dart b/packages/firebase_ai/firebase_ai/lib/src/chat.dart index 2c1debab50a8..3addfc184ab1 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/chat.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/chat.dart @@ -166,23 +166,54 @@ final class ChatSession { } } +/// A back-and-forth chat with a server template. +/// +/// Records messages sent and received in [history]. The history will always +/// record the content from the first candidate in the +/// [GenerateContentResponse], other candidates may be available on the returned +/// response. The history is maintained and updated by the `google_generative_ai` +/// package and reflects the most current state of the chat session. final class TemplateChatSession { - TemplateChatSession._(this._templateGenerateContent, this._templateId, - this._params, this._history); - - final Future Function(Iterable content, - String templateId, Map params) _templateGenerateContent; + TemplateChatSession._( + this._templateHistoryGenerateContent, + this._templateId, + this._history, + ); + + final Future Function( + Iterable content, + String templateId, + Map params) _templateHistoryGenerateContent; final String _templateId; - final Map _params; final List _history; final _mutex = Mutex(); - Future sendMessage(Content message) async { + /// The content that has been successfully sent to, or received from, the + /// generative model. + /// + /// If there are outstanding requests from calls to [sendMessage], + /// these will not be reflected in the history. + /// Messages without a candidate in the response are not recorded in history, + /// including the message sent to the model. + Iterable get history => _history.skip(0); + + /// Sends [params] to the server template as a continuation of the chat [history]. + /// + /// Prepends the history to the request and uses the provided model to + /// generate new content. + /// + /// When there are no candidates in the response, the [message] and response + /// are ignored and will not be recorded in the [history]. + Future sendMessage( + Content message, Map params) async { final lock = await _mutex.acquire(); try { - final response = await _templateGenerateContent( - _history.followedBy([message]), _templateId, _params); + final response = await _templateHistoryGenerateContent( + _history.followedBy([message]), + _templateId, + params, + ); if (response.candidates case [final candidate, ...]) { _history.add(message); final normalizedContent = candidate.content.role == null @@ -213,9 +244,8 @@ extension StartChatExtension on GenerativeModel { ChatSession._(generateContent, generateContentStream, history ?? [], safetySettings, generationConfig); - TemplateChatSession startTemplateChat( - String templateId, Map params, + TemplateChatSession startTemplateChat(String templateId, {List? history}) => - TemplateChatSession._(templateGenerateContentWithHistory, templateId, - params, history ?? []); + TemplateChatSession._( + templateGenerateContentWithHistory, templateId, history ?? []); } From 9d4a70f3055560c0ccc9edf869a74dc328465b60 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Thu, 2 Oct 2025 22:38:38 -0700 Subject: [PATCH 07/11] create new models for template request --- .../firebase_ai/example/lib/main.dart | 8 +- .../example/lib/pages/chat_page.dart | 22 +-- .../lib/pages/server_template_page.dart | 13 ++ .../firebase_ai/lib/firebase_ai.dart | 13 +- .../firebase_ai/lib/src/base_model.dart | 155 +++++++++++------- .../firebase_ai/firebase_ai/lib/src/chat.dart | 67 -------- .../firebase_ai/lib/src/generative_model.dart | 18 -- .../lib/src/imagen/imagen_model.dart | 15 -- .../src/server_template/template_chat.dart | 94 +++++++++++ .../template_generative_model.dart | 82 +++++++++ .../template_imagen_model.dart | 76 +++++++++ 11 files changed, 389 insertions(+), 174 deletions(-) create mode 100644 packages/firebase_ai/firebase_ai/example/lib/pages/server_template_page.dart create mode 100644 packages/firebase_ai/firebase_ai/lib/src/server_template/template_chat.dart create mode 100644 packages/firebase_ai/firebase_ai/lib/src/server_template/template_generative_model.dart create mode 100644 packages/firebase_ai/firebase_ai/lib/src/server_template/template_imagen_model.dart diff --git a/packages/firebase_ai/firebase_ai/example/lib/main.dart b/packages/firebase_ai/firebase_ai/example/lib/main.dart index db1344210deb..dd0a317d1c58 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/main.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/main.dart @@ -18,7 +18,7 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; // Import after file is generated through flutterfire_cli. -// import 'package:firebase_ai_example/firebase_options.dart'; +import 'package:firebase_ai_example/firebase_options.dart'; import 'pages/audio_page.dart'; import 'pages/bidi_page.dart'; @@ -36,9 +36,9 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); // Enable this line instead once have the firebase_options.dart generated and // imported through flutterfire_cli. - // await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - await Firebase.initializeApp(); - await FirebaseAuth.instance.signInAnonymously(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + //await Firebase.initializeApp(); + //await FirebaseAuth.instance.signInAnonymously(); runApp(const GenerativeAISample()); } 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 93b6aad704e9..0b56c95f4ede 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 @@ -222,21 +222,21 @@ class _ChatPageState extends State { }); try { - var response = await _model?.templateGenerateContent( - 'greeting.prompt', + //var response = await _model?.templateGenerateContent( + // 'greeting.prompt', + // { + // 'name': templatePrompt, + // 'language': 'Chinese', + // }, + //); + + var response = await _templateChat?.sendMessage( + Content.text(templatePrompt), { - 'name': templatePrompt, - 'language': 'Chinese', + 'message': templatePrompt, }, ); - // var response = await _templateChat?.sendMessage( - // Content.text(templatePrompt), - // { - // 'message': templatePrompt, - // }, - // ); - var text = response?.text; _messages.add( diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/server_template_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/server_template_page.dart new file mode 100644 index 000000000000..8615b4f8142d --- /dev/null +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/server_template_page.dart @@ -0,0 +1,13 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. diff --git a/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart b/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart index 4fb35679af43..ddce7f3e98b8 100644 --- a/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart +++ b/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart @@ -33,9 +33,13 @@ export 'src/api.dart' SafetySetting, UsageMetadata; export 'src/base_model.dart' - show GenerativeModel, ImagenModel, LiveGenerativeModel; -export 'src/chat.dart' - show ChatSession, StartChatExtension, TemplateChatSession; + show + GenerativeModel, + ImagenModel, + LiveGenerativeModel, + TemplateGenerativeModel, + TemplateImagenModel; +export 'src/chat.dart' show ChatSession, StartChatExtension; export 'src/content.dart' show Content, @@ -101,6 +105,9 @@ export 'src/live_api.dart' LiveServerResponse; export 'src/live_session.dart' show LiveSession; export 'src/schema.dart' show Schema, SchemaType; +export 'src/server_template/template_chat.dart' + show TemplateChatSession, StartTemplateChatExtension; + export 'src/tool.dart' show FunctionCallingConfig, diff --git a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart index c522b7d27d53..0800e748923d 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart @@ -41,6 +41,8 @@ import 'vertex_version.dart'; part 'generative_model.dart'; part 'imagen/imagen_model.dart'; part 'live_model.dart'; +part 'server_template/template_generative_model.dart'; +part 'server_template/template_imagen_model.dart'; /// [Task] enum class for [GenerativeModel] to make request. enum Task { @@ -69,8 +71,6 @@ abstract interface class _ModelUri { String get baseAuthority; String get apiVersion; Uri taskUri(Task task); - Uri templateTaskUri(TemplateTask task, String templateId); - String templateName(String templateId); ({String prefix, String name}) get model; } @@ -80,9 +80,7 @@ final class _VertexUri implements _ModelUri { required String location, required FirebaseApp app}) : model = _normalizeModelName(model), - _projectUri = _vertexUri(app, location), - _templateUri = _vertexTemplateUri(app, location), - _templateName = _vertexTemplateName(app, location); + _projectUri = _vertexUri(app, location); static const _baseAuthority = 'firebasevertexai.googleapis.com'; static const _apiVersion = 'v1beta'; @@ -105,25 +103,8 @@ final class _VertexUri implements _ModelUri { ); } - static Uri _vertexTemplateUri(FirebaseApp app, String location) { - var projectId = app.options.projectId; - return Uri.https( - _baseAuthority, - '/$_apiVersion/projects/$projectId/locations/$location', - ); - } - - static String _vertexTemplateName(FirebaseApp app, String location) { - var projectId = app.options.projectId; - return 'projects/$projectId/locations/$location'; - } - final Uri _projectUri; - final Uri _templateUri; - - final String _templateName; - @override final ({String prefix, String name}) model; @@ -139,17 +120,6 @@ final class _VertexUri implements _ModelUri { pathSegments: _projectUri.pathSegments .followedBy([model.prefix, '${model.name}:${task.name}'])); } - - @override - Uri templateTaskUri(TemplateTask task, String templateId) { - return _templateUri.replace( - pathSegments: _templateUri.pathSegments - .followedBy(['templates', '$templateId:${task.name}'])); - } - - @override - String templateName(String templateId) => - '$_templateName/templates/$templateId'; } final class _GoogleAIUri implements _ModelUri { @@ -157,9 +127,7 @@ final class _GoogleAIUri implements _ModelUri { required String model, required FirebaseApp app, }) : model = _normalizeModelName(model), - _baseUri = _googleAIBaseUri(app: app), - _baseTemplateUri = _googleAIBaseTemplateUri(app: app), - _baseTemplateName = _googleAIBaseTemplateName(app: app); + _baseUri = _googleAIBaseUri(app: app); /// Returns the model code for a user friendly model name. /// @@ -179,20 +147,8 @@ final class _GoogleAIUri implements _ModelUri { Uri.https( _baseAuthority, '$apiVersion/projects/${app.options.projectId}'); - static Uri _googleAIBaseTemplateUri( - {String apiVersion = _apiVersion, required FirebaseApp app}) => - Uri.https( - _baseAuthority, '$apiVersion/projects/${app.options.projectId}'); - - static String _googleAIBaseTemplateName({required FirebaseApp app}) => - 'projects/${app.options.projectId}'; - final Uri _baseUri; - final Uri _baseTemplateUri; - - final String _baseTemplateName; - @override final ({String prefix, String name}) model; @@ -206,17 +162,92 @@ final class _GoogleAIUri implements _ModelUri { Uri taskUri(Task task) => _baseUri.replace( pathSegments: _baseUri.pathSegments .followedBy([model.prefix, '${model.name}:${task.name}'])); +} + +abstract interface class _TemplateUri { + String get baseAuthority; + String get apiVersion; + Uri templateTaskUri(TemplateTask task, String templateId); + String templateName(String templateId); +} + +final class _TemplateVertexUri implements _TemplateUri { + _TemplateVertexUri({required String location, required FirebaseApp app}) + : _templateUri = _vertexTemplateUri(app, location), + _templateName = _vertexTemplateName(app, location); + + static const _baseAuthority = 'firebasevertexai.googleapis.com'; + static const _apiVersion = 'v1beta'; + + final Uri _templateUri; + final String _templateName; + + static Uri _vertexTemplateUri(FirebaseApp app, String location) { + var projectId = app.options.projectId; + return Uri.https( + _baseAuthority, + '/$_apiVersion/projects/$projectId/locations/$location', + ); + } + + static String _vertexTemplateName(FirebaseApp app, String location) { + var projectId = app.options.projectId; + return 'projects/$projectId/locations/$location'; + } + + @override + String get baseAuthority => _baseAuthority; + + @override + String get apiVersion => _apiVersion; + + @override + Uri templateTaskUri(TemplateTask task, String templateId) { + return _templateUri.replace( + pathSegments: _templateUri.pathSegments + .followedBy(['templates', '$templateId:${task.name}'])); + } + + @override + String templateName(String templateId) => + '$_templateName/templates/$templateId'; +} + +final class _TemplateGoogleAIUri implements _TemplateUri { + _TemplateGoogleAIUri({ + required FirebaseApp app, + }) : _templateUri = _googleAITemplateUri(app: app), + _templateName = _googleAITemplateName(app: app); + + static const _baseAuthority = 'firebasevertexai.googleapis.com'; + static const _apiVersion = 'v1beta'; + final Uri _templateUri; + final String _templateName; + + static Uri _googleAITemplateUri( + {String apiVersion = _apiVersion, required FirebaseApp app}) => + Uri.https( + _baseAuthority, '$apiVersion/projects/${app.options.projectId}'); + + static String _googleAITemplateName({required FirebaseApp app}) => + 'projects/${app.options.projectId}'; + + @override + String get baseAuthority => _baseAuthority; + + @override + String get apiVersion => _apiVersion; @override Uri templateTaskUri(TemplateTask task, String templateId) { - return _baseUri.replace( - pathSegments: _baseUri.pathSegments + return _templateUri.replace( + pathSegments: _templateUri.pathSegments .followedBy(['templates', '$templateId:${task.name}'])); } @override String templateName(String templateId) => - '$_baseTemplateName/templates/$templateId'; + '$_templateName/templates/$templateId'; } /// Base class for models. @@ -270,11 +301,6 @@ abstract class BaseModel { /// Returns a URI for the given [task]. Uri taskUri(Task task) => _modelUri.taskUri(task); - - Uri templateTaskUri(TemplateTask task, String templateId) => - _modelUri.templateTaskUri(task, templateId); - - String templateName(String templateId) => _modelUri.templateName(templateId); } /// An abstract base class for models that interact with an API using an [ApiClient]. @@ -303,6 +329,17 @@ abstract class BaseApiClientModel extends BaseModel { Future makeRequest(Task task, Map params, T Function(Map) parse) => _client.makeRequest(taskUri(task), params).then(parse); +} + +abstract class BaseTemplateApiClientModel extends BaseApiClientModel { + BaseTemplateApiClientModel( + {required super.serializationStrategy, + required super.modelUri, + required super.client, + required _TemplateUri templateUri}) + : _templateUri = templateUri; + + final _TemplateUri _templateUri; /// Make a unary request for [task] with [templateId] and JSON encodable /// [params]. @@ -323,4 +360,10 @@ abstract class BaseApiClientModel extends BaseModel { .makeRequest(templateTaskUri(task, templateId), body) .then(parse); } + + Uri templateTaskUri(TemplateTask task, String templateId) => + _templateUri.templateTaskUri(task, templateId); + + String templateName(String templateId) => + _templateUri.templateName(templateId); } diff --git a/packages/firebase_ai/firebase_ai/lib/src/chat.dart b/packages/firebase_ai/firebase_ai/lib/src/chat.dart index 3addfc184ab1..fdcbd3cb2920 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/chat.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/chat.dart @@ -166,68 +166,6 @@ final class ChatSession { } } -/// A back-and-forth chat with a server template. -/// -/// Records messages sent and received in [history]. The history will always -/// record the content from the first candidate in the -/// [GenerateContentResponse], other candidates may be available on the returned -/// response. The history is maintained and updated by the `google_generative_ai` -/// package and reflects the most current state of the chat session. -final class TemplateChatSession { - TemplateChatSession._( - this._templateHistoryGenerateContent, - this._templateId, - this._history, - ); - - final Future Function( - Iterable content, - String templateId, - Map params) _templateHistoryGenerateContent; - final String _templateId; - final List _history; - - final _mutex = Mutex(); - - /// The content that has been successfully sent to, or received from, the - /// generative model. - /// - /// If there are outstanding requests from calls to [sendMessage], - /// these will not be reflected in the history. - /// Messages without a candidate in the response are not recorded in history, - /// including the message sent to the model. - Iterable get history => _history.skip(0); - - /// Sends [params] to the server template as a continuation of the chat [history]. - /// - /// Prepends the history to the request and uses the provided model to - /// generate new content. - /// - /// When there are no candidates in the response, the [message] and response - /// are ignored and will not be recorded in the [history]. - Future sendMessage( - Content message, Map params) async { - final lock = await _mutex.acquire(); - try { - final response = await _templateHistoryGenerateContent( - _history.followedBy([message]), - _templateId, - params, - ); - if (response.candidates case [final candidate, ...]) { - _history.add(message); - final normalizedContent = candidate.content.role == null - ? Content('model', candidate.content.parts) - : candidate.content; - _history.add(normalizedContent); - } - return response; - } finally { - lock.release(); - } - } -} - /// [StartChatExtension] on [GenerativeModel] extension StartChatExtension on GenerativeModel { /// Starts a [ChatSession] that will use this model to respond to messages. @@ -243,9 +181,4 @@ extension StartChatExtension on GenerativeModel { GenerationConfig? generationConfig}) => ChatSession._(generateContent, generateContentStream, history ?? [], safetySettings, generationConfig); - - TemplateChatSession startTemplateChat(String templateId, - {List? history}) => - TemplateChatSession._( - templateGenerateContentWithHistory, templateId, history ?? []); } diff --git a/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart b/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart index b0cae90e071d..4f570e9446d6 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart @@ -194,24 +194,6 @@ final class GenerativeModel extends BaseApiClientModel { return makeRequest(Task.countTokens, parameters, _serializationStrategy.parseCountTokensResponse); } - - /// Generates content from a template with the given [templateId] and [params]. - /// - /// Sends a "templateGenerateContent" API request for the configured model. - Future templateGenerateContent( - String templateId, - Map params, - ) => - makeTemplateRequest(TemplateTask.templateGenerateContent, templateId, - params, null, _serializationStrategy.parseGenerateContentResponse); - - Future templateGenerateContentWithHistory( - Iterable history, - String templateId, - Map params, - ) => - makeTemplateRequest(TemplateTask.templateGenerateContent, templateId, - params, history, _serializationStrategy.parseGenerateContentResponse); } /// Returns a [GenerativeModel] using it's private constructor. diff --git a/packages/firebase_ai/firebase_ai/lib/src/imagen/imagen_model.dart b/packages/firebase_ai/firebase_ai/lib/src/imagen/imagen_model.dart index 26e7d14e8028..4fc6e84d2626 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/imagen/imagen_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/imagen/imagen_model.dart @@ -97,21 +97,6 @@ final class ImagenModel extends BaseApiClientModel { parseImagenGenerationResponse(jsonObject), ); - /// Generates images from a template with the given [templateId] and [params]. - @experimental - Future> templateGenerateImages( - String templateId, - Map params, - ) => - makeTemplateRequest( - TemplateTask.templatePredict, - templateId, - params, - null, - (jsonObject) => - parseImagenGenerationResponse(jsonObject), - ); - /// Generates images with format of [ImagenGCSImage] based on the given /// prompt. /// Note: Keep this API private until future release. diff --git a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_chat.dart b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_chat.dart new file mode 100644 index 000000000000..b7b1b663ea0f --- /dev/null +++ b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_chat.dart @@ -0,0 +1,94 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import '../api.dart'; +import '../base_model.dart'; +import '../content.dart'; +import '../utils/mutex.dart'; + +/// A back-and-forth chat with a server template. +/// +/// Records messages sent and received in [history]. The history will always +/// record the content from the first candidate in the +/// [GenerateContentResponse], other candidates may be available on the returned +/// response. The history is maintained and updated by the `google_generative_ai` +/// package and reflects the most current state of the chat session. +final class TemplateChatSession { + TemplateChatSession._( + this._templateHistoryGenerateContent, + this._templateId, + this._history, + ); + + final Future Function( + Iterable content, + String templateId, + Map params) _templateHistoryGenerateContent; + final String _templateId; + final List _history; + + final _mutex = Mutex(); + + /// The content that has been successfully sent to, or received from, the + /// generative model. + /// + /// If there are outstanding requests from calls to [sendMessage], + /// these will not be reflected in the history. + /// Messages without a candidate in the response are not recorded in history, + /// including the message sent to the model. + Iterable get history => _history.skip(0); + + /// Sends [params] to the server template as a continuation of the chat [history]. + /// + /// Prepends the history to the request and uses the provided model to + /// generate new content. + /// + /// When there are no candidates in the response, the [message] and response + /// are ignored and will not be recorded in the [history]. + Future sendMessage( + Content message, Map params) async { + final lock = await _mutex.acquire(); + try { + final response = await _templateHistoryGenerateContent( + _history.followedBy([message]), + _templateId, + params, + ); + if (response.candidates case [final candidate, ...]) { + _history.add(message); + final normalizedContent = candidate.content.role == null + ? Content('model', candidate.content.parts) + : candidate.content; + _history.add(normalizedContent); + } + return response; + } finally { + lock.release(); + } + } +} + +/// [StartTemplateChatExtension] on [GenerativeModel] +extension StartTemplateChatExtension on TemplateGenerativeModel { + /// Starts a [TemplateChatSession] that will use this model to respond to messages. + /// + /// ```dart + /// final chat = model.startChat(); + /// final response = await chat.sendMessage(Content.text('Hello there.')); + /// print(response.text); + /// ``` + TemplateChatSession startTemplateChat(String templateId, + {List? history}) => + TemplateChatSession._( + templateGenerateContentWithHistory, templateId, history ?? []); +} diff --git a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_generative_model.dart b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_generative_model.dart new file mode 100644 index 000000000000..ea3cde227d22 --- /dev/null +++ b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_generative_model.dart @@ -0,0 +1,82 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ignore_for_file: use_late_for_private_fields_and_variables +part of '../base_model.dart'; + +@experimental +final class TemplateGenerativeModel extends BaseTemplateApiClientModel { + TemplateGenerativeModel._({ + required String location, + required FirebaseApp app, + required bool useVertexBackend, + bool? useLimitedUseAppCheckTokens, + FirebaseAppCheck? appCheck, + FirebaseAuth? auth, + http.Client? httpClient, + }) : super( + serializationStrategy: useVertexBackend + ? VertexSerialization() + : DeveloperSerialization(), + modelUri: useVertexBackend + ? _VertexUri(app: app, model: '', location: location) + : _GoogleAIUri(app: app, model: ''), + client: HttpApiClient( + apiKey: app.options.apiKey, + httpClient: httpClient, + requestHeaders: BaseModel.firebaseTokens( + appCheck, auth, app, useLimitedUseAppCheckTokens)), + templateUri: useVertexBackend + ? _TemplateVertexUri(app: app, location: location) + : _TemplateGoogleAIUri(app: app), + ); + + /// Generates content from a template with the given [templateId] and [params]. + /// + /// Sends a "templateGenerateContent" API request for the configured model. + @experimental + Future templateGenerateContent( + String templateId, + Map params, + ) => + makeTemplateRequest(TemplateTask.templateGenerateContent, templateId, + params, null, _serializationStrategy.parseGenerateContentResponse); + @experimental + Future templateGenerateContentWithHistory( + Iterable history, + String templateId, + Map params, + ) => + makeTemplateRequest(TemplateTask.templateGenerateContent, templateId, + params, history, _serializationStrategy.parseGenerateContentResponse); +} + +/// Returns a [TemplateGenerativeModel] using it's private constructor. +@experimental +TemplateGenerativeModel createTemplateGenerativeModel({ + required FirebaseApp app, + required String location, + required bool useVertexBackend, + bool? useLimitedUseAppCheckTokens, + FirebaseAppCheck? appCheck, + FirebaseAuth? auth, +}) => + TemplateGenerativeModel._( + app: app, + appCheck: appCheck, + useVertexBackend: useVertexBackend, + useLimitedUseAppCheckTokens: useLimitedUseAppCheckTokens, + auth: auth, + location: location, + ); diff --git a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_imagen_model.dart b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_imagen_model.dart new file mode 100644 index 000000000000..742d552d9a89 --- /dev/null +++ b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_imagen_model.dart @@ -0,0 +1,76 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +part of '../base_model.dart'; + +@experimental +final class TemplateImagenModel extends BaseTemplateApiClientModel { + TemplateImagenModel._( + {required FirebaseApp app, + required String location, + required bool useVertexBackend, + bool? useLimitedUseAppCheckTokens, + FirebaseAppCheck? appCheck, + FirebaseAuth? auth}) + : _useVertexBackend = useVertexBackend, + super( + serializationStrategy: VertexSerialization(), + modelUri: useVertexBackend + ? _VertexUri(app: app, model: '', location: location) + : _GoogleAIUri(app: app, model: ''), + client: HttpApiClient( + apiKey: app.options.apiKey, + requestHeaders: BaseModel.firebaseTokens( + appCheck, auth, app, useLimitedUseAppCheckTokens)), + templateUri: useVertexBackend + ? _TemplateVertexUri(app: app, location: location) + : _TemplateGoogleAIUri(app: app), + ); + + final bool _useVertexBackend; + + /// Generates images from a template with the given [templateId] and [params]. + @experimental + Future> templateGenerateImages( + String templateId, + Map params, + ) => + makeTemplateRequest( + TemplateTask.templatePredict, + templateId, + params, + null, + (jsonObject) => + parseImagenGenerationResponse(jsonObject), + ); +} + +/// Returns a [TemplateImagenModel] using it's private constructor. +@experimental +TemplateImagenModel createTemplateImagenModel({ + required FirebaseApp app, + required String location, + required bool useVertexBackend, + bool? useLimitedUseAppCheckTokens, + FirebaseAppCheck? appCheck, + FirebaseAuth? auth, +}) => + TemplateImagenModel._( + app: app, + appCheck: appCheck, + auth: auth, + location: location, + useVertexBackend: useVertexBackend, + useLimitedUseAppCheckTokens: useLimitedUseAppCheckTokens, + ); From c5f6171caf924dc98d76e43f06f176964c03f851 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Fri, 3 Oct 2025 15:38:49 -0700 Subject: [PATCH 08/11] working example with new architecture --- .../firebase_ai/example/lib/main.dart | 40 +- .../example/lib/pages/chat_page.dart | 65 ---- .../example/lib/pages/image_prompt_page.dart | 69 ---- .../example/lib/pages/imagen_page.dart | 53 --- .../lib/pages/server_template_page.dart | 345 ++++++++++++++++++ .../firebase_ai/lib/src/client.dart | 1 + .../firebase_ai/lib/src/firebase_ai.dart | 23 ++ 7 files changed, 394 insertions(+), 202 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/example/lib/main.dart b/packages/firebase_ai/firebase_ai/example/lib/main.dart index dd0a317d1c58..75d179c4a25e 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/main.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/main.dart @@ -18,7 +18,7 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; // Import after file is generated through flutterfire_cli. -import 'package:firebase_ai_example/firebase_options.dart'; +// import 'package:firebase_ai_example/firebase_options.dart'; import 'pages/audio_page.dart'; import 'pages/bidi_page.dart'; @@ -31,14 +31,15 @@ import 'pages/json_schema_page.dart'; import 'pages/schema_page.dart'; import 'pages/token_count_page.dart'; import 'pages/video_page.dart'; +import 'pages/server_template_page.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // Enable this line instead once have the firebase_options.dart generated and // imported through flutterfire_cli. - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - //await Firebase.initializeApp(); - //await FirebaseAuth.instance.signInAnonymously(); + // await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + await Firebase.initializeApp(); + await FirebaseAuth.instance.signInAnonymously(); runApp(const GenerativeAISample()); } @@ -65,11 +66,11 @@ class _GenerativeAISampleState extends State { void _initializeModel(bool useVertexBackend) { if (useVertexBackend) { final vertexInstance = FirebaseAI.vertexAI(auth: FirebaseAuth.instance); - _currentModel = vertexInstance.generativeModel(model: 'gemini-2.5-flash'); + _currentModel = vertexInstance.generativeModel(model: 'gemini-1.5-flash'); _currentImagenModel = _initializeImagenModel(vertexInstance); } else { final googleAI = FirebaseAI.googleAI(auth: FirebaseAuth.instance); - _currentModel = googleAI.generativeModel(model: 'gemini-2.5-flash'); + _currentModel = googleAI.generativeModel(model: 'gemini-1.5-flash'); _currentImagenModel = _initializeImagenModel(googleAI); } } @@ -81,7 +82,7 @@ class _GenerativeAISampleState extends State { imageFormat: ImagenFormat.jpeg(compressionQuality: 75), ); return instance.imagenModel( - model: 'imagen-3.0-capability-001', + model: 'imagen-3.0-flash-001', generationConfig: generationConfig, safetySettings: ImagenSafetySettings( ImagenSafetyFilterLevel.blockLowAndAbove, @@ -199,6 +200,11 @@ class _HomeScreenState extends State { model: currentModel, useVertexBackend: useVertexBackend, ); + case 11: + return ServerTemplatePage( + title: 'Server Template', + useVertexBackend: useVertexBackend, + ); default: // Fallback to the first page in case of an unexpected index @@ -227,18 +233,15 @@ class _HomeScreenState extends State { style: TextStyle( fontSize: 12, color: widget.useVertexBackend - ? Theme.of(context) - .colorScheme - .onSurface - .withValues(alpha: 0.7) + ? Theme.of(context).colorScheme.onSurface.withAlpha(180) : Theme.of(context).colorScheme.primary, ), ), Switch( value: widget.useVertexBackend, onChanged: widget.onBackendChanged, - activeTrackColor: Colors.green.withValues(alpha: 0.5), - inactiveTrackColor: Colors.blueGrey.withValues(alpha: 0.5), + activeTrackColor: Colors.green.withAlpha(128), + inactiveTrackColor: Colors.blueGrey.withAlpha(128), activeThumbColor: Colors.green, inactiveThumbColor: Colors.blueGrey, ), @@ -251,7 +254,7 @@ class _HomeScreenState extends State { : Theme.of(context) .colorScheme .onSurface - .withValues(alpha: 0.7), + .withAlpha(180), ), ), ], @@ -273,7 +276,7 @@ class _HomeScreenState extends State { unselectedFontSize: 9, selectedItemColor: Theme.of(context).colorScheme.primary, unselectedItemColor: widget.useVertexBackend - ? Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7) + ? Theme.of(context).colorScheme.onSurface.withAlpha(180) : Colors.grey, items: const [ BottomNavigationBarItem( @@ -333,6 +336,13 @@ class _HomeScreenState extends State { label: 'Live', tooltip: 'Live Stream', ), + BottomNavigationBarItem( + icon: Icon( + Icons.storage, + ), + label: 'Server', + tooltip: 'Server Template', + ), ], currentIndex: widget.selectedIndex, onTap: _onItemTapped, 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 0b56c95f4ede..af0f05b1c4ae 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 @@ -65,9 +65,6 @@ class _ChatPageState extends State { ); } _chat = _model?.startChat(); - _templateChat = _model?.startTemplateChat( - 'chat_history.prompt', - ); } void _scrollDown() { @@ -142,18 +139,6 @@ class _ChatPageState extends State { const SizedBox.square( dimension: 15, ), - if (!_loading) - IconButton( - onPressed: () async { - await _sendServerTemplateMessage(_textController.text); - }, - icon: Icon( - Icons.ten_mp, - color: Theme.of(context).colorScheme.primary, - ), - ) - else - const CircularProgressIndicator(), if (!_loading) IconButton( onPressed: () async { @@ -216,56 +201,6 @@ class _ChatPageState extends State { } } - Future _sendServerTemplateMessage(String templatePrompt) async { - setState(() { - _loading = true; - }); - - try { - //var response = await _model?.templateGenerateContent( - // 'greeting.prompt', - // { - // 'name': templatePrompt, - // 'language': 'Chinese', - // }, - //); - - var response = await _templateChat?.sendMessage( - Content.text(templatePrompt), - { - 'message': templatePrompt, - }, - ); - - var text = response?.text; - - _messages.add( - MessageData(text: text, fromUser: false), - ); - - if (text == null) { - _showError('No response from API.'); - return; - } else { - setState(() { - _loading = false; - _scrollDown(); - }); - } - } catch (e) { - _showError(e.toString()); - setState(() { - _loading = false; - }); - } finally { - _textController.clear(); - setState(() { - _loading = false; - }); - _textFieldFocus.requestFocus(); - } - } - void _showError(String message) { showDialog( context: context, 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 4b32dabbfd14..463380cf3ca5 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 @@ -95,18 +95,6 @@ class _ImagePromptPageState extends State { const SizedBox.square( dimension: 15, ), - if (!_loading) - IconButton( - onPressed: () async { - await _sendTemplateStorageUriPrompt( - _textController.text, - ); - }, - icon: Icon( - Icons.ten_mp, - color: Theme.of(context).colorScheme.primary, - ), - ), if (!_loading) IconButton( onPressed: () async { @@ -237,63 +225,6 @@ class _ImagePromptPageState extends State { } } - Future _sendTemplateStorageUriPrompt(String message) async { - setState(() { - _loading = true; - }); - try { - // final content = [ - // Content.multi([ - // TextPart(message), - // const FileData( - // 'image/jpeg', - // 'gs://vertex-ai-example-ef5a2.appspot.com/foodpic.jpg', - // ), - // ]), - // ]; - // _generatedContent.add(MessageData(text: message, fromUser: true)); - - ByteData catBytes = await rootBundle.load('assets/images/cat.jpg'); - _generatedContent.add(MessageData( - text: message, - imageBytes: catBytes.buffer.asUint8List(), - fromUser: true)); - var response = await widget.model.templateGenerateContent( - 'media.prompt', - { - 'imageData': { - 'isInline': true, - 'mimeType': 'image/jpeg', - 'contents': base64Encode(catBytes.buffer.asUint8List()), - }, - }, - ); - var text = response.text; - _generatedContent.add(MessageData(text: text, fromUser: false)); - - if (text == null) { - _showError('No response from API.'); - return; - } else { - setState(() { - _loading = false; - _scrollDown(); - }); - } - } catch (e) { - _showError(e.toString()); - setState(() { - _loading = false; - }); - } finally { - _textController.clear(); - setState(() { - _loading = false; - }); - _textFieldFocus.requestFocus(); - } - } - void _showError(String message) { showDialog( context: context, diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/imagen_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/imagen_page.dart index cad086545afc..518c4bdce40f 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/pages/imagen_page.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/imagen_page.dart @@ -169,20 +169,6 @@ class _ImagenPageState extends State { ), tooltip: 'Inpaint', ), - if (!_loading) - IconButton( - onPressed: () async { - await _generateServerTemplateImage( - _textController.text, - ); - }, - icon: Icon( - Icons.ten_mp, - color: Theme.of(context).colorScheme.primary, - ), - ) - else - const CircularProgressIndicator(), if (!_loading) IconButton( onPressed: () async { @@ -458,45 +444,6 @@ class _ImagenPageState extends State { }); } - Future _generateServerTemplateImage(String prompt) async { - setState(() { - _loading = true; - }); - MessageData? resultMessage; - try { - var response = await widget.model.templateGenerateImages( - 'generate_images.prompt', - { - 'prompt': prompt, - }, - ); - - if (response.images.isNotEmpty) { - var imagenImage = response.images[0]; - - resultMessage = MessageData( - imageBytes: imagenImage.bytesBase64Encoded, - text: prompt, - fromUser: false, - ); - } else { - // Handle the case where no images were generated - _showError('Error: No images were generated.'); - } - } catch (e) { - _showError(e.toString()); - } - - setState(() { - if (resultMessage != null) { - _generatedContent.add(resultMessage); - } - - _loading = false; - _scrollDown(); - }); - } - Future _generateImageFromPrompt(String prompt) async { setState(() { _loading = true; diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/server_template_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/server_template_page.dart index 8615b4f8142d..9a518337141e 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/pages/server_template_page.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/server_template_page.dart @@ -11,3 +11,348 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + +import 'package:flutter/material.dart'; +import '../widgets/message_widget.dart'; +import 'package:firebase_ai/firebase_ai.dart'; + +class ServerTemplatePage extends StatefulWidget { + const ServerTemplatePage({ + super.key, + required this.title, + required this.useVertexBackend, + }); + + final String title; + final bool useVertexBackend; + + @override + State createState() => _ServerTemplatePageState(); +} + +class _ServerTemplatePageState extends State { + final ScrollController _scrollController = ScrollController(); + final TextEditingController _textController = TextEditingController(); + final FocusNode _textFieldFocus = FocusNode(); + final List _messages = []; + bool _loading = false; + + TemplateGenerativeModel? _templateGenerativeModel; + TemplateChatSession? _chatSession; + TemplateImagenModel? _templateImagenModel; + + @override + void initState() { + super.initState(); + _initializeServerTemplate(); + } + + void _initializeServerTemplate() { + if (widget.useVertexBackend) { + _templateGenerativeModel = + FirebaseAI.vertexAI().templateGenerativeModel(); + _templateImagenModel = FirebaseAI.vertexAI().templateImagenModel(); + } else { + _templateGenerativeModel = + FirebaseAI.googleAI().templateGenerativeModel(); + _templateImagenModel = FirebaseAI.googleAI().templateImagenModel(); + } + _chatSession = + _templateGenerativeModel?.startTemplateChat('chat_history.prompt'); + } + + void _scrollDown() { + WidgetsBinding.instance.addPostFrameCallback( + (_) => _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration( + milliseconds: 750, + ), + curve: Curves.easeOutCirc, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListView.builder( + controller: _scrollController, + itemBuilder: (context, idx) { + final message = _messages[idx]; + return MessageWidget( + text: message.text, + image: message.imageBytes != null + ? Image.memory( + message.imageBytes!, + cacheWidth: 400, + cacheHeight: 400, + ) + : null, + isFromUser: message.fromUser ?? false, + ); + }, + itemCount: _messages.length, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 25, + horizontal: 15, + ), + child: Row( + children: [ + Expanded( + child: TextField( + autofocus: true, + focusNode: _textFieldFocus, + controller: _textController, + onSubmitted: _sendServerTemplateMessage, + ), + ), + const SizedBox.square( + dimension: 15, + ), + if (!_loading) + IconButton( + onPressed: () async { + await _serverTemplateImagen(_textController.text); + }, + icon: Icon( + Icons.image_search, + color: Theme.of(context).colorScheme.primary, + ), + tooltip: 'Imagen', + ) + else + const CircularProgressIndicator(), + if (!_loading) + IconButton( + onPressed: () async { + await _serverTemplateImageInput(_textController.text); + }, + icon: Icon( + Icons.image, + color: Theme.of(context).colorScheme.primary, + ), + tooltip: 'Image Input', + ) + else + const CircularProgressIndicator(), + if (!_loading) + IconButton( + onPressed: () async { + await _serverTemplateChat(_textController.text); + }, + icon: Icon( + Icons.chat, + color: Theme.of(context).colorScheme.primary, + ), + tooltip: 'Chat', + ) + else + const CircularProgressIndicator(), + if (!_loading) + IconButton( + onPressed: () async { + await _sendServerTemplateMessage(_textController.text); + }, + icon: Icon( + Icons.send, + color: Theme.of(context).colorScheme.primary, + ), + tooltip: 'Generate', + ) + else + const CircularProgressIndicator(), + ], + ), + ), + ], + ), + ), + ); + } + + Future _serverTemplateImagen(String message) async { + setState(() { + _loading = true; + }); + MessageData? resultMessage; + try { + _messages.add(MessageData(text: message, fromUser: true)); + // TODO: Add call to Firebase AI SDK + var response = await _templateImagenModel?.templateGenerateImages( + 'generate_images.prompt', + { + 'prompt': message, + }, + ); + + if (response!.images.isNotEmpty) { + var imagenImage = response.images[0]; + + resultMessage = MessageData( + imageBytes: imagenImage.bytesBase64Encoded, + text: message, + fromUser: false, + ); + } else { + // Handle the case where no images were generated + _showError('Error: No images were generated.'); + } + + setState(() { + if (resultMessage != null) { + _messages.add(resultMessage); + } + _loading = false; + _scrollDown(); + }); + } catch (e) { + _showError(e.toString()); + setState(() { + _loading = false; + }); + } finally { + _textController.clear(); + setState(() { + _loading = false; + }); + _textFieldFocus.requestFocus(); + } + } + + Future _serverTemplateImageInput(String message) async { + setState(() { + _loading = true; + }); + + try { + _messages.add(MessageData(text: message, fromUser: true)); + // TODO: Add call to Firebase AI SDK + var response = 'Hello! This is a mocked response.'; + _messages.add(MessageData(text: response, fromUser: false)); + + setState(() { + _loading = false; + _scrollDown(); + }); + } catch (e) { + _showError(e.toString()); + setState(() { + _loading = false; + }); + } finally { + _textController.clear(); + setState(() { + _loading = false; + }); + _textFieldFocus.requestFocus(); + } + } + + Future _serverTemplateChat(String message) async { + setState(() { + _loading = true; + }); + + try { + _messages.add( + MessageData(text: message, fromUser: true), + ); + var response = await _chatSession?.sendMessage( + Content.text(message), + { + 'message': message, + }, + ); + + var text = response?.text; + + _messages.add(MessageData(text: text, fromUser: false)); + + setState(() { + _loading = false; + _scrollDown(); + }); + } catch (e) { + _showError(e.toString()); + setState(() { + _loading = false; + }); + } finally { + _textController.clear(); + setState(() { + _loading = false; + }); + _textFieldFocus.requestFocus(); + } + } + + Future _sendServerTemplateMessage(String message) async { + setState(() { + _loading = true; + }); + + try { + var response = await _templateGenerativeModel?.templateGenerateContent( + 'greeting.prompt', + { + 'name': message, + 'language': 'Chinese', + }, + ); + + _messages.add(MessageData(text: response?.text, fromUser: false)); + + setState(() { + _loading = false; + _scrollDown(); + }); + } catch (e) { + _showError(e.toString()); + setState(() { + _loading = false; + }); + } finally { + _textController.clear(); + setState(() { + _loading = false; + }); + _textFieldFocus.requestFocus(); + } + } + + 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/lib/src/client.dart b/packages/firebase_ai/firebase_ai/lib/src/client.dart index 221ea50e1af1..1f06d0e9eb99 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/client.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/client.dart @@ -64,6 +64,7 @@ final class HttpApiClient implements ApiClient { Future> makeRequest( Uri uri, Map body) async { final headers = await _headers(); + print('uri: $uri \nbody: $body \nheaders: $headers'); final response = await (_httpClient?.post ?? http.post)( uri, headers: headers, diff --git a/packages/firebase_ai/firebase_ai/lib/src/firebase_ai.dart b/packages/firebase_ai/firebase_ai/lib/src/firebase_ai.dart index 7f3df0d1a3ef..78a060a2343c 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/firebase_ai.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/firebase_ai.dart @@ -198,4 +198,27 @@ class FirebaseAI extends FirebasePluginPlatform { useLimitedUseAppCheckTokens: useLimitedUseAppCheckTokens, ); } + + @experimental + TemplateGenerativeModel templateGenerativeModel() { + return createTemplateGenerativeModel( + app: app, + location: location, + useVertexBackend: _useVertexBackend, + useLimitedUseAppCheckTokens: useLimitedUseAppCheckTokens, + auth: auth, + appCheck: appCheck); + } + + @experimental + TemplateImagenModel templateImagenModel() { + return createTemplateImagenModel( + app: app, + location: location, + useVertexBackend: _useVertexBackend, + useLimitedUseAppCheckTokens: useLimitedUseAppCheckTokens, + auth: auth, + appCheck: appCheck, + ); + } } From ba45ecc13311d2a2dc5208cb48195d51f737ccbe Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Fri, 3 Oct 2025 15:42:48 -0700 Subject: [PATCH 09/11] some tweak to clean up the PR --- packages/firebase_ai/firebase_ai/example/lib/main.dart | 6 +++--- .../firebase_ai/example/lib/pages/chat_page.dart | 1 - .../firebase_ai/example/lib/pages/image_prompt_page.dart | 1 - 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/example/lib/main.dart b/packages/firebase_ai/firebase_ai/example/lib/main.dart index 75d179c4a25e..ed9748965723 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/main.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/main.dart @@ -66,11 +66,11 @@ class _GenerativeAISampleState extends State { void _initializeModel(bool useVertexBackend) { if (useVertexBackend) { final vertexInstance = FirebaseAI.vertexAI(auth: FirebaseAuth.instance); - _currentModel = vertexInstance.generativeModel(model: 'gemini-1.5-flash'); + _currentModel = vertexInstance.generativeModel(model: 'gemini-2.5-flash'); _currentImagenModel = _initializeImagenModel(vertexInstance); } else { final googleAI = FirebaseAI.googleAI(auth: FirebaseAuth.instance); - _currentModel = googleAI.generativeModel(model: 'gemini-1.5-flash'); + _currentModel = googleAI.generativeModel(model: 'gemini-2.5-flash'); _currentImagenModel = _initializeImagenModel(googleAI); } } @@ -82,7 +82,7 @@ class _GenerativeAISampleState extends State { imageFormat: ImagenFormat.jpeg(compressionQuality: 75), ); return instance.imagenModel( - model: 'imagen-3.0-flash-001', + model: 'imagen-3.0-capability-001', generationConfig: generationConfig, safetySettings: ImagenSafetySettings( ImagenSafetyFilterLevel.blockLowAndAbove, 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 af0f05b1c4ae..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 @@ -33,7 +33,6 @@ class ChatPage extends StatefulWidget { class _ChatPageState extends State { ChatSession? _chat; - TemplateChatSession? _templateChat; GenerativeModel? _model; final ScrollController _scrollController = ScrollController(); final TextEditingController _textController = TextEditingController(); 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 463380cf3ca5..5c5009ca3158 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 @@ -11,7 +11,6 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:firebase_ai/firebase_ai.dart'; import 'package:flutter/services.dart'; From 93d8045dca7c39c0d215ccd98ae0d7c46e455169 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Mon, 6 Oct 2025 18:14:35 -0700 Subject: [PATCH 10/11] Update server prompt api name and generateContentStream --- .../lib/pages/server_template_page.dart | 7 +++---- .../firebase_ai/lib/src/base_model.dart | 4 ++++ .../lib/src/server_template/template_chat.dart | 3 +-- .../template_generative_model.dart | 18 +++++++++++++++++- .../server_template/template_imagen_model.dart | 2 +- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/server_template_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/server_template_page.dart index 9a518337141e..8ba346d8db9b 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/pages/server_template_page.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/server_template_page.dart @@ -57,8 +57,7 @@ class _ServerTemplatePageState extends State { FirebaseAI.googleAI().templateGenerativeModel(); _templateImagenModel = FirebaseAI.googleAI().templateImagenModel(); } - _chatSession = - _templateGenerativeModel?.startTemplateChat('chat_history.prompt'); + _chatSession = _templateGenerativeModel?.startChat('chat_history.prompt'); } void _scrollDown() { @@ -192,7 +191,7 @@ class _ServerTemplatePageState extends State { try { _messages.add(MessageData(text: message, fromUser: true)); // TODO: Add call to Firebase AI SDK - var response = await _templateImagenModel?.templateGenerateImages( + var response = await _templateImagenModel?.generateImages( 'generate_images.prompt', { 'prompt': message, @@ -306,7 +305,7 @@ class _ServerTemplatePageState extends State { }); try { - var response = await _templateGenerativeModel?.templateGenerateContent( + var response = await _templateGenerativeModel?.generateContent( 'greeting.prompt', { 'name': message, diff --git a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart index 0800e748923d..870121e075f2 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart @@ -59,10 +59,14 @@ enum Task { predict, } +/// [TemplateTask] enum class for [TemplateGenerativeModel] to make request. enum TemplateTask { /// Request type for server template generate content. templateGenerateContent, + /// Request type for server template stream generate content + templateStreamGenerateContent, + /// Request type for server template for Prediction Services like Imagen. templatePredict, } diff --git a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_chat.dart b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_chat.dart index b7b1b663ea0f..a28092b842ac 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_chat.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_chat.dart @@ -87,8 +87,7 @@ extension StartTemplateChatExtension on TemplateGenerativeModel { /// final response = await chat.sendMessage(Content.text('Hello there.')); /// print(response.text); /// ``` - TemplateChatSession startTemplateChat(String templateId, - {List? history}) => + TemplateChatSession startChat(String templateId, {List? history}) => TemplateChatSession._( templateGenerateContentWithHistory, templateId, history ?? []); } diff --git a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_generative_model.dart b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_generative_model.dart index ea3cde227d22..a84499f9f7f2 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_generative_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_generative_model.dart @@ -46,12 +46,28 @@ final class TemplateGenerativeModel extends BaseTemplateApiClientModel { /// /// Sends a "templateGenerateContent" API request for the configured model. @experimental - Future templateGenerateContent( + Future generateContent( String templateId, Map params, ) => makeTemplateRequest(TemplateTask.templateGenerateContent, templateId, params, null, _serializationStrategy.parseGenerateContentResponse); + + /// Generates a stream of content responding to [templateId] and [params]. + /// + /// Sends a "templateStreamGenerateContent" API request for the server template, + /// and waits for the response. + @experimental + Stream generateContentStream( + String templateId, + Map params, + ) { + final response = client.streamRequest( + templateTaskUri(TemplateTask.templateStreamGenerateContent, templateId), + params); + return response.map(_serializationStrategy.parseGenerateContentResponse); + } + @experimental Future templateGenerateContentWithHistory( Iterable history, diff --git a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_imagen_model.dart b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_imagen_model.dart index 742d552d9a89..7e978f985589 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_imagen_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_imagen_model.dart @@ -42,7 +42,7 @@ final class TemplateImagenModel extends BaseTemplateApiClientModel { /// Generates images from a template with the given [templateId] and [params]. @experimental - Future> templateGenerateImages( + Future> generateImages( String templateId, Map params, ) => From 6f0dfbd7728ee1b9d4eed5550ea477705afb9b27 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Wed, 8 Oct 2025 11:25:03 -0700 Subject: [PATCH 11/11] name updates and stream for chat history --- .../lib/pages/server_template_page.dart | 1 - .../firebase_ai/lib/src/base_model.dart | 28 ++++++++-- .../firebase_ai/firebase_ai/lib/src/chat.dart | 43 +-------------- .../src/server_template/template_chat.dart | 53 ++++++++++++++++-- .../template_generative_model.dart | 38 +++++++++---- .../template_imagen_model.dart | 6 +- .../firebase_ai/lib/src/utils/chat_utils.dart | 55 +++++++++++++++++++ 7 files changed, 159 insertions(+), 65 deletions(-) create mode 100644 packages/firebase_ai/firebase_ai/lib/src/utils/chat_utils.dart diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/server_template_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/server_template_page.dart index 8ba346d8db9b..ecbb5e9f6779 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/pages/server_template_page.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/server_template_page.dart @@ -309,7 +309,6 @@ class _ServerTemplatePageState extends State { 'greeting.prompt', { 'name': message, - 'language': 'Chinese', }, ); diff --git a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart index 870121e075f2..06ce091d0617 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart @@ -346,16 +346,16 @@ abstract class BaseTemplateApiClientModel extends BaseApiClientModel { final _TemplateUri _templateUri; /// Make a unary request for [task] with [templateId] and JSON encodable - /// [params]. + /// [inputs]. Future makeTemplateRequest( TemplateTask task, String templateId, - Map? params, + Map? inputs, Iterable? history, T Function(Map) parse) { Map body = {}; - if (params != null) { - body['inputs'] = params; + if (inputs != null) { + body['inputs'] = inputs; } if (history != null) { body['history'] = history.map((c) => c.toJson()).toList(); @@ -365,6 +365,26 @@ abstract class BaseTemplateApiClientModel extends BaseApiClientModel { .then(parse); } + /// Make a unary request for [task] with [templateId] and JSON encodable + /// [inputs]. + Stream streamTemplateRequest( + TemplateTask task, + String templateId, + Map? inputs, + Iterable? history, + T Function(Map) parse) { + Map body = {}; + if (inputs != null) { + body['inputs'] = inputs; + } + if (history != null) { + body['history'] = history.map((c) => c.toJson()).toList(); + } + final response = + _client.streamRequest(templateTaskUri(task, templateId), body); + return response.map(parse); + } + Uri templateTaskUri(TemplateTask task, String templateId) => _templateUri.templateTaskUri(task, templateId); diff --git a/packages/firebase_ai/firebase_ai/lib/src/chat.dart b/packages/firebase_ai/firebase_ai/lib/src/chat.dart index fdcbd3cb2920..991a0421f98d 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/chat.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/chat.dart @@ -17,6 +17,7 @@ import 'dart:async'; import 'api.dart'; import 'base_model.dart'; import 'content.dart'; +import 'utils/chat_utils.dart'; import 'utils/mutex.dart'; /// A back-and-forth chat with a generative model. @@ -114,7 +115,7 @@ final class ChatSession { } if (content.isNotEmpty) { _history.add(message); - _history.add(_aggregate(content)); + _history.add(historyAggregate(content)); } } catch (e, s) { controller.addError(e, s); @@ -124,46 +125,6 @@ final class ChatSession { }); return controller.stream; } - - /// Aggregates a list of [Content] responses into a single [Content]. - /// - /// Includes all the [Content.parts] of every element of [contents], - /// and concatenates adjacent [TextPart]s into a single [TextPart], - /// even across adjacent [Content]s. - Content _aggregate(List contents) { - assert(contents.isNotEmpty); - final role = contents.first.role ?? 'model'; - final textBuffer = StringBuffer(); - // If non-null, only a single text part has been seen. - TextPart? previousText; - final parts = []; - void addBufferedText() { - if (textBuffer.isEmpty) return; - if (previousText case final singleText?) { - parts.add(singleText); - previousText = null; - } else { - parts.add(TextPart(textBuffer.toString())); - } - textBuffer.clear(); - } - - for (final content in contents) { - for (final part in content.parts) { - if (part case TextPart(:final text)) { - if (text.isNotEmpty) { - previousText = textBuffer.isEmpty ? part : null; - textBuffer.write(text); - } - } else { - addBufferedText(); - parts.add(part); - } - } - } - addBufferedText(); - return Content(role, parts); - } } /// [StartChatExtension] on [GenerativeModel] diff --git a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_chat.dart b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_chat.dart index a28092b842ac..80cee3c807ec 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_chat.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_chat.dart @@ -11,9 +11,12 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +import 'dart:async'; + import '../api.dart'; import '../base_model.dart'; import '../content.dart'; +import '../utils/chat_utils.dart'; import '../utils/mutex.dart'; /// A back-and-forth chat with a server template. @@ -26,6 +29,7 @@ import '../utils/mutex.dart'; final class TemplateChatSession { TemplateChatSession._( this._templateHistoryGenerateContent, + this._templateHistoryGenerateContentStream, this._templateId, this._history, ); @@ -33,7 +37,12 @@ final class TemplateChatSession { final Future Function( Iterable content, String templateId, - Map params) _templateHistoryGenerateContent; + Map inputs) _templateHistoryGenerateContent; + + final Stream Function( + Iterable content, + String templateId, + Map inputs) _templateHistoryGenerateContentStream; final String _templateId; final List _history; @@ -48,7 +57,7 @@ final class TemplateChatSession { /// including the message sent to the model. Iterable get history => _history.skip(0); - /// Sends [params] to the server template as a continuation of the chat [history]. + /// Sends [inputs] to the server template as a continuation of the chat [history]. /// /// Prepends the history to the request and uses the provided model to /// generate new content. @@ -56,13 +65,13 @@ final class TemplateChatSession { /// When there are no candidates in the response, the [message] and response /// are ignored and will not be recorded in the [history]. Future sendMessage( - Content message, Map params) async { + Content message, Map inputs) async { final lock = await _mutex.acquire(); try { final response = await _templateHistoryGenerateContent( _history.followedBy([message]), _templateId, - params, + inputs, ); if (response.candidates case [final candidate, ...]) { _history.add(message); @@ -76,6 +85,36 @@ final class TemplateChatSession { lock.release(); } } + + Stream sendMessageStream( + Content message, Map inputs) { + final controller = StreamController(sync: true); + _mutex.acquire().then((lock) async { + try { + final responses = _templateHistoryGenerateContentStream( + _history.followedBy([message]), + _templateId, + inputs, + ); + final content = []; + await for (final response in responses) { + if (response.candidates case [final candidate, ...]) { + content.add(candidate.content); + } + controller.add(response); + } + if (content.isNotEmpty) { + _history.add(message); + _history.add(historyAggregate(content)); + } + } catch (e, s) { + controller.addError(e, s); + } + lock.release(); + unawaited(controller.close()); + }); + return controller.stream; + } } /// [StartTemplateChatExtension] on [GenerativeModel] @@ -89,5 +128,9 @@ extension StartTemplateChatExtension on TemplateGenerativeModel { /// ``` TemplateChatSession startChat(String templateId, {List? history}) => TemplateChatSession._( - templateGenerateContentWithHistory, templateId, history ?? []); + templateGenerateContentWithHistory, + templateGenerateContentWithHistoryStream, + templateId, + history ?? [], + ); } diff --git a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_generative_model.dart b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_generative_model.dart index a84499f9f7f2..016916b5f4e0 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_generative_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_generative_model.dart @@ -42,40 +42,56 @@ final class TemplateGenerativeModel extends BaseTemplateApiClientModel { : _TemplateGoogleAIUri(app: app), ); - /// Generates content from a template with the given [templateId] and [params]. + /// Generates content from a template with the given [templateId] and [inputs]. /// /// Sends a "templateGenerateContent" API request for the configured model. @experimental Future generateContent( String templateId, - Map params, + Map inputs, ) => makeTemplateRequest(TemplateTask.templateGenerateContent, templateId, - params, null, _serializationStrategy.parseGenerateContentResponse); + inputs, null, _serializationStrategy.parseGenerateContentResponse); - /// Generates a stream of content responding to [templateId] and [params]. + /// Generates a stream of content responding to [templateId] and [inputs]. /// /// Sends a "templateStreamGenerateContent" API request for the server template, /// and waits for the response. @experimental Stream generateContentStream( String templateId, - Map params, + Map inputs, ) { - final response = client.streamRequest( - templateTaskUri(TemplateTask.templateStreamGenerateContent, templateId), - params); - return response.map(_serializationStrategy.parseGenerateContentResponse); + return streamTemplateRequest( + TemplateTask.templateStreamGenerateContent, + templateId, + inputs, + null, + _serializationStrategy.parseGenerateContentResponse); } @experimental Future templateGenerateContentWithHistory( Iterable history, String templateId, - Map params, + Map inputs, ) => makeTemplateRequest(TemplateTask.templateGenerateContent, templateId, - params, history, _serializationStrategy.parseGenerateContentResponse); + inputs, history, _serializationStrategy.parseGenerateContentResponse); + + @experimental + Stream templateGenerateContentWithHistoryStream( + Iterable history, + String templateId, + Map inputs, + ) { + return streamTemplateRequest( + TemplateTask.templateStreamGenerateContent, + templateId, + inputs, + history, + _serializationStrategy.parseGenerateContentResponse); + } } /// Returns a [TemplateGenerativeModel] using it's private constructor. diff --git a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_imagen_model.dart b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_imagen_model.dart index 7e978f985589..46f286d9c296 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_imagen_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_imagen_model.dart @@ -40,16 +40,16 @@ final class TemplateImagenModel extends BaseTemplateApiClientModel { final bool _useVertexBackend; - /// Generates images from a template with the given [templateId] and [params]. + /// Generates images from a template with the given [templateId] and [inputs]. @experimental Future> generateImages( String templateId, - Map params, + Map inputs, ) => makeTemplateRequest( TemplateTask.templatePredict, templateId, - params, + inputs, null, (jsonObject) => parseImagenGenerationResponse(jsonObject), diff --git a/packages/firebase_ai/firebase_ai/lib/src/utils/chat_utils.dart b/packages/firebase_ai/firebase_ai/lib/src/utils/chat_utils.dart new file mode 100644 index 000000000000..265759b0085d --- /dev/null +++ b/packages/firebase_ai/firebase_ai/lib/src/utils/chat_utils.dart @@ -0,0 +1,55 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '../content.dart'; + +/// Aggregates a list of [Content] responses into a single [Content]. +/// +/// Includes all the [Content.parts] of every element of [contents], +/// and concatenates adjacent [TextPart]s into a single [TextPart], +/// even across adjacent [Content]s. +Content historyAggregate(List contents) { + assert(contents.isNotEmpty); + final role = contents.first.role ?? 'model'; + final textBuffer = StringBuffer(); + // If non-null, only a single text part has been seen. + TextPart? previousText; + final parts = []; + void addBufferedText() { + if (textBuffer.isEmpty) return; + if (previousText case final singleText?) { + parts.add(singleText); + previousText = null; + } else { + parts.add(TextPart(textBuffer.toString())); + } + textBuffer.clear(); + } + + for (final content in contents) { + for (final part in content.parts) { + if (part case TextPart(:final text)) { + if (text.isNotEmpty) { + previousText = textBuffer.isEmpty ? part : null; + textBuffer.write(text); + } + } else { + addBufferedText(); + parts.add(part); + } + } + } + addBufferedText(); + return Content(role, parts); +}