Skip to content

Commit 24bc9c3

Browse files
authored
[Firebase AI] Add support for Code Execution (#1326)
* [Firebase AI] Add support for Code Execution * Cleanup pass
1 parent 7ccdb43 commit 24bc9c3

File tree

6 files changed

+309
-5
lines changed

6 files changed

+309
-5
lines changed

docs/readme.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ Support
109109

110110
Release Notes
111111
-------------
112+
### Upcoming
113+
- Changes
114+
- Firebase AI: Add support for enabling the model to use Code Execution.
115+
112116
### 13.2.0
113117
- Changes
114118
- General: Update to Firebase C++ SDK version 13.1.0.

firebaseai/src/FunctionCalling.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ internal Dictionary<string, object> ToJson() {
8282
/// </summary>
8383
public readonly struct GoogleSearch {}
8484

85+
/// <summary>
86+
/// A tool that allows the model to execute code.
87+
///
88+
/// This tool can be used to solve complex problems, for example, by generating and executing Python
89+
/// code to solve a math problem.
90+
/// </summary>
91+
public readonly struct CodeExecution {}
92+
8593
/// <summary>
8694
/// A helper tool that the model may use when generating responses.
8795
///
@@ -93,6 +101,7 @@ public readonly struct Tool {
93101

94102
private List<FunctionDeclaration> FunctionDeclarations { get; }
95103
private GoogleSearch? GoogleSearch { get; }
104+
private CodeExecution? CodeExecution { get; }
96105

97106
/// <summary>
98107
/// Creates a tool that allows the model to perform function calling.
@@ -102,6 +111,7 @@ public readonly struct Tool {
102111
public Tool(params FunctionDeclaration[] functionDeclarations) {
103112
FunctionDeclarations = new List<FunctionDeclaration>(functionDeclarations);
104113
GoogleSearch = null;
114+
CodeExecution = null;
105115
}
106116
/// <summary>
107117
/// Creates a tool that allows the model to perform function calling.
@@ -111,6 +121,7 @@ public Tool(params FunctionDeclaration[] functionDeclarations) {
111121
public Tool(IEnumerable<FunctionDeclaration> functionDeclarations) {
112122
FunctionDeclarations = new List<FunctionDeclaration>(functionDeclarations);
113123
GoogleSearch = null;
124+
CodeExecution = null;
114125
}
115126

116127
/// <summary>
@@ -121,6 +132,18 @@ public Tool(IEnumerable<FunctionDeclaration> functionDeclarations) {
121132
public Tool(GoogleSearch googleSearch) {
122133
FunctionDeclarations = null;
123134
GoogleSearch = googleSearch;
135+
CodeExecution = null;
136+
}
137+
138+
/// <summary>
139+
/// Creates a tool that allows the model to use Code Execution.
140+
/// </summary>
141+
/// <param name="codeExecution">An empty `CodeExecution` object. The presence of this object
142+
/// in the list of tools enables the model to use Code Execution.</param>
143+
public Tool(CodeExecution codeExecution) {
144+
FunctionDeclarations = null;
145+
GoogleSearch = null;
146+
CodeExecution = codeExecution;
124147
}
125148

126149
/// <summary>
@@ -135,6 +158,9 @@ internal Dictionary<string, object> ToJson() {
135158
if (GoogleSearch.HasValue) {
136159
json["googleSearch"] = new Dictionary<string, object>();
137160
}
161+
if (CodeExecution.HasValue) {
162+
json["codeExecution"] = new Dictionary<string, object>();
163+
}
138164
return json;
139165
}
140166
}

firebaseai/src/ModelContent.cs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,153 @@ Dictionary<string, object> Part.ToJson() {
379379
};
380380
}
381381
}
382+
383+
/// <summary>
384+
/// A part containing code that was executed by the model.
385+
/// </summary>
386+
public readonly struct ExecutableCodePart : Part {
387+
public enum CodeLanguage {
388+
Unspecified = 0,
389+
Python
390+
}
391+
392+
/// <summary>
393+
/// The language
394+
/// </summary>
395+
public CodeLanguage Language { get; }
396+
/// <summary>
397+
/// The code that was executed.
398+
/// </summary>
399+
public string Code { get; }
400+
401+
private readonly bool? _isThought;
402+
public bool IsThought { get { return _isThought ?? false; } }
403+
404+
private readonly string _thoughtSignature;
405+
406+
private static CodeLanguage ParseLanguage(string str) {
407+
return str switch {
408+
"PYTHON" => CodeLanguage.Python,
409+
_ => CodeLanguage.Unspecified,
410+
};
411+
}
412+
413+
private string LanguageAsString {
414+
get {
415+
return Language switch {
416+
CodeLanguage.Python => "PYTHON",
417+
_ => "LANGUAGE_UNSPECIFIED"
418+
};
419+
}
420+
}
421+
422+
/// <summary>
423+
/// Intended for internal use only.
424+
/// </summary>
425+
internal ExecutableCodePart(string language, string code,
426+
bool? isThought, string thoughtSignature) {
427+
Language = ParseLanguage(language);
428+
Code = code;
429+
_isThought = isThought;
430+
_thoughtSignature = thoughtSignature;
431+
}
432+
433+
Dictionary<string, object> Part.ToJson() {
434+
var jsonDict = new Dictionary<string, object>() {
435+
{ "executableCode", new Dictionary<string, object>() {
436+
{ "language", LanguageAsString },
437+
{ "code", Code }
438+
}
439+
}
440+
};
441+
jsonDict.AddIfHasValue("thought", _isThought);
442+
jsonDict.AddIfHasValue("thoughtSignature", _thoughtSignature);
443+
return jsonDict;
444+
}
445+
}
446+
447+
/// <summary>
448+
/// A part containing the result of executing code.
449+
/// </summary>
450+
public readonly struct CodeExecutionResultPart : Part {
451+
/// <summary>
452+
/// The outcome of a code execution.
453+
/// </summary>
454+
public enum ExecutionOutcome {
455+
Unspecified = 0,
456+
/// <summary>
457+
/// The code executed without errors.
458+
/// </summary>
459+
Ok,
460+
/// <summary>
461+
/// The code failed to execute.
462+
/// </summary>
463+
Failed,
464+
/// <summary>
465+
/// The code took too long to execute.
466+
/// </summary>
467+
DeadlineExceeded
468+
}
469+
470+
/// <summary>
471+
/// The outcome of the code execution.
472+
/// </summary>
473+
public ExecutionOutcome Outcome { get; }
474+
/// <summary>
475+
/// The output of the code execution.
476+
/// </summary>
477+
public string Output { get; }
478+
479+
private readonly bool? _isThought;
480+
public bool IsThought { get { return _isThought ?? false; } }
481+
482+
private readonly string _thoughtSignature;
483+
484+
private static ExecutionOutcome ParseOutcome(string str) {
485+
return str switch {
486+
"OUTCOME_UNSPECIFIED" => ExecutionOutcome.Unspecified,
487+
"OUTCOME_OK" => ExecutionOutcome.Ok,
488+
"OUTCOME_FAILED" => ExecutionOutcome.Failed,
489+
"OUTCOME_DEADLINE_EXCEEDED" => ExecutionOutcome.DeadlineExceeded,
490+
_ => ExecutionOutcome.Unspecified,
491+
};
492+
}
493+
494+
private string OutcomeAsString {
495+
get {
496+
return Outcome switch {
497+
ExecutionOutcome.Ok => "OUTCOME_OK",
498+
ExecutionOutcome.Failed => "OUTCOME_FAILED",
499+
ExecutionOutcome.DeadlineExceeded => "OUTCOME_DEADLINE_EXCEEDED",
500+
_ => "OUTCOME_UNSPECIFIED"
501+
};
502+
}
503+
}
504+
505+
/// <summary>
506+
/// Intended for internal use only.
507+
/// </summary>
508+
internal CodeExecutionResultPart(string outcome, string output,
509+
bool? isThought, string thoughtSignature) {
510+
Outcome = ParseOutcome(outcome);
511+
Output = output;
512+
_isThought = isThought;
513+
_thoughtSignature = thoughtSignature;
514+
}
515+
516+
Dictionary<string, object> Part.ToJson() {
517+
var jsonDict = new Dictionary<string, object>() {
518+
{ "codeExecutionResult", new Dictionary<string, object>() {
519+
{ "outcome", OutcomeAsString },
520+
{ "output", Output }
521+
}
522+
}
523+
};
524+
jsonDict.AddIfHasValue("thought", _isThought);
525+
jsonDict.AddIfHasValue("thoughtSignature", _thoughtSignature);
526+
return jsonDict;
527+
}
528+
}
382529

383530
#endregion
384531

@@ -413,6 +560,24 @@ private static InlineDataPart InlineDataPartFromJson(Dictionary<string, object>
413560
isThought,
414561
thoughtSignature);
415562
}
563+
564+
private static ExecutableCodePart ExecutableCodePartFromJson(Dictionary<string, object> jsonDict,
565+
bool? isThought, string thoughtSignature) {
566+
return new ExecutableCodePart(
567+
jsonDict.ParseValue<string>("language", JsonParseOptions.ThrowEverything),
568+
jsonDict.ParseValue<string>("code", JsonParseOptions.ThrowEverything),
569+
isThought,
570+
thoughtSignature);
571+
}
572+
573+
private static CodeExecutionResultPart CodeExecutionResultPartFromJson(Dictionary<string, object> jsonDict,
574+
bool? isThought, string thoughtSignature) {
575+
return new CodeExecutionResultPart(
576+
jsonDict.ParseValue<string>("outcome", JsonParseOptions.ThrowEverything),
577+
jsonDict.ParseValue<string>("output", JsonParseOptions.ThrowEverything),
578+
isThought,
579+
thoughtSignature);
580+
}
416581

417582
private static Part PartFromJson(Dictionary<string, object> jsonDict) {
418583
bool? isThought = jsonDict.ParseNullableValue<bool>("thought");
@@ -427,6 +592,14 @@ private static Part PartFromJson(Dictionary<string, object> jsonDict) {
427592
innerDict => InlineDataPartFromJson(innerDict, isThought, thoughtSignature),
428593
out var inlineDataPart)) {
429594
return inlineDataPart;
595+
} else if (jsonDict.TryParseObject("executableCode",
596+
innerDict => ExecutableCodePartFromJson(innerDict, isThought, thoughtSignature),
597+
out var executableCodePart)) {
598+
return executableCodePart;
599+
} else if (jsonDict.TryParseObject("codeExecutionResult",
600+
innerDict => CodeExecutionResultPartFromJson(innerDict, isThought, thoughtSignature),
601+
out var codeExecutionResultPart)) {
602+
return codeExecutionResultPart;
430603
} else {
431604
#if FIREBASEAI_DEBUG_LOGGING
432605
UnityEngine.Debug.LogWarning($"Received unknown part, with keys: {string.Join(',', jsonDict.Keys)}");

firebaseai/testapp/Assets/Firebase/Sample/FirebaseAI/UIHandlerAutomated.cs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ protected override void Start() {
7575
TestImagenGenerateImageOptions,
7676
TestThinkingBudget,
7777
TestIncludeThoughts,
78+
TestCodeExecution,
7879
};
7980
// Set of tests that only run the single time.
8081
Func<Task>[] singleTests = {
@@ -100,6 +101,7 @@ protected override void Start() {
100101
InternalTestGenerateImagesAllFiltered,
101102
InternalTestGenerateImagesBase64SomeFiltered,
102103
InternalTestThoughtSummary,
104+
InternalTestCodeExecution,
103105
};
104106

105107
// Create the set of tests, combining the above lists.
@@ -819,11 +821,6 @@ async Task TestThinkingBudget(Backend backend) {
819821
// Test requesting thought summaries.
820822
async Task TestIncludeThoughts(Backend backend) {
821823
// Thinking Budget requires at least the 2.5 model.
822-
var tool = new Tool(new FunctionDeclaration(
823-
"GetKeyword", "Call to retrieve a special keyword.",
824-
new Dictionary<string, Schema>() {
825-
{ "input", Schema.String("Input string") },
826-
}));
827824
var model = GetFirebaseAI(backend).GetGenerativeModel(
828825
modelName: "gemini-2.5-flash",
829826
generationConfig: new GenerationConfig(
@@ -844,6 +841,26 @@ async Task TestIncludeThoughts(Backend backend) {
844841
Assert("ThoughtSummary was missing", !string.IsNullOrWhiteSpace(response.ThoughtSummary));
845842
}
846843

844+
async Task TestCodeExecution(Backend backend) {
845+
var model = GetFirebaseAI(backend).GetGenerativeModel(
846+
modelName: ModelName,
847+
tools: new Tool[] { new Tool(new CodeExecution()) }
848+
);
849+
850+
var prompt = "What is the sum of the first 50 prime numbers? Generate and run code for the calculation.";
851+
var chat = model.StartChat();
852+
var response = await chat.SendMessageAsync(prompt);
853+
854+
string result = response.Text;
855+
Assert("Response text was missing", !string.IsNullOrWhiteSpace(result));
856+
857+
var executableCodeParts = response.Candidates.First().Content.Parts.OfType<ModelContent.ExecutableCodePart>();
858+
Assert("Missing ExecutableCodeParts", executableCodeParts.Any());
859+
860+
var codeExecutionResultParts = response.Candidates.First().Content.Parts.OfType<ModelContent.CodeExecutionResultPart>();
861+
Assert("Missing CodeExecutionResultParts", codeExecutionResultParts.Any());
862+
}
863+
847864
// Test providing a file from a GCS bucket (Firebase Storage) to the model.
848865
async Task TestReadFile() {
849866
// GCS is currently only supported with VertexAI.
@@ -1519,5 +1536,23 @@ async Task InternalTestThoughtSummary() {
15191536

15201537
ValidateUsageMetadata(response.UsageMetadata, 13, 2, 39, 54);
15211538
}
1539+
1540+
async Task InternalTestCodeExecution() {
1541+
Dictionary<string, object> json = await GetVertexJsonTestData("unary-success-code-execution.json");
1542+
GenerateContentResponse response = GenerateContentResponse.FromJson(json, FirebaseAI.Backend.InternalProvider.VertexAI);
1543+
1544+
AssertEq("Candidate count", response.Candidates.Count(), 1);
1545+
var candidate = response.Candidates.First();
1546+
1547+
var executableCodeParts = candidate.Content.Parts.OfType<ModelContent.ExecutableCodePart>().ToList();
1548+
AssertEq("ExecutableCodePart count", executableCodeParts.Count(), 1);
1549+
AssertEq("ExecutableCodePart language", executableCodeParts[0].Language, ModelContent.ExecutableCodePart.CodeLanguage.Python);
1550+
AssertEq("ExecutableCodePart code", executableCodeParts[0].Code, "prime_numbers = [2, 3, 5, 7, 11]\nsum_of_primes = sum(prime_numbers)\nprint(f'The sum of the first 5 prime numbers is: {sum_of_primes}')\n");
1551+
1552+
var codeExecutionResultParts = candidate.Content.Parts.OfType<ModelContent.CodeExecutionResultPart>().ToList();
1553+
AssertEq("CodeExecutionResultPart count", codeExecutionResultParts.Count(), 1);
1554+
AssertEq("CodeExecutionResultPart outcome", codeExecutionResultParts[0].Outcome, ModelContent.CodeExecutionResultPart.ExecutionOutcome.Ok);
1555+
AssertEq("CodeExecutionResultPart output", codeExecutionResultParts[0].Output, "The sum of the first 5 prime numbers is: 28\n");
1556+
}
15221557
}
15231558
}

0 commit comments

Comments
 (0)