Skip to content

Commit 032a707

Browse files
authored
feat(firebaseai): code execution (#17661)
* init structure * Add example * make language enum and test for thought * fix analyzers * Address review comment * fix analyzer
1 parent 3c63826 commit 032a707

File tree

6 files changed

+262
-8
lines changed

6 files changed

+262
-8
lines changed

packages/firebase_ai/firebase_ai/example/lib/pages/function_calling_page.dart

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class Location {
4040

4141
class _FunctionCallingPageState extends State<FunctionCallingPage> {
4242
late GenerativeModel _functionCallModel;
43+
late GenerativeModel _codeExecutionModel;
4344
final List<MessageData> _messages = <MessageData>[];
4445
bool _loading = false;
4546
bool _enableThinking = false;
@@ -59,19 +60,33 @@ class _FunctionCallingPageState extends State<FunctionCallingPage> {
5960
var vertexAI = FirebaseAI.vertexAI(auth: FirebaseAuth.instance);
6061
_functionCallModel = vertexAI.generativeModel(
6162
model: 'gemini-2.5-flash',
63+
generationConfig: generationConfig,
6264
tools: [
6365
Tool.functionDeclarations([fetchWeatherTool]),
6466
],
67+
);
68+
_codeExecutionModel = vertexAI.generativeModel(
69+
model: 'gemini-2.5-flash',
6570
generationConfig: generationConfig,
71+
tools: [
72+
Tool.codeExecution(),
73+
],
6674
);
6775
} else {
6876
var googleAI = FirebaseAI.googleAI(auth: FirebaseAuth.instance);
6977
_functionCallModel = googleAI.generativeModel(
7078
model: 'gemini-2.5-flash',
79+
generationConfig: generationConfig,
7180
tools: [
7281
Tool.functionDeclarations([fetchWeatherTool]),
7382
],
83+
);
84+
_codeExecutionModel = googleAI.generativeModel(
85+
model: 'gemini-2.5-flash',
7486
generationConfig: generationConfig,
87+
tools: [
88+
Tool.codeExecution(),
89+
],
7590
);
7691
}
7792
}
@@ -169,6 +184,17 @@ class _FunctionCallingPageState extends State<FunctionCallingPage> {
169184
child: const Text('Test Function Calling'),
170185
),
171186
),
187+
const SizedBox(width: 8),
188+
Expanded(
189+
child: ElevatedButton(
190+
onPressed: !_loading
191+
? () async {
192+
await _testCodeExecution();
193+
}
194+
: null,
195+
child: const Text('Test Code Execution'),
196+
),
197+
),
172198
],
173199
),
174200
),
@@ -243,6 +269,67 @@ class _FunctionCallingPageState extends State<FunctionCallingPage> {
243269
}
244270
}
245271

272+
Future<void> _testCodeExecution() async {
273+
setState(() {
274+
_loading = true;
275+
});
276+
try {
277+
final codeExecutionChat = _codeExecutionModel.startChat();
278+
const prompt = 'What is the sum of the first 50 prime numbers? '
279+
'Generate and run code for the calculation, and make sure you get all 50.';
280+
281+
_messages.add(MessageData(text: prompt, fromUser: true));
282+
283+
final response =
284+
await codeExecutionChat.sendMessage(Content.text(prompt));
285+
286+
final thought = response.thoughtSummary;
287+
if (thought != null) {
288+
_messages
289+
.add(MessageData(text: thought, fromUser: false, isThought: true));
290+
}
291+
292+
final buffer = StringBuffer();
293+
for (final part in response.candidates.first.content.parts) {
294+
if (part is ExecutableCodePart) {
295+
buffer.writeln('Executable Code:');
296+
buffer.writeln('Language: ${part.language}');
297+
buffer.writeln('Code:');
298+
buffer.writeln(part.code);
299+
} else if (part is CodeExecutionResultPart) {
300+
buffer.writeln('Code Execution Result:');
301+
buffer.writeln('Outcome: ${part.outcome}');
302+
buffer.writeln('Output:');
303+
buffer.writeln(part.output);
304+
} else if (part is TextPart) {
305+
buffer.writeln(part.text);
306+
}
307+
}
308+
309+
if (buffer.isNotEmpty) {
310+
_messages.add(
311+
MessageData(
312+
text: buffer.toString(),
313+
fromUser: false,
314+
),
315+
);
316+
}
317+
318+
setState(() {
319+
_loading = false;
320+
});
321+
} catch (e) {
322+
_showError(e.toString());
323+
setState(() {
324+
_loading = false;
325+
});
326+
} finally {
327+
setState(() {
328+
_loading = false;
329+
});
330+
}
331+
}
332+
246333
void _showError(String message) {
247334
showDialog<void>(
248335
context: context,

packages/firebase_ai/firebase_ai/lib/firebase_ai.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export 'src/content.dart'
4444
FunctionResponse,
4545
Part,
4646
TextPart,
47+
ExecutableCodePart,
48+
CodeExecutionResultPart,
4749
UnknownPart;
4850
export 'src/error.dart'
4951
show
@@ -104,4 +106,6 @@ export 'src/tool.dart'
104106
FunctionCallingMode,
105107
FunctionDeclaration,
106108
Tool,
107-
ToolConfig;
109+
ToolConfig,
110+
GoogleSearch,
111+
CodeExecution;

packages/firebase_ai/firebase_ai/lib/src/api.dart

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,6 @@ final class UsageMetadata {
211211

212212
/// Response candidate generated from a [GenerativeModel].
213213
final class Candidate {
214-
// TODO: token count?
215214
// ignore: public_member_api_docs
216215
Candidate(this.content, this.safetyRatings, this.citationMetadata,
217216
this.finishReason, this.finishMessage,
@@ -1173,15 +1172,15 @@ final class VertexSerialization implements SerializationStrategy {
11731172
_parsePromptFeedback(promptFeedback),
11741173
_ => null,
11751174
};
1176-
final usageMedata = switch (jsonObject) {
1175+
final usageMetadata = switch (jsonObject) {
11771176
{'usageMetadata': final usageMetadata?} =>
11781177
parseUsageMetadata(usageMetadata),
11791178
{'totalTokens': final int totalTokens} =>
11801179
UsageMetadata._(totalTokenCount: totalTokens),
11811180
_ => null,
11821181
};
11831182
return GenerateContentResponse(candidates, promptFeedback,
1184-
usageMetadata: usageMedata);
1183+
usageMetadata: usageMetadata);
11851184
}
11861185

11871186
/// Parse the json to [CountTokensResponse]
@@ -1523,3 +1522,64 @@ SearchEntryPoint _parseSearchEntryPoint(Object? jsonObject) {
15231522
renderedContent: renderedContent,
15241523
);
15251524
}
1525+
1526+
/// Supported programming languages for the generated code.
1527+
enum CodeLanguage {
1528+
/// Unspecified status. This value should not be used.
1529+
unspecified('LANGUAGE_UNSPECIFIED'),
1530+
1531+
/// Python language.
1532+
python('PYTHON');
1533+
1534+
const CodeLanguage(this._jsonString);
1535+
1536+
final String _jsonString;
1537+
1538+
/// Convert to json format.
1539+
String toJson() => _jsonString;
1540+
1541+
/// Parse the json string to [CodeLanguage].
1542+
static CodeLanguage parseValue(String jsonObject) {
1543+
return switch (jsonObject) {
1544+
'LANGUAGE_UNSPECIFIED' => CodeLanguage.unspecified,
1545+
'PYTHON' => CodeLanguage.python,
1546+
_ => CodeLanguage
1547+
.unspecified, // If backend has new change, return unspecified.
1548+
};
1549+
}
1550+
}
1551+
1552+
/// Represents the result of the code execution.
1553+
enum Outcome {
1554+
/// Unspecified status. This value should not be used.
1555+
unspecified('OUTCOME_UNSPECIFIED'),
1556+
1557+
/// Code execution completed successfully.
1558+
ok('OUTCOME_OK'),
1559+
1560+
/// Code execution finished but with a failure. `stderr` should contain the
1561+
/// reason.
1562+
failed('OUTCOME_FAILED'),
1563+
1564+
/// Code execution ran for too long, and was cancelled. There may or may not
1565+
/// be a partial output present.
1566+
deadlineExceeded('OUTCOME_DEADLINE_EXCEEDED');
1567+
1568+
const Outcome(this._jsonString);
1569+
1570+
final String _jsonString;
1571+
1572+
/// Convert to json format.
1573+
String toJson() => _jsonString;
1574+
1575+
/// Parse the json string to [Outcome].
1576+
static Outcome parseValue(String jsonObject) {
1577+
return switch (jsonObject) {
1578+
'OUTCOME_UNSPECIFIED' => Outcome.unspecified,
1579+
'OUTCOME_OK' => Outcome.ok,
1580+
'OUTCOME_FAILED' => Outcome.failed,
1581+
'OUTCOME_DEADLINE_EXCEEDED' => Outcome.deadlineExceeded,
1582+
_ => throw FormatException('Unhandled Outcome format', jsonObject),
1583+
};
1584+
}
1585+
}

packages/firebase_ai/firebase_ai/lib/src/content.dart

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'dart:convert';
1616
import 'dart:developer';
1717
import 'dart:typed_data';
1818

19+
import 'api.dart';
1920
import 'error.dart';
2021

2122
/// The base structured datatype containing multi-part content of a message.
@@ -114,6 +115,32 @@ Part parsePart(Object? jsonObject) {
114115
throw unhandledFormat('functionCall', functionCall);
115116
}
116117
}
118+
if (jsonObject.containsKey('executableCode')) {
119+
final executableCode = jsonObject['executableCode'];
120+
if (executableCode is Map &&
121+
executableCode.containsKey('language') &&
122+
executableCode.containsKey('code')) {
123+
return ExecutableCodePart(
124+
language: CodeLanguage.parseValue(executableCode['language'] as String),
125+
code: executableCode['code'] as String,
126+
);
127+
} else {
128+
throw unhandledFormat('executableCode', executableCode);
129+
}
130+
}
131+
if (jsonObject.containsKey('codeExecutionResult')) {
132+
final codeExecutionResult = jsonObject['codeExecutionResult'];
133+
if (codeExecutionResult is Map &&
134+
codeExecutionResult.containsKey('outcome') &&
135+
codeExecutionResult.containsKey('output')) {
136+
return CodeExecutionResultPart(
137+
outcome: Outcome.parseValue(codeExecutionResult['outcome'] as String),
138+
output: codeExecutionResult['output'] as String,
139+
);
140+
} else {
141+
throw unhandledFormat('codeExecutionResult', codeExecutionResult);
142+
}
143+
}
117144
return switch (jsonObject) {
118145
{'text': final String text} => TextPart._(text,
119146
isThought: isThought, thoughtSignature: thoughtSignature),
@@ -353,3 +380,51 @@ final class FileData extends Part {
353380
'file_data': {'file_uri': fileUri, 'mime_type': mimeType}
354381
};
355382
}
383+
384+
/// A `Part` that represents the code that is executed by the model.
385+
final class ExecutableCodePart extends Part {
386+
// ignore: public_member_api_docs
387+
ExecutableCodePart({
388+
required this.language,
389+
required this.code,
390+
bool? isThought,
391+
}) : super(
392+
isThought: isThought,
393+
thoughtSignature: null,
394+
);
395+
396+
/// The programming language of the code.
397+
final CodeLanguage language;
398+
399+
/// The source code to be executed.
400+
final String code;
401+
402+
@override
403+
Object toJson() => {
404+
'executableCode': {'language': language.toJson(), 'code': code}
405+
};
406+
}
407+
408+
/// A `Part` that represents the code execution result from the model.
409+
final class CodeExecutionResultPart extends Part {
410+
// ignore: public_member_api_docs
411+
CodeExecutionResultPart({
412+
required this.outcome,
413+
required this.output,
414+
bool? isThought,
415+
}) : super(
416+
isThought: isThought,
417+
thoughtSignature: null,
418+
);
419+
420+
/// The result of the execution.
421+
final Outcome outcome;
422+
423+
/// The stdout from the code execution, or an error message if it failed.
424+
final String output;
425+
426+
@override
427+
Object toJson() => {
428+
'codeExecutionResult': {'outcome': outcome.toJson(), 'output': output}
429+
};
430+
}

packages/firebase_ai/firebase_ai/lib/src/tool.dart

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ import 'schema.dart';
2121
/// knowledge and scope of the model.
2222
final class Tool {
2323
// ignore: public_member_api_docs
24-
Tool._(this._functionDeclarations, this._googleSearch);
24+
Tool._(this._functionDeclarations, this._googleSearch, this._codeExecution);
2525

2626
/// Returns a [Tool] instance with list of [FunctionDeclaration].
2727
static Tool functionDeclarations(
2828
List<FunctionDeclaration> functionDeclarations) {
29-
return Tool._(functionDeclarations, null);
29+
return Tool._(functionDeclarations, null, null);
3030
}
3131

3232
/// Creates a tool that allows the model to use Grounding with Google Search.
@@ -47,7 +47,13 @@ final class Tool {
4747
///
4848
/// Returns a `Tool` configured for Google Search.
4949
static Tool googleSearch({GoogleSearch googleSearch = const GoogleSearch()}) {
50-
return Tool._(null, googleSearch);
50+
return Tool._(null, googleSearch, null);
51+
}
52+
53+
/// Returns a [Tool] instance that enables the model to use Code Execution.
54+
static Tool codeExecution(
55+
{CodeExecution codeExecution = const CodeExecution()}) {
56+
return Tool._(null, null, codeExecution);
5157
}
5258

5359
/// A list of `FunctionDeclarations` available to the model that can be used
@@ -65,13 +71,18 @@ final class Tool {
6571
/// responses.
6672
final GoogleSearch? _googleSearch;
6773

74+
/// A tool that allows the model to use Code Execution.
75+
final CodeExecution? _codeExecution;
76+
6877
/// Convert to json object.
6978
Map<String, Object> toJson() => {
7079
if (_functionDeclarations case final _functionDeclarations?)
7180
'functionDeclarations':
7281
_functionDeclarations.map((f) => f.toJson()).toList(),
7382
if (_googleSearch case final _googleSearch?)
74-
'googleSearch': _googleSearch.toJson()
83+
'googleSearch': _googleSearch.toJson(),
84+
if (_codeExecution case final _codeExecution?)
85+
'codeExecution': _codeExecution.toJson()
7586
};
7687
}
7788

@@ -93,6 +104,15 @@ final class GoogleSearch {
93104
Map<String, Object> toJson() => {};
94105
}
95106

107+
/// A tool that allows the model to use Code Execution.
108+
final class CodeExecution {
109+
// ignore: public_member_api_docs
110+
const CodeExecution();
111+
112+
/// Convert to json object.
113+
Map<String, Object> toJson() => {};
114+
}
115+
96116
/// Structured representation of a function declaration as defined by the
97117
/// [OpenAPI 3.03 specification](https://spec.openapis.org/oas/v3.0.3).
98118
///

0 commit comments

Comments
 (0)