Skip to content

Commit ffc44cb

Browse files
authored
[FirebaseAI] Add support for Server Prompt Templates (#1361)
* [Firebase AI] Initial work on Server Templates * Add doxygen comments * Update TemplateGenerativeModel.cs * Add tests, and fix streaming * Update readme.md * Addressing feedback * Update readme.md * Update readme.md
1 parent c805d12 commit ffc44cb

File tree

7 files changed

+389
-6
lines changed

7 files changed

+389
-6
lines changed

docs/readme.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,14 @@ Release Notes
114114
- Analytics: Added `SetDefaultEventParameters()` which allows developers to
115115
specify a list of parameters that will be set on every event logged.
116116
- Analytics: Added a new `LogEvent()` that take in a IEnumerable of
117-
parameters.
117+
parameters.
118+
- Firebase AI: Added support for using
119+
[Server Prompt Templates](https://firebase.google.com/docs/ai-logic/server-prompt-templates/get-started).
118120

119121
### 13.5.0
120122
- Changes
121123
- Firebase AI: Add support for receiving Live API Transcripts.
122-
- Storage: Add support for Firebase Storage emulator via `UseEmulator`.
124+
- Storage: Add support for Firebase Storage emulator via `UseEmulator`.
123125
The `UseEmulator` method should be called before invoking any other
124126
methods on a new instance of Storage. Default port is 9199.
125127

firebaseai/src/FirebaseAI.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,28 @@ public ImagenModel GetImagenModel(
225225
return new ImagenModel(_firebaseApp, _backend, modelName,
226226
generationConfig, safetySettings, requestOptions);
227227
}
228+
229+
/// <summary>
230+
/// Initializes a `TemplateGenerativeModel` with the given parameters.
231+
/// </summary>
232+
/// <param name="requestOptions">Configuration parameters for sending requests to the backend.</param>
233+
/// <returns>The initialized `TemplateGenerativeModel` instance.</returns>
234+
public TemplateGenerativeModel GetTemplateGenerativeModel(
235+
RequestOptions? requestOptions = null)
236+
{
237+
return new TemplateGenerativeModel(_firebaseApp, _backend, requestOptions);
238+
}
239+
240+
/// <summary>
241+
/// Initializes a `TemplateImagenModel` with the given parameters.
242+
/// </summary>
243+
/// <param name="requestOptions">Configuration parameters for sending requests to the backend.</param>
244+
/// <returns>The initialized `TemplateImagenModel` instance.</returns>
245+
public TemplateImagenModel GetTemplateImagenModel(
246+
RequestOptions? requestOptions = null)
247+
{
248+
return new TemplateImagenModel(_firebaseApp, _backend, requestOptions);
249+
}
228250
}
229251

230252
}

firebaseai/src/Imagen/ImagenModel.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ public class ImagenModel
4545

4646
private readonly HttpClient _httpClient;
4747

48+
/// <summary>
49+
/// Intended for internal use only.
50+
/// Use `FirebaseAI.GetImagenModel` instead to ensure proper initialization and configuration of the `ImagenModel`.
51+
/// </summary>
4852
internal ImagenModel(FirebaseApp firebaseApp,
4953
FirebaseAI.Backend backend,
5054
string modelName,
@@ -157,4 +161,72 @@ private Dictionary<string, object> MakeGenerateImagenRequestAsDictionary(
157161
}
158162
}
159163

164+
/// <summary>
165+
/// Represents a remote Imagen model with the ability to generate images using server template prompts.
166+
/// </summary>
167+
public class TemplateImagenModel
168+
{
169+
private readonly FirebaseApp _firebaseApp;
170+
private readonly FirebaseAI.Backend _backend;
171+
172+
private readonly HttpClient _httpClient;
173+
174+
/// <summary>
175+
/// Intended for internal use only.
176+
/// Use `FirebaseAI.GetTemplateImagenModel` instead to ensure proper initialization and configuration of the `TemplateImagenModel`.
177+
/// </summary>
178+
internal TemplateImagenModel(FirebaseApp firebaseApp,
179+
FirebaseAI.Backend backend, RequestOptions? requestOptions = null)
180+
{
181+
_firebaseApp = firebaseApp;
182+
_backend = backend;
183+
184+
// Create a HttpClient using the timeout requested, or the default one.
185+
_httpClient = new HttpClient()
186+
{
187+
Timeout = requestOptions?.Timeout ?? RequestOptions.DefaultTimeout
188+
};
189+
}
190+
191+
/// <summary>
192+
/// Generates images using the Template Imagen model and returns them as inline data.
193+
/// </summary>
194+
/// <param name="templateId">The id of the server prompt template to use.</param>
195+
/// <param name="inputs">Any input parameters expected by the server prompt template.</param>
196+
/// <param name="cancellationToken">An optional token to cancel the operation.</param>
197+
/// <returns>The generated content response from the model.</returns>
198+
/// <exception cref="HttpRequestException">Thrown when an error occurs during content generation.</exception>
199+
public async Task<ImagenGenerationResponse<ImagenInlineImage>> GenerateImagesAsync(
200+
string templateId, IDictionary<string, object> inputs, CancellationToken cancellationToken = default)
201+
{
202+
HttpRequestMessage request = new(HttpMethod.Post,
203+
HttpHelpers.GetTemplateURL(_firebaseApp, _backend, templateId) + ":templatePredict");
204+
205+
// Set the request headers
206+
await HttpHelpers.SetRequestHeaders(request, _firebaseApp);
207+
208+
// Set the content
209+
Dictionary<string, object> jsonDict = new()
210+
{
211+
["inputs"] = inputs
212+
};
213+
string bodyJson = Json.Serialize(jsonDict);
214+
request.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json");
215+
216+
#if FIREBASE_LOG_REST_CALLS
217+
UnityEngine.Debug.Log("Request:\n" + bodyJson);
218+
#endif
219+
220+
var response = await _httpClient.SendAsync(request, cancellationToken);
221+
await HttpHelpers.ValidateHttpResponse(response);
222+
223+
string result = await response.Content.ReadAsStringAsync();
224+
225+
#if FIREBASE_LOG_REST_CALLS
226+
UnityEngine.Debug.Log("Response:\n" + result);
227+
#endif
228+
229+
return ImagenGenerationResponse<ImagenInlineImage>.FromJson(result);
230+
}
231+
}
160232
}

firebaseai/src/Internal/HttpHelpers.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ namespace Firebase.AI.Internal
2323
// Helper functions to help handling the Http calls.
2424
internal static class HttpHelpers
2525
{
26+
internal static readonly string StreamPrefix = "data: ";
27+
2628
// Get the URL to use for the rest calls based on the backend.
2729
internal static string GetURL(FirebaseApp firebaseApp,
2830
FirebaseAI.Backend backend, string modelName)
@@ -46,6 +48,25 @@ internal static string GetURL(FirebaseApp firebaseApp,
4648
}
4749
}
4850

51+
internal static string GetTemplateURL(FirebaseApp firebaseApp,
52+
FirebaseAI.Backend backend, string templateId)
53+
{
54+
var projectUrl = "https://firebasevertexai.googleapis.com/v1beta" +
55+
$"/projects/{firebaseApp.Options.ProjectId}";
56+
if (backend.Provider == FirebaseAI.Backend.InternalProvider.VertexAI)
57+
{
58+
return $"{projectUrl}/locations/{backend.Location}/templates/{templateId}";
59+
}
60+
else if (backend.Provider == FirebaseAI.Backend.InternalProvider.GoogleAI)
61+
{
62+
return $"{projectUrl}/templates/{templateId}";
63+
}
64+
else
65+
{
66+
throw new NotSupportedException($"Missing support for backend: {backend.Provider}");
67+
}
68+
}
69+
4970
internal static async Task SetRequestHeaders(HttpRequestMessage request, FirebaseApp firebaseApp)
5071
{
5172
request.Headers.Add("x-goog-api-key", firebaseApp.Options.ApiKey);
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System;
18+
using System.Collections.Generic;
19+
using System.Net.Http;
20+
using System.Text;
21+
using System.Threading;
22+
using System.Threading.Tasks;
23+
using Google.MiniJSON;
24+
using Firebase.AI.Internal;
25+
using System.Linq;
26+
using System.Runtime.CompilerServices;
27+
using System.IO;
28+
29+
namespace Firebase.AI
30+
{
31+
/// <summary>
32+
/// A type that represents a remote multimodal model (like Gemini), with the ability to generate
33+
/// content based on defined server prompt templates.
34+
/// </summary>
35+
public class TemplateGenerativeModel
36+
{
37+
private readonly FirebaseApp _firebaseApp;
38+
private readonly FirebaseAI.Backend _backend;
39+
40+
private readonly HttpClient _httpClient;
41+
42+
/// <summary>
43+
/// Intended for internal use only.
44+
/// Use `FirebaseAI.GetTemplateGenerativeModel` instead to ensure proper
45+
/// initialization and configuration of the `TemplateGenerativeModel`.
46+
/// </summary>
47+
internal TemplateGenerativeModel(FirebaseApp firebaseApp,
48+
FirebaseAI.Backend backend,
49+
RequestOptions? requestOptions = null)
50+
{
51+
_firebaseApp = firebaseApp;
52+
_backend = backend;
53+
54+
// Create a HttpClient using the timeout requested, or the default one.
55+
_httpClient = new HttpClient()
56+
{
57+
Timeout = requestOptions?.Timeout ?? RequestOptions.DefaultTimeout
58+
};
59+
}
60+
61+
/// <summary>
62+
/// Generates new content by calling into a server prompt template.
63+
/// </summary>
64+
/// <param name="templateId">The id of the server prompt template to use.</param>
65+
/// <param name="inputs">Any input parameters expected by the server prompt template.</param>
66+
/// <param name="cancellationToken">An optional token to cancel the operation.</param>
67+
/// <returns>The generated content response from the model.</returns>
68+
/// <exception cref="HttpRequestException">Thrown when an error occurs during content generation.</exception>
69+
public Task<GenerateContentResponse> GenerateContentAsync(
70+
string templateId, IDictionary<string, object> inputs,
71+
CancellationToken cancellationToken = default)
72+
{
73+
return GenerateContentAsyncInternal(templateId, inputs, null, cancellationToken);
74+
}
75+
76+
/// <summary>
77+
/// Generates new content as a stream by calling into a server prompt template.
78+
/// </summary>
79+
/// <param name="templateId">The id of the server prompt template to use.</param>
80+
/// <param name="inputs">Any input parameters expected by the server prompt template.</param>
81+
/// <param name="cancellationToken">An optional token to cancel the operation.</param>
82+
/// <returns>A stream of generated content responses from the model.</returns>
83+
/// <exception cref="HttpRequestException">Thrown when an error occurs during content generation.</exception>
84+
public IAsyncEnumerable<GenerateContentResponse> GenerateContentStreamAsync(
85+
string templateId, IDictionary<string, object> inputs,
86+
CancellationToken cancellationToken = default)
87+
{
88+
return GenerateContentStreamAsyncInternal(templateId, inputs, null, cancellationToken);
89+
}
90+
91+
private string MakeGenerateContentRequest(IDictionary<string, object> inputs,
92+
IEnumerable<ModelContent> chatHistory)
93+
{
94+
var jsonDict = new Dictionary<string, object>()
95+
{
96+
["inputs"] = inputs
97+
};
98+
if (chatHistory != null)
99+
{
100+
jsonDict["history"] = chatHistory.Select(t => t.ToJson()).ToList();
101+
}
102+
return Json.Serialize(jsonDict);
103+
}
104+
105+
private async Task<GenerateContentResponse> GenerateContentAsyncInternal(
106+
string templateId, IDictionary<string, object> inputs,
107+
IEnumerable<ModelContent> chatHistory,
108+
CancellationToken cancellationToken)
109+
{
110+
HttpRequestMessage request = new(HttpMethod.Post,
111+
HttpHelpers.GetTemplateURL(_firebaseApp, _backend, templateId) + ":templateGenerateContent");
112+
113+
// Set the request headers
114+
await HttpHelpers.SetRequestHeaders(request, _firebaseApp);
115+
116+
// Set the content
117+
string bodyJson = MakeGenerateContentRequest(inputs, chatHistory);
118+
request.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json");
119+
120+
#if FIREBASE_LOG_REST_CALLS
121+
UnityEngine.Debug.Log("Request:\n" + bodyJson);
122+
#endif
123+
124+
var response = await _httpClient.SendAsync(request, cancellationToken);
125+
await HttpHelpers.ValidateHttpResponse(response);
126+
127+
string result = await response.Content.ReadAsStringAsync();
128+
129+
#if FIREBASE_LOG_REST_CALLS
130+
UnityEngine.Debug.Log("Response:\n" + result);
131+
#endif
132+
133+
return GenerateContentResponse.FromJson(result, _backend.Provider);
134+
}
135+
136+
private async IAsyncEnumerable<GenerateContentResponse> GenerateContentStreamAsyncInternal(
137+
string templateId, IDictionary<string, object> inputs,
138+
IEnumerable<ModelContent> chatHistory,
139+
[EnumeratorCancellation] CancellationToken cancellationToken)
140+
{
141+
HttpRequestMessage request = new(HttpMethod.Post,
142+
HttpHelpers.GetTemplateURL(_firebaseApp, _backend, templateId) + ":templateStreamGenerateContent?alt=sse");
143+
144+
// Set the request headers
145+
await HttpHelpers.SetRequestHeaders(request, _firebaseApp);
146+
147+
// Set the content
148+
string bodyJson = MakeGenerateContentRequest(inputs, chatHistory);
149+
request.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json");
150+
151+
#if FIREBASE_LOG_REST_CALLS
152+
UnityEngine.Debug.Log("Request:\n" + bodyJson);
153+
#endif
154+
155+
var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
156+
await HttpHelpers.ValidateHttpResponse(response);
157+
158+
// We are expecting a Stream as the response, so handle that.
159+
using var stream = await response.Content.ReadAsStreamAsync();
160+
using var reader = new StreamReader(stream);
161+
162+
string line;
163+
while ((line = await reader.ReadLineAsync()) != null)
164+
{
165+
// Only pass along strings that begin with the expected prefix.
166+
if (line.StartsWith(HttpHelpers.StreamPrefix))
167+
{
168+
#if FIREBASE_LOG_REST_CALLS
169+
UnityEngine.Debug.Log("Streaming Response:\n" + line);
170+
#endif
171+
172+
yield return GenerateContentResponse.FromJson(line[HttpHelpers.StreamPrefix.Length..], _backend.Provider);
173+
}
174+
}
175+
}
176+
}
177+
}

firebaseai/src/TemplateGenerativeModel.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)