diff --git a/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart b/packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart index cf79b61a7104..348ef93fe82e 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,6 +40,7 @@ class Location { class _FunctionCallingPageState extends State { late final GenerativeModel _functionCallModel; + late final GenerativeModel _codeExecutionModel; final List _messages = []; bool _loading = false; @@ -49,19 +50,31 @@ class _FunctionCallingPageState extends State { if (widget.useVertexBackend) { var vertexAI = FirebaseAI.vertexAI(auth: FirebaseAuth.instance); _functionCallModel = vertexAI.generativeModel( - model: 'gemini-2.0-flash', + model: 'gemini-2.5-flash', tools: [ Tool.functionDeclarations([fetchWeatherTool]), ], ); + _codeExecutionModel = vertexAI.generativeModel( + model: 'gemini-2.5-flash', + tools: [ + Tool.codeExecution(), + ], + ); } else { var googleAI = FirebaseAI.googleAI(auth: FirebaseAuth.instance); _functionCallModel = googleAI.generativeModel( - model: 'gemini-2.0-flash', + model: 'gemini-2.5-flash', tools: [ Tool.functionDeclarations([fetchWeatherTool]), ], ); + _codeExecutionModel = googleAI.generativeModel( + model: 'gemini-2.5-flash', + tools: [ + Tool.codeExecution(), + ], + ); } } @@ -146,6 +159,17 @@ class _FunctionCallingPageState extends State { 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'), + ), + ), ], ), ), @@ -197,4 +221,42 @@ class _FunctionCallingPageState extends State { }); } } + + Future _testCodeExecution() async { + setState(() { + _loading = true; + }); + 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)); + + final response = await codeExecutionChat.sendMessage(Content.text(prompt)); + + final buffer = StringBuffer(); + for (final part in response.candidates.first.content.parts) { + if (part is ExecutableCodePart) { + buffer.writeln('Executable Code:'); + buffer.writeln('Language: ${part.language}'); + buffer.writeln('Code:'); + buffer.writeln(part.code); + } else if (part is CodeExecutionResultPart) { + buffer.writeln('Code Execution Result:'); + buffer.writeln('Outcome: ${part.outcome}'); + buffer.writeln('Output:'); + buffer.writeln(part.output); + } else if (part is TextPart) { + buffer.writeln(part.text); + } + } + + if (buffer.isNotEmpty) { + _messages.add(MessageData(text: buffer.toString())); + } + + setState(() { + _loading = false; + }); + } } diff --git a/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart b/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart index 59ba03daaca3..921512fdab94 100644 --- a/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart +++ b/packages/firebase_ai/firebase_ai/lib/firebase_ai.dart @@ -43,7 +43,9 @@ export 'src/content.dart' FunctionCall, FunctionResponse, Part, - TextPart; + TextPart, + ExecutableCodePart, + CodeExecutionResultPart; export 'src/error.dart' show FirebaseAIException, @@ -103,4 +105,6 @@ export 'src/tool.dart' FunctionCallingMode, FunctionDeclaration, Tool, - ToolConfig; + ToolConfig, + GoogleSearch, + CodeExecution; diff --git a/packages/firebase_ai/firebase_ai/lib/src/api.dart b/packages/firebase_ai/firebase_ai/lib/src/api.dart index 7e4e8fc96d87..48602ebac90d 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/api.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/api.dart @@ -172,7 +172,7 @@ final class UsageMetadata { final List? candidatesTokensDetails; } -/// Constructe a UsageMetadata with all it's fields. +/// Construct a UsageMetadata with all it's fields. /// /// Expose access to the private constructor for use within the package.. UsageMetadata createUsageMetadata({ @@ -1150,7 +1150,7 @@ final class VertexSerialization implements SerializationStrategy { _parsePromptFeedback(promptFeedback), _ => null, }; - final usageMedata = switch (jsonObject) { + final usageMetadata = switch (jsonObject) { {'usageMetadata': final usageMetadata?} => _parseUsageMetadata(usageMetadata), {'totalTokens': final int totalTokens} => @@ -1158,7 +1158,7 @@ final class VertexSerialization implements SerializationStrategy { _ => null, }; return GenerateContentResponse(candidates, promptFeedback, - usageMetadata: usageMedata); + usageMetadata: usageMetadata); } /// Parse the json to [CountTokensResponse] @@ -1489,3 +1489,38 @@ SearchEntryPoint _parseSearchEntryPoint(Object? jsonObject) { renderedContent: renderedContent, ); } + +/// Represents the result of the code execution. +enum Outcome { + /// Unspecified status. This value should not be used. + unspecified('OUTCOME_UNSPECIFIED'), + + /// Code execution completed successfully. + ok('OUTCOME_OK'), + + /// Code execution finished but with a failure. `stderr` should contain the + /// reason. + failed('OUTCOME_FAILED'), + + /// Code execution ran for too long, and was cancelled. There may or may not + /// be a partial output present. + deadlineExceeded('OUTCOME_DEADLINE_EXCEEDED'); + + const Outcome(this._jsonString); + + final String _jsonString; + + /// Convert to json format. + String toJson() => _jsonString; + + /// Parse the json string to [Outcome]. + static Outcome parseValue(String jsonObject) { + return switch (jsonObject) { + 'OUTCOME_UNSPECIFIED' => Outcome.unspecified, + 'OUTCOME_OK' => Outcome.ok, + 'OUTCOME_FAILED' => Outcome.failed, + 'OUTCOME_DEADLINE_EXCEEDED' => Outcome.deadlineExceeded, + _ => throw FormatException('Unhandled Outcome format', jsonObject), + }; + } +} diff --git a/packages/firebase_ai/firebase_ai/lib/src/content.dart b/packages/firebase_ai/firebase_ai/lib/src/content.dart index fb627a9871c1..d6a3d042098b 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/content.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/content.dart @@ -16,6 +16,7 @@ import 'dart:convert'; import 'dart:developer'; import 'dart:typed_data'; +import 'api.dart'; import 'error.dart'; /// The base structured datatype containing multi-part content of a message. @@ -105,6 +106,32 @@ Part parsePart(Object? jsonObject) { throw unhandledFormat('functionCall', functionCall); } } + if (jsonObject.containsKey('executableCode')) { + final executableCode = jsonObject['executableCode']; + if (executableCode is Map && + executableCode.containsKey('language') && + executableCode.containsKey('code')) { + return ExecutableCodePart( + executableCode['language'] as String, + executableCode['code'] as String, + ); + } else { + throw unhandledFormat('executableCode', executableCode); + } + } + if (jsonObject.containsKey('codeExecutionResult')) { + final codeExecutionResult = jsonObject['codeExecutionResult']; + if (codeExecutionResult is Map && + codeExecutionResult.containsKey('outcome') && + codeExecutionResult.containsKey('output')) { + return CodeExecutionResultPart( + Outcome.parseValue(codeExecutionResult['outcome'] as String), + codeExecutionResult['output'] as String, + ); + } else { + throw unhandledFormat('codeExecutionResult', codeExecutionResult); + } + } return switch (jsonObject) { {'text': final String text} => TextPart(text), { @@ -258,3 +285,40 @@ final class FileData implements Part { 'file_data': {'file_uri': fileUri, 'mime_type': mimeType} }; } + +/// A `Part` that represents the code that is executed by the model. +final class ExecutableCodePart implements Part { + /// The programming language of the code. + final String language; + + /// The source code to be executed. + final String code; + + // ignore: public_member_api_docs + ExecutableCodePart(this.language, this.code); + + @override + Object toJson() => { + 'executableCode': {'language': language, 'code': code} + }; +} + +/// A `Part` that represents the code execution result from the model. +final class CodeExecutionResultPart implements Part { + /// The result of the execution. + final Outcome outcome; + + /// The stdout from the code execution, or an error message if it failed. + final String output; + + // ignore: public_member_api_docs + CodeExecutionResultPart(this.outcome, this.output); + + @override + Object toJson() => { + 'codeExecutionResult': { + 'outcome': outcome.toJson(), + 'output': output + } + }; +} diff --git a/packages/firebase_ai/firebase_ai/lib/src/tool.dart b/packages/firebase_ai/firebase_ai/lib/src/tool.dart index 394cb555e7af..4e0f3378bf72 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/tool.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/tool.dart @@ -21,12 +21,12 @@ import 'schema.dart'; /// knowledge and scope of the model. final class Tool { // ignore: public_member_api_docs - Tool._(this._functionDeclarations, this._googleSearch); + Tool._(this._functionDeclarations, this._googleSearch, this._codeExecution); /// Returns a [Tool] instance with list of [FunctionDeclaration]. static Tool functionDeclarations( List functionDeclarations) { - return Tool._(functionDeclarations, null); + return Tool._(functionDeclarations, null, null); } /// Creates a tool that allows the model to use Grounding with Google Search. @@ -47,7 +47,13 @@ final class Tool { /// /// Returns a `Tool` configured for Google Search. static Tool googleSearch({GoogleSearch googleSearch = const GoogleSearch()}) { - return Tool._(null, googleSearch); + return Tool._(null, googleSearch, null); + } + + /// Returns a [Tool] instance that enables the model to use Code Execution. + static Tool codeExecution( + {CodeExecution codeExecution = const CodeExecution()}) { + return Tool._(null, null, codeExecution); } /// A list of `FunctionDeclarations` available to the model that can be used @@ -65,13 +71,18 @@ final class Tool { /// responses. final GoogleSearch? _googleSearch; + /// A tool that allows the model to use Code Execution. + final CodeExecution? _codeExecution; + /// Convert to json object. Map toJson() => { if (_functionDeclarations case final _functionDeclarations?) 'functionDeclarations': _functionDeclarations.map((f) => f.toJson()).toList(), if (_googleSearch case final _googleSearch?) - 'googleSearch': _googleSearch.toJson() + 'googleSearch': _googleSearch.toJson(), + if (_codeExecution case final _codeExecution?) + 'codeExecution': _codeExecution.toJson() }; } @@ -93,6 +104,15 @@ final class GoogleSearch { Map toJson() => {}; } +/// A tool that allows the model to use Code Execution. +final class CodeExecution { + // ignore: public_member_api_docs + const CodeExecution(); + + /// Convert to json object. + Map toJson() => {}; +} + /// Structured representation of a function declaration as defined by the /// [OpenAPI 3.03 specification](https://spec.openapis.org/oas/v3.0.3). ///