Skip to content

Commit 1815c6e

Browse files
authored
[Firebase AI] Add Thought summaries (#1317)
* [Firebase AI] Add logic for receiving Thoughts * Fix internal test issues * Fix errors
1 parent 15a1b9a commit 1815c6e

File tree

8 files changed

+252
-36
lines changed

8 files changed

+252
-36
lines changed

firebaseai/src/GenerateContentResponse.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,26 @@ public IReadOnlyList<Candidate> Candidates {
5353
/// </summary>
5454
public string Text {
5555
get {
56-
// Concatenate all of the text parts from the first candidate.
56+
// Concatenate all of the text parts that aren't thoughts from the first candidate.
5757
return string.Join(" ",
5858
Candidates.FirstOrDefault().Content.Parts
59-
.OfType<ModelContent.TextPart>().Select(tp => tp.Text));
59+
.OfType<ModelContent.TextPart>().Where(tp => !tp.IsThought).Select(tp => tp.Text));
60+
}
61+
}
62+
63+
/// <summary>
64+
/// A summary of the model's thinking process, if available.
65+
///
66+
/// Note that Thought Summaries are only available when `IncludeThoughts` is enabled
67+
/// in the `ThinkingConfig`. For more information, see the
68+
/// [Thinking](https://firebase.google.com/docs/ai-logic/thinking) documentation.
69+
/// </summary>
70+
public string ThoughtSummary {
71+
get {
72+
// Concatenate all of the text parts that are thoughts from the first candidate.
73+
return string.Join(" ",
74+
Candidates.FirstOrDefault().Content.Parts
75+
.OfType<ModelContent.TextPart>().Where(tp => tp.IsThought).Select(tp => tp.Text));
6076
}
6177
}
6278

@@ -65,7 +81,8 @@ public string Text {
6581
/// </summary>
6682
public IReadOnlyList<ModelContent.FunctionCallPart> FunctionCalls {
6783
get {
68-
return Candidates.FirstOrDefault().Content.Parts.OfType<ModelContent.FunctionCallPart>().ToList();
84+
return Candidates.FirstOrDefault().Content.Parts
85+
.OfType<ModelContent.FunctionCallPart>().Where(tp => !tp.IsThought).ToList();
6986
}
7087
}
7188

firebaseai/src/GenerationConfig.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -216,14 +216,19 @@ internal Dictionary<string, object> ToJson() {
216216
public readonly struct ThinkingConfig {
217217
#if !DOXYGEN
218218
public readonly int? ThinkingBudget { get; }
219+
public readonly bool? IncludeThoughts { get; }
219220
#endif
220221

221222
/// <summary>
222223
/// Initializes configuration options for Thinking features.
223224
/// </summary>
224225
/// <param name="thinkingBudget">The token budget for the model's thinking process.</param>
225-
public ThinkingConfig(int? thinkingBudget = null) {
226+
/// <param name="includeThoughts">
227+
/// If true, summaries of the model's "thoughts" are included in responses.
228+
/// </param>
229+
public ThinkingConfig(int? thinkingBudget = null, bool? includeThoughts = null) {
226230
ThinkingBudget = thinkingBudget;
231+
IncludeThoughts = includeThoughts;
227232
}
228233

229234
/// <summary>
@@ -232,9 +237,8 @@ public ThinkingConfig(int? thinkingBudget = null) {
232237
/// </summary>
233238
internal Dictionary<string, object> ToJson() {
234239
Dictionary<string, object> jsonDict = new();
235-
if (ThinkingBudget.HasValue) {
236-
jsonDict["thinkingBudget"] = ThinkingBudget.Value;
237-
}
240+
jsonDict.AddIfHasValue("thinkingBudget", ThinkingBudget);
241+
jsonDict.AddIfHasValue("includeThoughts", IncludeThoughts);
238242
return jsonDict;
239243
}
240244
}

firebaseai/src/Internal/InternalHelpers.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,20 @@ public static ModelContent ConvertToModel(this ModelContent content) {
234234
public static ModelContent ConvertToSystem(this ModelContent content) {
235235
return content.ConvertRole("system");
236236
}
237+
238+
public static void AddIfHasValue<T>(this JsonDict jsonDict, string key,
239+
T? value) where T : struct {
240+
if (value.HasValue) {
241+
jsonDict.Add(key, value.Value);
242+
}
243+
}
244+
245+
public static void AddIfHasValue<T>(this JsonDict jsonDict, string key,
246+
T value) where T : class {
247+
if (value != null) {
248+
jsonDict.Add(key, value);
249+
}
250+
}
237251
}
238252

239253
}

firebaseai/src/LiveSessionResponse.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@ private LiveSessionToolCall(List<ModelContent.FunctionCallPart> functionCalls) {
199199
/// </summary>
200200
internal static LiveSessionToolCall FromJson(Dictionary<string, object> jsonDict) {
201201
return new LiveSessionToolCall(
202-
jsonDict.ParseObjectList("functionCalls", ModelContentJsonParsers.FunctionCallPartFromJson));
202+
jsonDict.ParseObjectList("functionCalls",
203+
innerDict => ModelContentJsonParsers.FunctionCallPartFromJson(innerDict, null, null)));
203204
}
204205
}
205206

firebaseai/src/ModelContent.cs

Lines changed: 98 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,15 @@ public static ModelContent FunctionResponse(
119119
/// single value of `Part`, different data types may not mix.
120120
/// </summary>
121121
public interface Part {
122+
/// <summary>
123+
/// Indicates whether this `Part` is a summary of the model's internal thinking process.
124+
///
125+
/// When `IncludeThoughts` is set to `true` in `ThinkingConfig`, the model may return one or
126+
/// more "thought" parts that provide insight into how it reasoned through the prompt to arrive
127+
/// at the final answer. These parts will have `IsThought` set to `true`.
128+
/// </summary>
129+
public bool IsThought { get; }
130+
122131
#if !DOXYGEN
123132
/// <summary>
124133
/// Intended for internal use only.
@@ -136,15 +145,39 @@ public interface Part {
136145
/// Text value.
137146
/// </summary>
138147
public string Text { get; }
148+
149+
private readonly bool? _isThought;
150+
public bool IsThought { get { return _isThought ?? false; } }
151+
152+
private readonly string _thoughtSignature;
139153

140154
/// <summary>
141155
/// Creates a `TextPart` with the given text.
142156
/// </summary>
143157
/// <param name="text">The text value to use.</param>
144-
public TextPart(string text) { Text = text; }
158+
public TextPart(string text) {
159+
Text = text;
160+
_isThought = null;
161+
_thoughtSignature = null;
162+
}
163+
164+
/// <summary>
165+
/// Intended for internal use only.
166+
/// </summary>
167+
internal TextPart(string text, bool? isThought, string thoughtSignature) {
168+
Text = text;
169+
_isThought = isThought;
170+
_thoughtSignature = thoughtSignature;
171+
}
145172

146173
Dictionary<string, object> Part.ToJson() {
147-
return new Dictionary<string, object>() { { "text", Text } };
174+
var jsonDict = new Dictionary<string, object>() {
175+
{ "text", Text }
176+
};
177+
178+
jsonDict.AddIfHasValue("thought", _isThought);
179+
jsonDict.AddIfHasValue("thoughtSignature", _thoughtSignature);
180+
return jsonDict;
148181
}
149182
}
150183

@@ -161,6 +194,11 @@ Dictionary<string, object> Part.ToJson() {
161194
/// The data provided in the inline data part.
162195
/// </summary>
163196
public byte[] Data { get; }
197+
198+
private readonly bool? _isThought;
199+
public bool IsThought { get { return _isThought ?? false; } }
200+
201+
private readonly string _thoughtSignature;
164202

165203
/// <summary>
166204
/// Creates an `InlineDataPart` from data and a MIME type.
@@ -176,16 +214,31 @@ Dictionary<string, object> Part.ToJson() {
176214
/// <param name="data">The data representation of an image, video, audio or document; see [input files and
177215
/// requirements](https://firebase.google.com/docs/vertex-ai/input-file-requirements) for
178216
/// supported media types.</param>
179-
public InlineDataPart(string mimeType, byte[] data) { MimeType = mimeType; Data = data; }
217+
public InlineDataPart(string mimeType, byte[] data) {
218+
MimeType = mimeType;
219+
Data = data;
220+
_isThought = null;
221+
_thoughtSignature = null;
222+
}
223+
224+
internal InlineDataPart(string mimeType, byte[] data, bool? isThought, string thoughtSignature) {
225+
MimeType = mimeType;
226+
Data = data;
227+
_isThought = isThought;
228+
_thoughtSignature = thoughtSignature;
229+
}
180230

181231
Dictionary<string, object> Part.ToJson() {
182-
return new Dictionary<string, object>() {
232+
var jsonDict = new Dictionary<string, object>() {
183233
{ "inlineData", new Dictionary<string, object>() {
184234
{ "mimeType", MimeType },
185235
{ "data", Convert.ToBase64String(Data) }
186236
}
187237
}
188238
};
239+
jsonDict.AddIfHasValue("thought", _isThought);
240+
jsonDict.AddIfHasValue("thoughtSignature", _thoughtSignature);
241+
return jsonDict;
189242
}
190243
}
191244

@@ -201,6 +254,9 @@ Dictionary<string, object> Part.ToJson() {
201254
/// The URI of the file.
202255
/// </summary>
203256
public System.Uri Uri { get; }
257+
258+
// This Part can only come from the user, and thus will never be a thought.
259+
public bool IsThought { get { return false; } }
204260

205261
/// <summary>
206262
/// Constructs a new file data part.
@@ -241,27 +297,36 @@ Dictionary<string, object> Part.ToJson() {
241297
/// </summary>
242298
public string Id { get; }
243299

300+
private readonly bool? _isThought;
301+
public bool IsThought { get { return _isThought ?? false; } }
302+
303+
private readonly string _thoughtSignature;
304+
244305
/// <summary>
245306
/// Intended for internal use only.
246307
/// </summary>
247-
internal FunctionCallPart(string name, IDictionary<string, object> args, string id) {
308+
internal FunctionCallPart(string name, IDictionary<string, object> args, string id,
309+
bool? isThought, string thoughtSignature) {
248310
Name = name;
249311
Args = new Dictionary<string, object>(args);
250312
Id = id;
313+
_isThought = isThought;
314+
_thoughtSignature = thoughtSignature;
251315
}
252316

253317
Dictionary<string, object> Part.ToJson() {
254-
var jsonDict = new Dictionary<string, object>() {
318+
var innerDict = new Dictionary<string, object>() {
255319
{ "name", Name },
256320
{ "args", Args }
257321
};
258-
if (!string.IsNullOrEmpty(Id)) {
259-
jsonDict["id"] = Id;
260-
}
322+
innerDict.AddIfHasValue("id", Id);
261323

262-
return new Dictionary<string, object>() {
263-
{ "functionCall", jsonDict }
324+
var jsonDict = new Dictionary<string, object>() {
325+
{ "functionCall", innerDict }
264326
};
327+
jsonDict.AddIfHasValue("thought", _isThought);
328+
jsonDict.AddIfHasValue("thoughtSignature", _thoughtSignature);
329+
return jsonDict;
265330
}
266331
}
267332

@@ -285,6 +350,9 @@ Dictionary<string, object> Part.ToJson() {
285350
/// The id from the FunctionCallPart this is in response to.
286351
/// </summary>
287352
public string Id { get; }
353+
354+
// This Part can only come from the user, and thus will never be a thought.
355+
public bool IsThought { get { return false; } }
288356

289357
/// <summary>
290358
/// Constructs a new `FunctionResponsePart`.
@@ -337,20 +405,27 @@ internal static ModelContent FromJson(Dictionary<string, object> jsonDict) {
337405
jsonDict.ParseObjectList("parts", PartFromJson, JsonParseOptions.ThrowEverything).Where(p => p is not null));
338406
}
339407

340-
private static InlineDataPart InlineDataPartFromJson(Dictionary<string, object> jsonDict) {
408+
private static InlineDataPart InlineDataPartFromJson(Dictionary<string, object> jsonDict,
409+
bool? isThought, string thoughtSignature) {
341410
return new InlineDataPart(
342411
jsonDict.ParseValue<string>("mimeType", JsonParseOptions.ThrowEverything),
343-
Convert.FromBase64String(jsonDict.ParseValue<string>("data", JsonParseOptions.ThrowEverything)));
412+
Convert.FromBase64String(jsonDict.ParseValue<string>("data", JsonParseOptions.ThrowEverything)),
413+
isThought,
414+
thoughtSignature);
344415
}
345416

346417
private static Part PartFromJson(Dictionary<string, object> jsonDict) {
418+
bool? isThought = jsonDict.ParseNullableValue<bool>("thought");
419+
string thoughtSignature = jsonDict.ParseValue<string>("thoughtSignature");
347420
if (jsonDict.TryParseValue("text", out string text)) {
348-
return new TextPart(text);
349-
} else if (jsonDict.TryParseObject("functionCall", ModelContentJsonParsers.FunctionCallPartFromJson,
350-
out var fcPart)) {
421+
return new TextPart(text, isThought, thoughtSignature);
422+
} else if (jsonDict.TryParseObject("functionCall",
423+
innerDict => ModelContentJsonParsers.FunctionCallPartFromJson(innerDict, isThought, thoughtSignature),
424+
out var fcPart)) {
351425
return fcPart;
352-
} else if (jsonDict.TryParseObject("inlineData", InlineDataPartFromJson,
353-
out var inlineDataPart)) {
426+
} else if (jsonDict.TryParseObject("inlineData",
427+
innerDict => InlineDataPartFromJson(innerDict, isThought, thoughtSignature),
428+
out var inlineDataPart)) {
354429
return inlineDataPart;
355430
} else {
356431
#if FIREBASEAI_DEBUG_LOGGING
@@ -365,11 +440,14 @@ namespace Internal {
365440

366441
// Class for parsing Parts that need to be called from other files as well.
367442
internal static class ModelContentJsonParsers {
368-
internal static ModelContent.FunctionCallPart FunctionCallPartFromJson(Dictionary<string, object> jsonDict) {
443+
internal static ModelContent.FunctionCallPart FunctionCallPartFromJson(Dictionary<string, object> jsonDict,
444+
bool? isThought, string thoughtSignature) {
369445
return new ModelContent.FunctionCallPart(
370446
jsonDict.ParseValue<string>("name", JsonParseOptions.ThrowEverything),
371447
jsonDict.ParseValue<Dictionary<string, object>>("args", JsonParseOptions.ThrowEverything),
372-
jsonDict.ParseValue<string>("id"));
448+
jsonDict.ParseValue<string>("id"),
449+
isThought,
450+
thoughtSignature);
373451
}
374452
}
375453

0 commit comments

Comments
 (0)