Skip to content

Commit 7448bea

Browse files
authored
[Firebase AI] Add support for automatic functions (#1426)
* [Firebase AI] Add support for automatic functions * Addressing gemini review feedback * Update RequestOptions.cs
1 parent 9470a01 commit 7448bea

10 files changed

Lines changed: 626 additions & 92 deletions

File tree

docs/readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ Release Notes
116116
[migrate your apps to use Gemini Image models (the "Nano Banana" models)](https://firebase.google.com/docs/ai-logic/imagen-models-migration).
117117
- Firebase AI: Add support for the JsonSchema formatting.
118118
- Firebase AI: Add support for simplified object generation with GenerateObjectAsync.
119+
- Firebase AI: Add support for automated function calling in Chat with AutoFunctionDeclaration.
119120
- Firebase AI: Add support for TemplateChatSession.
120121
- Functions: Rewrote internal serialization logic to C#. Removes dependency on internal C++ implementation.
121122

firebaseai/src/Chat.cs

Lines changed: 31 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,18 @@ public class Chat
3232
private readonly GenerativeModel generativeModel;
3333
private readonly List<ModelContent> chatHistory;
3434

35+
private readonly Dictionary<string, BaseAutoFunctionDeclaration> autoFunctionDeclarations;
36+
private readonly int autoFunctionTurnLimit;
37+
3538
/// <summary>
3639
/// The previous content from the chat that has been successfully sent and received from the
3740
/// model. This will be provided to the model for each message sent as context for the discussion.
3841
/// </summary>
3942
public IReadOnlyList<ModelContent> History => chatHistory;
4043

4144
// Note: No public constructor, get one through GenerativeModel.StartChat
42-
private Chat(GenerativeModel model, IEnumerable<ModelContent> initialHistory)
45+
private Chat(GenerativeModel model, IEnumerable<ModelContent> initialHistory,
46+
IEnumerable<BaseAutoFunctionDeclaration> autoFunctions, int autoFunctionTurnLimit)
4347
{
4448
generativeModel = model;
4549

@@ -51,15 +55,26 @@ private Chat(GenerativeModel model, IEnumerable<ModelContent> initialHistory)
5155
{
5256
chatHistory = new List<ModelContent>();
5357
}
58+
59+
if (autoFunctions != null && autoFunctions.Any())
60+
{
61+
autoFunctionDeclarations = autoFunctions.ToDictionary(afd => afd.Name);
62+
}
63+
else
64+
{
65+
autoFunctionDeclarations = null;
66+
}
67+
this.autoFunctionTurnLimit = autoFunctionTurnLimit;
5468
}
5569

5670
/// <summary>
5771
/// Intended for internal use only.
5872
/// Use `GenerativeModel.StartChat` instead to ensure proper initialization and configuration of the `Chat`.
5973
/// </summary>
60-
internal static Chat InternalCreateChat(GenerativeModel model, IEnumerable<ModelContent> initialHistory)
74+
internal static Chat InternalCreateChat(GenerativeModel model, IEnumerable<ModelContent> initialHistory,
75+
IEnumerable<BaseAutoFunctionDeclaration> autoFunctionDeclarations, int autoFunctionTurnLimit)
6176
{
62-
return new Chat(model, initialHistory);
77+
return new Chat(model, initialHistory, autoFunctionDeclarations, autoFunctionTurnLimit);
6378
}
6479

6580
/// <summary>
@@ -142,70 +157,31 @@ public IAsyncEnumerable<GenerateContentResponse> SendMessageStreamAsync(
142157
return SendMessageStreamAsyncInternal(content, cancellationToken);
143158
}
144159

145-
private async Task<GenerateContentResponse> SendMessageAsyncInternal(
160+
private Task<GenerateContentResponse> SendMessageAsyncInternal(
146161
IEnumerable<ModelContent> requestContent, CancellationToken cancellationToken = default)
147162
{
148-
// Make sure that the requests are set to to role "user".
149-
List<ModelContent> fixedRequests = requestContent.Select(FirebaseAIExtensions.ConvertToUser).ToList();
150-
// Set up the context to send in the request
151-
List<ModelContent> fullRequest = new(chatHistory);
152-
fullRequest.AddRange(fixedRequests);
153-
154-
// Note: GenerateContentAsync can throw exceptions if there was a problem, but
155-
// we allow it to just be passed back to the user.
156-
GenerateContentResponse response = await generativeModel.GenerateContentAsync(fullRequest, cancellationToken);
157-
158-
// Only after getting a valid response, add both to the history for later.
159-
// But either way pass the response along to the user.
160-
if (response.Candidates.Any())
163+
Task<GenerateContentResponse> generateContentFunc(List<ModelContent> fullRequest)
161164
{
162-
ModelContent responseContent = response.Candidates.First().Content;
163-
164-
chatHistory.AddRange(fixedRequests);
165-
chatHistory.Add(responseContent.ConvertToModel());
165+
return generativeModel.GenerateContentAsync(fullRequest, cancellationToken);
166166
}
167167

168-
return response;
168+
return ChatSessionHelpers.SendMessageAsync(chatHistory,
169+
autoFunctionDeclarations, autoFunctionTurnLimit,
170+
requestContent, generateContentFunc);
169171
}
170172

171-
private async IAsyncEnumerable<GenerateContentResponse> SendMessageStreamAsyncInternal(
173+
private IAsyncEnumerable<GenerateContentResponse> SendMessageStreamAsyncInternal(
172174
IEnumerable<ModelContent> requestContent,
173-
[EnumeratorCancellation] CancellationToken cancellationToken = default)
175+
CancellationToken cancellationToken = default)
174176
{
175-
// Make sure that the requests are set to to role "user".
176-
List<ModelContent> fixedRequests = requestContent.Select(FirebaseAIExtensions.ConvertToUser).ToList();
177-
// Set up the context to send in the request
178-
List<ModelContent> fullRequest = new(chatHistory);
179-
fullRequest.AddRange(fixedRequests);
180-
181-
List<ModelContent> responseContents = new();
182-
bool saveHistory = true;
183-
// Note: GenerateContentStreamAsync can throw exceptions if there was a problem, but
184-
// we allow it to just be passed back to the user.
185-
await foreach (GenerateContentResponse response in
186-
generativeModel.GenerateContentStreamAsync(fullRequest, cancellationToken))
177+
IAsyncEnumerable<GenerateContentResponse> generateContentStreamFunc(List<ModelContent> fullRequest)
187178
{
188-
// If the response had a problem, we still want to pass it along to the user for context,
189-
// but we don't want to save the history anymore.
190-
if (response.Candidates.Any())
191-
{
192-
ModelContent responseContent = response.Candidates.First().Content;
193-
responseContents.Add(responseContent.ConvertToModel());
194-
}
195-
else
196-
{
197-
saveHistory = false;
198-
}
199-
200-
yield return response;
179+
return generativeModel.GenerateContentStreamAsync(fullRequest, cancellationToken);
201180
}
202181

203-
// After getting all the responses, and they were all valid, add everything to the history
204-
if (saveHistory)
205-
{
206-
chatHistory.AddRange(fixedRequests);
207-
chatHistory.AddRange(responseContents);
208-
}
182+
return ChatSessionHelpers.SendMessageStreamAsync(chatHistory,
183+
autoFunctionDeclarations, autoFunctionTurnLimit,
184+
requestContent, generateContentStreamFunc);
209185
}
210186
}
211187

firebaseai/src/FunctionCalling.cs

Lines changed: 138 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
using System;
1818
using System.Collections.Generic;
1919
using System.Linq;
20+
using System.Reflection;
21+
using Firebase.AI.Internal;
2022

2123
namespace Firebase.AI
2224
{
@@ -86,9 +88,9 @@ public FunctionDeclaration(string name, string description,
8688
internal Dictionary<string, object> ToJson()
8789
{
8890
var json = new Dictionary<string, object>() {
89-
{ "name", Name },
90-
{ "description", Description },
91-
};
91+
{ "name", Name },
92+
{ "description", Description },
93+
};
9294
// Only one of these will likely be set, but just check
9395
if (JsonParameters != null)
9496
{
@@ -103,6 +105,27 @@ internal Dictionary<string, object> ToJson()
103105
}
104106
}
105107

108+
/// <summary>
109+
/// Structured representation of a function declaration, designed
110+
/// to be automatically handled when using Chat, instead of requiring
111+
/// manual handling.
112+
///
113+
/// See `FunctionDeclaration` for more information.
114+
/// </summary>
115+
public class AutoFunctionDeclaration : BaseAutoFunctionDeclaration
116+
{
117+
/// <summary>
118+
/// Constructs a new `AutoFunctionDeclaration`
119+
/// </summary>
120+
/// <param name="callable">The delegate that will be called automatically when requested by the model.</param>
121+
/// <param name="description">A brief description of the function.</param>
122+
/// <param name="name">Optional name to use for the function, used to overwrite the delegate name.</param>
123+
public AutoFunctionDeclaration(Delegate callable, string description,
124+
string name = null)
125+
: base(callable, description, name)
126+
{ }
127+
}
128+
106129
/// <summary>
107130
/// A tool that allows the generative model to connect to Google Search to access and incorporate
108131
/// up-to-date information from the web into its responses.
@@ -134,6 +157,7 @@ public readonly struct Tool
134157
// No public properties, on purpose since it is meant for user input only
135158

136159
private List<FunctionDeclaration> FunctionDeclarations { get; }
160+
internal List<AutoFunctionDeclaration> AutoFunctionDeclarations { get; }
137161
private GoogleSearch? GoogleSearch { get; }
138162
private CodeExecution? CodeExecution { get; }
139163
private UrlContext? UrlContext { get; }
@@ -146,6 +170,7 @@ public readonly struct Tool
146170
public Tool(params FunctionDeclaration[] functionDeclarations)
147171
{
148172
FunctionDeclarations = new List<FunctionDeclaration>(functionDeclarations);
173+
AutoFunctionDeclarations = null;
149174
GoogleSearch = null;
150175
CodeExecution = null;
151176
UrlContext = null;
@@ -158,6 +183,34 @@ public Tool(params FunctionDeclaration[] functionDeclarations)
158183
public Tool(IEnumerable<FunctionDeclaration> functionDeclarations)
159184
{
160185
FunctionDeclarations = new List<FunctionDeclaration>(functionDeclarations);
186+
AutoFunctionDeclarations = null;
187+
GoogleSearch = null;
188+
CodeExecution = null;
189+
UrlContext = null;
190+
}
191+
192+
/// <summary>
193+
/// Creates a tool that allows the model to perform function calling.
194+
/// </summary>
195+
/// <param name="functionDeclarations">A list of `FunctionDeclarations` available to the model
196+
/// that can be used for function calling.</param>
197+
public Tool(params AutoFunctionDeclaration[] functionDeclarations)
198+
{
199+
FunctionDeclarations = null;
200+
AutoFunctionDeclarations = new List<AutoFunctionDeclaration>(functionDeclarations);
201+
GoogleSearch = null;
202+
CodeExecution = null;
203+
UrlContext = null;
204+
}
205+
/// <summary>
206+
/// Creates a tool that allows the model to perform function calling.
207+
/// </summary>
208+
/// <param name="functionDeclarations">A list of `FunctionDeclarations` available to the model
209+
/// that can be used for function calling.</param>
210+
public Tool(IEnumerable<AutoFunctionDeclaration> functionDeclarations)
211+
{
212+
FunctionDeclarations = null;
213+
AutoFunctionDeclarations = new List<AutoFunctionDeclaration>(functionDeclarations);
161214
GoogleSearch = null;
162215
CodeExecution = null;
163216
UrlContext = null;
@@ -171,6 +224,7 @@ public Tool(IEnumerable<FunctionDeclaration> functionDeclarations)
171224
public Tool(GoogleSearch googleSearch)
172225
{
173226
FunctionDeclarations = null;
227+
AutoFunctionDeclarations = null;
174228
GoogleSearch = googleSearch;
175229
CodeExecution = null;
176230
UrlContext = null;
@@ -184,6 +238,7 @@ public Tool(GoogleSearch googleSearch)
184238
public Tool(CodeExecution codeExecution)
185239
{
186240
FunctionDeclarations = null;
241+
AutoFunctionDeclarations = null;
187242
GoogleSearch = null;
188243
CodeExecution = codeExecution;
189244
UrlContext = null;
@@ -198,6 +253,7 @@ public Tool(CodeExecution codeExecution)
198253
public Tool(UrlContext urlContext)
199254
{
200255
FunctionDeclarations = null;
256+
AutoFunctionDeclarations = null;
201257
GoogleSearch = null;
202258
CodeExecution = null;
203259
UrlContext = urlContext;
@@ -210,9 +266,18 @@ public Tool(UrlContext urlContext)
210266
internal Dictionary<string, object> ToJson()
211267
{
212268
var json = new Dictionary<string, object>();
269+
List<Dictionary<string, object>> functionDeclarations = new();
213270
if (FunctionDeclarations != null && FunctionDeclarations.Any())
214271
{
215-
json["functionDeclarations"] = FunctionDeclarations.Select(f => f.ToJson()).ToList();
272+
functionDeclarations.AddRange(FunctionDeclarations.Select(f => f.ToJson()));
273+
}
274+
if (AutoFunctionDeclarations != null && AutoFunctionDeclarations.Any())
275+
{
276+
functionDeclarations.AddRange(AutoFunctionDeclarations.Select(f => f.ToJson()));
277+
}
278+
if (functionDeclarations.Count > 0)
279+
{
280+
json["functionDeclarations"] = functionDeclarations;
216281
}
217282
if (GoogleSearch.HasValue)
218283
{
@@ -342,4 +407,73 @@ internal Dictionary<string, object> ToJson()
342407
}
343408
}
344409

410+
/// <summary>
411+
/// Attribute that can be attached to parameters to give them a description,
412+
/// used when using `AutoFunctionDeclaration`.
413+
/// </summary>
414+
[AttributeUsage(AttributeTargets.Parameter)]
415+
public class AutoFunctionDescriptionAttribute : Attribute
416+
{
417+
public AutoFunctionDescriptionAttribute(string description)
418+
{
419+
Description = description;
420+
}
421+
422+
public string Description { get; set; } = null;
423+
}
424+
425+
#if !DOXYGEN
426+
public abstract class BaseAutoFunctionDeclaration
427+
{
428+
internal string Name { get; }
429+
internal string Description { get; }
430+
internal JsonSchema Parameters { get; }
431+
432+
internal Delegate Callable { get; }
433+
434+
public BaseAutoFunctionDeclaration(Delegate callable, string description,
435+
string name = null)
436+
{
437+
if (callable == null)
438+
{
439+
throw new ArgumentNullException(nameof(callable));
440+
}
441+
Name = name ?? callable.Method.Name;
442+
Description = description;
443+
444+
Dictionary<string, JsonSchema> parameters = new();
445+
List<string> optionalParameters = new();
446+
// Construct the Parameters based on the given Delegate
447+
foreach (var pInfo in callable.Method.GetParameters())
448+
{
449+
var attr = pInfo.GetCustomAttribute<AutoFunctionDescriptionAttribute>();
450+
parameters.Add(pInfo.Name,
451+
JsonSchema.FromType(pInfo.ParameterType, attr?.Description));
452+
if (pInfo.HasDefaultValue)
453+
{
454+
optionalParameters.Add(pInfo.Name);
455+
}
456+
}
457+
Parameters = JsonSchema.Object(parameters, optionalParameters);
458+
459+
Callable = callable;
460+
}
461+
462+
/// <summary>
463+
/// Intended for internal use only.
464+
/// This method is used for serializing the object to JSON for the API request.
465+
/// </summary>
466+
internal Dictionary<string, object> ToJson()
467+
{
468+
var json = new Dictionary<string, object>() {
469+
{ "name", Name },
470+
{ "parametersJsonSchema", Parameters.ToJson() }
471+
};
472+
json.AddIfHasValue("description", Description);
473+
474+
return json;
475+
}
476+
}
477+
#endif // !DOXYGEN
478+
345479
}

firebaseai/src/GenerativeModel.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,12 @@ public Chat StartChat(params ModelContent[] history)
255255
/// <param name="history">Initial content history to start with.</param>
256256
public Chat StartChat(IEnumerable<ModelContent> history)
257257
{
258-
return Chat.InternalCreateChat(this, history);
258+
// If we have auto functions, we pass them separately.
259+
var autoFunctions = _tools?.Select(tool => tool.AutoFunctionDeclarations)
260+
.Where(afd => afd != null)
261+
.SelectMany(inner => inner);
262+
return Chat.InternalCreateChat(this, history, autoFunctions,
263+
_requestOptions?.AutoFunctionTurnLimit ?? RequestOptions.DefaultAutoFunctionTurnLimit);
259264
}
260265
#endregion
261266

0 commit comments

Comments
 (0)