From 183a6512f8efceeb1e3363d657837bd54124d85c Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Sun, 25 Jan 2026 20:35:58 -0800 Subject: [PATCH 01/17] add chat and function back to server prompt template --- .../lib/pages/server_template_page.dart | 122 +++++++++++++++ .../firebase_ai/lib/firebase_ai.dart | 3 +- .../src/server_template/template_chat.dart | 145 ++++++++++++++++++ .../template_generative_model.dart | 23 +++ 4 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 packages/firebase_ai/firebase_ai/lib/src/server_template/template_chat.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 efe0c8946b94..3baa0fe2fb0f 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 @@ -41,6 +41,9 @@ class _ServerTemplatePageState extends State { TemplateGenerativeModel? _templateGenerativeModel; TemplateImagenModel? _templateImagenModel; + TemplateChatSession? _chatSession; + TemplateChatSession? _chatFunctionSession; + @override void initState() { super.initState(); @@ -58,6 +61,9 @@ class _ServerTemplatePageState extends State { FirebaseAI.googleAI().templateGenerativeModel(); _templateImagenModel = FirebaseAI.googleAI().templateImagenModel(); } + _chatSession = _templateGenerativeModel?.startChat('chat_history.prompt'); + _chatFunctionSession = + _templateGenerativeModel?.startChat('function-calling'); } void _scrollDown() { @@ -122,6 +128,28 @@ class _ServerTemplatePageState extends State { const SizedBox.square( dimension: 15, ), + if (!_loading) + IconButton( + onPressed: () async { + await _serverTemplateFunctionCall(_textController.text); + }, + icon: Icon( + Icons.functions, + color: Theme.of(context).colorScheme.primary, + ), + tooltip: 'Function Calling', + ), + if (!_loading) + IconButton( + onPressed: () async { + await _serverTemplateChat(_textController.text); + }, + icon: Icon( + Icons.chat, + color: Theme.of(context).colorScheme.primary, + ), + tooltip: 'Chat', + ), if (!_loading) IconButton( onPressed: () async { @@ -166,6 +194,100 @@ class _ServerTemplatePageState extends State { ); } + Future _serverTemplateFunctionCall(String message) async { + setState(() { + _loading = true; + }); + + try { + _messages.add( + MessageData(text: message, fromUser: true), + ); + var response = await _chatFunctionSession?.sendMessage( + Content.text(message), + inputs: { + 'customerName': message, + 'orientation': 'PORTRAIT', + 'useFlash': true, + 'zoom': 2, + }, + ); + + _messages.add(MessageData(text: response?.text, fromUser: false)); + final functionCalls = response?.functionCalls.toList(); + if (functionCalls!.isNotEmpty) { + final functionCall = functionCalls.first; + if (functionCall.name == 'takePicture') { + ByteData catBytes = await rootBundle.load('assets/images/cat.jpg'); + var imageBytes = catBytes.buffer.asUint8List(); + final functionResult = { + 'aspectRatio': '16:9', + 'mimeType': 'image/jpeg', + 'data': base64Encode(imageBytes), + }; + var functionResponse = await _chatFunctionSession?.sendMessage( + Content.functionResponse(functionCall.name, functionResult), + inputs: {}, + ); + _messages + .add(MessageData(text: functionResponse?.text, 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), + inputs: { + '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 _serverTemplateImagen(String message) 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 681989831e53..437f40a875e1 100644 --- a/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart +++ b/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart @@ -111,7 +111,8 @@ export 'src/live_api.dart' Transcription; 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 AutoFunctionDeclaration, 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..95ea9c9bb1af --- /dev/null +++ b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_chat.dart @@ -0,0 +1,145 @@ +// Copyright 2026 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 '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. +/// +/// 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 reflects the most current state of the chat session. +final class TemplateChatSession { + TemplateChatSession._( + this._templateHistoryGenerateContent, + this._templateHistoryGenerateContentStream, + this._templateId, + this._history, + ); + + final Future Function( + Iterable content, String templateId, + {required Map inputs}) _templateHistoryGenerateContent; + + final Stream Function( + Iterable content, String templateId, + {required Map inputs}) + _templateHistoryGenerateContentStream; + 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 [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. + /// + /// 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, + {required Map inputs}) async { + final lock = await _mutex.acquire(); + try { + final response = await _templateHistoryGenerateContent( + _history.followedBy([message]), + _templateId, + inputs: inputs, + ); + 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(); + } + } + + /// Sends [message] to the server template as a continuation of the chat + /// [history]. + /// + /// Returns a stream of responses, which may be chunks of a single aggregate + /// response. + /// + /// 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]. + Stream sendMessageStream(Content message, + {required Map inputs}) { + final controller = StreamController(sync: true); + _mutex.acquire().then((lock) async { + try { + final responses = _templateHistoryGenerateContentStream( + _history.followedBy([message]), + _templateId, + inputs: 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; + } +} + +/// An extension on [TemplateGenerativeModel] that provides a `startChat` method. +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 startChat(String templateId, {List? history}) => + TemplateChatSession._( + 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 75e9029f44b4..78e302b5df47 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 @@ -87,6 +87,29 @@ final class TemplateGenerativeModel extends BaseTemplateApiClientModel { null, _serializationStrategy.parseGenerateContentResponse); } + + /// Generates content from a template with the given [templateId], [inputs] and + /// [history]. + @experimental + Future templateGenerateContentWithHistory( + Iterable history, String templateId, + {required Map inputs}) => + makeTemplateRequest(TemplateTask.templateGenerateContent, templateId, + inputs, history, _serializationStrategy.parseGenerateContentResponse); + + /// Generates a stream of content from a template with the given [templateId], + /// [inputs] and [history]. + @experimental + Stream templateGenerateContentWithHistoryStream( + Iterable history, String templateId, + {required Map inputs}) { + return streamTemplateRequest( + TemplateTask.templateStreamGenerateContent, + templateId, + inputs, + history, + _serializationStrategy.parseGenerateContentResponse); + } } /// Returns a [TemplateGenerativeModel] using its private constructor. From 20e2769363cd0f8457457c3e5e25b4c2373066bc Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Mon, 26 Jan 2026 10:19:29 -0800 Subject: [PATCH 02/17] auto function calling for server prompt template --- .../lib/pages/function_calling_page.dart | 37 +-- .../lib/pages/server_template_page.dart | 230 ++++++++---------- .../lib/utils/function_call_utils.dart | 47 ++++ .../firebase_ai/lib/firebase_ai.dart | 2 +- .../src/server_template/template_chat.dart | 95 ++++++-- 5 files changed, 225 insertions(+), 186 deletions(-) create mode 100644 packages/firebase_ai/firebase_ai/example/lib/utils/function_call_utils.dart diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart index 902fa9812bec..08984b41f0fb 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart @@ -15,6 +15,7 @@ import 'package:flutter/material.dart'; import 'package:firebase_ai/firebase_ai.dart'; import 'package:firebase_auth/firebase_auth.dart'; +import '../utils/function_call_utils.dart'; import '../widgets/message_widget.dart'; class FunctionCallingPage extends StatefulWidget { @@ -31,13 +32,6 @@ class FunctionCallingPage extends StatefulWidget { State createState() => _FunctionCallingPageState(); } -class Location { - final String city; - final String state; - - Location(this.city, this.state); -} - class _FunctionCallingPageState extends State { late GenerativeModel _functionCallModel; late GenerativeModel _autoFunctionCallModel; @@ -76,7 +70,7 @@ class _FunctionCallingPageState extends State { 'The date for which to get the weather. Date must be in the format: YYYY-MM-DD.', ), }, - callable: _fetchWeatherCallable, + callable: fetchWeatherCallable, ); _autoFindRestaurantsTool = AutoFunctionDeclaration( name: 'findRestaurants', @@ -148,16 +142,6 @@ class _FunctionCallingPageState extends State { }; } - Future> _fetchWeatherCallable( - Map args, - ) async { - final locationData = args['location']! as Map; - final city = locationData['city']! as String; - final state = locationData['state']! as String; - final date = args['date']! as String; - return fetchWeather(Location(city, state), date); - } - void _initializeModel() { final generationConfig = GenerationConfig( thinkingConfig: _enableThinking @@ -204,23 +188,6 @@ class _FunctionCallingPageState extends State { ); } - // This is a hypothetical API to return a fake weather data collection for - // certain location - Future> fetchWeather( - Location location, - String date, - ) async { - // TODO(developer): Call a real weather API. - // Mock response from the API. In developer live code this would call the - // external API and return what that API returns. - final apiResponse = { - 'temperature': 38, - 'chancePrecipitation': '56%', - 'cloudConditions': 'partly-cloudy', - }; - return apiResponse; - } - /// Actual function to demonstrate the function calling feature. final fetchWeatherTool = FunctionDeclaration( 'fetchWeather', 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 3baa0fe2fb0f..275033519d9f 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 @@ -14,6 +14,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import '../utils/function_call_utils.dart'; import '../widgets/message_widget.dart'; import 'package:firebase_ai/firebase_ai.dart'; @@ -43,6 +44,7 @@ class _ServerTemplatePageState extends State { TemplateChatSession? _chatSession; TemplateChatSession? _chatFunctionSession; + TemplateChatSession? _chatAutoFunctionSession; @override void initState() { @@ -63,7 +65,16 @@ class _ServerTemplatePageState extends State { } _chatSession = _templateGenerativeModel?.startChat('chat_history.prompt'); _chatFunctionSession = - _templateGenerativeModel?.startChat('function-calling'); + _templateGenerativeModel?.startChat('cj-function-calling-weather'); + _chatAutoFunctionSession = _templateGenerativeModel?.startChat( + 'cj-function-calling-weather', + autoFunctions: [ + TemplateAutoFunction( + name: 'fetchWeather', + callable: fetchWeatherCallable, + ), + ], + ); } void _scrollDown() { @@ -128,6 +139,19 @@ class _ServerTemplatePageState extends State { const SizedBox.square( dimension: 15, ), + if (!_loading) + IconButton( + onPressed: () async { + await _serverTemplateAutoFunctionCall( + _textController.text, + ); + }, + icon: Icon( + Icons.auto_mode, + color: Theme.of(context).colorScheme.primary, + ), + tooltip: 'Auto Function Calling', + ), if (!_loading) IconButton( onPressed: () async { @@ -194,37 +218,79 @@ class _ServerTemplatePageState extends State { ); } - Future _serverTemplateFunctionCall(String message) async { + Future _serverTemplateAutoFunctionCall(String message) async { + await _handleServerTemplateMessage(message, (message) async { + var response = await _chatAutoFunctionSession?.sendMessage( + Content.text(message), + inputs: {}, + ); + + _messages.add(MessageData(text: response?.text, fromUser: false)); + + // final functionCalls = response?.functionCalls.toList(); + // if (functionCalls!.isNotEmpty) { + // final functionCall = functionCalls.first; + // if (functionCall.name == 'fetchWeather') { + // final location = + // functionCall.args['location']! as Map; + // final date = functionCall.args['date']! as String; + // final city = location['city'] as String; + // final state = location['state'] as String; + // final functionResult = + // await fetchWeather(Location(city, state), date); + // var functionResponse = await _chatFunctionSession?.sendMessage( + // Content.functionResponse(functionCall.name, functionResult), + // inputs: {}, + // ); + // _messages + // .add(MessageData(text: functionResponse?.text, fromUser: false)); + // } + // } + }); + } + + Future _handleServerTemplateMessage( + String message, + Future Function(String) generateContent, + ) async { setState(() { _loading = true; }); try { - _messages.add( - MessageData(text: message, fromUser: true), - ); + _messages.add(MessageData(text: message, fromUser: true)); + await generateContent(message); + } catch (e) { + _showError(e.toString()); + } finally { + _textController.clear(); + setState(() { + _loading = false; + }); + _textFieldFocus.requestFocus(); + _scrollDown(); + } + } + + Future _serverTemplateFunctionCall(String message) async { + await _handleServerTemplateMessage(message, (message) async { var response = await _chatFunctionSession?.sendMessage( Content.text(message), - inputs: { - 'customerName': message, - 'orientation': 'PORTRAIT', - 'useFlash': true, - 'zoom': 2, - }, + inputs: {}, ); _messages.add(MessageData(text: response?.text, fromUser: false)); final functionCalls = response?.functionCalls.toList(); if (functionCalls!.isNotEmpty) { final functionCall = functionCalls.first; - if (functionCall.name == 'takePicture') { - ByteData catBytes = await rootBundle.load('assets/images/cat.jpg'); - var imageBytes = catBytes.buffer.asUint8List(); - final functionResult = { - 'aspectRatio': '16:9', - 'mimeType': 'image/jpeg', - 'data': base64Encode(imageBytes), - }; + if (functionCall.name == 'fetchWeather') { + final location = + functionCall.args['location']! as Map; + final date = functionCall.args['date']! as String; + final city = location['city'] as String; + final state = location['state'] as String; + final functionResult = + await fetchWeather(Location(city, state), date); var functionResponse = await _chatFunctionSession?.sendMessage( Content.functionResponse(functionCall.name, functionResult), inputs: {}, @@ -233,33 +299,11 @@ class _ServerTemplatePageState extends State { .add(MessageData(text: functionResponse?.text, 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), - ); + await _handleServerTemplateMessage(message, (message) async { var response = await _chatSession?.sendMessage( Content.text(message), inputs: { @@ -270,31 +314,12 @@ class _ServerTemplatePageState extends State { 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 _serverTemplateImagen(String message) async { - setState(() { - _loading = true; - }); - MessageData? resultMessage; - try { - _messages.add(MessageData(text: message, fromUser: true)); + await _handleServerTemplateMessage(message, (message) async { + MessageData? resultMessage; var response = await _templateImagenModel?.generateImages( 'portrait-googleai', inputs: { @@ -314,34 +339,14 @@ class _ServerTemplatePageState extends State { // 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(); - } + if (resultMessage != null) { + _messages.add(resultMessage); + } + }); } Future _serverTemplateImageInput(String message) async { - setState(() { - _loading = true; - }); - - try { + await _handleServerTemplateMessage(message, (message) async { ByteData catBytes = await rootBundle.load('assets/images/cat.jpg'); var imageBytes = catBytes.buffer.asUint8List(); _messages.add( @@ -353,7 +358,7 @@ class _ServerTemplatePageState extends State { ); var response = await _templateGenerativeModel?.generateContent( - 'media.prompt', + 'media', inputs: { 'imageData': { 'isInline': true, @@ -363,53 +368,16 @@ class _ServerTemplatePageState extends State { }, ); _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(); - } + }); } Future _sendServerTemplateMessage(String message) async { - setState(() { - _loading = true; - }); - - try { - _messages.add(MessageData(text: message, fromUser: true)); + await _handleServerTemplateMessage(message, (message) async { var response = await _templateGenerativeModel ?.generateContent('new-greeting', inputs: {}); _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) { diff --git a/packages/firebase_ai/firebase_ai/example/lib/utils/function_call_utils.dart b/packages/firebase_ai/firebase_ai/example/lib/utils/function_call_utils.dart new file mode 100644 index 000000000000..ff4a5d6991e0 --- /dev/null +++ b/packages/firebase_ai/firebase_ai/example/lib/utils/function_call_utils.dart @@ -0,0 +1,47 @@ +// 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. + +class Location { + final String city; + final String state; + + Location(this.city, this.state); +} + +// This is a hypothetical API to return a fake weather data collection for +// certain location +Future> fetchWeather( + Location location, + String date, +) async { + // TODO(developer): Call a real weather API. + // Mock response from the API. In developer live code this would call the + // external API and return what that API returns. + final apiResponse = { + 'temperature': 38, + 'chancePrecipitation': '56%', + 'cloudConditions': 'partly-cloudy', + }; + return apiResponse; +} + +Future> fetchWeatherCallable( + Map args, +) async { + final locationData = args['location']! as Map; + final city = locationData['city']! as String; + final state = locationData['state']! as String; + final date = args['date']! as String; + return fetchWeather(Location(city, state), date); +} diff --git a/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart b/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart index 437f40a875e1..ad35755ff06e 100644 --- a/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart +++ b/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart @@ -112,7 +112,7 @@ export 'src/live_api.dart' export 'src/live_session.dart' show LiveSession; export 'src/schema.dart' show Schema, SchemaType; export 'src/server_template/template_chat.dart' - show TemplateChatSession, StartTemplateChatExtension; + show TemplateChatSession, TemplateAutoFunction, StartTemplateChatExtension; export 'src/tool.dart' show AutoFunctionDeclaration, 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 95ea9c9bb1af..fb97ecdb0501 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 @@ -19,6 +19,23 @@ import '../content.dart'; import '../utils/chat_utils.dart'; import '../utils/mutex.dart'; +final class TemplateAutoFunction { + TemplateAutoFunction({ + required this.name, + required this.callable, + }); + + /// The name of the function. + /// + /// Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum + /// length of 63. + final String name; + + /// The callable function that this declaration represents. + final FutureOr> Function(Map args) + callable; +} + /// A back-and-forth chat with a server template. /// /// Records messages sent and received in [history]. The history will always @@ -31,7 +48,9 @@ final class TemplateChatSession { this._templateHistoryGenerateContentStream, this._templateId, this._history, - ); + List autoFunctionLists, + this._maxTurns, + ) : _autoFunctions = {for (var item in autoFunctionLists) item.name: item}; final Future Function( Iterable content, String templateId, @@ -43,6 +62,8 @@ final class TemplateChatSession { _templateHistoryGenerateContentStream; final String _templateId; final List _history; + final Map _autoFunctions; + final int _maxTurns; final _mutex = Mutex(); @@ -66,19 +87,51 @@ final class TemplateChatSession { {required Map inputs}) async { final lock = await _mutex.acquire(); try { - final response = await _templateHistoryGenerateContent( - _history.followedBy([message]), - _templateId, - inputs: inputs, - ); - 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); + final requestHistory = [message]; + var turn = 0; + while (turn < _maxTurns) { + final response = await _templateHistoryGenerateContent( + _history.followedBy(requestHistory), + _templateId, + inputs: inputs, + ); + + final functionCalls = response.functionCalls; + final shouldAutoExecute = _autoFunctions.isNotEmpty && + functionCalls.isNotEmpty && + functionCalls.every((c) => _autoFunctions.containsKey(c.name)); + + if (!shouldAutoExecute) { + // Standard handling: Update history and return the response to the user. + 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; + } + + // Auto function execution + requestHistory.add(response.candidates.first.content); + final functionResponses = []; + for (final functionCall in functionCalls) { + final function = _autoFunctions[functionCall.name]; + + Object? result; + try { + result = await function!.callable(functionCall.args); + } catch (e) { + result = e.toString(); + } + functionResponses + .add(FunctionResponse(functionCall.name, {'result': result})); + } + requestHistory.add(Content('function', functionResponses)); + turn++; } - return response; + throw Exception('Max turns of $_maxTurns reached.'); } finally { lock.release(); } @@ -135,11 +188,15 @@ extension StartTemplateChatExtension on TemplateGenerativeModel { /// final response = await chat.sendMessage(Content.text('Hello there.')); /// print(response.text); /// ``` - TemplateChatSession startChat(String templateId, {List? history}) => + TemplateChatSession startChat(String templateId, + {List? history, + List? autoFunctions, + int? maxTurns}) => TemplateChatSession._( - templateGenerateContentWithHistory, - templateGenerateContentWithHistoryStream, - templateId, - history ?? [], - ); + templateGenerateContentWithHistory, + templateGenerateContentWithHistoryStream, + templateId, + history ?? [], + autoFunctions ?? [], + maxTurns ?? 5); } From be55d7c22d407cc0d1dee124bc742f1e2fd26b97 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Thu, 12 Feb 2026 18:26:34 -0800 Subject: [PATCH 03/17] update with api decision --- .../lib/pages/server_template_page.dart | 168 +++++++----------- .../src/server_template/template_chat.dart | 25 +-- 2 files changed, 83 insertions(+), 110 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 03b8b295d501..155c401d81a3 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 @@ -63,11 +63,19 @@ class _ServerTemplatePageState extends State { FirebaseAI.googleAI().templateGenerativeModel(); _templateImagenModel = FirebaseAI.googleAI().templateImagenModel(); } - _chatSession = _templateGenerativeModel?.startChat('chat_history.prompt'); - _chatFunctionSession = - _templateGenerativeModel?.startChat('cj-function-calling-weather'); + + // Inputs are now provided ONCE here when creating the session + _chatSession = _templateGenerativeModel?.startChat( + 'chat_history.prompt', + inputs: {}, + ); + _chatFunctionSession = _templateGenerativeModel?.startChat( + 'cj-function-calling-weather', + inputs: {}, + ); _chatAutoFunctionSession = _templateGenerativeModel?.startChat( 'cj-function-calling-weather', + inputs: {}, autoFunctions: [ TemplateAutoFunction( name: 'fetchWeather', @@ -229,129 +237,92 @@ class _ServerTemplatePageState extends State { ); } - Future _serverTemplateUrlContext(String message) async { + Future _handleServerTemplateMessage( + String message, + Future Function(String) generateContent, + ) async { setState(() { _loading = true; }); try { _messages.add(MessageData(text: message, fromUser: true)); - var response = await _templateGenerativeModel - ?.generateContent('cj-urlcontext', inputs: {'url': message}); - - final candidate = response?.candidates.first; - if (candidate == null) { - _messages.add(MessageData(text: 'No response', fromUser: false)); - } else { - final responseText = candidate.text ?? ''; - final groundingMetadata = candidate.groundingMetadata; - final urlContextMetadata = candidate.urlContextMetadata; - - final buffer = StringBuffer(responseText); - if (groundingMetadata != null) { - buffer.writeln('\n\n--- Grounding Metadata ---'); - buffer.writeln('Web Search Queries:'); - for (final query in groundingMetadata.webSearchQueries) { - buffer.writeln(' - $query'); - } - buffer.writeln('\nGrounding Chunks:'); - for (final chunk in groundingMetadata.groundingChunks) { - if (chunk.web != null) { - buffer.writeln(' - Web Chunk:'); - buffer.writeln(' - Title: ${chunk.web!.title}'); - buffer.writeln(' - URI: ${chunk.web!.uri}'); - buffer.writeln(' - Domain: ${chunk.web!.domain}'); - } - } - } - - if (urlContextMetadata != null) { - buffer.writeln('\n\n--- URL Context Metadata ---'); - for (final data in urlContextMetadata.urlMetadata) { - buffer.writeln(' - URL: ${data.retrievedUrl}'); - buffer.writeln(' Status: ${data.urlRetrievalStatus}'); - } - } - _messages.add(MessageData(text: buffer.toString(), fromUser: false)); - } - - setState(() { - _loading = false; - _scrollDown(); - }); + await generateContent(message); } catch (e) { _showError(e.toString()); - setState(() { - _loading = false; - }); } finally { _textController.clear(); setState(() { _loading = false; }); _textFieldFocus.requestFocus(); + _scrollDown(); } } + Future _serverTemplateUrlContext(String message) async { + await _handleServerTemplateMessage( + message, + (message) async { + _messages.add(MessageData(text: message, fromUser: true)); + var response = await _templateGenerativeModel + ?.generateContent('cj-urlcontext', inputs: {'url': message}); + + final candidate = response?.candidates.first; + if (candidate == null) { + _messages.add(MessageData(text: 'No response', fromUser: false)); + } else { + final responseText = candidate.text ?? ''; + final groundingMetadata = candidate.groundingMetadata; + final urlContextMetadata = candidate.urlContextMetadata; + + final buffer = StringBuffer(responseText); + if (groundingMetadata != null) { + buffer.writeln('\n\n--- Grounding Metadata ---'); + buffer.writeln('Web Search Queries:'); + for (final query in groundingMetadata.webSearchQueries) { + buffer.writeln(' - $query'); + } + buffer.writeln('\nGrounding Chunks:'); + for (final chunk in groundingMetadata.groundingChunks) { + if (chunk.web != null) { + buffer.writeln(' - Web Chunk:'); + buffer.writeln(' - Title: ${chunk.web!.title}'); + buffer.writeln(' - URI: ${chunk.web!.uri}'); + buffer.writeln(' - Domain: ${chunk.web!.domain}'); + } + } + } + + if (urlContextMetadata != null) { + buffer.writeln('\n\n--- URL Context Metadata ---'); + for (final data in urlContextMetadata.urlMetadata) { + buffer.writeln(' - URL: ${data.retrievedUrl}'); + buffer.writeln(' Status: ${data.urlRetrievalStatus}'); + } + } + _messages.add(MessageData(text: buffer.toString(), fromUser: false)); + } + }, + ); + } + Future _serverTemplateAutoFunctionCall(String message) async { await _handleServerTemplateMessage(message, (message) async { + // Inputs are no longer passed during sendMessage var response = await _chatAutoFunctionSession?.sendMessage( Content.text(message), - inputs: {}, ); _messages.add(MessageData(text: response?.text, fromUser: false)); - - // final functionCalls = response?.functionCalls.toList(); - // if (functionCalls!.isNotEmpty) { - // final functionCall = functionCalls.first; - // if (functionCall.name == 'fetchWeather') { - // final location = - // functionCall.args['location']! as Map; - // final date = functionCall.args['date']! as String; - // final city = location['city'] as String; - // final state = location['state'] as String; - // final functionResult = - // await fetchWeather(Location(city, state), date); - // var functionResponse = await _chatFunctionSession?.sendMessage( - // Content.functionResponse(functionCall.name, functionResult), - // inputs: {}, - // ); - // _messages - // .add(MessageData(text: functionResponse?.text, fromUser: false)); - // } - // } - }); - } - - Future _handleServerTemplateMessage( - String message, - Future Function(String) generateContent, - ) async { - setState(() { - _loading = true; }); - - try { - _messages.add(MessageData(text: message, fromUser: true)); - await generateContent(message); - } catch (e) { - _showError(e.toString()); - } finally { - _textController.clear(); - setState(() { - _loading = false; - }); - _textFieldFocus.requestFocus(); - _scrollDown(); - } } Future _serverTemplateFunctionCall(String message) async { await _handleServerTemplateMessage(message, (message) async { + // Inputs are no longer passed during sendMessage var response = await _chatFunctionSession?.sendMessage( Content.text(message), - inputs: {}, ); _messages.add(MessageData(text: response?.text, fromUser: false)); @@ -366,9 +337,10 @@ class _ServerTemplatePageState extends State { final state = location['state'] as String; final functionResult = await fetchWeather(Location(city, state), date); + + // Respond to the function call var functionResponse = await _chatFunctionSession?.sendMessage( Content.functionResponse(functionCall.name, functionResult), - inputs: {}, ); _messages .add(MessageData(text: functionResponse?.text, fromUser: false)); @@ -379,11 +351,9 @@ class _ServerTemplatePageState extends State { Future _serverTemplateChat(String message) async { await _handleServerTemplateMessage(message, (message) async { + // Inputs are no longer passed during sendMessage var response = await _chatSession?.sendMessage( Content.text(message), - inputs: { - 'message': message, - }, ); var text = response?.text; 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 fb97ecdb0501..b945d3b4ea6e 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 @@ -47,6 +47,7 @@ final class TemplateChatSession { this._templateHistoryGenerateContent, this._templateHistoryGenerateContentStream, this._templateId, + this._inputs, this._history, List autoFunctionLists, this._maxTurns, @@ -60,7 +61,9 @@ final class TemplateChatSession { Iterable content, String templateId, {required Map inputs}) _templateHistoryGenerateContentStream; + final String _templateId; + final Map _inputs; final List _history; final Map _autoFunctions; final int _maxTurns; @@ -76,15 +79,14 @@ final class TemplateChatSession { /// including the message sent to the model. Iterable get history => _history.skip(0); - /// Sends [inputs] to the server template as a continuation of the chat [history]. + /// Sends [message] 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. + /// generate new content, providing the session's initialized inputs. /// /// 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, - {required Map inputs}) async { + Future sendMessage(Content message) async { final lock = await _mutex.acquire(); try { final requestHistory = [message]; @@ -93,7 +95,7 @@ final class TemplateChatSession { final response = await _templateHistoryGenerateContent( _history.followedBy(requestHistory), _templateId, - inputs: inputs, + inputs: _inputs, ); final functionCalls = response.functionCalls; @@ -144,19 +146,18 @@ final class TemplateChatSession { /// response. /// /// Prepends the history to the request and uses the provided model to - /// generate new content. + /// generate new content, providing the session's initialized inputs. /// /// When there are no candidates in the response, the [message] and response /// are ignored and will not be recorded in the [history]. - Stream sendMessageStream(Content message, - {required Map inputs}) { + Stream sendMessageStream(Content message) { final controller = StreamController(sync: true); _mutex.acquire().then((lock) async { try { final responses = _templateHistoryGenerateContentStream( _history.followedBy([message]), _templateId, - inputs: inputs, + inputs: _inputs, ); final content = []; await for (final response in responses) { @@ -184,18 +185,20 @@ extension StartTemplateChatExtension on TemplateGenerativeModel { /// Starts a [TemplateChatSession] that will use this model to respond to messages. /// /// ```dart - /// final chat = model.startChat(); + /// final chat = model.startChat('my_template', inputs: {'language': 'en'}); /// final response = await chat.sendMessage(Content.text('Hello there.')); /// print(response.text); /// ``` TemplateChatSession startChat(String templateId, - {List? history, + {required Map inputs, + List? history, List? autoFunctions, int? maxTurns}) => TemplateChatSession._( templateGenerateContentWithHistory, templateGenerateContentWithHistoryStream, templateId, + inputs, history ?? [], autoFunctions ?? [], maxTurns ?? 5); From a716459ee859e233d84f4da34a18a8ee23252842 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Fri, 20 Mar 2026 13:38:25 -0700 Subject: [PATCH 04/17] add tool structure --- .../lib/pages/server_template_page.dart | 12 ++- .../firebase_ai/lib/firebase_ai.dart | 7 +- .../src/server_template/template_chat.dart | 35 +++----- .../src/server_template/template_tool.dart | 85 +++++++++++++++++++ 4 files changed, 111 insertions(+), 28 deletions(-) create mode 100644 packages/firebase_ai/firebase_ai/lib/src/server_template/template_tool.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 155c401d81a3..03d995f88618 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 @@ -76,10 +76,14 @@ class _ServerTemplatePageState extends State { _chatAutoFunctionSession = _templateGenerativeModel?.startChat( 'cj-function-calling-weather', inputs: {}, - autoFunctions: [ - TemplateAutoFunction( - name: 'fetchWeather', - callable: fetchWeatherCallable, + tools: [ + TemplateTool.functionDeclarations( + [ + TemplateAutoFunctionDeclaration( + name: 'fetchWeather', + callable: fetchWeatherCallable, + ), + ], ), ], ); diff --git a/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart b/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart index ad35755ff06e..56b58e8712e3 100644 --- a/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart +++ b/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart @@ -112,7 +112,12 @@ export 'src/live_api.dart' export 'src/live_session.dart' show LiveSession; export 'src/schema.dart' show Schema, SchemaType; export 'src/server_template/template_chat.dart' - show TemplateChatSession, TemplateAutoFunction, StartTemplateChatExtension; + show TemplateChatSession, StartTemplateChatExtension; +export 'src/server_template/template_tool.dart' + show + TemplateAutoFunctionDeclaration, + TemplateFunctionDeclaration, + TemplateTool; export 'src/tool.dart' show AutoFunctionDeclaration, 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 b945d3b4ea6e..edeedb7630bf 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 @@ -18,23 +18,7 @@ import '../base_model.dart'; import '../content.dart'; import '../utils/chat_utils.dart'; import '../utils/mutex.dart'; - -final class TemplateAutoFunction { - TemplateAutoFunction({ - required this.name, - required this.callable, - }); - - /// The name of the function. - /// - /// Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum - /// length of 63. - final String name; - - /// The callable function that this declaration represents. - final FutureOr> Function(Map args) - callable; -} +import 'template_tool.dart'; /// A back-and-forth chat with a server template. /// @@ -49,9 +33,14 @@ final class TemplateChatSession { this._templateId, this._inputs, this._history, - List autoFunctionLists, + List? tools, this._maxTurns, - ) : _autoFunctions = {for (var item in autoFunctionLists) item.name: item}; + ) : _autoFunctions = tools + ?.expand((tool) => tool.templateAutoFunctionDeclarations) + .fold({}, (map, function) { + map?[function.name] = function; + return map; + }); final Future Function( Iterable content, String templateId, @@ -65,7 +54,7 @@ final class TemplateChatSession { final String _templateId; final Map _inputs; final List _history; - final Map _autoFunctions; + final Map? _autoFunctions; final int _maxTurns; final _mutex = Mutex(); @@ -99,7 +88,7 @@ final class TemplateChatSession { ); final functionCalls = response.functionCalls; - final shouldAutoExecute = _autoFunctions.isNotEmpty && + final shouldAutoExecute = _autoFunctions!.isNotEmpty && functionCalls.isNotEmpty && functionCalls.every((c) => _autoFunctions.containsKey(c.name)); @@ -192,7 +181,7 @@ extension StartTemplateChatExtension on TemplateGenerativeModel { TemplateChatSession startChat(String templateId, {required Map inputs, List? history, - List? autoFunctions, + List? tools, int? maxTurns}) => TemplateChatSession._( templateGenerateContentWithHistory, @@ -200,6 +189,6 @@ extension StartTemplateChatExtension on TemplateGenerativeModel { templateId, inputs, history ?? [], - autoFunctions ?? [], + tools ?? [], maxTurns ?? 5); } diff --git a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_tool.dart b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_tool.dart new file mode 100644 index 000000000000..0055954653f1 --- /dev/null +++ b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_tool.dart @@ -0,0 +1,85 @@ +// Copyright 2026 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 'dart:async'; + +import '../schema.dart'; + +final class TemplateTool { + // ignore: public_member_api_docs + TemplateTool._(this._functionDeclarations); + + /// Returns a [TemplateTool] instance with list of [TemplateFunctionDeclaration]. + static TemplateTool functionDeclarations( + List functionDeclarations) { + return TemplateTool._(functionDeclarations); + } + + /// Returns a list of all [TemplateAutoFunctionDeclaration] objects + /// found within the [_functionDeclarations] list. + List get templateAutoFunctionDeclarations { + return _functionDeclarations + ?.whereType() + .toList() ?? + []; + } + + final List? _functionDeclarations; + + /// Convert to json object. + Map toJson() => { + if (_functionDeclarations case final _functionDeclarations?) + 'functionDeclarations': + _functionDeclarations.map((f) => f.toJson()).toList(), + }; +} + +class TemplateFunctionDeclaration { + // ignore: public_member_api_docs + TemplateFunctionDeclaration(this.name, + {Map? parameters, + List optionalParameters = const []}) + : _schemaObject = parameters != null + ? Schema.object( + properties: parameters, optionalProperties: optionalParameters) + : null; + + /// The name of the function. + /// + /// Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum + /// length of 63. + final String name; + + final Schema? _schemaObject; + + /// Convert to json object. + Map toJson() => { + 'name': name, + 'input_schema': _schemaObject != null ? _schemaObject.toJson() : '', + }; +} + +final class TemplateAutoFunctionDeclaration + extends TemplateFunctionDeclaration { + TemplateAutoFunctionDeclaration( + {required String name, + required this.callable, + Map? parameters, + List optionalParameters = const []}) + : super(name, + parameters: parameters, optionalParameters: optionalParameters); + + /// The callable function that this declaration represents. + final FutureOr> Function(Map args) + callable; +} From cad10e3112c5dee4be97b7b12f68e51e9f8ee907 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Fri, 20 Mar 2026 13:38:53 -0700 Subject: [PATCH 05/17] kts support --- .../firebase_ai/example/android/app/build.gradle.kts | 3 +++ .../firebase_ai/example/android/settings.gradle.kts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/firebase_ai/firebase_ai/example/android/app/build.gradle.kts b/packages/firebase_ai/firebase_ai/example/android/app/build.gradle.kts index 5b2cf7547615..d818671f2416 100644 --- a/packages/firebase_ai/firebase_ai/example/android/app/build.gradle.kts +++ b/packages/firebase_ai/firebase_ai/example/android/app/build.gradle.kts @@ -1,5 +1,8 @@ plugins { id("com.android.application") + // START: FlutterFire Configuration + id("com.google.gms.google-services") + // END: FlutterFire Configuration id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") diff --git a/packages/firebase_ai/firebase_ai/example/android/settings.gradle.kts b/packages/firebase_ai/firebase_ai/example/android/settings.gradle.kts index ab39a10a29ba..bd7522f75402 100644 --- a/packages/firebase_ai/firebase_ai/example/android/settings.gradle.kts +++ b/packages/firebase_ai/firebase_ai/example/android/settings.gradle.kts @@ -19,6 +19,9 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.7.3" apply false + // START: FlutterFire Configuration + id("com.google.gms.google-services") version("4.3.15") apply false + // END: FlutterFire Configuration id("org.jetbrains.kotlin.android") version "2.1.0" apply false } From 9eb759798be1c40ae743807b8a35b9ba7d81147f Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Fri, 20 Mar 2026 13:43:14 -0700 Subject: [PATCH 06/17] fix compile error for server template page --- .../example/lib/pages/server_template_page.dart | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 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 fece88bdae62..bb46c90d5dc5 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 @@ -283,7 +283,7 @@ class _ServerTemplatePageState extends State { _messages.add(MessageData(text: message, fromUser: true)); var response = await _templateGenerativeModel // ignore: experimental_member_use - ?.generateContent('cj-urlcontext', inputs: {'url': message}); + ?.generateContent('cj-urlcontext', inputs: {'url': message}); final candidate = response?.candidates.first; if (candidate == null) { @@ -447,7 +447,18 @@ class _ServerTemplatePageState extends State { ?.generateContent('new-greeting', inputs: {}); _messages.add(MessageData(text: response?.text, fromUser: false)); - }); + } catch (e) { + _showError(e.toString()); + setState(() { + _loading = false; + }); + } finally { + _textController.clear(); + setState(() { + _loading = false; + }); + _textFieldFocus.requestFocus(); + } } Future _testCodeExecution() async { From 8b4d873cd3434ab9b2fe6b49ea85e7aae507046e Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Sun, 22 Mar 2026 21:10:34 -0700 Subject: [PATCH 07/17] Add auto function calling to chat stream, and add test cases --- .../lib/pages/server_template_page.dart | 122 ++++++++++++++++++ .../src/server_template/template_chat.dart | 75 ++++++++--- .../src/server_template/template_tool.dart | 13 ++ 3 files changed, 193 insertions(+), 17 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 bb46c90d5dc5..d7ca9494ffff 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 @@ -182,6 +182,32 @@ class _ServerTemplatePageState extends State { ), tooltip: 'Function Calling', ), + if (!_loading) + IconButton( + onPressed: () async { + await _serverTemplateAutoStreamFunctionCall( + _textController.text, + ); + }, + icon: Icon( + Icons.smart_toy, + color: Theme.of(context).colorScheme.primary, + ), + tooltip: 'Auto Stream Function Calling', + ), + if (!_loading) + IconButton( + onPressed: () async { + await _serverTemplateStreamFunctionCall( + _textController.text, + ); + }, + icon: Icon( + Icons.settings_system_daydream, + color: Theme.of(context).colorScheme.primary, + ), + tooltip: 'Stream Function Calling', + ), if (!_loading) IconButton( onPressed: () async { @@ -366,6 +392,102 @@ class _ServerTemplatePageState extends State { }); } + Future _serverTemplateAutoStreamFunctionCall(String message) async { + await _handleServerTemplateMessage(message, (message) async { + var responseStream = _chatAutoFunctionSession?.sendMessageStream( + Content.text(message), + ); + + var accumulatedText = ''; + MessageData? modelMessage; + + if (responseStream != null) { + await for (final response in responseStream) { + if (response.text case final text?) { + accumulatedText += text; + if (modelMessage == null) { + modelMessage = + MessageData(text: accumulatedText, fromUser: false); + _messages.add(modelMessage); + } else { + modelMessage = modelMessage.copyWith(text: accumulatedText); + _messages.last = modelMessage; + } + setState(() {}); + } + } + } + + if (accumulatedText.isEmpty) { + _messages.add(MessageData( + text: 'No text response from model.', fromUser: false)); + } + }); + } + + Future _serverTemplateStreamFunctionCall(String message) async { + await _handleServerTemplateMessage(message, (message) async { + var responseStream = _chatFunctionSession?.sendMessageStream( + Content.text(message), + ); + + GenerateContentResponse? lastResponse; + if (responseStream != null) { + await for (final response in responseStream) { + lastResponse = response; + } + } + + final functionCalls = lastResponse?.functionCalls.toList(); + if (functionCalls != null && functionCalls.isNotEmpty) { + final functionCall = functionCalls.first; + if (functionCall.name == 'fetchWeather') { + final location = + functionCall.args['location']! as Map; + final date = functionCall.args['date']! as String; + final city = location['city'] as String; + final state = location['state'] as String; + final functionResult = + await fetchWeather(Location(city, state), date); + + // Stream the function response + var responseStream2 = _chatFunctionSession?.sendMessageStream( + Content.functionResponse(functionCall.name, functionResult), + ); + + var accumulatedText = ''; + MessageData? modelMessage; + + if (responseStream2 != null) { + await for (final response in responseStream2) { + if (response.text case final text?) { + accumulatedText += text; + if (modelMessage == null) { + modelMessage = + MessageData(text: accumulatedText, fromUser: false); + _messages.add(modelMessage); + } else { + modelMessage = modelMessage.copyWith(text: accumulatedText); + _messages.last = modelMessage; + } + setState(() {}); + } + } + } + if (accumulatedText.isEmpty) { + _messages.add(MessageData( + text: 'No text response from model.', fromUser: false)); + } + } + } else if (lastResponse?.text case final text?) { + _messages.add(MessageData(text: text, fromUser: false)); + } else { + _messages.add(MessageData( + text: 'No text response from model.', fromUser: false)); + } + }); + } + Future _serverTemplateChat(String message) async { await _handleServerTemplateMessage(message, (message) async { // Inputs are no longer passed during sendMessage 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 edeedb7630bf..a5245f761adc 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 @@ -140,30 +140,71 @@ 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]. Stream sendMessageStream(Content message) { - final controller = StreamController(sync: true); + final controller = StreamController(); _mutex.acquire().then((lock) async { try { - final responses = _templateHistoryGenerateContentStream( - _history.followedBy([message]), - _templateId, - inputs: _inputs, - ); - final content = []; - await for (final response in responses) { - if (response.candidates case [final candidate, ...]) { - content.add(candidate.content); + final requestHistory = [message]; + var turn = 0; + while (turn < _maxTurns) { + final responses = _templateHistoryGenerateContentStream( + _history.followedBy(requestHistory), + _templateId, + inputs: _inputs, + ); + + final turnChunks = []; + await for (final response in responses) { + turnChunks.add(response); + controller.add(response); } - controller.add(response); - } - if (content.isNotEmpty) { - _history.add(message); - _history.add(historyAggregate(content)); + if (turnChunks.isEmpty) break; + final aggregatedContent = historyAggregate(turnChunks.map((r) { + final content = r.candidates.firstOrNull?.content; + if (content == null) { + throw Exception('No content in response candidate'); + } + return content; + }).toList()); + + final functionCalls = + aggregatedContent.parts.whereType().toList(); + + final shouldAutoExecute = _autoFunctions != null && + _autoFunctions!.isNotEmpty && + functionCalls.isNotEmpty && + functionCalls.every((c) => _autoFunctions.containsKey(c.name)); + + if (!shouldAutoExecute) { + _history.addAll(requestHistory); + _history.add(aggregatedContent); + return; + } + + requestHistory.add(aggregatedContent); + final functionResponseFutures = + functionCalls.map((functionCall) async { + final function = _autoFunctions![functionCall.name]; + + Object? result; + try { + result = await function!.callable(functionCall.args); + } catch (e) { + result = e.toString(); + } + return FunctionResponse(functionCall.name, {'result': result}); + }); + final functionResponseParts = + await Future.wait(functionResponseFutures); + requestHistory.add(Content.functionResponses(functionResponseParts)); + turn++; } + throw Exception('Max turns of $_maxTurns reached.'); } catch (e, s) { controller.addError(e, s); + } finally { + lock.release(); + unawaited(controller.close()); } - lock.release(); - unawaited(controller.close()); }); return controller.stream; } diff --git a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_tool.dart b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_tool.dart index 0055954653f1..2dc05c26f72c 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_tool.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_tool.dart @@ -15,6 +15,7 @@ import 'dart:async'; import '../schema.dart'; +/// A collection of template tools. final class TemplateTool { // ignore: public_member_api_docs TemplateTool._(this._functionDeclarations); @@ -44,6 +45,7 @@ final class TemplateTool { }; } +/// A function declaration for a template tool. class TemplateFunctionDeclaration { // ignore: public_member_api_docs TemplateFunctionDeclaration(this.name, @@ -69,8 +71,10 @@ class TemplateFunctionDeclaration { }; } +/// A function declaration for a template tool that can be called by the model. final class TemplateAutoFunctionDeclaration extends TemplateFunctionDeclaration { + // ignore: public_member_api_docs TemplateAutoFunctionDeclaration( {required String name, required this.callable, @@ -83,3 +87,12 @@ final class TemplateAutoFunctionDeclaration final FutureOr> Function(Map args) callable; } + +/// Config for template tools to use with server prompts. +final class TemplateToolConfig { + // ignore: public_member_api_docs + TemplateToolConfig(); + + /// Convert to json object. + Map toJson() => {}; +} From 79e61c9acfd64739eea689fc34166bc7918842c5 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Sun, 22 Mar 2026 21:48:06 -0700 Subject: [PATCH 08/17] add tools and toolConfig into template request --- .../firebase_ai/lib/src/base_model.dart | 17 ++++++++++++ .../src/server_template/template_chat.dart | 25 +++++++++++++----- .../template_generative_model.dart | 26 +++++++++++++++---- .../template_imagen_model.dart | 4 ++- 4 files changed, 60 insertions(+), 12 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 01ac7eb834b3..56b4bcd27692 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart @@ -36,6 +36,7 @@ import 'imagen/imagen_edit.dart'; import 'imagen/imagen_reference.dart'; import 'live_api.dart'; import 'live_session.dart'; +import 'server_template/template_tool.dart'; import 'tool.dart'; part 'generative_model.dart'; @@ -364,6 +365,8 @@ abstract class BaseTemplateApiClientModel extends BaseApiClientModel { String templateId, Map? inputs, Iterable? history, + List? tools, + TemplateToolConfig? toolConfig, T Function(Map) parse) { Map body = {}; if (inputs != null) { @@ -372,6 +375,12 @@ abstract class BaseTemplateApiClientModel extends BaseApiClientModel { if (history != null) { body['history'] = history.map((c) => c.toJson()).toList(); } + if (tools != null) { + body['tools'] = tools.map((t) => t.toJson()).toList(); + } + if (toolConfig != null) { + body['toolConfig'] = toolConfig.toJson(); + } return _client .makeRequest(templateTaskUri(task, templateId), body) .then(parse); @@ -386,6 +395,8 @@ abstract class BaseTemplateApiClientModel extends BaseApiClientModel { String templateId, Map? inputs, Iterable? history, + List? tools, + TemplateToolConfig? toolConfig, T Function(Map) parse) { Map body = {}; if (inputs != null) { @@ -394,6 +405,12 @@ abstract class BaseTemplateApiClientModel extends BaseApiClientModel { if (history != null) { body['history'] = history.map((c) => c.toJson()).toList(); } + if (tools != null) { + body['tools'] = tools.map((t) => t.toJson()).toList(); + } + if (toolConfig != null) { + body['toolConfig'] = toolConfig.toJson(); + } final response = _client.streamRequest(templateTaskUri(task, templateId), body); return response.map(parse); 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 a5245f761adc..c47e5f615fad 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 @@ -33,9 +33,10 @@ final class TemplateChatSession { this._templateId, this._inputs, this._history, - List? tools, + this._tools, + this._toolConfig, this._maxTurns, - ) : _autoFunctions = tools + ) : _autoFunctions = _tools ?.expand((tool) => tool.templateAutoFunctionDeclarations) .fold({}, (map, function) { map?[function.name] = function; @@ -44,16 +45,22 @@ final class TemplateChatSession { final Future Function( Iterable content, String templateId, - {required Map inputs}) _templateHistoryGenerateContent; + {required Map inputs, + List? tools, + TemplateToolConfig? templateToolConfig}) _templateHistoryGenerateContent; final Stream Function( Iterable content, String templateId, - {required Map inputs}) + {required Map inputs, + List? tools, + TemplateToolConfig? templateToolConfig}) _templateHistoryGenerateContentStream; final String _templateId; final Map _inputs; final List _history; + final List? _tools; + final TemplateToolConfig? _toolConfig; final Map? _autoFunctions; final int _maxTurns; @@ -85,6 +92,8 @@ final class TemplateChatSession { _history.followedBy(requestHistory), _templateId, inputs: _inputs, + tools: _tools, + templateToolConfig: _toolConfig, ); final functionCalls = response.functionCalls; @@ -150,6 +159,8 @@ final class TemplateChatSession { _history.followedBy(requestHistory), _templateId, inputs: _inputs, + tools: _tools, + templateToolConfig: _toolConfig, ); final turnChunks = []; @@ -170,7 +181,7 @@ final class TemplateChatSession { aggregatedContent.parts.whereType().toList(); final shouldAutoExecute = _autoFunctions != null && - _autoFunctions!.isNotEmpty && + _autoFunctions.isNotEmpty && functionCalls.isNotEmpty && functionCalls.every((c) => _autoFunctions.containsKey(c.name)); @@ -183,7 +194,7 @@ final class TemplateChatSession { requestHistory.add(aggregatedContent); final functionResponseFutures = functionCalls.map((functionCall) async { - final function = _autoFunctions![functionCall.name]; + final function = _autoFunctions[functionCall.name]; Object? result; try { @@ -223,6 +234,7 @@ extension StartTemplateChatExtension on TemplateGenerativeModel { {required Map inputs, List? history, List? tools, + TemplateToolConfig? toolConfig, int? maxTurns}) => TemplateChatSession._( templateGenerateContentWithHistory, @@ -231,5 +243,6 @@ extension StartTemplateChatExtension on TemplateGenerativeModel { inputs, history ?? [], tools ?? [], + toolConfig, maxTurns ?? 5); } 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 78e302b5df47..0c31f5b9b517 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 @@ -71,7 +71,11 @@ final class TemplateGenerativeModel extends BaseTemplateApiClientModel { Future generateContent(String templateId, {required Map inputs}) => makeTemplateRequest(TemplateTask.templateGenerateContent, templateId, - inputs, null, _serializationStrategy.parseGenerateContentResponse); + inputs, + null, // history + null, // tools + null, // toolConfig + _serializationStrategy.parseGenerateContentResponse); /// Generates a stream of content responding to [templateId] and [inputs]. /// @@ -84,7 +88,9 @@ final class TemplateGenerativeModel extends BaseTemplateApiClientModel { TemplateTask.templateStreamGenerateContent, templateId, inputs, - null, + null, // history + null, // tools + null, // toolConfig _serializationStrategy.parseGenerateContentResponse); } @@ -93,21 +99,31 @@ final class TemplateGenerativeModel extends BaseTemplateApiClientModel { @experimental Future templateGenerateContentWithHistory( Iterable history, String templateId, - {required Map inputs}) => + {required Map inputs, + List? tools, + TemplateToolConfig? templateToolConfig}) => makeTemplateRequest(TemplateTask.templateGenerateContent, templateId, - inputs, history, _serializationStrategy.parseGenerateContentResponse); + inputs, + history, + tools, + templateToolConfig, + _serializationStrategy.parseGenerateContentResponse); /// Generates a stream of content from a template with the given [templateId], /// [inputs] and [history]. @experimental Stream templateGenerateContentWithHistoryStream( Iterable history, String templateId, - {required Map inputs}) { + {required Map inputs, + List? tools, + TemplateToolConfig? templateToolConfig}) { return streamTemplateRequest( TemplateTask.templateStreamGenerateContent, templateId, inputs, history, + tools, + templateToolConfig, _serializationStrategy.parseGenerateContentResponse); } } 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 e27dd5eaaa9d..85649ecf11ba 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 @@ -68,7 +68,9 @@ final class TemplateImagenModel extends BaseTemplateApiClientModel { TemplateTask.templatePredict, templateId, inputs, - null, + null, // history + null, // tools + null, // toolConfig (jsonObject) => parseImagenGenerationResponse(jsonObject), ); From 99de68bfcd0756e68ff2728a1546c2bf9b09e9ee Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Tue, 24 Mar 2026 13:41:59 -0700 Subject: [PATCH 09/17] fix the template function name and update the schema override sample --- .../lib/pages/server_template_page.dart | 39 +++++++++++++++++++ .../src/server_template/template_tool.dart | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) 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 d7ca9494ffff..a66bae948ab0 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 @@ -79,6 +79,45 @@ class _ServerTemplatePageState extends State { _chatFunctionSession = _templateGenerativeModel?.startChat( 'cj-function-calling-weather', inputs: {}, + tools: [ + TemplateTool.functionDeclarations( + [ + TemplateFunctionDeclaration( + 'fetchWeather', + parameters: { + 'location': Schema.object( + description: + 'The name of the city and its state for which to get ' + 'the weather. Only cities in the USA are supported.', + properties: { + 'city': Schema.string( + description: 'The city of the location.', + ), + 'state': Schema.string( + description: 'The state of the location.', + ), + 'zipCode': Schema.integer( + description: 'Optional zip code of the location.', + nullable: true, + ), + }, + optionalProperties: ['zipCode'], + ), + 'date': Schema.string( + description: 'The date for which to get the weather. ' + 'Date must be in the format: YYYY-MM-DD.', + ), + 'unit': Schema.enumString( + enumValues: ['CELSIUS', 'FAHRENHEIT'], + description: 'The temperature unit.', + nullable: true, + ), + }, + optionalParameters: ['unit'], + ), + ], + ), + ], ); _chatAutoFunctionSession = _templateGenerativeModel?.startChat( 'cj-function-calling-weather', diff --git a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_tool.dart b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_tool.dart index 2dc05c26f72c..1f0455450466 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_tool.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_tool.dart @@ -40,7 +40,7 @@ final class TemplateTool { /// Convert to json object. Map toJson() => { if (_functionDeclarations case final _functionDeclarations?) - 'functionDeclarations': + 'templateFunctions': _functionDeclarations.map((f) => f.toJson()).toList(), }; } From aa02f220bfed8597c8354fe5ecdf1d88e417280f Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Wed, 25 Mar 2026 09:53:11 -0700 Subject: [PATCH 10/17] add too config --- packages/firebase_ai/firebase_ai/lib/firebase_ai.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart b/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart index 5cc86e89ce39..730c8516c045 100644 --- a/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart +++ b/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart @@ -117,7 +117,8 @@ export 'src/server_template/template_tool.dart' show TemplateAutoFunctionDeclaration, TemplateFunctionDeclaration, - TemplateTool; + TemplateTool, + TemplateToolConfig; export 'src/tool.dart' show AutoFunctionDeclaration, From 41b12efcc6c5934265d6c0801c6cb80e0b91c783 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Wed, 25 Mar 2026 10:56:22 -0700 Subject: [PATCH 11/17] some fix for e2e test --- .../lib/pages/server_template_page.dart | 122 +++++------------- .../src/server_template/template_tool.dart | 21 ++- 2 files changed, 45 insertions(+), 98 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 a66bae948ab0..5aa10b518b7c 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 @@ -45,8 +45,9 @@ class _ServerTemplatePageState extends State { TemplateImagenModel? _templateImagenModel; TemplateChatSession? _chatSession; - TemplateChatSession? _chatFunctionSession; + TemplateChatSession? _chatFunctionOverrideSession; TemplateChatSession? _chatAutoFunctionSession; + TemplateChatSession? _chatStreamFunctionSession; @override void initState() { @@ -76,8 +77,8 @@ class _ServerTemplatePageState extends State { 'chat_history.prompt', inputs: {}, ); - _chatFunctionSession = _templateGenerativeModel?.startChat( - 'cj-function-calling-weather', + _chatFunctionOverrideSession = _templateGenerativeModel?.startChat( + 'cj-function-calling-weather-override', inputs: {}, tools: [ TemplateTool.functionDeclarations( @@ -85,29 +86,29 @@ class _ServerTemplatePageState extends State { TemplateFunctionDeclaration( 'fetchWeather', parameters: { - 'location': Schema.object( + 'location': JSONSchema.object( description: 'The name of the city and its state for which to get ' 'the weather. Only cities in the USA are supported.', properties: { - 'city': Schema.string( + 'city': JSONSchema.string( description: 'The city of the location.', ), - 'state': Schema.string( + 'state': JSONSchema.string( description: 'The state of the location.', ), - 'zipCode': Schema.integer( + 'zipCode': JSONSchema.integer( description: 'Optional zip code of the location.', nullable: true, ), }, optionalProperties: ['zipCode'], ), - 'date': Schema.string( + 'date': JSONSchema.string( description: 'The date for which to get the weather. ' 'Date must be in the format: YYYY-MM-DD.', ), - 'unit': Schema.enumString( + 'unit': JSONSchema.enumString( enumValues: ['CELSIUS', 'FAHRENHEIT'], description: 'The temperature unit.', nullable: true, @@ -133,6 +134,20 @@ class _ServerTemplatePageState extends State { ), ], ); + _chatStreamFunctionSession = _templateGenerativeModel?.startChat( + 'cj-function-calling-weather-stream', + inputs: {}, + tools: [ + TemplateTool.functionDeclarations( + [ + TemplateAutoFunctionDeclaration( + name: 'fetchWeather', + callable: fetchWeatherCallable, + ), + ], + ), + ], + ); } void _scrollDown() { @@ -219,7 +234,7 @@ class _ServerTemplatePageState extends State { Icons.functions, color: Theme.of(context).colorScheme.primary, ), - tooltip: 'Function Calling', + tooltip: 'Function Calling (client override)', ), if (!_loading) IconButton( @@ -234,19 +249,6 @@ class _ServerTemplatePageState extends State { ), tooltip: 'Auto Stream Function Calling', ), - if (!_loading) - IconButton( - onPressed: () async { - await _serverTemplateStreamFunctionCall( - _textController.text, - ); - }, - icon: Icon( - Icons.settings_system_daydream, - color: Theme.of(context).colorScheme.primary, - ), - tooltip: 'Stream Function Calling', - ), if (!_loading) IconButton( onPressed: () async { @@ -403,7 +405,7 @@ class _ServerTemplatePageState extends State { Future _serverTemplateFunctionCall(String message) async { await _handleServerTemplateMessage(message, (message) async { // Inputs are no longer passed during sendMessage - var response = await _chatFunctionSession?.sendMessage( + var response = await _chatFunctionOverrideSession?.sendMessage( Content.text(message), ); @@ -421,7 +423,8 @@ class _ServerTemplatePageState extends State { await fetchWeather(Location(city, state), date); // Respond to the function call - var functionResponse = await _chatFunctionSession?.sendMessage( + var functionResponse = + await _chatFunctionOverrideSession?.sendMessage( Content.functionResponse(functionCall.name, functionResult), ); _messages @@ -433,7 +436,7 @@ class _ServerTemplatePageState extends State { Future _serverTemplateAutoStreamFunctionCall(String message) async { await _handleServerTemplateMessage(message, (message) async { - var responseStream = _chatAutoFunctionSession?.sendMessageStream( + var responseStream = _chatStreamFunctionSession?.sendMessageStream( Content.text(message), ); @@ -458,71 +461,8 @@ class _ServerTemplatePageState extends State { } if (accumulatedText.isEmpty) { - _messages.add(MessageData( - text: 'No text response from model.', fromUser: false)); - } - }); - } - - Future _serverTemplateStreamFunctionCall(String message) async { - await _handleServerTemplateMessage(message, (message) async { - var responseStream = _chatFunctionSession?.sendMessageStream( - Content.text(message), - ); - - GenerateContentResponse? lastResponse; - if (responseStream != null) { - await for (final response in responseStream) { - lastResponse = response; - } - } - - final functionCalls = lastResponse?.functionCalls.toList(); - if (functionCalls != null && functionCalls.isNotEmpty) { - final functionCall = functionCalls.first; - if (functionCall.name == 'fetchWeather') { - final location = - functionCall.args['location']! as Map; - final date = functionCall.args['date']! as String; - final city = location['city'] as String; - final state = location['state'] as String; - final functionResult = - await fetchWeather(Location(city, state), date); - - // Stream the function response - var responseStream2 = _chatFunctionSession?.sendMessageStream( - Content.functionResponse(functionCall.name, functionResult), - ); - - var accumulatedText = ''; - MessageData? modelMessage; - - if (responseStream2 != null) { - await for (final response in responseStream2) { - if (response.text case final text?) { - accumulatedText += text; - if (modelMessage == null) { - modelMessage = - MessageData(text: accumulatedText, fromUser: false); - _messages.add(modelMessage); - } else { - modelMessage = modelMessage.copyWith(text: accumulatedText); - _messages.last = modelMessage; - } - setState(() {}); - } - } - } - if (accumulatedText.isEmpty) { - _messages.add(MessageData( - text: 'No text response from model.', fromUser: false)); - } - } - } else if (lastResponse?.text case final text?) { - _messages.add(MessageData(text: text, fromUser: false)); - } else { - _messages.add(MessageData( - text: 'No text response from model.', fromUser: false)); + _messages.add( + MessageData(text: 'No text response from model.', fromUser: false)); } }); } diff --git a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_tool.dart b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_tool.dart index 1f0455450466..31c60af4b82f 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/server_template/template_tool.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/server_template/template_tool.dart @@ -39,9 +39,12 @@ final class TemplateTool { /// Convert to json object. Map toJson() => { - if (_functionDeclarations case final _functionDeclarations?) - 'templateFunctions': - _functionDeclarations.map((f) => f.toJson()).toList(), + if (_functionDeclarations case final functionDeclarations? + when functionDeclarations.isNotEmpty) + 'templateFunctions': functionDeclarations + .map((f) => f.hasSchema ? f.toJson() : null) + .where((f) => f != null) + .toList(), }; } @@ -49,10 +52,10 @@ final class TemplateTool { class TemplateFunctionDeclaration { // ignore: public_member_api_docs TemplateFunctionDeclaration(this.name, - {Map? parameters, + {Map? parameters, List optionalParameters = const []}) : _schemaObject = parameters != null - ? Schema.object( + ? JSONSchema.object( properties: parameters, optionalProperties: optionalParameters) : null; @@ -64,10 +67,14 @@ class TemplateFunctionDeclaration { final Schema? _schemaObject; + /// Whether the function declaration has a schema override. + bool get hasSchema => _schemaObject != null; + /// Convert to json object. Map toJson() => { 'name': name, - 'input_schema': _schemaObject != null ? _schemaObject.toJson() : '', + if (_schemaObject case final schemaObject?) + 'inputSchema': schemaObject.toJson(), }; } @@ -78,7 +85,7 @@ final class TemplateAutoFunctionDeclaration TemplateAutoFunctionDeclaration( {required String name, required this.callable, - Map? parameters, + Map? parameters, List optionalParameters = const []}) : super(name, parameters: parameters, optionalParameters: optionalParameters); From 8bfd117621913a93f7a6afb07593d71252b81c5b Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Wed, 25 Mar 2026 11:12:41 -0700 Subject: [PATCH 12/17] fix analyzer --- .../firebase_ai/example/lib/pages/server_template_page.dart | 3 ++- packages/firebase_ai/firebase_ai/lib/src/base_model.dart | 2 +- 2 files changed, 3 insertions(+), 2 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 5aa10b518b7c..b3b9ce8c3883 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 @@ -462,7 +462,8 @@ class _ServerTemplatePageState extends State { if (accumulatedText.isEmpty) { _messages.add( - MessageData(text: 'No text response from model.', fromUser: false)); + MessageData(text: 'No text response from model.', fromUser: false), + ); } }); } 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 a15c74287d12..edd5a9a7c7b2 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/base_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/base_model.dart @@ -36,8 +36,8 @@ import 'imagen/imagen_edit.dart'; import 'imagen/imagen_reference.dart'; import 'live_api.dart'; import 'live_session.dart'; -import 'server_template/template_tool.dart'; import 'platform_header_helper.dart'; +import 'server_template/template_tool.dart'; import 'tool.dart'; part 'generative_model.dart'; From 5f65d173dd745b47eb5a75dd833729644584fc04 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Tue, 31 Mar 2026 11:49:25 -0700 Subject: [PATCH 13/17] tiny year change --- .../firebase_ai/example/lib/utils/function_call_utils.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/firebase_ai/firebase_ai/example/lib/utils/function_call_utils.dart b/packages/firebase_ai/firebase_ai/example/lib/utils/function_call_utils.dart index ff4a5d6991e0..fcbd01151063 100644 --- a/packages/firebase_ai/firebase_ai/example/lib/utils/function_call_utils.dart +++ b/packages/firebase_ai/firebase_ai/example/lib/utils/function_call_utils.dart @@ -1,4 +1,4 @@ -// Copyright 2025 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. From 4c826a2c3b7ceb4f4c5663af80d53ac710140d0f Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Tue, 31 Mar 2026 20:04:09 -0700 Subject: [PATCH 14/17] bot review comment --- .../firebase_ai/example/lib/pages/server_template_page.dart | 1 - .../firebase_ai/lib/src/server_template/template_chat.dart | 3 ++- 2 files changed, 2 insertions(+), 2 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 768636a2577e..673037adf195 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 @@ -330,7 +330,6 @@ class _ServerTemplatePageState extends State { await _handleServerTemplateMessage( message, (message) async { - _messages.add(MessageData(text: message, fromUser: true)); var response = await _templateGenerativeModel // ignore: experimental_member_use ?.generateContent('cj-urlcontext', inputs: {'url': message}); 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 c47e5f615fad..26fb90dabc7d 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 @@ -97,7 +97,8 @@ final class TemplateChatSession { ); final functionCalls = response.functionCalls; - final shouldAutoExecute = _autoFunctions!.isNotEmpty && + final shouldAutoExecute = _autoFunctions != null && + _autoFunctions.isNotEmpty && functionCalls.isNotEmpty && functionCalls.every((c) => _autoFunctions.containsKey(c.name)); From 83e22825b33f5264c82e0d03f0489f141f120b0b Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Tue, 31 Mar 2026 20:21:53 -0700 Subject: [PATCH 15/17] fix analyzer and formatter --- .../example/lib/pages/server_template_page.dart | 3 ++- .../src/server_template/template_generative_model.dart | 8 ++++++-- 2 files changed, 8 insertions(+), 3 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 673037adf195..0a7c288fb411 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 @@ -222,7 +222,8 @@ class _ServerTemplatePageState extends State { IconButton( onPressed: () async { await _serverTemplateFunctionCall( - _textController.text); + _textController.text, + ); }, icon: Icon( Icons.functions, 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 0c31f5b9b517..41efccb4f460 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 @@ -70,7 +70,9 @@ final class TemplateGenerativeModel extends BaseTemplateApiClientModel { @experimental Future generateContent(String templateId, {required Map inputs}) => - makeTemplateRequest(TemplateTask.templateGenerateContent, templateId, + makeTemplateRequest( + TemplateTask.templateGenerateContent, + templateId, inputs, null, // history null, // tools @@ -102,7 +104,9 @@ final class TemplateGenerativeModel extends BaseTemplateApiClientModel { {required Map inputs, List? tools, TemplateToolConfig? templateToolConfig}) => - makeTemplateRequest(TemplateTask.templateGenerateContent, templateId, + makeTemplateRequest( + TemplateTask.templateGenerateContent, + templateId, inputs, history, tools, From e62eaaa4d2c51d955c9bfeb10d0aa6720b0580cb Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Thu, 2 Apr 2026 11:50:10 -0700 Subject: [PATCH 16/17] remove unrelated files from PR --- .../firebase_ai/example/android/app/build.gradle.kts | 3 --- .../firebase_ai/example/android/settings.gradle.kts | 3 --- 2 files changed, 6 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/example/android/app/build.gradle.kts b/packages/firebase_ai/firebase_ai/example/android/app/build.gradle.kts index d818671f2416..5b2cf7547615 100644 --- a/packages/firebase_ai/firebase_ai/example/android/app/build.gradle.kts +++ b/packages/firebase_ai/firebase_ai/example/android/app/build.gradle.kts @@ -1,8 +1,5 @@ plugins { id("com.android.application") - // START: FlutterFire Configuration - id("com.google.gms.google-services") - // END: FlutterFire Configuration id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") diff --git a/packages/firebase_ai/firebase_ai/example/android/settings.gradle.kts b/packages/firebase_ai/firebase_ai/example/android/settings.gradle.kts index bd7522f75402..ab39a10a29ba 100644 --- a/packages/firebase_ai/firebase_ai/example/android/settings.gradle.kts +++ b/packages/firebase_ai/firebase_ai/example/android/settings.gradle.kts @@ -19,9 +19,6 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.7.3" apply false - // START: FlutterFire Configuration - id("com.google.gms.google-services") version("4.3.15") apply false - // END: FlutterFire Configuration id("org.jetbrains.kotlin.android") version "2.1.0" apply false } From 5620afc47cd70b9635e65f79473f1b8160b15e7d Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Thu, 2 Apr 2026 16:59:54 -0700 Subject: [PATCH 17/17] fix formatter --- .../lib/pages/server_template_page.dart | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 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 c3fc6527688c..c10c6e243323 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 @@ -96,8 +96,7 @@ class _ServerTemplatePageState extends State { optionalProperties: ['zipCode'], ), 'date': JSONSchema.string( - description: - 'The date for which to get the weather. ' + description: 'The date for which to get the weather. ' 'Date must be in the format: YYYY-MM-DD.', ), 'unit': JSONSchema.enumString( @@ -314,12 +313,10 @@ class _ServerTemplatePageState extends State { } Future _serverTemplateUrlContext(String message) async { - await _handleServerTemplateMessage( - message, - (message) async { - var response = await _templateGenerativeModel - // ignore: experimental_member_use - ?.generateContent('cj-urlcontext', inputs: {'url': message}); + await _handleServerTemplateMessage(message, (message) async { + var response = await _templateGenerativeModel + // ignore: experimental_member_use + ?.generateContent('cj-urlcontext', inputs: {'url': message}); final candidate = response?.candidates.first; if (candidate == null) { @@ -393,10 +390,10 @@ class _ServerTemplatePageState extends State { ); // Respond to the function call - var functionResponse = await _chatFunctionOverrideSession - ?.sendMessage( - Content.functionResponse(functionCall.name, functionResult), - ); + var functionResponse = + await _chatFunctionOverrideSession?.sendMessage( + Content.functionResponse(functionCall.name, functionResult), + ); _messages.add( MessageData(text: functionResponse?.text, fromUser: false), );