From 15aa3fec59f87fb7682df8320a79d59fa7ec9d12 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Wed, 14 Jan 2026 10:30:18 -0800 Subject: [PATCH 01/15] add tool_test, add AutoFunctionDeclaration --- .../firebase_ai/firebase_ai/lib/src/tool.dart | 27 +- .../firebase_ai/test/tool_test.dart | 235 ++++++++++++++++++ 2 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 packages/firebase_ai/firebase_ai/test/tool_test.dart diff --git a/packages/firebase_ai/firebase_ai/lib/src/tool.dart b/packages/firebase_ai/firebase_ai/lib/src/tool.dart index 25677fd9cb54..e2d69dfe4229 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/tool.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/tool.dart @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'dart:async'; + import 'schema.dart'; /// Tool details that the model may use to generate a response. @@ -158,7 +160,7 @@ final class CodeExecution { /// Included in this declaration are the function name and parameters. This /// FunctionDeclaration is a representation of a block of code that can be used /// as a `Tool` by the model and executed by the client. -final class FunctionDeclaration { +class FunctionDeclaration { // ignore: public_member_api_docs FunctionDeclaration(this.name, this.description, {required Map parameters, @@ -185,6 +187,29 @@ final class FunctionDeclaration { }; } +/// A [FunctionDeclaration] for auto function calling. +final class AutoFunctionDeclaration extends FunctionDeclaration { + /// Creates an [AutoFunctionDeclaration]. + /// + /// - [name]: The name of the function. + /// - [description]: A brief description of the function. + /// - [parameters]: The parameters of the function as a map of names to + /// [Schema] objects. + /// - [callable]: The actual function implementation. + AutoFunctionDeclaration({ + required String name, + required String description, + required Map parameters, + List optionalParameters = const [], + required this.callable, + }) : super(name, description, + parameters: parameters, optionalParameters: optionalParameters); + + /// The callable function that this declaration represents. + final FutureOr> Function(Map args) + callable; +} + /// Config for tools to use with model. final class ToolConfig { // ignore: public_member_api_docs diff --git a/packages/firebase_ai/firebase_ai/test/tool_test.dart b/packages/firebase_ai/firebase_ai/test/tool_test.dart new file mode 100644 index 000000000000..02720d2455be --- /dev/null +++ b/packages/firebase_ai/firebase_ai/test/tool_test.dart @@ -0,0 +1,235 @@ +// Copyright 2024 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 'package:firebase_ai/src/schema.dart'; +import 'package:firebase_ai/src/tool.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Tool Tests', () { + test('AutoFunctionDeclaration basic properties and toJson', () async { + // Define a simple callable function + Future> myFunction(Map args) async { + return { + 'result': 'Hello, ${args['name']}!', + 'age_plus_ten': (args['age'] as int) + 10, + }; + } + + // Define the schema for the function's parameters + final parametersSchema = { + 'name': Schema.string(description: 'The name to greet'), + 'age': Schema.integer(description: 'The age of the person'), + }; + + // Create an AutoFunctionDeclaration + final autoDeclaration = AutoFunctionDeclaration( + name: 'greetUser', + description: + 'Greets a user with their name and calculates age plus ten.', + parameters: parametersSchema, + optionalParameters: const [], + callable: myFunction, + ); + + // Verify properties + expect(autoDeclaration.name, 'greetUser'); + expect(autoDeclaration.description, + 'Greets a user with their name and calculates age plus ten.'); + expect(autoDeclaration.callable, myFunction); + + // Verify toJson output (should match FunctionDeclaration's toJson) + expect(autoDeclaration.toJson(), { + 'name': 'greetUser', + 'description': + 'Greets a user with their name and calculates age plus ten.', + 'parameters': { + 'type': 'OBJECT', + 'properties': { + 'name': {'type': 'STRING', 'description': 'The name to greet'}, + 'age': {'type': 'INTEGER', 'description': 'The age of the person'}, + }, + 'required': ['name', 'age'], + }, + }); + + // Optionally, test invoking the callable directly (simulating client execution) + final result = + await autoDeclaration.callable({'name': 'Alice', 'age': 30}); + expect(result, {'result': 'Hello, Alice!', 'age_plus_ten': 40}); + }); + + test('AutoFunctionDeclaration with optional parameters', () async { + Future> optionalParamFunction( + Map args) async { + final greeting = + args['name'] != null ? 'Hello, ${args['name']}!' : 'Hello!'; + + return {'message': greeting}; + } + + final parametersSchema = { + 'name': Schema.string(description: 'An optional name'), + }; + + final autoDeclaration = AutoFunctionDeclaration( + name: 'optionalGreet', + description: 'Greets a user, optionally by name.', + parameters: parametersSchema, + optionalParameters: const ['name'], + callable: optionalParamFunction, + ); + + expect(autoDeclaration.name, 'optionalGreet'); + expect(autoDeclaration.description, 'Greets a user, optionally by name.'); + expect(autoDeclaration.callable, optionalParamFunction); + expect(autoDeclaration.toJson(), { + 'name': 'optionalGreet', + 'description': 'Greets a user, optionally by name.', + 'parameters': { + 'type': 'OBJECT', + + 'properties': { + 'name': {'type': 'STRING', 'description': 'An optional name'}, + }, + + 'required': [], // 'name' is optional, so 'required' is empty + }, + }); + + final resultWithoutName = await autoDeclaration.callable({}); + expect(resultWithoutName, {'message': 'Hello!'}); + final resultWithName = await autoDeclaration.callable({'name': 'Bob'}); + expect(resultWithName, {'message': 'Hello, Bob!'}); + }); + + // Test FunctionCallingConfig + test('FunctionCallingConfig.auto()', () { + final config = FunctionCallingConfig.auto(); + expect(config.mode, FunctionCallingMode.auto); + expect(config.allowedFunctionNames, isNull); + expect(config.toJson(), {'mode': 'AUTO'}); + }); + + test('FunctionCallingConfig.any()', () { + final allowedNames = {'func1', 'func2'}; + final config = FunctionCallingConfig.any(allowedNames); + expect(config.mode, FunctionCallingMode.any); + expect(config.allowedFunctionNames, allowedNames); + expect(config.toJson(), { + 'mode': 'ANY', + 'allowedFunctionNames': ['func1', 'func2'], + }); + }); + + test('FunctionCallingConfig.none()', () { + final config = FunctionCallingConfig.none(); + expect(config.mode, FunctionCallingMode.none); + expect(config.allowedFunctionNames, isNull); + expect(config.toJson(), {'mode': 'NONE'}); + }); + + // Test FunctionCallingMode.toJson() + test('FunctionCallingMode.toJson()', () { + expect(FunctionCallingMode.auto.toJson(), 'AUTO'); + expect(FunctionCallingMode.any.toJson(), 'ANY'); + expect(FunctionCallingMode.none.toJson(), 'NONE'); + }); + + // Test Tool.functionDeclarations() + test('Tool.functionDeclarations()', () { + final functionDeclaration = AutoFunctionDeclaration( + name: 'myFunction', + description: 'Does something.', + parameters: {'param1': Schema.string()}, + callable: (args) async => {'result': 'Success'}, + ); + + final tool = Tool.functionDeclarations([functionDeclaration]); + + expect(tool.toJson(), { + 'functionDeclarations': [ + { + 'name': 'myFunction', + 'description': 'Does something.', + 'parameters': { + 'type': 'OBJECT', + 'properties': { + 'param1': {'type': 'STRING'}, + }, + 'required': ['param1'], + }, + } + ] + }); + }); + + // Test Tool.googleSearch() + + test('Tool.googleSearch()', () { + final tool = Tool.googleSearch(); + expect(tool.toJson(), { + 'googleSearch': {}, + }); + }); + + // Test Tool.codeExecution() + + test('Tool.codeExecution()', () { + final tool = Tool.codeExecution(); + expect(tool.toJson(), { + 'codeExecution': {}, + }); + }); + + // Test Tool.urlContext() + test('Tool.urlContext()', () { + final tool = Tool.urlContext(); + expect(tool.toJson(), { + 'urlContext': {}, + }); + }); + + // Test ToolConfig + test('ToolConfig with FunctionCallingConfig', () { + final config = ToolConfig( + functionCallingConfig: FunctionCallingConfig.auto(), + ); + expect(config.toJson(), { + 'functionCallingConfig': {'mode': 'AUTO'}, + }); + }); + + test('ToolConfig with null FunctionCallingConfig', () { + final config = ToolConfig(); + expect(config.toJson(), {}); + }); + + // Test GoogleSearch, CodeExecution, UrlContext toJson() + test('GoogleSearch.toJson()', () { + const search = GoogleSearch(); + expect(search.toJson(), {}); + }); + + test('CodeExecution.toJson()', () { + const execution = CodeExecution(); + expect(execution.toJson(), {}); + }); + + test('UrlContext.toJson()', () { + const context = UrlContext(); + expect(context.toJson(), {}); + }); + }); +} From 7cb62d6149d6b21830c30256d42d8aa51ddce12e Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Wed, 14 Jan 2026 14:29:34 -0800 Subject: [PATCH 02/15] first pass of the auto function call --- .../firebase_ai/firebase_ai/lib/src/chat.dart | 75 +++++++++++++++---- .../firebase_ai/lib/src/generative_model.dart | 14 ++-- .../firebase_ai/firebase_ai/lib/src/tool.dart | 9 +++ 3 files changed, 79 insertions(+), 19 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/lib/src/chat.dart b/packages/firebase_ai/firebase_ai/lib/src/chat.dart index 6a0846fd0214..4955d1edb6ec 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 'tool.dart'; import 'utils/chat_utils.dart'; import 'utils/mutex.dart'; @@ -27,8 +28,20 @@ import 'utils/mutex.dart'; /// [GenerateContentResponse], other candidates may be available on the returned /// response. The history reflects the most current state of the chat session. final class ChatSession { - ChatSession._(this._generateContent, this._generateContentStream, - this._history, this._safetySettings, this._generationConfig); + ChatSession._( + this._generateContent, + this._generateContentStream, + this._history, + this._safetySettings, + this._generationConfig, + this._tools, + this._maxTurns) + : _autoFunctionDeclarations = _tools + ?.expand((tool) => tool.autoFunctionDeclarations) + .fold({}, (map, function) { + map?[function.name] = function; + return map; + }); final Future Function(Iterable content, {List? safetySettings, GenerationConfig? generationConfig}) _generateContent; @@ -41,6 +54,9 @@ final class ChatSession { final List _history; final List? _safetySettings; final GenerationConfig? _generationConfig; + final List? _tools; + final Map? _autoFunctionDeclarations; + final int _maxTurns; /// The content that has been successfully sent to, or received from, the /// generative model. @@ -66,16 +82,48 @@ final class ChatSession { Future sendMessage(Content message) async { final lock = await _mutex.acquire(); try { - final response = await _generateContent(_history.followedBy([message]), - safetySettings: _safetySettings, generationConfig: _generationConfig); - 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 _generateContent( + _history.followedBy(requestHistory), + safetySettings: _safetySettings, + generationConfig: _generationConfig); + + final functionCalls = response.functionCalls; + if (functionCalls.isEmpty) { + if (response.candidates case [final candidate, ...]) { + _history.addAll(requestHistory); + final normalizedContent = candidate.content.role == null + ? Content('model', candidate.content.parts) + : candidate.content; + _history.add(normalizedContent); + } + return response; + } + + requestHistory.add(response.candidates.first.content); + final functionResponses = []; + for (final functionCall in functionCalls) { + final function = _autoFunctionDeclarations?[functionCall.name]; + if (function == null) { + throw Exception( + 'Unknown function call: ${functionCall.name}, please add ' + 'the function to the tools.'); + } + 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(); } @@ -138,7 +186,8 @@ extension StartChatExtension on GenerativeModel { ChatSession startChat( {List? history, List? safetySettings, - GenerationConfig? generationConfig}) => + GenerationConfig? generationConfig, + int? maxTurns}) => ChatSession._(generateContent, generateContentStream, history ?? [], - safetySettings, generationConfig); + safetySettings, generationConfig, tools, maxTurns ?? 5); } 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..eed04f8488c8 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart @@ -48,7 +48,7 @@ final class GenerativeModel extends BaseApiClientModel { http.Client? httpClient, }) : _safetySettings = safetySettings ?? [], _generationConfig = generationConfig, - _tools = tools, + this.tools = tools, _toolConfig = toolConfig, _systemInstruction = systemInstruction, super( @@ -80,7 +80,7 @@ final class GenerativeModel extends BaseApiClientModel { ApiClient? apiClient, }) : _safetySettings = safetySettings ?? [], _generationConfig = generationConfig, - _tools = tools, + this.tools = tools, _toolConfig = toolConfig, _systemInstruction = systemInstruction, super( @@ -98,7 +98,9 @@ final class GenerativeModel extends BaseApiClientModel { final List _safetySettings; final GenerationConfig? _generationConfig; - final List? _tools; + + /// List of [Tool] registered in the model + final List? tools; final ToolConfig? _toolConfig; final Content? _systemInstruction; @@ -125,7 +127,7 @@ final class GenerativeModel extends BaseApiClientModel { model, safetySettings ?? _safetySettings, generationConfig ?? _generationConfig, - tools ?? _tools, + tools ?? this.tools, toolConfig ?? _toolConfig, _systemInstruction, ), @@ -156,7 +158,7 @@ final class GenerativeModel extends BaseApiClientModel { model, safetySettings ?? _safetySettings, generationConfig ?? _generationConfig, - tools ?? _tools, + tools ?? this.tools, toolConfig ?? _toolConfig, _systemInstruction, )); @@ -188,7 +190,7 @@ final class GenerativeModel extends BaseApiClientModel { model, _safetySettings, _generationConfig, - _tools, + tools, _toolConfig, ); return makeRequest(Task.countTokens, parameters, diff --git a/packages/firebase_ai/firebase_ai/lib/src/tool.dart b/packages/firebase_ai/firebase_ai/lib/src/tool.dart index e2d69dfe4229..6b92fbd5d96c 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/tool.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/tool.dart @@ -97,6 +97,15 @@ final class Tool { /// A tool that allows providing URL context to the model. final UrlContext? _urlContext; + /// Returns a list of all [AutoFunctionDeclaration] objects + /// found within the [_functionDeclarations] list. + List get autoFunctionDeclarations { + return _functionDeclarations + ?.whereType() + .toList() ?? + []; + } + /// Convert to json object. Map toJson() => { if (_functionDeclarations case final _functionDeclarations?) From fb67fa3872ab0be871fb72ea1ec3150fb8ee7966 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Wed, 14 Jan 2026 14:38:53 -0800 Subject: [PATCH 03/15] Added auto function call to sendMessageStream --- .../firebase_ai/firebase_ai/lib/src/chat.dart | 71 +++++++++++++++---- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/lib/src/chat.dart b/packages/firebase_ai/firebase_ai/lib/src/chat.dart index 4955d1edb6ec..74afaa9837ff 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/chat.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/chat.dart @@ -147,28 +147,69 @@ final class ChatSession { /// Waits to read the entire streamed response before recording the message /// and response and allowing pending messages to be sent. Stream sendMessageStream(Content message) { - final controller = StreamController(sync: true); + final controller = StreamController(); _mutex.acquire().then((lock) async { try { - final responses = _generateContentStream(_history.followedBy([message]), - safetySettings: _safetySettings, - generationConfig: _generationConfig); - 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 = _generateContentStream( + _history.followedBy(requestHistory), + safetySettings: _safetySettings, + generationConfig: _generationConfig); + + 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(); + + if (functionCalls.isEmpty) { + _history.addAll(requestHistory); + _history.add(aggregatedContent); + return; + } + + requestHistory.add(aggregatedContent); + final functionResponseFutures = + functionCalls.map((functionCall) async { + final function = _autoFunctionDeclarations?[functionCall.name]; + if (function == null) { + throw Exception( + 'Unknown function call: ${functionCall.name}, please add ' + 'the function to the tools.'); + } + 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; } From f2c3ce8c8c3354bd2c4d6aa3a939847c6225ed09 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Wed, 14 Jan 2026 15:45:17 -0800 Subject: [PATCH 04/15] update the example test --- .../lib/pages/function_calling_page.dart | 151 +++++++++++++++--- .../firebase_ai/lib/firebase_ai.dart | 1 + 2 files changed, 131 insertions(+), 21 deletions(-) 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 4c500aefde1c..72f203e6af8a 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 @@ -40,7 +40,9 @@ class Location { class _FunctionCallingPageState extends State { late GenerativeModel _functionCallModel; + late GenerativeModel _autoFunctionCallModel; late GenerativeModel _codeExecutionModel; + late final AutoFunctionDeclaration _autoFetchWeatherTool; final List _messages = []; bool _loading = false; bool _enableThinking = false; @@ -48,9 +50,43 @@ class _FunctionCallingPageState extends State { @override void initState() { super.initState(); + _autoFetchWeatherTool = AutoFunctionDeclaration( + name: 'fetchWeather', + description: + 'Get the weather conditions for a specific city on a specific date.', + 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.', + ), + }, + ), + 'date': Schema.string( + description: + 'The date for which to get the weather. Date must be in the format: YYYY-MM-DD.', + ), + }, + callable: _fetchWeatherCallable, + ); _initializeModel(); } + 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 @@ -69,6 +105,13 @@ class _FunctionCallingPageState extends State { Tool.functionDeclarations([fetchWeatherTool]), ], ); + _autoFunctionCallModel = vertexAI.generativeModel( + model: 'gemini-2.5-flash', + generationConfig: generationConfig, + tools: [ + Tool.functionDeclarations([_autoFetchWeatherTool]), + ], + ); _codeExecutionModel = vertexAI.generativeModel( model: 'gemini-2.5-flash', generationConfig: generationConfig, @@ -85,6 +128,13 @@ class _FunctionCallingPageState extends State { Tool.functionDeclarations([fetchWeatherTool]), ], ); + _autoFunctionCallModel = googleAI.generativeModel( + model: 'gemini-2.5-flash', + generationConfig: generationConfig, + tools: [ + Tool.functionDeclarations([_autoFetchWeatherTool]), + ], + ); _codeExecutionModel = googleAI.generativeModel( model: 'gemini-2.5-flash', generationConfig: generationConfig, @@ -176,29 +226,48 @@ class _FunctionCallingPageState extends State { vertical: 25, horizontal: 15, ), - child: Row( + child: Column( children: [ - Expanded( - child: ElevatedButton( - onPressed: !_loading - ? () async { - await _testFunctionCalling(); - } - : null, - child: const Text('Test Function Calling'), - ), - ), - const SizedBox(width: 8), - Expanded( - child: ElevatedButton( - onPressed: !_loading - ? () async { - await _testCodeExecution(); - } - : null, - child: const Text('Test Code Execution'), - ), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: !_loading + ? () async { + await _testFunctionCalling(); + } + : null, + child: const Text('Manual FC'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: !_loading + ? () async { + await _testCodeExecution(); + } + : null, + child: const Text('Code Execution'), + ), + ), + ], ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: !_loading + ? () async { + await _testAutoFunctionCalling(); + } + : null, + child: const Text('Auto Function Calling'), + ), + ), + ], + ) ], ), ), @@ -208,6 +277,46 @@ class _FunctionCallingPageState extends State { ); } + Future _testAutoFunctionCalling() async { + setState(() { + _loading = true; + _messages.clear(); + }); + try { + final autoFunctionCallChat = _autoFunctionCallModel.startChat(); + const prompt = + 'What is the weather like in Boston on 10/02 in year 2024?'; + + _messages.add(MessageData(text: prompt, fromUser: true)); + setState(() {}); + + // Send the message to the generative model. + final response = await autoFunctionCallChat.sendMessage( + Content.text(prompt), + ); + + final thought = response.thoughtSummary; + if (thought != null) { + _messages + .add(MessageData(text: thought, fromUser: false, isThought: true)); + } + + // The SDK should have handled the function call automatically. + // The final response should contain the text from the model. + if (response.text case final text?) { + _messages.add(MessageData(text: text)); + } else { + _messages.add(MessageData(text: 'No text response from model.')); + } + } catch (e) { + _showError(e.toString()); + } finally { + setState(() { + _loading = false; + }); + } + } + Future _testFunctionCalling() 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 6c05e772f062..ea3031b09970 100644 --- a/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart +++ b/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart @@ -113,6 +113,7 @@ export 'src/schema.dart' show Schema, SchemaType; export 'src/tool.dart' show + AutoFunctionDeclaration, FunctionCallingConfig, FunctionCallingMode, FunctionDeclaration, From c2764cc47b9bc515412e6fed857754f09313062b Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Wed, 14 Jan 2026 16:27:08 -0800 Subject: [PATCH 05/15] fix the auto function calling logic --- .../lib/pages/function_calling_page.dart | 6 +-- .../firebase_ai/firebase_ai/lib/src/chat.dart | 43 ++++++++++++------- 2 files changed, 30 insertions(+), 19 deletions(-) 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 72f203e6af8a..595d2bda017c 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 @@ -51,7 +51,7 @@ class _FunctionCallingPageState extends State { void initState() { super.initState(); _autoFetchWeatherTool = AutoFunctionDeclaration( - name: 'fetchWeather', + name: 'autofetchWeather', description: 'Get the weather conditions for a specific city on a specific date.', parameters: { @@ -285,7 +285,7 @@ class _FunctionCallingPageState extends State { try { final autoFunctionCallChat = _autoFunctionCallModel.startChat(); const prompt = - 'What is the weather like in Boston on 10/02 in year 2024?'; + 'What is the weather like in Boston, MA on 10/02 in year 2024?'; _messages.add(MessageData(text: prompt, fromUser: true)); setState(() {}); @@ -325,7 +325,7 @@ class _FunctionCallingPageState extends State { try { final functionCallChat = _functionCallModel.startChat(); const prompt = - 'What is the weather like in Boston on 10/02 in year 2024?'; + 'What is the weather like in Boston, MA on 10/02 in year 2024?'; _messages.add(MessageData(text: prompt, fromUser: true)); diff --git a/packages/firebase_ai/firebase_ai/lib/src/chat.dart b/packages/firebase_ai/firebase_ai/lib/src/chat.dart index 74afaa9837ff..054caa389249 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/chat.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/chat.dart @@ -91,7 +91,18 @@ final class ChatSession { generationConfig: _generationConfig); final functionCalls = response.functionCalls; - if (functionCalls.isEmpty) { + + // Only trigger auto-execution if: + // 1. We have auto-functions configured. + // 2. The response actually contains function calls. + // 3. ALL called functions exist in our declarations (prevents crashes). + final shouldAutoExecute = _autoFunctionDeclarations != null && + _autoFunctionDeclarations.isNotEmpty && + functionCalls.isNotEmpty && + functionCalls + .every((c) => _autoFunctionDeclarations.containsKey(c.name)); + if (!shouldAutoExecute) { + // Standard handling: Update history and return the response to the user. if (response.candidates case [final candidate, ...]) { _history.addAll(requestHistory); final normalizedContent = candidate.content.role == null @@ -102,18 +113,15 @@ final class ChatSession { return response; } + // Auto function execution requestHistory.add(response.candidates.first.content); final functionResponses = []; for (final functionCall in functionCalls) { - final function = _autoFunctionDeclarations?[functionCall.name]; - if (function == null) { - throw Exception( - 'Unknown function call: ${functionCall.name}, please add ' - 'the function to the tools.'); - } + final function = _autoFunctionDeclarations[functionCall.name]; + Object? result; try { - result = await function.callable(functionCall.args); + result = await function!.callable(functionCall.args); } catch (e) { result = e.toString(); } @@ -175,7 +183,14 @@ final class ChatSession { final functionCalls = aggregatedContent.parts.whereType().toList(); - if (functionCalls.isEmpty) { + // Check if we should actually execute these functions. + final shouldAutoExecute = _autoFunctionDeclarations != null && + _autoFunctionDeclarations.isNotEmpty && + functionCalls.isNotEmpty && + functionCalls + .every((c) => _autoFunctionDeclarations.containsKey(c.name)); + + if (!shouldAutoExecute) { _history.addAll(requestHistory); _history.add(aggregatedContent); return; @@ -184,15 +199,11 @@ final class ChatSession { requestHistory.add(aggregatedContent); final functionResponseFutures = functionCalls.map((functionCall) async { - final function = _autoFunctionDeclarations?[functionCall.name]; - if (function == null) { - throw Exception( - 'Unknown function call: ${functionCall.name}, please add ' - 'the function to the tools.'); - } + final function = _autoFunctionDeclarations[functionCall.name]; + Object? result; try { - result = await function.callable(functionCall.args); + result = await function!.callable(functionCall.args); } catch (e) { result = e.toString(); } From 20f957ae6f37df0202c1e308957730328bfc6263 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Wed, 14 Jan 2026 16:43:53 -0800 Subject: [PATCH 06/15] Add test for normal stream function calling and auto stream function calling --- .../lib/pages/function_calling_page.dart | 166 +++++++++++++++++- 1 file changed, 165 insertions(+), 1 deletion(-) 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 595d2bda017c..930cdf8f40e2 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 @@ -267,7 +267,33 @@ class _FunctionCallingPageState extends State { ), ), ], - ) + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: !_loading + ? () async { + await _testStreamFunctionCalling(); + } + : null, + child: const Text('Stream FC'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: !_loading + ? () async { + await _testAutoStreamFunctionCalling(); + } + : null, + child: const Text('Auto Stream FC'), + ), + ), + ], + ), ], ), ), @@ -277,6 +303,144 @@ class _FunctionCallingPageState extends State { ); } + Future _testStreamFunctionCalling() async { + setState(() { + _loading = true; + _messages.clear(); + }); + try { + final functionCallChat = _functionCallModel.startChat(); + const prompt = + 'What is the weather like in Boston, MA on 10/02 in year 2024?'; + + _messages.add(MessageData(text: prompt, fromUser: true)); + setState(() {}); + + // Send the message to the generative model. + final responseStream = functionCallChat.sendMessageStream( + Content.text(prompt), + ); + + GenerateContentResponse? lastResponse; + await for (final response in responseStream) { + lastResponse = response; + final thought = response.thoughtSummary; + if (thought != null) { + _messages.add( + MessageData(text: thought, fromUser: false, isThought: true), + ); + setState(() {}); + } + } + + final functionCalls = lastResponse?.functionCalls.toList(); + // When the model response with a function call, invoke the function. + 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); + // Send the response to the model so that it can use the result to + // generate text for the user. + final responseStream2 = functionCallChat.sendMessageStream( + Content.functionResponse(functionCall.name, functionResult), + ); + + var accumulatedText = ''; + _messages.add(MessageData(text: accumulatedText)); + setState(() {}); + + await for (final response in responseStream2) { + if (response.text case final text?) { + accumulatedText += text; + _messages.last = _messages.last.copyWith(text: accumulatedText); + setState(() {}); + } + } + } else { + throw UnimplementedError( + 'Function not declared to the model: ${functionCall.name}', + ); + } + } else if (lastResponse?.text case final text?) { + // This would be if no function call was returned. + _messages.add(MessageData(text: text)); + setState(() {}); + } else { + _messages.add(MessageData(text: 'No text response from model.')); + } + } catch (e) { + _showError(e.toString()); + } finally { + setState(() { + _loading = false; + }); + } + } + + Future _testAutoStreamFunctionCalling() async { + setState(() { + _loading = true; + _messages.clear(); + }); + try { + final autoFunctionCallChat = _autoFunctionCallModel.startChat(); + const prompt = + 'What is the weather like in Boston, MA on 10/02 in year 2024?'; + + _messages.add(MessageData(text: prompt, fromUser: true)); + setState(() {}); + + // Send the message to the generative model. + final responseStream = autoFunctionCallChat.sendMessageStream( + Content.text(prompt), + ); + + var accumulatedText = ''; + MessageData? modelMessage; + + await for (final response in responseStream) { + final thought = response.thoughtSummary; + if (thought != null) { + _messages.add( + MessageData(text: thought, fromUser: false, isThought: true), + ); + setState(() {}); + } + + // The SDK should have handled the function call automatically. + // The final response should contain the text from the model. + if (response.text case final text?) { + accumulatedText += text; + if (modelMessage == null) { + modelMessage = MessageData(text: accumulatedText); + _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.')); + setState(() {}); + } + } catch (e) { + _showError(e.toString()); + } finally { + setState(() { + _loading = false; + }); + } + } + Future _testAutoFunctionCalling() async { setState(() { _loading = true; From 383bd005d7e3bdaba221f1f747b95f8a1dd67cae Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Wed, 14 Jan 2026 18:33:58 -0800 Subject: [PATCH 07/15] Added parallel auto function calling test case --- .../lib/pages/function_calling_page.dart | 138 +++++++++++++++++- 1 file changed, 137 insertions(+), 1 deletion(-) 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 930cdf8f40e2..da95b640e8aa 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 @@ -41,12 +41,16 @@ class Location { class _FunctionCallingPageState extends State { late GenerativeModel _functionCallModel; late GenerativeModel _autoFunctionCallModel; + late GenerativeModel _parallelAutoFunctionCallModel; late GenerativeModel _codeExecutionModel; late final AutoFunctionDeclaration _autoFetchWeatherTool; final List _messages = []; bool _loading = false; bool _enableThinking = false; + late final AutoFunctionDeclaration _autoFindRestaurantsTool; + late final AutoFunctionDeclaration _autoGetRestaurantMenuTool; + @override void initState() { super.initState(); @@ -74,9 +78,72 @@ class _FunctionCallingPageState extends State { }, callable: _fetchWeatherCallable, ); + _autoFindRestaurantsTool = AutoFunctionDeclaration( + name: 'findRestaurants', + description: 'Find restaurants of a certain cuisine in a given location.', + parameters: { + 'cuisine': Schema.string( + description: 'The cuisine of the restaurant.', + ), + 'location': Schema.string( + description: + 'The location to search for restaurants. e.g. San Francisco, CA', + ), + }, + callable: (args) async { + final cuisine = args['cuisine']! as String; + final location = args['location']! as String; + return findRestaurants(cuisine, location); + }, + ); + _autoGetRestaurantMenuTool = AutoFunctionDeclaration( + name: 'getRestaurantMenu', + description: 'Get the menu for a specific restaurant.', + parameters: { + 'restaurantName': Schema.string( + description: 'The name of the restaurant.', + ), + }, + callable: (args) async { + final restaurantName = args['restaurantName']! as String; + return getRestaurantMenu(restaurantName); + }, + ); _initializeModel(); } + Future> findRestaurants( + String cuisine, + String location, + ) async { + // This is a mock response. + return { + 'restaurants': [ + { + 'name': 'The Golden Spoon', + 'cuisine': 'Vegetarian', + 'location': 'San Francisco, CA', + }, + { + 'name': 'Green Leaf Bistro', + 'cuisine': 'Vegetarian', + 'location': 'San Francisco, CA', + }, + ], + }; + } + + Future> getRestaurantMenu(String restaurantName) async { + // This is a mock response. + return { + 'menu': [ + {'name': 'Lentil Soup', 'price': '8.99'}, + {'name': 'Garden Salad', 'price': '10.99'}, + {'name': 'Mushroom Risotto', 'price': '15.99'}, + ], + }; + } + Future> _fetchWeatherCallable( Map args, ) async { @@ -112,6 +179,15 @@ class _FunctionCallingPageState extends State { Tool.functionDeclarations([_autoFetchWeatherTool]), ], ); + _parallelAutoFunctionCallModel = vertexAI.generativeModel( + model: 'gemini-2.5-flash', + generationConfig: generationConfig, + tools: [ + Tool.functionDeclarations( + [_autoFindRestaurantsTool, _autoGetRestaurantMenuTool], + ), + ], + ); _codeExecutionModel = vertexAI.generativeModel( model: 'gemini-2.5-flash', generationConfig: generationConfig, @@ -135,6 +211,15 @@ class _FunctionCallingPageState extends State { Tool.functionDeclarations([_autoFetchWeatherTool]), ], ); + _parallelAutoFunctionCallModel = googleAI.generativeModel( + model: 'gemini-2.5-flash', + generationConfig: generationConfig, + tools: [ + Tool.functionDeclarations( + [_autoFindRestaurantsTool, _autoGetRestaurantMenuTool], + ), + ], + ); _codeExecutionModel = googleAI.generativeModel( model: 'gemini-2.5-flash', generationConfig: generationConfig, @@ -266,6 +351,17 @@ class _FunctionCallingPageState extends State { child: const Text('Auto Function Calling'), ), ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: !_loading + ? () async { + await _testParallelAutoFunctionCalling(); + } + : null, + child: const Text('Parallel Auto FC'), + ), + ), ], ), const SizedBox(height: 8), @@ -293,7 +389,7 @@ class _FunctionCallingPageState extends State { ), ), ], - ), + ) ], ), ), @@ -303,6 +399,46 @@ class _FunctionCallingPageState extends State { ); } + Future _testParallelAutoFunctionCalling() async { + setState(() { + _loading = true; + _messages.clear(); + }); + try { + final autoFunctionCallChat = _parallelAutoFunctionCallModel.startChat(); + const prompt = + 'Find me a good vegetarian restaurant in San Francisco and get its menu.'; + + _messages.add(MessageData(text: prompt, fromUser: true)); + setState(() {}); + + // Send the message to the generative model. + final response = await autoFunctionCallChat.sendMessage( + Content.text(prompt), + ); + + final thought = response.thoughtSummary; + if (thought != null) { + _messages + .add(MessageData(text: thought, fromUser: false, isThought: true)); + } + + // The SDK should have handled the function call automatically. + // The final response should contain the text from the model. + if (response.text case final text?) { + _messages.add(MessageData(text: text)); + } else { + _messages.add(MessageData(text: 'No text response from model.')); + } + } catch (e) { + _showError(e.toString()); + } finally { + setState(() { + _loading = false; + }); + } + } + Future _testStreamFunctionCalling() async { setState(() { _loading = true; From 2c052e93589bd4cec445697b93270279339c9ad3 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Thu, 15 Jan 2026 11:19:16 -0800 Subject: [PATCH 08/15] fix analyzer --- .../firebase_ai/example/lib/pages/function_calling_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 da95b640e8aa..4c66f2c3473b 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 @@ -389,7 +389,7 @@ class _FunctionCallingPageState extends State { ), ), ], - ) + ), ], ), ), From ee8a0ef9f12949cb947d488bc6ccd3f993e860f0 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Thu, 15 Jan 2026 12:19:47 -0800 Subject: [PATCH 09/15] Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../example/lib/pages/function_calling_page.dart | 10 +++++++--- packages/firebase_ai/firebase_ai/lib/src/chat.dart | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) 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 4c66f2c3473b..93ab24afe4a3 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 @@ -91,9 +91,13 @@ class _FunctionCallingPageState extends State { ), }, callable: (args) async { - final cuisine = args['cuisine']! as String; - final location = args['location']! as String; - return findRestaurants(cuisine, location); + final cuisine = args['cuisine']; + final location = args['location']; + if (cuisine is String && location is String) { + return findRestaurants(cuisine, location); + } + // It's good practice to handle cases where arguments are missing or have the wrong type. + throw Exception('Missing or invalid arguments for findRestaurants'); }, ); _autoGetRestaurantMenuTool = AutoFunctionDeclaration( diff --git a/packages/firebase_ai/firebase_ai/lib/src/chat.dart b/packages/firebase_ai/firebase_ai/lib/src/chat.dart index 054caa389249..48f0bd1fb6c2 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/chat.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/chat.dart @@ -39,7 +39,7 @@ final class ChatSession { : _autoFunctionDeclarations = _tools ?.expand((tool) => tool.autoFunctionDeclarations) .fold({}, (map, function) { - map?[function.name] = function; + map[function.name] = function; return map; }); final Future Function(Iterable content, From 14b894b78e3829626777257d8aead7ce911bb305 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Thu, 15 Jan 2026 12:21:30 -0800 Subject: [PATCH 10/15] convert on of the code review suggestion --- packages/firebase_ai/firebase_ai/lib/src/chat.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/firebase_ai/firebase_ai/lib/src/chat.dart b/packages/firebase_ai/firebase_ai/lib/src/chat.dart index 48f0bd1fb6c2..054caa389249 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/chat.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/chat.dart @@ -39,7 +39,7 @@ final class ChatSession { : _autoFunctionDeclarations = _tools ?.expand((tool) => tool.autoFunctionDeclarations) .fold({}, (map, function) { - map[function.name] = function; + map?[function.name] = function; return map; }); final Future Function(Iterable content, From 5e83d238f73af11f39bc39ee3e2934af90081171 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Thu, 15 Jan 2026 17:14:54 -0800 Subject: [PATCH 11/15] clean up and refactor the page --- .../lib/pages/function_calling_page.dart | 255 ++++++------------ 1 file changed, 80 insertions(+), 175 deletions(-) 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 93ab24afe4a3..03662ded1402 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 @@ -167,71 +167,41 @@ class _FunctionCallingPageState extends State { ) : null, ); - if (widget.useVertexBackend) { - var vertexAI = FirebaseAI.vertexAI(auth: FirebaseAuth.instance); - _functionCallModel = vertexAI.generativeModel( - model: 'gemini-2.5-flash', - generationConfig: generationConfig, - tools: [ - Tool.functionDeclarations([fetchWeatherTool]), - ], - ); - _autoFunctionCallModel = vertexAI.generativeModel( - model: 'gemini-2.5-flash', - generationConfig: generationConfig, - tools: [ - Tool.functionDeclarations([_autoFetchWeatherTool]), - ], - ); - _parallelAutoFunctionCallModel = vertexAI.generativeModel( - model: 'gemini-2.5-flash', - generationConfig: generationConfig, - tools: [ - Tool.functionDeclarations( - [_autoFindRestaurantsTool, _autoGetRestaurantMenuTool], - ), - ], - ); - _codeExecutionModel = vertexAI.generativeModel( - model: 'gemini-2.5-flash', - generationConfig: generationConfig, - tools: [ - Tool.codeExecution(), - ], - ); - } else { - var googleAI = FirebaseAI.googleAI(auth: FirebaseAuth.instance); - _functionCallModel = googleAI.generativeModel( - model: 'gemini-2.5-flash', - generationConfig: generationConfig, - tools: [ - Tool.functionDeclarations([fetchWeatherTool]), - ], - ); - _autoFunctionCallModel = googleAI.generativeModel( - model: 'gemini-2.5-flash', - generationConfig: generationConfig, - tools: [ - Tool.functionDeclarations([_autoFetchWeatherTool]), - ], - ); - _parallelAutoFunctionCallModel = googleAI.generativeModel( - model: 'gemini-2.5-flash', - generationConfig: generationConfig, - tools: [ - Tool.functionDeclarations( - [_autoFindRestaurantsTool, _autoGetRestaurantMenuTool], - ), - ], - ); - _codeExecutionModel = googleAI.generativeModel( - model: 'gemini-2.5-flash', - generationConfig: generationConfig, - tools: [ - Tool.codeExecution(), - ], - ); - } + + final aiClient = widget.useVertexBackend + ? FirebaseAI.vertexAI(auth: FirebaseAuth.instance) + : FirebaseAI.googleAI(auth: FirebaseAuth.instance); + + _functionCallModel = aiClient.generativeModel( + model: 'gemini-2.5-flash', + generationConfig: generationConfig, + tools: [ + Tool.functionDeclarations([fetchWeatherTool]), + ], + ); + _autoFunctionCallModel = aiClient.generativeModel( + model: 'gemini-2.5-flash', + generationConfig: generationConfig, + tools: [ + Tool.functionDeclarations([_autoFetchWeatherTool]), + ], + ); + _parallelAutoFunctionCallModel = aiClient.generativeModel( + model: 'gemini-2.5-flash', + generationConfig: generationConfig, + tools: [ + Tool.functionDeclarations( + [_autoFindRestaurantsTool, _autoGetRestaurantMenuTool], + ), + ], + ); + _codeExecutionModel = aiClient.generativeModel( + model: 'gemini-2.5-flash', + generationConfig: generationConfig, + tools: [ + Tool.codeExecution(), + ], + ); } // This is a hypothetical API to return a fake weather data collection for @@ -275,6 +245,17 @@ class _FunctionCallingPageState extends State { }, ); + Future> _executeFunctionCall(FunctionCall call) async { + if (call.name == 'fetchWeather') { + final location = call.args['location']! as Map; + final date = call.args['date']! as String; + final city = location['city'] as String; + final state = location['state'] as String; + return fetchWeather(Location(city, state), date); + } + throw UnimplementedError('Function not declared to the model: ${call.name}'); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -321,22 +302,14 @@ class _FunctionCallingPageState extends State { children: [ Expanded( child: ElevatedButton( - onPressed: !_loading - ? () async { - await _testFunctionCalling(); - } - : null, + onPressed: !_loading ? _testFunctionCalling : null, child: const Text('Manual FC'), ), ), const SizedBox(width: 8), Expanded( child: ElevatedButton( - onPressed: !_loading - ? () async { - await _testCodeExecution(); - } - : null, + onPressed: !_loading ? _testCodeExecution : null, child: const Text('Code Execution'), ), ), @@ -347,11 +320,7 @@ class _FunctionCallingPageState extends State { children: [ Expanded( child: ElevatedButton( - onPressed: !_loading - ? () async { - await _testAutoFunctionCalling(); - } - : null, + onPressed: !_loading ? _testAutoFunctionCalling : null, child: const Text('Auto Function Calling'), ), ), @@ -359,9 +328,7 @@ class _FunctionCallingPageState extends State { Expanded( child: ElevatedButton( onPressed: !_loading - ? () async { - await _testParallelAutoFunctionCalling(); - } + ? () => _testAutoFunctionCalling(parallel: true) : null, child: const Text('Parallel Auto FC'), ), @@ -374,9 +341,7 @@ class _FunctionCallingPageState extends State { Expanded( child: ElevatedButton( onPressed: !_loading - ? () async { - await _testStreamFunctionCalling(); - } + ? _testStreamFunctionCalling : null, child: const Text('Stream FC'), ), @@ -385,9 +350,7 @@ class _FunctionCallingPageState extends State { Expanded( child: ElevatedButton( onPressed: !_loading - ? () async { - await _testAutoStreamFunctionCalling(); - } + ? _testAutoStreamFunctionCalling : null, child: const Text('Auto Stream FC'), ), @@ -403,15 +366,19 @@ class _FunctionCallingPageState extends State { ); } - Future _testParallelAutoFunctionCalling() async { + Future _testAutoFunctionCalling({bool parallel = false}) async { setState(() { _loading = true; _messages.clear(); }); try { - final autoFunctionCallChat = _parallelAutoFunctionCallModel.startChat(); - const prompt = - 'Find me a good vegetarian restaurant in San Francisco and get its menu.'; + final model = + parallel ? _parallelAutoFunctionCallModel : _autoFunctionCallModel; + final prompt = parallel + ? 'Find me a good vegetarian restaurant in San Francisco and get its menu.' + : 'What is the weather like in Boston, MA on 10/02 in year 2024?'; + + final autoFunctionCallChat = model.startChat(); _messages.add(MessageData(text: prompt, fromUser: true)); setState(() {}); @@ -477,35 +444,23 @@ class _FunctionCallingPageState extends State { // When the model response with a function call, invoke the function. 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); - // Send the response to the model so that it can use the result to - // generate text for the user. - final responseStream2 = functionCallChat.sendMessageStream( - Content.functionResponse(functionCall.name, functionResult), - ); + final functionResult = await _executeFunctionCall(functionCall); + // Send the response to the model so that it can use the result to + // generate text for the user. + final responseStream2 = functionCallChat.sendMessageStream( + Content.functionResponse(functionCall.name, functionResult), + ); - var accumulatedText = ''; - _messages.add(MessageData(text: accumulatedText)); - setState(() {}); + var accumulatedText = ''; + _messages.add(MessageData(text: accumulatedText)); + setState(() {}); - await for (final response in responseStream2) { - if (response.text case final text?) { - accumulatedText += text; - _messages.last = _messages.last.copyWith(text: accumulatedText); - setState(() {}); - } + await for (final response in responseStream2) { + if (response.text case final text?) { + accumulatedText += text; + _messages.last = _messages.last.copyWith(text: accumulatedText); + setState(() {}); } - } else { - throw UnimplementedError( - 'Function not declared to the model: ${functionCall.name}', - ); } } else if (lastResponse?.text case final text?) { // This would be if no function call was returned. @@ -581,45 +536,7 @@ class _FunctionCallingPageState extends State { } } - Future _testAutoFunctionCalling() async { - setState(() { - _loading = true; - _messages.clear(); - }); - try { - final autoFunctionCallChat = _autoFunctionCallModel.startChat(); - const prompt = - 'What is the weather like in Boston, MA on 10/02 in year 2024?'; - - _messages.add(MessageData(text: prompt, fromUser: true)); - setState(() {}); - - // Send the message to the generative model. - final response = await autoFunctionCallChat.sendMessage( - Content.text(prompt), - ); - final thought = response.thoughtSummary; - if (thought != null) { - _messages - .add(MessageData(text: thought, fromUser: false, isThought: true)); - } - - // The SDK should have handled the function call automatically. - // The final response should contain the text from the model. - if (response.text case final text?) { - _messages.add(MessageData(text: text)); - } else { - _messages.add(MessageData(text: 'No text response from model.')); - } - } catch (e) { - _showError(e.toString()); - } finally { - setState(() { - _loading = false; - }); - } - } Future _testFunctionCalling() async { setState(() { @@ -648,24 +565,12 @@ class _FunctionCallingPageState extends State { // When the model response with a function call, invoke the function. if (functionCalls.isNotEmpty) { final functionCall = functionCalls.first; - if (functionCall.name == 'fetchWeather') { - Map location = - functionCall.args['location']! as Map; - var date = functionCall.args['date']! as String; - var city = location['city'] as String; - var state = location['state'] as String; - final functionResult = - await fetchWeather(Location(city, state), date); - // Send the response to the model so that it can use the result to - // generate text for the user. - response = await functionCallChat.sendMessage( - Content.functionResponse(functionCall.name, functionResult), - ); - } else { - throw UnimplementedError( - 'Function not declared to the model: ${functionCall.name}', - ); - } + final functionResult = await _executeFunctionCall(functionCall); + // Send the response to the model so that it can use the result to + // generate text for the user. + response = await functionCallChat.sendMessage( + Content.functionResponse(functionCall.name, functionResult), + ); } // When the model responds with non-null text content, print it. if (response.text case final text?) { From 3de0afe0edd5ff7cc2d0b78c8e8af95096d21d85 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Thu, 15 Jan 2026 17:56:36 -0800 Subject: [PATCH 12/15] address review comment --- .../lib/pages/function_calling_page.dart | 101 +++++------------- 1 file changed, 29 insertions(+), 72 deletions(-) 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 03662ded1402..a54397f2b76e 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 @@ -256,6 +256,23 @@ class _FunctionCallingPageState extends State { throw UnimplementedError('Function not declared to the model: ${call.name}'); } + Future _runTest(Future Function() testBody) async { + if (_loading) return; + setState(() { + _loading = true; + _messages.clear(); + }); + try { + await testBody(); + } catch (e) { + _showError(e.toString()); + } finally { + setState(() { + _loading = false; + }); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -367,11 +384,7 @@ class _FunctionCallingPageState extends State { } Future _testAutoFunctionCalling({bool parallel = false}) async { - setState(() { - _loading = true; - _messages.clear(); - }); - try { + await _runTest(() async { final model = parallel ? _parallelAutoFunctionCallModel : _autoFunctionCallModel; final prompt = parallel @@ -401,21 +414,11 @@ class _FunctionCallingPageState extends State { } else { _messages.add(MessageData(text: 'No text response from model.')); } - } catch (e) { - _showError(e.toString()); - } finally { - setState(() { - _loading = false; - }); - } + }); } Future _testStreamFunctionCalling() async { - setState(() { - _loading = true; - _messages.clear(); - }); - try { + await _runTest(() async { final functionCallChat = _functionCallModel.startChat(); const prompt = 'What is the weather like in Boston, MA on 10/02 in year 2024?'; @@ -469,21 +472,11 @@ class _FunctionCallingPageState extends State { } else { _messages.add(MessageData(text: 'No text response from model.')); } - } catch (e) { - _showError(e.toString()); - } finally { - setState(() { - _loading = false; - }); - } + }); } Future _testAutoStreamFunctionCalling() async { - setState(() { - _loading = true; - _messages.clear(); - }); - try { + await _runTest(() async { final autoFunctionCallChat = _autoFunctionCallModel.startChat(); const prompt = 'What is the weather like in Boston, MA on 10/02 in year 2024?'; @@ -527,28 +520,19 @@ class _FunctionCallingPageState extends State { _messages.add(MessageData(text: 'No text response from model.')); setState(() {}); } - } catch (e) { - _showError(e.toString()); - } finally { - setState(() { - _loading = false; - }); - } + }); } Future _testFunctionCalling() async { - setState(() { - _loading = true; - _messages.clear(); - }); - try { + await _runTest(() async { final functionCallChat = _functionCallModel.startChat(); const prompt = 'What is the weather like in Boston, MA on 10/02 in year 2024?'; _messages.add(MessageData(text: prompt, fromUser: true)); + setState(() {}); // Send the message to the generative model. var response = await functionCallChat.sendMessage( @@ -575,32 +559,18 @@ class _FunctionCallingPageState extends State { // When the model responds with non-null text content, print it. if (response.text case final text?) { _messages.add(MessageData(text: text)); - setState(() { - _loading = false; - }); } - } catch (e) { - _showError(e.toString()); - setState(() { - _loading = false; - }); - } finally { - setState(() { - _loading = false; - }); - } + }); } Future _testCodeExecution() async { - setState(() { - _loading = true; - }); - try { + await _runTest(() async { final codeExecutionChat = _codeExecutionModel.startChat(); const prompt = 'What is the sum of the first 50 prime numbers? ' 'Generate and run code for the calculation, and make sure you get all 50.'; _messages.add(MessageData(text: prompt, fromUser: true)); + setState(() {}); final response = await codeExecutionChat.sendMessage(Content.text(prompt)); @@ -636,20 +606,7 @@ class _FunctionCallingPageState extends State { ), ); } - - setState(() { - _loading = false; - }); - } catch (e) { - _showError(e.toString()); - setState(() { - _loading = false; - }); - } finally { - setState(() { - _loading = false; - }); - } + }); } void _showError(String message) { From 8d8148a37882fe3b325cf6a7ad0fca38777ca7ea Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Thu, 15 Jan 2026 18:17:10 -0800 Subject: [PATCH 13/15] fix formatter --- .../lib/pages/function_calling_page.dart | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) 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 a54397f2b76e..20069808673d 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 @@ -253,7 +253,8 @@ class _FunctionCallingPageState extends State { final state = location['state'] as String; return fetchWeather(Location(city, state), date); } - throw UnimplementedError('Function not declared to the model: ${call.name}'); + throw UnimplementedError( + 'Function not declared to the model: ${call.name}'); } Future _runTest(Future Function() testBody) async { @@ -337,7 +338,8 @@ class _FunctionCallingPageState extends State { children: [ Expanded( child: ElevatedButton( - onPressed: !_loading ? _testAutoFunctionCalling : null, + onPressed: + !_loading ? _testAutoFunctionCalling : null, child: const Text('Auto Function Calling'), ), ), @@ -357,18 +359,16 @@ class _FunctionCallingPageState extends State { children: [ Expanded( child: ElevatedButton( - onPressed: !_loading - ? _testStreamFunctionCalling - : null, + onPressed: + !_loading ? _testStreamFunctionCalling : null, child: const Text('Stream FC'), ), ), const SizedBox(width: 8), Expanded( child: ElevatedButton( - onPressed: !_loading - ? _testAutoStreamFunctionCalling - : null, + onPressed: + !_loading ? _testAutoStreamFunctionCalling : null, child: const Text('Auto Stream FC'), ), ), @@ -523,8 +523,6 @@ class _FunctionCallingPageState extends State { }); } - - Future _testFunctionCalling() async { await _runTest(() async { final functionCallChat = _functionCallModel.startChat(); From b6132c96d43678348e112da30e7a661b8bc406f9 Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Thu, 15 Jan 2026 18:30:51 -0800 Subject: [PATCH 14/15] fix analyzer --- packages/firebase_ai/firebase_ai/lib/src/chat.dart | 5 ++--- .../firebase_ai/firebase_ai/lib/src/generative_model.dart | 6 ++---- packages/firebase_ai/firebase_ai/test/tool_test.dart | 3 +-- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/lib/src/chat.dart b/packages/firebase_ai/firebase_ai/lib/src/chat.dart index 054caa389249..f460b3749a60 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/chat.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/chat.dart @@ -34,9 +34,9 @@ final class ChatSession { this._history, this._safetySettings, this._generationConfig, - this._tools, + List? tools, this._maxTurns) - : _autoFunctionDeclarations = _tools + : _autoFunctionDeclarations = tools ?.expand((tool) => tool.autoFunctionDeclarations) .fold({}, (map, function) { map?[function.name] = function; @@ -54,7 +54,6 @@ final class ChatSession { final List _history; final List? _safetySettings; final GenerationConfig? _generationConfig; - final List? _tools; final Map? _autoFunctionDeclarations; final int _maxTurns; 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 eed04f8488c8..2bf58351eafe 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/generative_model.dart @@ -42,13 +42,12 @@ final class GenerativeModel extends BaseApiClientModel { FirebaseAuth? auth, List? safetySettings, GenerationConfig? generationConfig, - List? tools, + this.tools, ToolConfig? toolConfig, Content? systemInstruction, http.Client? httpClient, }) : _safetySettings = safetySettings ?? [], _generationConfig = generationConfig, - this.tools = tools, _toolConfig = toolConfig, _systemInstruction = systemInstruction, super( @@ -74,13 +73,12 @@ final class GenerativeModel extends BaseApiClientModel { FirebaseAuth? auth, List? safetySettings, GenerationConfig? generationConfig, - List? tools, + this.tools, ToolConfig? toolConfig, Content? systemInstruction, ApiClient? apiClient, }) : _safetySettings = safetySettings ?? [], _generationConfig = generationConfig, - this.tools = tools, _toolConfig = toolConfig, _systemInstruction = systemInstruction, super( diff --git a/packages/firebase_ai/firebase_ai/test/tool_test.dart b/packages/firebase_ai/firebase_ai/test/tool_test.dart index 02720d2455be..affd00691b59 100644 --- a/packages/firebase_ai/firebase_ai/test/tool_test.dart +++ b/packages/firebase_ai/firebase_ai/test/tool_test.dart @@ -23,7 +23,7 @@ void main() { Future> myFunction(Map args) async { return { 'result': 'Hello, ${args['name']}!', - 'age_plus_ten': (args['age'] as int) + 10, + 'age_plus_ten': (args['age']! as int) + 10, }; } @@ -39,7 +39,6 @@ void main() { description: 'Greets a user with their name and calculates age plus ten.', parameters: parametersSchema, - optionalParameters: const [], callable: myFunction, ); From d1618194989fbc3d02793cdec29213cae838688d Mon Sep 17 00:00:00 2001 From: Cynthia J Date: Thu, 15 Jan 2026 20:02:39 -0800 Subject: [PATCH 15/15] more fix --- .../firebase_ai/example/lib/pages/function_calling_page.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 20069808673d..902fa9812bec 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 @@ -254,7 +254,8 @@ class _FunctionCallingPageState extends State { return fetchWeather(Location(city, state), date); } throw UnimplementedError( - 'Function not declared to the model: ${call.name}'); + 'Function not declared to the model: ${call.name}', + ); } Future _runTest(Future Function() testBody) async {