diff --git a/docs/readme.md b/docs/readme.md index 431add0c..df5b7d0f 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -115,6 +115,7 @@ Release Notes as June 2026. As a replacement, you can [migrate your apps to use Gemini Image models (the "Nano Banana" models)](https://firebase.google.com/docs/ai-logic/imagen-models-migration). - Firebase AI: Add support for the JsonSchema formatting. + - Firebase AI: Add support for simplified object generation with GenerateObjectAsync. - Firebase AI: Add support for TemplateChatSession. - Functions: Rewrote internal serialization logic to C#. Removes dependency on internal C++ implementation. diff --git a/firebaseai/src/GenerateContentResponse.cs b/firebaseai/src/GenerateContentResponse.cs index f4b33656..78c8a01f 100644 --- a/firebaseai/src/GenerateContentResponse.cs +++ b/firebaseai/src/GenerateContentResponse.cs @@ -130,6 +130,63 @@ internal static GenerateContentResponse FromJson(Dictionary json } } + /// + /// The model's response to a generate object request. + /// + /// The object is deserialized using reflection. If you would like to implement your own + /// deserialization method, have your class T implement the `IFirebaseDeserializable` interface. + /// + /// The type of the underlying Result + public readonly struct GenerateObjectResponse + { + /// + /// The underlying `GenerateContentResponse` returned from the Model. + /// + public readonly GenerateContentResponse Response { get; } + + /// + /// The deserialized object from the first Candidate response. + /// + public T Result => GetResult(0); + + private readonly Dictionary parsedResults; + + /// + /// Intended for internal use. Call `GenerativeModel.GenerateObjectAsync` + /// to get a valid one. + /// + internal GenerateObjectResponse(GenerateContentResponse response) + { + Response = response; + parsedResults = new(); + } + + /// + /// Parse the resulting object from the requested candidate response. + /// + /// The index of the candidate to parse. + /// Note that getting multiple candidates requires configuration settings. + /// The deserialized object. + public T GetResult(int candidateIndex = 0) + { + if (parsedResults.TryGetValue(candidateIndex, out var t)) + { + return t; + } + + foreach (var part in Response.Candidates[candidateIndex].Content.Parts) + { + if (part is ModelContent.TextPart textPart && !textPart.IsThought) + { + T result = (T)SerializationHelpers.JsonStringToType(textPart.Text, typeof(T)); + parsedResults[candidateIndex] = result; + return result; + } + } + return default; + } + } + /// /// A type describing possible reasons to block a prompt. /// diff --git a/firebaseai/src/GenerativeModel.cs b/firebaseai/src/GenerativeModel.cs index 740d2c10..c623cd91 100644 --- a/firebaseai/src/GenerativeModel.cs +++ b/firebaseai/src/GenerativeModel.cs @@ -157,6 +157,54 @@ public IAsyncEnumerable GenerateContentStreamAsync( return GenerateContentStreamAsyncInternal(content, cancellationToken); } + /// + /// Generates an object from input `ModelContent` given to the model as a prompt. + /// Note that this requires configuring the model to respond with a `Schema` or `JsonSchema` + /// that matches the type T. + /// + /// The type of the object to generate. + /// The input given to the model as a prompt. + /// An optional token to cancel the operation. + /// The `GenerateObjectResponse` from the model, which contains the Result. + public async Task> GenerateObjectAsync( + ModelContent content, CancellationToken cancellationToken = default) + { + var response = await GenerateContentAsync(content, cancellationToken); + return new GenerateObjectResponse(response); + } + + /// + /// Generates an object from input text given to the model as a prompt. + /// Note that this requires configuring the model to respond with a Schema or JsonSchema + /// that matches the type T. + /// + /// The type of the object to generate. + /// The text given to the model as a prompt. + /// An optional token to cancel the operation. + /// The `GenerateObjectResponse` from the model, which contains the Result. + public async Task> GenerateObjectAsync( + string text, CancellationToken cancellationToken = default) + { + var response = await GenerateContentAsync(text, cancellationToken); + return new GenerateObjectResponse(response); + } + + /// + /// Generates an object from input `ModelContent` given to the model as a prompt. + /// Note that this requires configuring the model to respond with a `Schema` or `JsonSchema` + /// that matches the type T. + /// + /// The type of the object to generate. + /// The input given to the model as a prompt. + /// An optional token to cancel the operation. + /// The `GenerateObjectResponse` from the model, which contains the Result. + public async Task> GenerateObjectAsync( + IEnumerable content, CancellationToken cancellationToken = default) + { + var response = await GenerateContentAsync(content, cancellationToken); + return new GenerateObjectResponse(response); + } + /// /// Counts the number of tokens in a prompt using the model's tokenizer. /// diff --git a/firebaseai/src/Serialization.cs b/firebaseai/src/Serialization.cs new file mode 100644 index 00000000..ba092241 --- /dev/null +++ b/firebaseai/src/Serialization.cs @@ -0,0 +1,173 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using Google.MiniJSON; + +namespace Firebase.AI +{ + /// + /// Interface to define a method to construct the object from a Dictionary. + /// + /// The Firebase AI Logic SDK by default will attempt to use reflection when deserializing objects, + /// like in `GenerateObjectResponse`, but this allows developers to override that logic with their own. + /// + public interface IFirebaseDeserializable + { + /// + /// Populate an object's fields with the given dictionary. + /// + /// A deserialized Json blob, received from the underlying model. + public void FromDictionary(IDictionary dict); + } + + namespace Internal + { + // Internal class that contains logic for serialization and deserialization. + internal static class SerializationHelpers + { + // Given a serialized Json string, tries to convert it to the given type. + internal static object JsonStringToType(string jsonString, Type type) + { + var resultDict = Json.Deserialize(jsonString); + return ObjectToType(resultDict, type); + } + + // Given an object received from the model, tries to convert it to the given type. + internal static object ObjectToType(object obj, Type type) + { + if (obj == null) return null; + + // If the type is an interface, try to convert it to something we can handle. + if (type.IsInterface) + { + if (type.IsGenericType) + { + Type genericDef = type.GetGenericTypeDefinition(); + // Common interfaces to List + if (genericDef == typeof(IEnumerable<>) || + genericDef == typeof(IList<>) || + genericDef == typeof(ICollection<>)) + { + type = typeof(List<>).MakeGenericType(type.GetGenericArguments()[0]); + } + else if (type == typeof(IList) || + type == typeof(IEnumerable)) + { + type = typeof(List); + } + } + } + + // Convert the arg into the approriate type + if (obj is Type t) + { + return t; + } + else if (type.IsEnum) + { + if (obj is string str) return Enum.Parse(type, str); + return Enum.ToObject(type, obj); + } + else if (type.IsArray) + { + Type elementType = type.GetElementType(); + if (obj is not System.Collections.IList inputList) return null; + + Array array = Array.CreateInstance(elementType, inputList.Count); + for (int i = 0; i < inputList.Count; i++) + { + array.SetValue(ObjectToType(inputList[i], elementType), i); + } + return array; + } + else if (type.IsGenericType && typeof(IList).IsAssignableFrom(type)) + { + if (obj is not IList inputList) return null; + + Type elementType = type.GetGenericArguments()[0]; + + IList list = (IList)Activator.CreateInstance(type); + + foreach (var input in inputList) + { + list.Add(ObjectToType(input, elementType)); + } + return list; + } + else if (obj is Dictionary dict) + { + return DictionaryToType(dict, type); + } + + try + { + return Convert.ChangeType(obj, type); + } + catch + { + return obj; + } + } + + // Given a Json style dictionary, tries to convert it to the given type. + internal static object DictionaryToType(Dictionary dict, Type type) + { + object item = Activator.CreateInstance(type); + + // Check the class for the interface, and use that if available. + if (item is IFirebaseDeserializable deserializable) + { + deserializable.FromDictionary(dict); + return item; + } + + // Otherwise, fall back to reflection, which will be slower. + BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; + foreach (var kvp in dict) + { + try + { + // First, check for fields + FieldInfo field = type.GetField(kvp.Key, flags); + if (field != null) + { + field.SetValue(item, ObjectToType(kvp.Value, field.FieldType)); + continue; + } + + // Otherwise, check for properties + PropertyInfo prop = type.GetProperty(kvp.Key, flags); + if (prop != null && prop.CanWrite) + { + prop.SetValue(item, ObjectToType(kvp.Value, prop.PropertyType)); + continue; + } + } + catch (Exception e) + { + UnityEngine.Debug.LogError($"Failed to convert object key {kvp.Key}, {e.Message}"); + } + } + + return item; + } + } + } +} diff --git a/firebaseai/src/Serialization.cs.meta b/firebaseai/src/Serialization.cs.meta new file mode 100644 index 00000000..96bbb162 --- /dev/null +++ b/firebaseai/src/Serialization.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cf3aabbe4784e4525baebc61ac111451 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: