diff --git a/firebaseai/src/Candidate.cs b/firebaseai/src/Candidate.cs index c3053c95..9565291f 100644 --- a/firebaseai/src/Candidate.cs +++ b/firebaseai/src/Candidate.cs @@ -17,140 +17,148 @@ using System.Collections.Generic; using Firebase.AI.Internal; -namespace Firebase.AI { +namespace Firebase.AI +{ + /// + /// Represents the reason why the model stopped generating content. + /// + public enum FinishReason + { + /// + /// A new and not yet supported value. + /// + Unknown = 0, + /// + /// Natural stop point of the model or provided stop sequence. + /// + Stop, + /// + /// The maximum number of tokens as specified in the request was reached. + /// + MaxTokens, + /// + /// The token generation was stopped because the response was flagged for safety reasons. + /// + Safety, + /// + /// The token generation was stopped because the response was flagged for unauthorized citations. + /// + Recitation, + /// + /// All other reasons that stopped token generation. + /// + Other, + /// + /// Token generation was stopped because the response contained forbidden terms. + /// + Blocklist, + /// + /// Token generation was stopped because the response contained potentially prohibited content. + /// + ProhibitedContent, + /// + /// Token generation was stopped because of Sensitive Personally Identifiable Information (SPII). + /// + SPII, + /// + /// Token generation was stopped because the function call generated by the model was invalid. + /// + MalformedFunctionCall, + } -/// -/// Represents the reason why the model stopped generating content. -/// -public enum FinishReason { - /// - /// A new and not yet supported value. - /// - Unknown = 0, - /// - /// Natural stop point of the model or provided stop sequence. - /// - Stop, - /// - /// The maximum number of tokens as specified in the request was reached. - /// - MaxTokens, - /// - /// The token generation was stopped because the response was flagged for safety reasons. - /// - Safety, - /// - /// The token generation was stopped because the response was flagged for unauthorized citations. - /// - Recitation, /// - /// All other reasons that stopped token generation. + /// A struct representing a possible reply to a content generation prompt. + /// Each content generation prompt may produce multiple candidate responses. /// - Other, - /// - /// Token generation was stopped because the response contained forbidden terms. - /// - Blocklist, - /// - /// Token generation was stopped because the response contained potentially prohibited content. - /// - ProhibitedContent, - /// - /// Token generation was stopped because of Sensitive Personally Identifiable Information (SPII). - /// - SPII, - /// - /// Token generation was stopped because the function call generated by the model was invalid. - /// - MalformedFunctionCall, -} + public readonly struct Candidate + { + private readonly IReadOnlyList _safetyRatings; -/// -/// A struct representing a possible reply to a content generation prompt. -/// Each content generation prompt may produce multiple candidate responses. -/// -public readonly struct Candidate { - private readonly IReadOnlyList _safetyRatings; + /// + /// The response’s content. + /// + public ModelContent Content { get; } - /// - /// The response’s content. - /// - public ModelContent Content { get; } - - /// - /// The safety rating of the response content. - /// - public IReadOnlyList SafetyRatings { - get { - return _safetyRatings ?? new List(); + /// + /// The safety rating of the response content. + /// + public IReadOnlyList SafetyRatings + { + get + { + return _safetyRatings ?? new List(); + } } - } - /// - /// The reason the model stopped generating content, if it exists; - /// for example, if the model generated a predefined stop sequence. - /// - public FinishReason? FinishReason { get; } + /// + /// The reason the model stopped generating content, if it exists; + /// for example, if the model generated a predefined stop sequence. + /// + public FinishReason? FinishReason { get; } - /// - /// Cited works in the model’s response content, if it exists. - /// - public CitationMetadata? CitationMetadata { get; } + /// + /// Cited works in the model’s response content, if it exists. + /// + public CitationMetadata? CitationMetadata { get; } - /// - /// Grounding metadata for the response, if any. - /// - public GroundingMetadata? GroundingMetadata { get; } - - /// - /// Metadata related to the `URLContext` tool. - /// - public UrlContextMetadata? UrlContextMetadata { get; } + /// + /// Grounding metadata for the response, if any. + /// + public GroundingMetadata? GroundingMetadata { get; } - // Hidden constructor, users don't need to make this. - private Candidate(ModelContent content, List safetyRatings, - FinishReason? finishReason, CitationMetadata? citationMetadata, - GroundingMetadata? groundingMetadata, UrlContextMetadata? urlContextMetadata) { - Content = content; - _safetyRatings = safetyRatings ?? new List(); - FinishReason = finishReason; - CitationMetadata = citationMetadata; - GroundingMetadata = groundingMetadata; - UrlContextMetadata = urlContextMetadata; - } + /// + /// Metadata related to the `URLContext` tool. + /// + public UrlContextMetadata? UrlContextMetadata { get; } - private static FinishReason ParseFinishReason(string str) { - return str switch { - "STOP" => Firebase.AI.FinishReason.Stop, - "MAX_TOKENS" => Firebase.AI.FinishReason.MaxTokens, - "SAFETY" => Firebase.AI.FinishReason.Safety, - "RECITATION" => Firebase.AI.FinishReason.Recitation, - "OTHER" => Firebase.AI.FinishReason.Other, - "BLOCKLIST" => Firebase.AI.FinishReason.Blocklist, - "PROHIBITED_CONTENT" => Firebase.AI.FinishReason.ProhibitedContent, - "SPII" => Firebase.AI.FinishReason.SPII, - "MALFORMED_FUNCTION_CALL" => Firebase.AI.FinishReason.MalformedFunctionCall, - _ => Firebase.AI.FinishReason.Unknown, - }; - } + // Hidden constructor, users don't need to make this. + private Candidate(ModelContent content, List safetyRatings, + FinishReason? finishReason, CitationMetadata? citationMetadata, + GroundingMetadata? groundingMetadata, UrlContextMetadata? urlContextMetadata) + { + Content = content; + _safetyRatings = safetyRatings ?? new List(); + FinishReason = finishReason; + CitationMetadata = citationMetadata; + GroundingMetadata = groundingMetadata; + UrlContextMetadata = urlContextMetadata; + } - /// - /// Intended for internal use only. - /// This method is used for deserializing JSON responses and should not be called directly. - /// - internal static Candidate FromJson(Dictionary jsonDict, - FirebaseAI.Backend.InternalProvider backend) { - return new Candidate( - jsonDict.ParseObject("content", ModelContent.FromJson, defaultValue: new ModelContent("model")), - jsonDict.ParseObjectList("safetyRatings", SafetyRating.FromJson), - jsonDict.ParseNullableEnum("finishReason", ParseFinishReason), - jsonDict.ParseNullableObject("citationMetadata", - (d) => Firebase.AI.CitationMetadata.FromJson(d, backend)), - jsonDict.ParseNullableObject("groundingMetadata", - Firebase.AI.GroundingMetadata.FromJson), - jsonDict.ParseNullableObject("urlContextMetadata", - Firebase.AI.UrlContextMetadata.FromJson)); + private static FinishReason ParseFinishReason(string str) + { + return str switch + { + "STOP" => Firebase.AI.FinishReason.Stop, + "MAX_TOKENS" => Firebase.AI.FinishReason.MaxTokens, + "SAFETY" => Firebase.AI.FinishReason.Safety, + "RECITATION" => Firebase.AI.FinishReason.Recitation, + "OTHER" => Firebase.AI.FinishReason.Other, + "BLOCKLIST" => Firebase.AI.FinishReason.Blocklist, + "PROHIBITED_CONTENT" => Firebase.AI.FinishReason.ProhibitedContent, + "SPII" => Firebase.AI.FinishReason.SPII, + "MALFORMED_FUNCTION_CALL" => Firebase.AI.FinishReason.MalformedFunctionCall, + _ => Firebase.AI.FinishReason.Unknown, + }; + } + + /// + /// Intended for internal use only. + /// This method is used for deserializing JSON responses and should not be called directly. + /// + internal static Candidate FromJson(Dictionary jsonDict, + FirebaseAI.Backend.InternalProvider backend) + { + return new Candidate( + jsonDict.ParseObject("content", ModelContent.FromJson, defaultValue: new ModelContent("model")), + jsonDict.ParseObjectList("safetyRatings", SafetyRating.FromJson), + jsonDict.ParseNullableEnum("finishReason", ParseFinishReason), + jsonDict.ParseNullableObject("citationMetadata", + (d) => Firebase.AI.CitationMetadata.FromJson(d, backend)), + jsonDict.ParseNullableObject("groundingMetadata", + Firebase.AI.GroundingMetadata.FromJson), + jsonDict.ParseNullableObject("urlContextMetadata", + Firebase.AI.UrlContextMetadata.FromJson)); + } } -} } diff --git a/firebaseai/src/Chat.cs b/firebaseai/src/Chat.cs index d8df8f75..a437a371 100644 --- a/firebaseai/src/Chat.cs +++ b/firebaseai/src/Chat.cs @@ -21,172 +21,192 @@ using System.Threading.Tasks; using Firebase.AI.Internal; -namespace Firebase.AI { - -/// -/// An object that represents a back-and-forth chat with a model, capturing the history and saving -/// the context in memory between each message sent. -/// -public class Chat { - private readonly GenerativeModel generativeModel; - private readonly List chatHistory; - +namespace Firebase.AI +{ /// - /// The previous content from the chat that has been successfully sent and received from the - /// model. This will be provided to the model for each message sent as context for the discussion. + /// An object that represents a back-and-forth chat with a model, capturing the history and saving + /// the context in memory between each message sent. /// - public IReadOnlyList History => chatHistory; - - // Note: No public constructor, get one through GenerativeModel.StartChat - private Chat(GenerativeModel model, IEnumerable initialHistory) { - generativeModel = model; - - if (initialHistory != null) { - chatHistory = new List(initialHistory); - } else { - chatHistory = new List(); + public class Chat + { + private readonly GenerativeModel generativeModel; + private readonly List chatHistory; + + /// + /// The previous content from the chat that has been successfully sent and received from the + /// model. This will be provided to the model for each message sent as context for the discussion. + /// + public IReadOnlyList History => chatHistory; + + // Note: No public constructor, get one through GenerativeModel.StartChat + private Chat(GenerativeModel model, IEnumerable initialHistory) + { + generativeModel = model; + + if (initialHistory != null) + { + chatHistory = new List(initialHistory); + } + else + { + chatHistory = new List(); + } } - } - - /// - /// Intended for internal use only. - /// Use `GenerativeModel.StartChat` instead to ensure proper initialization and configuration of the `Chat`. - /// - internal static Chat InternalCreateChat(GenerativeModel model, IEnumerable initialHistory) { - return new Chat(model, initialHistory); - } - - /// - /// Sends a message using the existing history of this chat as context. If successful, the message - /// and response will be added to the history. If unsuccessful, history will remain unchanged. - /// - /// The input given to the model as a prompt. - /// An optional token to cancel the operation. - /// The model's response if no error occurred. - /// Thrown when an error occurs during content generation. - public Task SendMessageAsync( - ModelContent content, CancellationToken cancellationToken = default) { - return SendMessageAsync(new[] { content }, cancellationToken); - } - /// - /// Sends a message using the existing history of this chat as context. If successful, the message - /// and response will be added to the history. If unsuccessful, history will remain unchanged. - /// - /// The text given to the model as a prompt. - /// An optional token to cancel the operation. - /// The model's response if no error occurred. - /// Thrown when an error occurs during content generation. - public Task SendMessageAsync( - string text, CancellationToken cancellationToken = default) { - return SendMessageAsync(new ModelContent[] { ModelContent.Text(text) }, cancellationToken); - } - /// - /// Sends a message using the existing history of this chat as context. If successful, the message - /// and response will be added to the history. If unsuccessful, history will remain unchanged. - /// - /// The input given to the model as a prompt. - /// An optional token to cancel the operation. - /// The model's response if no error occurred. - /// Thrown when an error occurs during content generation. - public Task SendMessageAsync( - IEnumerable content, CancellationToken cancellationToken = default) { - return SendMessageAsyncInternal(content, cancellationToken); - } - /// - /// Sends a message using the existing history of this chat as context. If successful, the message - /// and response will be added to the history. If unsuccessful, history will remain unchanged. - /// - /// The input given to the model as a prompt. - /// An optional token to cancel the operation. - /// A stream of generated content responses from the model. - /// Thrown when an error occurs during content generation. - public IAsyncEnumerable SendMessageStreamAsync( - ModelContent content, CancellationToken cancellationToken = default) { - return SendMessageStreamAsync(new[] { content }, cancellationToken); - } - /// - /// Sends a message using the existing history of this chat as context. If successful, the message - /// and response will be added to the history. If unsuccessful, history will remain unchanged. - /// - /// The text given to the model as a prompt. - /// An optional token to cancel the operation. - /// A stream of generated content responses from the model. - /// Thrown when an error occurs during content generation. - public IAsyncEnumerable SendMessageStreamAsync( - string text, CancellationToken cancellationToken = default) { - return SendMessageStreamAsync(new ModelContent[] { ModelContent.Text(text) }, cancellationToken); - } - /// - /// Sends a message using the existing history of this chat as context. If successful, the message - /// and response will be added to the history. If unsuccessful, history will remain unchanged. - /// - /// The input given to the model as a prompt. - /// An optional token to cancel the operation. - /// A stream of generated content responses from the model. - /// Thrown when an error occurs during content generation. - public IAsyncEnumerable SendMessageStreamAsync( - IEnumerable content, CancellationToken cancellationToken = default) { - return SendMessageStreamAsyncInternal(content, cancellationToken); - } + /// + /// Intended for internal use only. + /// Use `GenerativeModel.StartChat` instead to ensure proper initialization and configuration of the `Chat`. + /// + internal static Chat InternalCreateChat(GenerativeModel model, IEnumerable initialHistory) + { + return new Chat(model, initialHistory); + } - private async Task SendMessageAsyncInternal( - IEnumerable requestContent, CancellationToken cancellationToken = default) { - // Make sure that the requests are set to to role "user". - List fixedRequests = requestContent.Select(FirebaseAIExtensions.ConvertToUser).ToList(); - // Set up the context to send in the request - List fullRequest = new(chatHistory); - fullRequest.AddRange(fixedRequests); - - // Note: GenerateContentAsync can throw exceptions if there was a problem, but - // we allow it to just be passed back to the user. - GenerateContentResponse response = await generativeModel.GenerateContentAsync(fullRequest, cancellationToken); - - // Only after getting a valid response, add both to the history for later. - // But either way pass the response along to the user. - if (response.Candidates.Any()) { - ModelContent responseContent = response.Candidates.First().Content; - - chatHistory.AddRange(fixedRequests); - chatHistory.Add(responseContent.ConvertToModel()); + /// + /// Sends a message using the existing history of this chat as context. If successful, the message + /// and response will be added to the history. If unsuccessful, history will remain unchanged. + /// + /// The input given to the model as a prompt. + /// An optional token to cancel the operation. + /// The model's response if no error occurred. + /// Thrown when an error occurs during content generation. + public Task SendMessageAsync( + ModelContent content, CancellationToken cancellationToken = default) + { + return SendMessageAsync(new[] { content }, cancellationToken); + } + /// + /// Sends a message using the existing history of this chat as context. If successful, the message + /// and response will be added to the history. If unsuccessful, history will remain unchanged. + /// + /// The text given to the model as a prompt. + /// An optional token to cancel the operation. + /// The model's response if no error occurred. + /// Thrown when an error occurs during content generation. + public Task SendMessageAsync( + string text, CancellationToken cancellationToken = default) + { + return SendMessageAsync(new ModelContent[] { ModelContent.Text(text) }, cancellationToken); + } + /// + /// Sends a message using the existing history of this chat as context. If successful, the message + /// and response will be added to the history. If unsuccessful, history will remain unchanged. + /// + /// The input given to the model as a prompt. + /// An optional token to cancel the operation. + /// The model's response if no error occurred. + /// Thrown when an error occurs during content generation. + public Task SendMessageAsync( + IEnumerable content, CancellationToken cancellationToken = default) + { + return SendMessageAsyncInternal(content, cancellationToken); } - return response; - } + /// + /// Sends a message using the existing history of this chat as context. If successful, the message + /// and response will be added to the history. If unsuccessful, history will remain unchanged. + /// + /// The input given to the model as a prompt. + /// An optional token to cancel the operation. + /// A stream of generated content responses from the model. + /// Thrown when an error occurs during content generation. + public IAsyncEnumerable SendMessageStreamAsync( + ModelContent content, CancellationToken cancellationToken = default) + { + return SendMessageStreamAsync(new[] { content }, cancellationToken); + } + /// + /// Sends a message using the existing history of this chat as context. If successful, the message + /// and response will be added to the history. If unsuccessful, history will remain unchanged. + /// + /// The text given to the model as a prompt. + /// An optional token to cancel the operation. + /// A stream of generated content responses from the model. + /// Thrown when an error occurs during content generation. + public IAsyncEnumerable SendMessageStreamAsync( + string text, CancellationToken cancellationToken = default) + { + return SendMessageStreamAsync(new ModelContent[] { ModelContent.Text(text) }, cancellationToken); + } + /// + /// Sends a message using the existing history of this chat as context. If successful, the message + /// and response will be added to the history. If unsuccessful, history will remain unchanged. + /// + /// The input given to the model as a prompt. + /// An optional token to cancel the operation. + /// A stream of generated content responses from the model. + /// Thrown when an error occurs during content generation. + public IAsyncEnumerable SendMessageStreamAsync( + IEnumerable content, CancellationToken cancellationToken = default) + { + return SendMessageStreamAsyncInternal(content, cancellationToken); + } - private async IAsyncEnumerable SendMessageStreamAsyncInternal( - IEnumerable requestContent, - [EnumeratorCancellation] CancellationToken cancellationToken = default) { - // Make sure that the requests are set to to role "user". - List fixedRequests = requestContent.Select(FirebaseAIExtensions.ConvertToUser).ToList(); - // Set up the context to send in the request - List fullRequest = new(chatHistory); - fullRequest.AddRange(fixedRequests); - - List responseContents = new(); - bool saveHistory = true; - // Note: GenerateContentStreamAsync can throw exceptions if there was a problem, but - // we allow it to just be passed back to the user. - await foreach (GenerateContentResponse response in - generativeModel.GenerateContentStreamAsync(fullRequest, cancellationToken)) { - // If the response had a problem, we still want to pass it along to the user for context, - // but we don't want to save the history anymore. - if (response.Candidates.Any()) { + private async Task SendMessageAsyncInternal( + IEnumerable requestContent, CancellationToken cancellationToken = default) + { + // Make sure that the requests are set to to role "user". + List fixedRequests = requestContent.Select(FirebaseAIExtensions.ConvertToUser).ToList(); + // Set up the context to send in the request + List fullRequest = new(chatHistory); + fullRequest.AddRange(fixedRequests); + + // Note: GenerateContentAsync can throw exceptions if there was a problem, but + // we allow it to just be passed back to the user. + GenerateContentResponse response = await generativeModel.GenerateContentAsync(fullRequest, cancellationToken); + + // Only after getting a valid response, add both to the history for later. + // But either way pass the response along to the user. + if (response.Candidates.Any()) + { ModelContent responseContent = response.Candidates.First().Content; - responseContents.Add(responseContent.ConvertToModel()); - } else { - saveHistory = false; + + chatHistory.AddRange(fixedRequests); + chatHistory.Add(responseContent.ConvertToModel()); } - yield return response; + return response; } - // After getting all the responses, and they were all valid, add everything to the history - if (saveHistory) { - chatHistory.AddRange(fixedRequests); - chatHistory.AddRange(responseContents); + private async IAsyncEnumerable SendMessageStreamAsyncInternal( + IEnumerable requestContent, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + // Make sure that the requests are set to to role "user". + List fixedRequests = requestContent.Select(FirebaseAIExtensions.ConvertToUser).ToList(); + // Set up the context to send in the request + List fullRequest = new(chatHistory); + fullRequest.AddRange(fixedRequests); + + List responseContents = new(); + bool saveHistory = true; + // Note: GenerateContentStreamAsync can throw exceptions if there was a problem, but + // we allow it to just be passed back to the user. + await foreach (GenerateContentResponse response in + generativeModel.GenerateContentStreamAsync(fullRequest, cancellationToken)) + { + // If the response had a problem, we still want to pass it along to the user for context, + // but we don't want to save the history anymore. + if (response.Candidates.Any()) + { + ModelContent responseContent = response.Candidates.First().Content; + responseContents.Add(responseContent.ConvertToModel()); + } + else + { + saveHistory = false; + } + + yield return response; + } + + // After getting all the responses, and they were all valid, add everything to the history + if (saveHistory) + { + chatHistory.AddRange(fixedRequests); + chatHistory.AddRange(responseContents); + } } } -} } diff --git a/firebaseai/src/Citation.cs b/firebaseai/src/Citation.cs index c98ab2a4..478978f0 100644 --- a/firebaseai/src/Citation.cs +++ b/firebaseai/src/Citation.cs @@ -18,114 +18,125 @@ using System.Collections.Generic; using Firebase.AI.Internal; -namespace Firebase.AI { - -/// -/// A collection of source attributions for a piece of content. -/// -public readonly struct CitationMetadata { - private readonly IReadOnlyList _citations; - +namespace Firebase.AI +{ /// - /// A list of individual cited sources and the parts of the content to which they apply. + /// A collection of source attributions for a piece of content. /// - public IReadOnlyList Citations { - get { - return _citations ?? new List(); + public readonly struct CitationMetadata + { + private readonly IReadOnlyList _citations; + + /// + /// A list of individual cited sources and the parts of the content to which they apply. + /// + public IReadOnlyList Citations + { + get + { + return _citations ?? new List(); + } } - } - // Hidden constructor, users don't need to make this. - private CitationMetadata(List citations) { - _citations = citations; - } + // Hidden constructor, users don't need to make this. + private CitationMetadata(List citations) + { + _citations = citations; + } - /// - /// Intended for internal use only. - /// This method is used for deserializing JSON responses and should not be called directly. - /// - internal static CitationMetadata FromJson(Dictionary jsonDict, - FirebaseAI.Backend.InternalProvider backend) { - string citationKey = backend switch { - FirebaseAI.Backend.InternalProvider.GoogleAI => "citationSources", - FirebaseAI.Backend.InternalProvider.VertexAI => "citations", - _ => throw new ArgumentOutOfRangeException(nameof(backend), backend, - "Unsupported or unhandled backend provider encountered.") - }; - return new CitationMetadata( - jsonDict.ParseObjectList(citationKey, Citation.FromJson)); + /// + /// Intended for internal use only. + /// This method is used for deserializing JSON responses and should not be called directly. + /// + internal static CitationMetadata FromJson(Dictionary jsonDict, + FirebaseAI.Backend.InternalProvider backend) + { + string citationKey = backend switch + { + FirebaseAI.Backend.InternalProvider.GoogleAI => "citationSources", + FirebaseAI.Backend.InternalProvider.VertexAI => "citations", + _ => throw new ArgumentOutOfRangeException(nameof(backend), backend, + "Unsupported or unhandled backend provider encountered.") + }; + return new CitationMetadata( + jsonDict.ParseObjectList(citationKey, Citation.FromJson)); + } } -} -/// -/// A struct describing a source attribution. -/// -public readonly struct Citation { - /// - /// The inclusive beginning of a sequence in a model response that derives from a cited source. - /// - public int StartIndex { get; } - /// - /// The exclusive end of a sequence in a model response that derives from a cited source. - /// - public int EndIndex { get; } /// - /// A link to the cited source, if available. + /// A struct describing a source attribution. /// - public System.Uri Uri { get; } - /// - /// The title of the cited source, if available. - /// - public string Title { get; } - /// - /// The license the cited source work is distributed under, if specified. - /// - public string License { get; } - /// - /// The publication date of the cited source, if available. - /// - public System.DateTime? PublicationDate { get; } - - // Hidden constructor, users don't need to make this. - private Citation(int startIndex, int endIndex, Uri uri, string title, - string license, DateTime? publicationDate) { - StartIndex = startIndex; - EndIndex = endIndex; - Uri = uri; - Title = title; - License = license; - PublicationDate = publicationDate; - } + public readonly struct Citation + { + /// + /// The inclusive beginning of a sequence in a model response that derives from a cited source. + /// + public int StartIndex { get; } + /// + /// The exclusive end of a sequence in a model response that derives from a cited source. + /// + public int EndIndex { get; } + /// + /// A link to the cited source, if available. + /// + public System.Uri Uri { get; } + /// + /// The title of the cited source, if available. + /// + public string Title { get; } + /// + /// The license the cited source work is distributed under, if specified. + /// + public string License { get; } + /// + /// The publication date of the cited source, if available. + /// + public System.DateTime? PublicationDate { get; } - /// - /// Intended for internal use only. - /// This method is used for deserializing JSON responses and should not be called directly. - /// - internal static Citation FromJson(Dictionary jsonDict) { - // If there is a Uri, need to convert it. - Uri uri = null; - if (jsonDict.TryParseValue("uri", out string uriString)) { - uri = new Uri(uriString); + // Hidden constructor, users don't need to make this. + private Citation(int startIndex, int endIndex, Uri uri, string title, + string license, DateTime? publicationDate) + { + StartIndex = startIndex; + EndIndex = endIndex; + Uri = uri; + Title = title; + License = license; + PublicationDate = publicationDate; } - // If there is a publication date, we need to convert it. - DateTime? pubDate = null; - if (jsonDict.TryParseValue("publicationDate", out Dictionary dateDict)) { - // Make sure that if any key is missing, it has a default value that will work with DateTime. - pubDate = new DateTime( - dateDict.ParseValue("year", defaultValue: 1), - dateDict.ParseValue("month", defaultValue: 1), - dateDict.ParseValue("day", defaultValue: 1)); - } + /// + /// Intended for internal use only. + /// This method is used for deserializing JSON responses and should not be called directly. + /// + internal static Citation FromJson(Dictionary jsonDict) + { + // If there is a Uri, need to convert it. + Uri uri = null; + if (jsonDict.TryParseValue("uri", out string uriString)) + { + uri = new Uri(uriString); + } - return new Citation( - jsonDict.ParseValue("startIndex"), - jsonDict.ParseValue("endIndex"), - uri, - jsonDict.ParseValue("title"), - jsonDict.ParseValue("license"), - pubDate); + // If there is a publication date, we need to convert it. + DateTime? pubDate = null; + if (jsonDict.TryParseValue("publicationDate", out Dictionary dateDict)) + { + // Make sure that if any key is missing, it has a default value that will work with DateTime. + pubDate = new DateTime( + dateDict.ParseValue("year", defaultValue: 1), + dateDict.ParseValue("month", defaultValue: 1), + dateDict.ParseValue("day", defaultValue: 1)); + } + + return new Citation( + jsonDict.ParseValue("startIndex"), + jsonDict.ParseValue("endIndex"), + uri, + jsonDict.ParseValue("title"), + jsonDict.ParseValue("license"), + pubDate); + } } -} } diff --git a/firebaseai/src/CountTokensResponse.cs b/firebaseai/src/CountTokensResponse.cs index 49314a4c..2a765559 100644 --- a/firebaseai/src/CountTokensResponse.cs +++ b/firebaseai/src/CountTokensResponse.cs @@ -19,69 +19,75 @@ using Google.MiniJSON; using Firebase.AI.Internal; -namespace Firebase.AI { - -/// -/// The model's response to a count tokens request. -/// -public readonly struct CountTokensResponse { - /// - /// The total number of tokens in the input given to the model as a prompt. - /// - public int TotalTokens { get; } +namespace Firebase.AI +{ /// - /// The total number of billable characters in the text input given to the model as a prompt. - /// - /// > Important: This does not include billable image, video or other non-text input. See - /// [Firebase AI pricing](https://firebase.google.com/docs/vertex-ai/pricing) for details. + /// The model's response to a count tokens request. /// - /// - /// Use TotalTokens instead; Gemini 2.0 series models and newer are always billed by token count. - /// - /// @deprecated Use TotalTokens instead; Gemini 2.0 series models and newer are always - /// billed by token count. - [Obsolete("Use TotalTokens instead; Gemini 2.0 series models and newer are always billed by token count.")] - public int? TotalBillableCharacters { get; } + public readonly struct CountTokensResponse + { + /// + /// The total number of tokens in the input given to the model as a prompt. + /// + public int TotalTokens { get; } + /// + /// The total number of billable characters in the text input given to the model as a prompt. + /// + /// > Important: This does not include billable image, video or other non-text input. See + /// [Firebase AI pricing](https://firebase.google.com/docs/vertex-ai/pricing) for details. + /// + /// + /// Use TotalTokens instead; Gemini 2.0 series models and newer are always billed by token count. + /// + /// @deprecated Use TotalTokens instead; Gemini 2.0 series models and newer are always + /// billed by token count. + [Obsolete("Use TotalTokens instead; Gemini 2.0 series models and newer are always billed by token count.")] + public int? TotalBillableCharacters { get; } - private readonly IReadOnlyList _promptTokensDetails; - /// - /// The breakdown, by modality, of how many tokens are consumed by the prompt. - /// - public IReadOnlyList PromptTokensDetails { - get { - return _promptTokensDetails ?? new List(); + private readonly IReadOnlyList _promptTokensDetails; + /// + /// The breakdown, by modality, of how many tokens are consumed by the prompt. + /// + public IReadOnlyList PromptTokensDetails + { + get + { + return _promptTokensDetails ?? new List(); + } } - } - // Hidden constructor, users don't need to make this - private CountTokensResponse(int totalTokens, - int? totalBillableCharacters = null, - List promptTokensDetails = null) { - TotalTokens = totalTokens; + // Hidden constructor, users don't need to make this + private CountTokensResponse(int totalTokens, + int? totalBillableCharacters = null, + List promptTokensDetails = null) + { + TotalTokens = totalTokens; #pragma warning disable CS0618 - TotalBillableCharacters = totalBillableCharacters; + TotalBillableCharacters = totalBillableCharacters; #pragma warning restore CS0618 - _promptTokensDetails = promptTokensDetails; - } + _promptTokensDetails = promptTokensDetails; + } - /// - /// Intended for internal use only. - /// This method is used for deserializing JSON responses and should not be called directly. - /// - internal static CountTokensResponse FromJson(string jsonString) { - return FromJson(Json.Deserialize(jsonString) as Dictionary); - } + /// + /// Intended for internal use only. + /// This method is used for deserializing JSON responses and should not be called directly. + /// + internal static CountTokensResponse FromJson(string jsonString) + { + return FromJson(Json.Deserialize(jsonString) as Dictionary); + } - /// - /// Intended for internal use only. - /// This method is used for deserializing JSON responses and should not be called directly. - /// - internal static CountTokensResponse FromJson(Dictionary jsonDict) { - return new CountTokensResponse( - jsonDict.ParseValue("totalTokens"), - jsonDict.ParseNullableValue("totalBillableCharacters"), - jsonDict.ParseObjectList("promptTokensDetails", ModalityTokenCount.FromJson)); + /// + /// Intended for internal use only. + /// This method is used for deserializing JSON responses and should not be called directly. + /// + internal static CountTokensResponse FromJson(Dictionary jsonDict) + { + return new CountTokensResponse( + jsonDict.ParseValue("totalTokens"), + jsonDict.ParseNullableValue("totalBillableCharacters"), + jsonDict.ParseObjectList("promptTokensDetails", ModalityTokenCount.FromJson)); + } } -} } diff --git a/firebaseai/src/FirebaseAI.cs b/firebaseai/src/FirebaseAI.cs index 3196c77d..1f43195a 100644 --- a/firebaseai/src/FirebaseAI.cs +++ b/firebaseai/src/FirebaseAI.cs @@ -17,196 +17,214 @@ using System; using System.Collections.Concurrent; -namespace Firebase.AI { - -/// -/// The entry point for all Firebase AI SDK functionality. -/// -public class FirebaseAI { - +namespace Firebase.AI +{ /// - /// Defines which backend AI service is being used, provided to `FirebaseAI.GetInstance`. + /// The entry point for all Firebase AI SDK functionality. /// - public readonly struct Backend { - /// - /// Intended for internal use only. - /// Defines the possible types of backend providers. - /// - internal enum InternalProvider { - GoogleAI, - VertexAI - } + public class FirebaseAI + { /// - /// Intended for internal use only. - /// The backend provider being used. - /// - internal InternalProvider Provider { get; } - /// - /// Intended for internal use only. - /// The region identifier used by the Vertex AI backend. + /// Defines which backend AI service is being used, provided to `FirebaseAI.GetInstance`. /// - internal string Location { get; } + public readonly struct Backend + { + /// + /// Intended for internal use only. + /// Defines the possible types of backend providers. + /// + internal enum InternalProvider + { + GoogleAI, + VertexAI + } - private Backend(InternalProvider provider, string location = null) { - Provider = provider; - Location = location; + /// + /// Intended for internal use only. + /// The backend provider being used. + /// + internal InternalProvider Provider { get; } + /// + /// Intended for internal use only. + /// The region identifier used by the Vertex AI backend. + /// + internal string Location { get; } + + private Backend(InternalProvider provider, string location = null) + { + Provider = provider; + Location = location; + } + + /// + /// The Google AI backend service configuration. + /// + public static Backend GoogleAI() + { + return new Backend(InternalProvider.GoogleAI); + } + + /// + /// The Vertex AI backend service configuration. + /// + /// The region identifier, defaulting to `us-central1`; see [Vertex AI + /// regions](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations#available-regions) + /// for a list of supported regions. + public static Backend VertexAI(string location = "us-central1") + { + if (string.IsNullOrWhiteSpace(location) || location.Contains("/")) + { + throw new ArgumentException( + $"The location argument must be non-empty, and not contain special characters like '/'"); + } + + return new Backend(InternalProvider.VertexAI, location); + } + + public override readonly string ToString() + { + return $"FirebaseAIBackend|{Provider}|{Location}"; + } } - /// - /// The Google AI backend service configuration. - /// - public static Backend GoogleAI() { - return new Backend(InternalProvider.GoogleAI); + private static readonly ConcurrentDictionary _instances = new(); + + private readonly FirebaseApp _firebaseApp; + private readonly Backend _backend; + + private FirebaseAI(FirebaseApp firebaseApp, Backend backend) + { + _firebaseApp = firebaseApp; + _backend = backend; } /// - /// The Vertex AI backend service configuration. + /// Returns a `FirebaseAI` instance with the default `FirebaseApp` and GoogleAI Backend. /// - /// The region identifier, defaulting to `us-central1`; see [Vertex AI - /// regions](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations#available-regions) - /// for a list of supported regions. - public static Backend VertexAI(string location = "us-central1") { - if (string.IsNullOrWhiteSpace(location) || location.Contains("/")) { - throw new ArgumentException( - $"The location argument must be non-empty, and not contain special characters like '/'"); + public static FirebaseAI DefaultInstance + { + get + { + return GetInstance(); } - - return new Backend(InternalProvider.VertexAI, location); } - public override readonly string ToString() { - return $"FirebaseAIBackend|{Provider}|{Location}"; + /// + /// Returns a `FirebaseAI` instance with the default `FirebaseApp` and the given Backend. + /// + /// The backend AI service to use. + /// A configured instance of `FirebaseAI`. + public static FirebaseAI GetInstance(Backend? backend = null) + { + return GetInstance(FirebaseApp.DefaultInstance, backend); } - } - - private static readonly ConcurrentDictionary _instances = new(); + /// + /// Returns a `FirebaseAI` instance with the given `FirebaseApp` and Backend. + /// + /// The custom `FirebaseApp` used for initialization. + /// The backend AI service to use. + /// A configured instance of `FirebaseAI`. + public static FirebaseAI GetInstance(FirebaseApp app, Backend? backend = null) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } - private readonly FirebaseApp _firebaseApp; - private readonly Backend _backend; + Backend resolvedBackend = backend ?? Backend.GoogleAI(); - private FirebaseAI(FirebaseApp firebaseApp, Backend backend) { - _firebaseApp = firebaseApp; - _backend = backend; - } + // FirebaseAI instances are keyed by a combination of the app name and backend. + string key = $"{app.Name}::{resolvedBackend}"; + if (_instances.ContainsKey(key)) + { + return _instances[key]; + } - /// - /// Returns a `FirebaseAI` instance with the default `FirebaseApp` and GoogleAI Backend. - /// - public static FirebaseAI DefaultInstance { - get { - return GetInstance(); + return _instances.GetOrAdd(key, _ => new FirebaseAI(app, resolvedBackend)); } - } - /// - /// Returns a `FirebaseAI` instance with the default `FirebaseApp` and the given Backend. - /// - /// The backend AI service to use. - /// A configured instance of `FirebaseAI`. - public static FirebaseAI GetInstance(Backend? backend = null) { - return GetInstance(FirebaseApp.DefaultInstance, backend); - } - /// - /// Returns a `FirebaseAI` instance with the given `FirebaseApp` and Backend. - /// - /// The custom `FirebaseApp` used for initialization. - /// The backend AI service to use. - /// A configured instance of `FirebaseAI`. - public static FirebaseAI GetInstance(FirebaseApp app, Backend? backend = null) { - if (app == null) { - throw new ArgumentNullException(nameof(app)); + /// + /// Initializes a generative model with the given parameters. + /// + /// - Note: Refer to [Gemini models](https://firebase.google.com/docs/vertex-ai/gemini-models) for + /// guidance on choosing an appropriate model for your use case. + /// + /// The name of the model to use; see + /// [available model names + /// ](https://firebase.google.com/docs/vertex-ai/gemini-models#available-model-names) for a + /// list of supported model names. + /// The content generation parameters your model should use. + /// A value describing what types of harmful content your model should allow. + /// A list of `Tool` objects that the model may use to generate the next response. + /// Tool configuration for any `Tool` specified in the request. + /// Instructions that direct the model to behave a certain way; + /// currently only text content is supported. + /// Configuration parameters for sending requests to the backend. + /// The initialized `GenerativeModel` instance. + public GenerativeModel GetGenerativeModel( + string modelName, + GenerationConfig? generationConfig = null, + SafetySetting[] safetySettings = null, + Tool[] tools = null, + ToolConfig? toolConfig = null, + ModelContent? systemInstruction = null, + RequestOptions? requestOptions = null) + { + return new GenerativeModel(_firebaseApp, _backend, modelName, + generationConfig, safetySettings, tools, + toolConfig, systemInstruction, requestOptions); } - Backend resolvedBackend = backend ?? Backend.GoogleAI(); - - // FirebaseAI instances are keyed by a combination of the app name and backend. - string key = $"{app.Name}::{resolvedBackend}"; - if (_instances.ContainsKey(key)) { - return _instances[key]; + /// + /// Initializes a `LiveGenerativeModel` for real-time interaction. + /// + /// - Note: Refer to [Gemini models](https://firebase.google.com/docs/vertex-ai/gemini-models) for + /// guidance on choosing an appropriate model for your use case. + /// + /// The name of the model to use; see + /// [available model names + /// ](https://firebase.google.com/docs/vertex-ai/gemini-models#available-model-names) for a + /// list of supported model names. + /// The content generation parameters your model should use. + /// A list of `Tool` objects that the model may use to generate the next response. + /// Instructions that direct the model to behave a certain way. + /// Configuration parameters for sending requests to the backend. + /// The initialized `LiveGenerativeModel` instance. + public LiveGenerativeModel GetLiveModel( + string modelName, + LiveGenerationConfig? liveGenerationConfig = null, + Tool[] tools = null, + ModelContent? systemInstruction = null, + RequestOptions? requestOptions = null) + { + return new LiveGenerativeModel(_firebaseApp, _backend, modelName, + liveGenerationConfig, tools, + systemInstruction, requestOptions); } - return _instances.GetOrAdd(key, _ => new FirebaseAI(app, resolvedBackend)); - } - - /// - /// Initializes a generative model with the given parameters. - /// - /// - Note: Refer to [Gemini models](https://firebase.google.com/docs/vertex-ai/gemini-models) for - /// guidance on choosing an appropriate model for your use case. - /// - /// The name of the model to use; see - /// [available model names - /// ](https://firebase.google.com/docs/vertex-ai/gemini-models#available-model-names) for a - /// list of supported model names. - /// The content generation parameters your model should use. - /// A value describing what types of harmful content your model should allow. - /// A list of `Tool` objects that the model may use to generate the next response. - /// Tool configuration for any `Tool` specified in the request. - /// Instructions that direct the model to behave a certain way; - /// currently only text content is supported. - /// Configuration parameters for sending requests to the backend. - /// The initialized `GenerativeModel` instance. - public GenerativeModel GetGenerativeModel( - string modelName, - GenerationConfig? generationConfig = null, - SafetySetting[] safetySettings = null, - Tool[] tools = null, - ToolConfig? toolConfig = null, - ModelContent? systemInstruction = null, - RequestOptions? requestOptions = null) { - return new GenerativeModel(_firebaseApp, _backend, modelName, - generationConfig, safetySettings, tools, - toolConfig, systemInstruction, requestOptions); - } - - /// - /// Initializes a `LiveGenerativeModel` for real-time interaction. - /// - /// - Note: Refer to [Gemini models](https://firebase.google.com/docs/vertex-ai/gemini-models) for - /// guidance on choosing an appropriate model for your use case. - /// - /// The name of the model to use; see - /// [available model names - /// ](https://firebase.google.com/docs/vertex-ai/gemini-models#available-model-names) for a - /// list of supported model names. - /// The content generation parameters your model should use. - /// A list of `Tool` objects that the model may use to generate the next response. - /// Instructions that direct the model to behave a certain way. - /// Configuration parameters for sending requests to the backend. - /// The initialized `LiveGenerativeModel` instance. - public LiveGenerativeModel GetLiveModel( - string modelName, - LiveGenerationConfig? liveGenerationConfig = null, - Tool[] tools = null, - ModelContent? systemInstruction = null, - RequestOptions? requestOptions = null) { - return new LiveGenerativeModel(_firebaseApp, _backend, modelName, - liveGenerationConfig, tools, - systemInstruction, requestOptions); - } - - /// - /// Initializes an `ImagenModel` with the given parameters. - /// - /// - Important: Only Imagen 3 models (named `imagen-3.0-*`) are supported. - /// - /// The name of the Imagen 3 model to use, for example `"imagen-3.0-generate-002"`; - /// see [model versions](https://firebase.google.com/docs/vertex-ai/models) for a list of - /// supported Imagen 3 models. - /// Configuration options for generating images with Imagen. - /// Settings describing what types of potentially harmful content your model - /// should allow. - /// Configuration parameters for sending requests to the backend. - /// The initialized `ImagenModel` instance. - public ImagenModel GetImagenModel( - string modelName, - ImagenGenerationConfig? generationConfig = null, - ImagenSafetySettings? safetySettings = null, - RequestOptions? requestOptions = null) { - return new ImagenModel(_firebaseApp, _backend, modelName, - generationConfig, safetySettings, requestOptions); + /// + /// Initializes an `ImagenModel` with the given parameters. + /// + /// - Important: Only Imagen 3 models (named `imagen-3.0-*`) are supported. + /// + /// The name of the Imagen 3 model to use, for example `"imagen-3.0-generate-002"`; + /// see [model versions](https://firebase.google.com/docs/vertex-ai/models) for a list of + /// supported Imagen 3 models. + /// Configuration options for generating images with Imagen. + /// Settings describing what types of potentially harmful content your model + /// should allow. + /// Configuration parameters for sending requests to the backend. + /// The initialized `ImagenModel` instance. + public ImagenModel GetImagenModel( + string modelName, + ImagenGenerationConfig? generationConfig = null, + ImagenSafetySettings? safetySettings = null, + RequestOptions? requestOptions = null) + { + return new ImagenModel(_firebaseApp, _backend, modelName, + generationConfig, safetySettings, requestOptions); + } } -} } diff --git a/firebaseai/src/FunctionCalling.cs b/firebaseai/src/FunctionCalling.cs index c02332ca..66e67359 100644 --- a/firebaseai/src/FunctionCalling.cs +++ b/firebaseai/src/FunctionCalling.cs @@ -19,268 +19,297 @@ using System.Linq; using Firebase.AI.Internal; -namespace Firebase.AI { - -/// -/// Structured representation of a function declaration. -/// -/// This `FunctionDeclaration` is a representation of a block of code that can be used -/// as a `Tool` by the model and executed by the client. -/// -/// Function calling can be used to provide data to the model that was not known at the time it -/// was trained (for example, the current date or weather conditions) or to allow it to interact -/// with external systems (for example, making an API request or querying/updating a database). -/// For more details and use cases, see [Introduction to function -/// calling](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling). -/// -public readonly struct FunctionDeclaration { - // No public properties, on purpose since it is meant for user input only - - private string Name { get; } - private string Description { get; } - private Schema Parameters { get; } - +namespace Firebase.AI +{ /// - /// Constructs a new `FunctionDeclaration`. + /// Structured representation of a function declaration. + /// + /// This `FunctionDeclaration` is a representation of a block of code that can be used + /// as a `Tool` by the model and executed by the client. + /// + /// Function calling can be used to provide data to the model that was not known at the time it + /// was trained (for example, the current date or weather conditions) or to allow it to interact + /// with external systems (for example, making an API request or querying/updating a database). + /// For more details and use cases, see [Introduction to function + /// calling](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling). /// - /// The name of the function; must be a-z, A-Z, 0-9, or contain - /// underscores and dashes, with a maximum length of 63. - /// A brief description of the function. - /// Describes the parameters to this function. - /// The names of parameters that may be omitted by the model - /// in function calls; by default, all parameters are considered required. - public FunctionDeclaration(string name, string description, - IDictionary parameters, - IEnumerable optionalParameters = null) { - Name = name; - Description = description; - Parameters = Schema.Object(parameters, optionalParameters); - } + public readonly struct FunctionDeclaration + { + // No public properties, on purpose since it is meant for user input only - /// - /// Intended for internal use only. - /// This method is used for serializing the object to JSON for the API request. - /// - internal Dictionary ToJson() { - return new() { + private string Name { get; } + private string Description { get; } + private Schema Parameters { get; } + + /// + /// Constructs a new `FunctionDeclaration`. + /// + /// The name of the function; must be a-z, A-Z, 0-9, or contain + /// underscores and dashes, with a maximum length of 63. + /// A brief description of the function. + /// Describes the parameters to this function. + /// The names of parameters that may be omitted by the model + /// in function calls; by default, all parameters are considered required. + public FunctionDeclaration(string name, string description, + IDictionary parameters, + IEnumerable optionalParameters = null) + { + Name = name; + Description = description; + Parameters = Schema.Object(parameters, optionalParameters); + } + + /// + /// Intended for internal use only. + /// This method is used for serializing the object to JSON for the API request. + /// + internal Dictionary ToJson() + { + return new() { { "name", Name }, { "description", Description }, { "parameters", Parameters.ToJson() }, }; + } } -} - -/// -/// A tool that allows the generative model to connect to Google Search to access and incorporate -/// up-to-date information from the web into its responses. -/// -/// > Important: When using this feature, you are required to comply with the -/// "Grounding with Google Search" usage requirements for your chosen API provider: -/// [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search) -/// or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms) -/// section within the Service Specific Terms). -/// -public readonly struct GoogleSearch {} - -/// -/// A tool that allows the model to execute code. -/// -/// This tool can be used to solve complex problems, for example, by generating and executing Python -/// code to solve a math problem. -/// -public readonly struct CodeExecution {} - -/// -/// A helper tool that the model may use when generating responses. -/// -/// A `Tool` is a piece of code that enables the system to interact with external systems to -/// perform an action, or set of actions, outside of knowledge and scope of the model. -/// -public readonly struct Tool { - // No public properties, on purpose since it is meant for user input only - - private List FunctionDeclarations { get; } - private GoogleSearch? GoogleSearch { get; } - private CodeExecution? CodeExecution { get; } - private UrlContext? UrlContext { get; } /// - /// Creates a tool that allows the model to perform function calling. - /// - /// A list of `FunctionDeclarations` available to the model - /// that can be used for function calling. - public Tool(params FunctionDeclaration[] functionDeclarations) { - FunctionDeclarations = new List(functionDeclarations); - GoogleSearch = null; - CodeExecution = null; - UrlContext = null; - } - /// - /// Creates a tool that allows the model to perform function calling. + /// A tool that allows the generative model to connect to Google Search to access and incorporate + /// up-to-date information from the web into its responses. + /// + /// > Important: When using this feature, you are required to comply with the + /// "Grounding with Google Search" usage requirements for your chosen API provider: + /// [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search) + /// or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms) + /// section within the Service Specific Terms). /// - /// A list of `FunctionDeclarations` available to the model - /// that can be used for function calling. - public Tool(IEnumerable functionDeclarations) { - FunctionDeclarations = new List(functionDeclarations); - GoogleSearch = null; - CodeExecution = null; - UrlContext = null; - } + public readonly struct GoogleSearch { } /// - /// Creates a tool that allows the model to use Grounding with Google Search. - /// - /// An empty `GoogleSearch` object. The presence of this object - /// in the list of tools enables the model to use Google Search. - public Tool(GoogleSearch googleSearch) { - FunctionDeclarations = null; - GoogleSearch = googleSearch; - CodeExecution = null; - UrlContext = null; - } - - /// - /// Creates a tool that allows the model to use Code Execution. - /// - /// An empty `CodeExecution` object. The presence of this object - /// in the list of tools enables the model to use Code Execution. - public Tool(CodeExecution codeExecution) { - FunctionDeclarations = null; - GoogleSearch = null; - CodeExecution = codeExecution; - UrlContext = null; - } - - /// - /// Creates a tool that allows you to provide additional context to the models in the form of - /// public web URLs. + /// A tool that allows the model to execute code. + /// + /// This tool can be used to solve complex problems, for example, by generating and executing Python + /// code to solve a math problem. /// - /// An empty `UrlContext` object. The presence of this object - /// in the list of tools enables the model to use Url Contexts. - public Tool(UrlContext urlContext) { - FunctionDeclarations = null; - GoogleSearch = null; - CodeExecution = null; - UrlContext = urlContext; - } + public readonly struct CodeExecution { } /// - /// Intended for internal use only. - /// This method is used for serializing the object to JSON for the API request. + /// A helper tool that the model may use when generating responses. + /// + /// A `Tool` is a piece of code that enables the system to interact with external systems to + /// perform an action, or set of actions, outside of knowledge and scope of the model. /// - internal Dictionary ToJson() { - var json = new Dictionary(); - if (FunctionDeclarations != null && FunctionDeclarations.Any()) { - json["functionDeclarations"] = FunctionDeclarations.Select(f => f.ToJson()).ToList(); - } - if (GoogleSearch.HasValue) { - json["googleSearch"] = new Dictionary(); + public readonly struct Tool + { + // No public properties, on purpose since it is meant for user input only + + private List FunctionDeclarations { get; } + private GoogleSearch? GoogleSearch { get; } + private CodeExecution? CodeExecution { get; } + private UrlContext? UrlContext { get; } + + /// + /// Creates a tool that allows the model to perform function calling. + /// + /// A list of `FunctionDeclarations` available to the model + /// that can be used for function calling. + public Tool(params FunctionDeclaration[] functionDeclarations) + { + FunctionDeclarations = new List(functionDeclarations); + GoogleSearch = null; + CodeExecution = null; + UrlContext = null; } - if (CodeExecution.HasValue) { - json["codeExecution"] = new Dictionary(); + /// + /// Creates a tool that allows the model to perform function calling. + /// + /// A list of `FunctionDeclarations` available to the model + /// that can be used for function calling. + public Tool(IEnumerable functionDeclarations) + { + FunctionDeclarations = new List(functionDeclarations); + GoogleSearch = null; + CodeExecution = null; + UrlContext = null; } - if (UrlContext.HasValue) { - json["urlContext"] = new Dictionary(); + + /// + /// Creates a tool that allows the model to use Grounding with Google Search. + /// + /// An empty `GoogleSearch` object. The presence of this object + /// in the list of tools enables the model to use Google Search. + public Tool(GoogleSearch googleSearch) + { + FunctionDeclarations = null; + GoogleSearch = googleSearch; + CodeExecution = null; + UrlContext = null; } - return json; - } -} -/// -/// Tool configuration for any `Tool` specified in the request. -/// -public readonly struct ToolConfig { - // No public properties, on purpose since it is meant for user input only + /// + /// Creates a tool that allows the model to use Code Execution. + /// + /// An empty `CodeExecution` object. The presence of this object + /// in the list of tools enables the model to use Code Execution. + public Tool(CodeExecution codeExecution) + { + FunctionDeclarations = null; + GoogleSearch = null; + CodeExecution = codeExecution; + UrlContext = null; + } - private FunctionCallingConfig? Config { get; } + /// + /// Creates a tool that allows you to provide additional context to the models in the form of + /// public web URLs. + /// + /// An empty `UrlContext` object. The presence of this object + /// in the list of tools enables the model to use Url Contexts. + public Tool(UrlContext urlContext) + { + FunctionDeclarations = null; + GoogleSearch = null; + CodeExecution = null; + UrlContext = urlContext; + } - /// - /// Constructs a new `ToolConfig`. - /// - /// Configures how the model should use the - /// provided functions. - public ToolConfig(FunctionCallingConfig? functionCallingConfig = null) { - Config = functionCallingConfig; + /// + /// Intended for internal use only. + /// This method is used for serializing the object to JSON for the API request. + /// + internal Dictionary ToJson() + { + var json = new Dictionary(); + if (FunctionDeclarations != null && FunctionDeclarations.Any()) + { + json["functionDeclarations"] = FunctionDeclarations.Select(f => f.ToJson()).ToList(); + } + if (GoogleSearch.HasValue) + { + json["googleSearch"] = new Dictionary(); + } + if (CodeExecution.HasValue) + { + json["codeExecution"] = new Dictionary(); + } + if (UrlContext.HasValue) + { + json["urlContext"] = new Dictionary(); + } + return json; + } } /// - /// Intended for internal use only. - /// This method is used for serializing the object to JSON for the API request. + /// Tool configuration for any `Tool` specified in the request. /// - internal Dictionary ToJson() { - var json = new Dictionary(); - if (Config.HasValue) { - json["functionCallingConfig"] = Config?.ToJson(); - } - return json; - } -} + public readonly struct ToolConfig + { + // No public properties, on purpose since it is meant for user input only -/// -/// Configuration for specifying function calling behavior. -/// -public readonly struct FunctionCallingConfig { - // No public properties, on purpose since it is meant for user input only + private FunctionCallingConfig? Config { get; } - private string Mode { get; } - private List AllowedFunctionNames { get; } + /// + /// Constructs a new `ToolConfig`. + /// + /// Configures how the model should use the + /// provided functions. + public ToolConfig(FunctionCallingConfig? functionCallingConfig = null) + { + Config = functionCallingConfig; + } - private FunctionCallingConfig(string mode, IEnumerable allowedFunctionNames = null) { - Mode = mode; - if (allowedFunctionNames != null) { - AllowedFunctionNames = new List(allowedFunctionNames); - } else { - AllowedFunctionNames = null; + /// + /// Intended for internal use only. + /// This method is used for serializing the object to JSON for the API request. + /// + internal Dictionary ToJson() + { + var json = new Dictionary(); + if (Config.HasValue) + { + json["functionCallingConfig"] = Config?.ToJson(); + } + return json; } } /// - /// Creates a function calling config where the model calls functions at its discretion. - /// - /// > Note: This is the default behavior. + /// Configuration for specifying function calling behavior. /// - public static FunctionCallingConfig Auto() { - return new FunctionCallingConfig("AUTO"); - } + public readonly struct FunctionCallingConfig + { + // No public properties, on purpose since it is meant for user input only - /// - /// Creates a function calling config where the model will always call a provided function. - /// - /// A set of function names that, when provided, limits the - /// function that the model will call. - public static FunctionCallingConfig Any(params string[] allowedFunctionNames) { - return new FunctionCallingConfig("ANY", allowedFunctionNames); - } - /// - /// Creates a function calling config where the model will always call a provided function. - /// - /// A set of function names that, when provided, limits the - /// function that the model will call. - public static FunctionCallingConfig Any(IEnumerable allowedFunctionNames) { - return new FunctionCallingConfig("ANY", allowedFunctionNames); - } + private string Mode { get; } + private List AllowedFunctionNames { get; } - /// Creates a function calling config where the model will never call a function. - /// - /// > Note: This can also be achieved by not passing any `FunctionDeclaration` tools when - /// instantiating the model. - public static FunctionCallingConfig None() { - return new FunctionCallingConfig("NONE"); - } + private FunctionCallingConfig(string mode, IEnumerable allowedFunctionNames = null) + { + Mode = mode; + if (allowedFunctionNames != null) + { + AllowedFunctionNames = new List(allowedFunctionNames); + } + else + { + AllowedFunctionNames = null; + } + } - /// - /// Intended for internal use only. - /// This method is used for serializing the object to JSON for the API request. - /// - internal Dictionary ToJson() { - var json = new Dictionary() { + /// + /// Creates a function calling config where the model calls functions at its discretion. + /// + /// > Note: This is the default behavior. + /// + public static FunctionCallingConfig Auto() + { + return new FunctionCallingConfig("AUTO"); + } + + /// + /// Creates a function calling config where the model will always call a provided function. + /// + /// A set of function names that, when provided, limits the + /// function that the model will call. + public static FunctionCallingConfig Any(params string[] allowedFunctionNames) + { + return new FunctionCallingConfig("ANY", allowedFunctionNames); + } + /// + /// Creates a function calling config where the model will always call a provided function. + /// + /// A set of function names that, when provided, limits the + /// function that the model will call. + public static FunctionCallingConfig Any(IEnumerable allowedFunctionNames) + { + return new FunctionCallingConfig("ANY", allowedFunctionNames); + } + + /// Creates a function calling config where the model will never call a function. + /// + /// > Note: This can also be achieved by not passing any `FunctionDeclaration` tools when + /// instantiating the model. + public static FunctionCallingConfig None() + { + return new FunctionCallingConfig("NONE"); + } + + /// + /// Intended for internal use only. + /// This method is used for serializing the object to JSON for the API request. + /// + internal Dictionary ToJson() + { + var json = new Dictionary() { { "mode", Mode } }; - if (AllowedFunctionNames != null) { - json["allowedFunctionNames"] = AllowedFunctionNames; + if (AllowedFunctionNames != null) + { + json["allowedFunctionNames"] = AllowedFunctionNames; + } + return json; } - return json; } -} } diff --git a/firebaseai/src/GenerateContentResponse.cs b/firebaseai/src/GenerateContentResponse.cs index 6d1adae2..8dd167df 100644 --- a/firebaseai/src/GenerateContentResponse.cs +++ b/firebaseai/src/GenerateContentResponse.cs @@ -20,525 +20,582 @@ using Google.MiniJSON; using Firebase.AI.Internal; -namespace Firebase.AI { - -/// -/// The model's response to a generate content request. -/// -public readonly struct GenerateContentResponse { - private readonly IReadOnlyList _candidates; - +namespace Firebase.AI +{ /// - /// A list of candidate response content, ordered from best to worst. + /// The model's response to a generate content request. /// - public IReadOnlyList Candidates { - get { - return _candidates ?? new List(); - } - } + public readonly struct GenerateContentResponse + { + private readonly IReadOnlyList _candidates; - /// - /// A value containing the safety ratings for the response, or, - /// if the request was blocked, a reason for blocking the request. - /// - public PromptFeedback? PromptFeedback { get; } - - /// - /// Token usage metadata for processing the generate content request. - /// - public UsageMetadata? UsageMetadata { get; } + /// + /// A list of candidate response content, ordered from best to worst. + /// + public IReadOnlyList Candidates + { + get + { + return _candidates ?? new List(); + } + } - /// - /// The response's content as text, if it exists. - /// - public string Text { - get { - // Concatenate all of the text parts that aren't thoughts from the first candidate. - return string.Join(" ", - Candidates.FirstOrDefault().Content.Parts - .OfType().Where(tp => !tp.IsThought).Select(tp => tp.Text)); + /// + /// A value containing the safety ratings for the response, or, + /// if the request was blocked, a reason for blocking the request. + /// + public PromptFeedback? PromptFeedback { get; } + + /// + /// Token usage metadata for processing the generate content request. + /// + public UsageMetadata? UsageMetadata { get; } + + /// + /// The response's content as text, if it exists. + /// + public string Text + { + get + { + // Concatenate all of the text parts that aren't thoughts from the first candidate. + return string.Join(" ", + Candidates.FirstOrDefault().Content.Parts + .OfType().Where(tp => !tp.IsThought).Select(tp => tp.Text)); + } } - } - - /// - /// A summary of the model's thinking process, if available. - /// - /// Note that Thought Summaries are only available when `IncludeThoughts` is enabled - /// in the `ThinkingConfig`. For more information, see the - /// [Thinking](https://firebase.google.com/docs/ai-logic/thinking) documentation. - /// - public string ThoughtSummary { - get { - // Concatenate all of the text parts that are thoughts from the first candidate. - return string.Join(" ", - Candidates.FirstOrDefault().Content.Parts - .OfType().Where(tp => tp.IsThought).Select(tp => tp.Text)); + + /// + /// A summary of the model's thinking process, if available. + /// + /// Note that Thought Summaries are only available when `IncludeThoughts` is enabled + /// in the `ThinkingConfig`. For more information, see the + /// [Thinking](https://firebase.google.com/docs/ai-logic/thinking) documentation. + /// + public string ThoughtSummary + { + get + { + // Concatenate all of the text parts that are thoughts from the first candidate. + return string.Join(" ", + Candidates.FirstOrDefault().Content.Parts + .OfType().Where(tp => tp.IsThought).Select(tp => tp.Text)); + } } - } - /// - /// Returns function calls found in any `Part`s of the first candidate of the response, if any. - /// - public IReadOnlyList FunctionCalls { - get { - return Candidates.FirstOrDefault().Content.Parts - .OfType().Where(tp => !tp.IsThought).ToList(); + /// + /// Returns function calls found in any `Part`s of the first candidate of the response, if any. + /// + public IReadOnlyList FunctionCalls + { + get + { + return Candidates.FirstOrDefault().Content.Parts + .OfType().Where(tp => !tp.IsThought).ToList(); + } } - } - // Hidden constructor, users don't need to make this. - private GenerateContentResponse(List candidates, PromptFeedback? promptFeedback, - UsageMetadata? usageMetadata) { - _candidates = candidates; - PromptFeedback = promptFeedback; - UsageMetadata = usageMetadata; - } + // Hidden constructor, users don't need to make this. + private GenerateContentResponse(List candidates, PromptFeedback? promptFeedback, + UsageMetadata? usageMetadata) + { + _candidates = candidates; + PromptFeedback = promptFeedback; + UsageMetadata = usageMetadata; + } - /// - /// Intended for internal use only. - /// This method is used for deserializing JSON responses and should not be called directly. - /// - internal static GenerateContentResponse FromJson(string jsonString, - FirebaseAI.Backend.InternalProvider backend) { - return FromJson(Json.Deserialize(jsonString) as Dictionary, backend); - } + /// + /// Intended for internal use only. + /// This method is used for deserializing JSON responses and should not be called directly. + /// + internal static GenerateContentResponse FromJson(string jsonString, + FirebaseAI.Backend.InternalProvider backend) + { + return FromJson(Json.Deserialize(jsonString) as Dictionary, backend); + } - /// - /// Intended for internal use only. - /// This method is used for deserializing JSON responses and should not be called directly. - /// - internal static GenerateContentResponse FromJson(Dictionary jsonDict, - FirebaseAI.Backend.InternalProvider backend) { - return new GenerateContentResponse( - jsonDict.ParseObjectList("candidates", (d) => Candidate.FromJson(d, backend)), - jsonDict.ParseNullableObject("promptFeedback", - Firebase.AI.PromptFeedback.FromJson), - jsonDict.ParseNullableObject("usageMetadata", - Firebase.AI.UsageMetadata.FromJson)); + /// + /// Intended for internal use only. + /// This method is used for deserializing JSON responses and should not be called directly. + /// + internal static GenerateContentResponse FromJson(Dictionary jsonDict, + FirebaseAI.Backend.InternalProvider backend) + { + return new GenerateContentResponse( + jsonDict.ParseObjectList("candidates", (d) => Candidate.FromJson(d, backend)), + jsonDict.ParseNullableObject("promptFeedback", + Firebase.AI.PromptFeedback.FromJson), + jsonDict.ParseNullableObject("usageMetadata", + Firebase.AI.UsageMetadata.FromJson)); + } } -} -/// -/// A type describing possible reasons to block a prompt. -/// -public enum BlockReason { /// - /// A new and not yet supported value. - /// - Unknown = 0, - /// - /// The prompt was blocked because it was deemed unsafe. - /// - Safety, - /// - /// All other block reasons. - /// - Other, - /// - /// The prompt was blocked because it contained terms from the terminology blocklist. - /// - Blocklist, - /// - /// The prompt was blocked due to prohibited content. - /// - ProhibitedContent, -} - -/// -/// A metadata struct containing any feedback the model had on the prompt it was provided. -/// -public readonly struct PromptFeedback { - private readonly IReadOnlyList _safetyRatings; - - /// - /// The reason a prompt was blocked, if it was blocked. - /// - public BlockReason? BlockReason { get; } - /// - /// A human-readable description of the `BlockReason`. - /// - public string BlockReasonMessage { get; } - /// - /// The safety ratings of the prompt. - /// - public IReadOnlyList SafetyRatings { - get { - return _safetyRatings ?? new List(); + /// A type describing possible reasons to block a prompt. + /// + public enum BlockReason + { + /// + /// A new and not yet supported value. + /// + Unknown = 0, + /// + /// The prompt was blocked because it was deemed unsafe. + /// + Safety, + /// + /// All other block reasons. + /// + Other, + /// + /// The prompt was blocked because it contained terms from the terminology blocklist. + /// + Blocklist, + /// + /// The prompt was blocked due to prohibited content. + /// + ProhibitedContent, + } + + /// + /// A metadata struct containing any feedback the model had on the prompt it was provided. + /// + public readonly struct PromptFeedback + { + private readonly IReadOnlyList _safetyRatings; + + /// + /// The reason a prompt was blocked, if it was blocked. + /// + public BlockReason? BlockReason { get; } + /// + /// A human-readable description of the `BlockReason`. + /// + public string BlockReasonMessage { get; } + /// + /// The safety ratings of the prompt. + /// + public IReadOnlyList SafetyRatings + { + get + { + return _safetyRatings ?? new List(); + } } - } - // Hidden constructor, users don't need to make this. - private PromptFeedback(BlockReason? blockReason, string blockReasonMessage, - List safetyRatings) { - BlockReason = blockReason; - BlockReasonMessage = blockReasonMessage; - _safetyRatings = safetyRatings; - } + // Hidden constructor, users don't need to make this. + private PromptFeedback(BlockReason? blockReason, string blockReasonMessage, + List safetyRatings) + { + BlockReason = blockReason; + BlockReasonMessage = blockReasonMessage; + _safetyRatings = safetyRatings; + } - private static BlockReason ParseBlockReason(string str) { - return str switch { - "SAFETY" => Firebase.AI.BlockReason.Safety, - "OTHER" => Firebase.AI.BlockReason.Other, - "BLOCKLIST" => Firebase.AI.BlockReason.Blocklist, - "PROHIBITED_CONTENT" => Firebase.AI.BlockReason.ProhibitedContent, - _ => Firebase.AI.BlockReason.Unknown, - }; - } + private static BlockReason ParseBlockReason(string str) + { + return str switch + { + "SAFETY" => Firebase.AI.BlockReason.Safety, + "OTHER" => Firebase.AI.BlockReason.Other, + "BLOCKLIST" => Firebase.AI.BlockReason.Blocklist, + "PROHIBITED_CONTENT" => Firebase.AI.BlockReason.ProhibitedContent, + _ => Firebase.AI.BlockReason.Unknown, + }; + } - /// - /// Intended for internal use only. - /// This method is used for deserializing JSON responses and should not be called directly. - /// - internal static PromptFeedback FromJson(Dictionary jsonDict) { - return new PromptFeedback( - jsonDict.ParseNullableEnum("blockReason", ParseBlockReason), - jsonDict.ParseValue("blockReasonMessage"), - jsonDict.ParseObjectList("safetyRatings", SafetyRating.FromJson)); + /// + /// Intended for internal use only. + /// This method is used for deserializing JSON responses and should not be called directly. + /// + internal static PromptFeedback FromJson(Dictionary jsonDict) + { + return new PromptFeedback( + jsonDict.ParseNullableEnum("blockReason", ParseBlockReason), + jsonDict.ParseValue("blockReasonMessage"), + jsonDict.ParseObjectList("safetyRatings", SafetyRating.FromJson)); + } } -} - -/// -/// Metadata returned to the client when grounding is enabled. -/// -/// > Important: If using Grounding with Google Search, you are required to comply with the -/// "Grounding with Google Search" usage requirements for your chosen API provider: -/// [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search) -/// or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms) -/// section within the Service Specific Terms). -/// -public readonly struct GroundingMetadata { - private readonly IReadOnlyList _webSearchQueries; - private readonly IReadOnlyList _groundingChunks; - private readonly IReadOnlyList _groundingSupports; /// - /// A list of web search queries that the model performed to gather the grounding information. - /// These can be used to allow users to explore the search results themselves. - /// - public IReadOnlyList WebSearchQueries { - get { - return _webSearchQueries ?? new List(); + /// Metadata returned to the client when grounding is enabled. + /// + /// > Important: If using Grounding with Google Search, you are required to comply with the + /// "Grounding with Google Search" usage requirements for your chosen API provider: + /// [Gemini Developer API](https://ai.google.dev/gemini-api/terms#grounding-with-google-search) + /// or Vertex AI Gemini API (see [Service Terms](https://cloud.google.com/terms/service-terms) + /// section within the Service Specific Terms). + /// + public readonly struct GroundingMetadata + { + private readonly IReadOnlyList _webSearchQueries; + private readonly IReadOnlyList _groundingChunks; + private readonly IReadOnlyList _groundingSupports; + + /// + /// A list of web search queries that the model performed to gather the grounding information. + /// These can be used to allow users to explore the search results themselves. + /// + public IReadOnlyList WebSearchQueries + { + get + { + return _webSearchQueries ?? new List(); + } } - } - /// - /// A list of `GroundingChunk` structs. Each chunk represents a piece of retrieved content - /// (e.g., from a web page) that the model used to ground its response. - /// - public IReadOnlyList GroundingChunks { - get { - return _groundingChunks ?? new List(); + /// + /// A list of `GroundingChunk` structs. Each chunk represents a piece of retrieved content + /// (e.g., from a web page) that the model used to ground its response. + /// + public IReadOnlyList GroundingChunks + { + get + { + return _groundingChunks ?? new List(); + } } - } - /// - /// A list of `GroundingSupport` structs. Each object details how specific segments of the - /// model's response are supported by the `groundingChunks`. - /// - public IReadOnlyList GroundingSupports { - get { - return _groundingSupports ?? new List(); + /// + /// A list of `GroundingSupport` structs. Each object details how specific segments of the + /// model's response are supported by the `groundingChunks`. + /// + public IReadOnlyList GroundingSupports + { + get + { + return _groundingSupports ?? new List(); + } } - } - /// - /// Google Search entry point for web searches. - /// This contains an HTML/CSS snippet that **must** be embedded in an app to display a Google - /// Search entry point for follow-up web searches related to the model's "Grounded Response". - /// - public SearchEntryPoint? SearchEntryPoint { get; } - - private GroundingMetadata(List webSearchQueries, List groundingChunks, - List groundingSupports, SearchEntryPoint? searchEntryPoint) { - _webSearchQueries = webSearchQueries; - _groundingChunks = groundingChunks; - _groundingSupports = groundingSupports; - SearchEntryPoint = searchEntryPoint; - } + /// + /// Google Search entry point for web searches. + /// This contains an HTML/CSS snippet that **must** be embedded in an app to display a Google + /// Search entry point for follow-up web searches related to the model's "Grounded Response". + /// + public SearchEntryPoint? SearchEntryPoint { get; } - internal static GroundingMetadata FromJson(Dictionary jsonDict) { - List supports = null; - if (jsonDict.TryParseValue("groundingSupports", out List supportListRaw)) + private GroundingMetadata(List webSearchQueries, List groundingChunks, + List groundingSupports, SearchEntryPoint? searchEntryPoint) { - supports = supportListRaw - .OfType>() - .Where(d => d.ContainsKey("segment")) // Filter out if segment is missing - .Select(GroundingSupport.FromJson) - .ToList(); + _webSearchQueries = webSearchQueries; + _groundingChunks = groundingChunks; + _groundingSupports = groundingSupports; + SearchEntryPoint = searchEntryPoint; } - return new GroundingMetadata( - jsonDict.ParseStringList("webSearchQueries"), - jsonDict.ParseObjectList("groundingChunks", GroundingChunk.FromJson), - supports, - jsonDict.ParseNullableObject("searchEntryPoint", Firebase.AI.SearchEntryPoint.FromJson) - ); + internal static GroundingMetadata FromJson(Dictionary jsonDict) + { + List supports = null; + if (jsonDict.TryParseValue("groundingSupports", out List supportListRaw)) + { + supports = supportListRaw + .OfType>() + .Where(d => d.ContainsKey("segment")) // Filter out if segment is missing + .Select(GroundingSupport.FromJson) + .ToList(); + } + + return new GroundingMetadata( + jsonDict.ParseStringList("webSearchQueries"), + jsonDict.ParseObjectList("groundingChunks", GroundingChunk.FromJson), + supports, + jsonDict.ParseNullableObject("searchEntryPoint", Firebase.AI.SearchEntryPoint.FromJson) + ); + } } -} -/// -/// A struct representing the Google Search entry point. -/// -public readonly struct SearchEntryPoint { /// - /// An HTML/CSS snippet that can be embedded in your app. - /// - /// To ensure proper rendering, it's recommended to display this content within a web view component. + /// A struct representing the Google Search entry point. /// - public string RenderedContent { get; } + public readonly struct SearchEntryPoint + { + /// + /// An HTML/CSS snippet that can be embedded in your app. + /// + /// To ensure proper rendering, it's recommended to display this content within a web view component. + /// + public string RenderedContent { get; } - private SearchEntryPoint(string renderedContent) { - RenderedContent = renderedContent; - } + private SearchEntryPoint(string renderedContent) + { + RenderedContent = renderedContent; + } - internal static SearchEntryPoint FromJson(Dictionary jsonDict) { - return new SearchEntryPoint( - jsonDict.ParseValue("renderedContent", JsonParseOptions.ThrowEverything) - ); + internal static SearchEntryPoint FromJson(Dictionary jsonDict) + { + return new SearchEntryPoint( + jsonDict.ParseValue("renderedContent", JsonParseOptions.ThrowEverything) + ); + } } -} -/// -/// Represents a chunk of retrieved data that supports a claim in the model's response. This is -/// part of the grounding information provided when grounding is enabled. -/// -public readonly struct GroundingChunk { /// - /// Contains details if the grounding chunk is from a web source. + /// Represents a chunk of retrieved data that supports a claim in the model's response. This is + /// part of the grounding information provided when grounding is enabled. /// - public WebGroundingChunk? Web { get; } + public readonly struct GroundingChunk + { + /// + /// Contains details if the grounding chunk is from a web source. + /// + public WebGroundingChunk? Web { get; } - private GroundingChunk(WebGroundingChunk? web) { - Web = web; - } + private GroundingChunk(WebGroundingChunk? web) + { + Web = web; + } - internal static GroundingChunk FromJson(Dictionary jsonDict) { - return new GroundingChunk( - jsonDict.ParseNullableObject("web", WebGroundingChunk.FromJson) - ); + internal static GroundingChunk FromJson(Dictionary jsonDict) + { + return new GroundingChunk( + jsonDict.ParseNullableObject("web", WebGroundingChunk.FromJson) + ); + } } -} -/// -/// A grounding chunk sourced from the web. -/// -public readonly struct WebGroundingChunk { /// - /// The URI of the retrieved web page. + /// A grounding chunk sourced from the web. /// - public System.Uri Uri { get; } - /// - /// The title of the retrieved web page. - /// - public string Title { get; } - /// - /// The domain of the original URI from which the content was retrieved. - /// - /// This field is only populated when using the Vertex AI Gemini API. - /// - public string Domain { get; } + public readonly struct WebGroundingChunk + { + /// + /// The URI of the retrieved web page. + /// + public System.Uri Uri { get; } + /// + /// The title of the retrieved web page. + /// + public string Title { get; } + /// + /// The domain of the original URI from which the content was retrieved. + /// + /// This field is only populated when using the Vertex AI Gemini API. + /// + public string Domain { get; } - private WebGroundingChunk(System.Uri uri, string title, string domain) { - Uri = uri; - Title = title; - Domain = domain; - } - - internal static WebGroundingChunk FromJson(Dictionary jsonDict) { - Uri uri = null; - if (jsonDict.TryParseValue("uri", out string uriString)) { - uri = new Uri(uriString); + private WebGroundingChunk(System.Uri uri, string title, string domain) + { + Uri = uri; + Title = title; + Domain = domain; } - return new WebGroundingChunk( - uri, - jsonDict.ParseValue("title"), - jsonDict.ParseValue("domain") - ); + internal static WebGroundingChunk FromJson(Dictionary jsonDict) + { + Uri uri = null; + if (jsonDict.TryParseValue("uri", out string uriString)) + { + uri = new Uri(uriString); + } + + return new WebGroundingChunk( + uri, + jsonDict.ParseValue("title"), + jsonDict.ParseValue("domain") + ); + } } -} - -/// -/// Provides information about how a specific segment of the model's response is supported by the -/// retrieved grounding chunks. -/// -public readonly struct GroundingSupport { - private readonly IReadOnlyList _groundingChunkIndices; /// - /// Specifies the segment of the model's response content that this grounding support pertains - /// to. + /// Provides information about how a specific segment of the model's response is supported by the + /// retrieved grounding chunks. /// - public Segment Segment { get; } + public readonly struct GroundingSupport + { + private readonly IReadOnlyList _groundingChunkIndices; - /// - /// A list of indices that refer to specific `GroundingChunk` structs within the - /// `GroundingMetadata.GroundingChunks` array. These referenced chunks are the sources that - /// support the claim made in the associated `segment` of the response. For example, an array - /// `[1, 3, 4]` - /// means that `groundingChunks[1]`, `groundingChunks[3]`, `groundingChunks[4]` are the - /// retrieved content supporting this part of the response. - /// - public IReadOnlyList GroundingChunkIndices { - get { - return _groundingChunkIndices ?? new List(); - } - } + /// + /// Specifies the segment of the model's response content that this grounding support pertains + /// to. + /// + public Segment Segment { get; } - private GroundingSupport(Segment segment, List groundingChunkIndices) { - Segment = segment; - _groundingChunkIndices = groundingChunkIndices; - } + /// + /// A list of indices that refer to specific `GroundingChunk` structs within the + /// `GroundingMetadata.GroundingChunks` array. These referenced chunks are the sources that + /// support the claim made in the associated `segment` of the response. For example, an array + /// `[1, 3, 4]` + /// means that `groundingChunks[1]`, `groundingChunks[3]`, `groundingChunks[4]` are the + /// retrieved content supporting this part of the response. + /// + public IReadOnlyList GroundingChunkIndices + { + get + { + return _groundingChunkIndices ?? new List(); + } + } - internal static GroundingSupport FromJson(Dictionary jsonDict) { - List indices = new List(); - if (jsonDict.TryParseValue("groundingChunkIndices", out List indicesRaw)) { - indices = indicesRaw.OfType().Select(l => (int)l).ToList(); + private GroundingSupport(Segment segment, List groundingChunkIndices) + { + Segment = segment; + _groundingChunkIndices = groundingChunkIndices; } - return new GroundingSupport( - jsonDict.ParseObject("segment", Segment.FromJson, JsonParseOptions.ThrowEverything), - indices - ); + internal static GroundingSupport FromJson(Dictionary jsonDict) + { + List indices = new List(); + if (jsonDict.TryParseValue("groundingChunkIndices", out List indicesRaw)) + { + indices = indicesRaw.OfType().Select(l => (int)l).ToList(); + } + + return new GroundingSupport( + jsonDict.ParseObject("segment", Segment.FromJson, JsonParseOptions.ThrowEverything), + indices + ); + } } -} -/// -/// Represents a specific segment within a `ModelContent` struct, often used to pinpoint the -/// exact location of text or data that grounding information refers to. -/// -public readonly struct Segment { - /// - /// The zero-based index of the `Part` object within the `parts` array of its parent - /// `ModelContent` object. This identifies which part of the content the segment belongs to. - /// - public int PartIndex { get; } - /// - /// The zero-based start index of the segment within the specified `Part`, measured in UTF-8 - /// bytes. This offset is inclusive, starting from 0 at the beginning of the part's content. - /// - public int StartIndex { get; } /// - /// The zero-based end index of the segment within the specified `Part`, measured in UTF-8 - /// bytes. This offset is exclusive, meaning the character at this index is not included in the - /// segment. - /// - public int EndIndex { get; } - /// - /// The text corresponding to the segment from the response. - /// - public string Text { get; } - - private Segment(int partIndex, int startIndex, int endIndex, string text) { - PartIndex = partIndex; - StartIndex = startIndex; - EndIndex = endIndex; - Text = text; - } + /// Represents a specific segment within a `ModelContent` struct, often used to pinpoint the + /// exact location of text or data that grounding information refers to. + /// + public readonly struct Segment + { + /// + /// The zero-based index of the `Part` object within the `parts` array of its parent + /// `ModelContent` object. This identifies which part of the content the segment belongs to. + /// + public int PartIndex { get; } + /// + /// The zero-based start index of the segment within the specified `Part`, measured in UTF-8 + /// bytes. This offset is inclusive, starting from 0 at the beginning of the part's content. + /// + public int StartIndex { get; } + /// + /// The zero-based end index of the segment within the specified `Part`, measured in UTF-8 + /// bytes. This offset is exclusive, meaning the character at this index is not included in the + /// segment. + /// + public int EndIndex { get; } + /// + /// The text corresponding to the segment from the response. + /// + public string Text { get; } + + private Segment(int partIndex, int startIndex, int endIndex, string text) + { + PartIndex = partIndex; + StartIndex = startIndex; + EndIndex = endIndex; + Text = text; + } - internal static Segment FromJson(Dictionary jsonDict) { - return new Segment( - jsonDict.ParseValue("partIndex"), - jsonDict.ParseValue("startIndex"), - jsonDict.ParseValue("endIndex"), - jsonDict.ParseValue("text") - ); + internal static Segment FromJson(Dictionary jsonDict) + { + return new Segment( + jsonDict.ParseValue("partIndex"), + jsonDict.ParseValue("startIndex"), + jsonDict.ParseValue("endIndex"), + jsonDict.ParseValue("text") + ); + } } -} - -/// -/// Token usage metadata for processing the generate content request. -/// -public readonly struct UsageMetadata { - /// - /// The number of tokens in the request prompt. - /// - public int PromptTokenCount { get; } - /// - /// The total number of tokens across the generated response candidates. - /// - public int CandidatesTokenCount { get; } - /// - /// The number of tokens used by the model's internal "thinking" process. - /// - /// For models that support thinking (like Gemini 2.5 Pro and Flash), this represents the actual - /// number of tokens consumed for reasoning before the model generated a response. For models - /// that do not support thinking, this value will be `0`. - /// - /// When thinking is used, this count will be less than or equal to the `thinkingBudget` set in - /// the `ThinkingConfig`. - /// - public int ThoughtsTokenCount { get; } - /// - /// The number of tokens used by any enabled tools. - /// - public int ToolUsePromptTokenCount { get; } - /// - /// The total number of tokens in both the request and response. - /// - public int TotalTokenCount { get; } - private readonly IReadOnlyList _promptTokensDetails; /// - /// The breakdown, by modality, of how many tokens are consumed by the prompt. + /// Token usage metadata for processing the generate content request. /// - public IReadOnlyList PromptTokensDetails { - get { - return _promptTokensDetails ?? new List(); + public readonly struct UsageMetadata + { + /// + /// The number of tokens in the request prompt. + /// + public int PromptTokenCount { get; } + /// + /// The total number of tokens across the generated response candidates. + /// + public int CandidatesTokenCount { get; } + /// + /// The number of tokens used by the model's internal "thinking" process. + /// + /// For models that support thinking (like Gemini 2.5 Pro and Flash), this represents the actual + /// number of tokens consumed for reasoning before the model generated a response. For models + /// that do not support thinking, this value will be `0`. + /// + /// When thinking is used, this count will be less than or equal to the `thinkingBudget` set in + /// the `ThinkingConfig`. + /// + public int ThoughtsTokenCount { get; } + /// + /// The number of tokens used by any enabled tools. + /// + public int ToolUsePromptTokenCount { get; } + /// + /// The total number of tokens in both the request and response. + /// + public int TotalTokenCount { get; } + + private readonly IReadOnlyList _promptTokensDetails; + /// + /// The breakdown, by modality, of how many tokens are consumed by the prompt. + /// + public IReadOnlyList PromptTokensDetails + { + get + { + return _promptTokensDetails ?? new List(); + } } - } - private readonly IReadOnlyList _candidatesTokensDetails; - /// - /// The breakdown, by modality, of how many tokens are consumed by the candidates. - /// - public IReadOnlyList CandidatesTokensDetails { - get { - return _candidatesTokensDetails ?? new List(); + private readonly IReadOnlyList _candidatesTokensDetails; + /// + /// The breakdown, by modality, of how many tokens are consumed by the candidates. + /// + public IReadOnlyList CandidatesTokensDetails + { + get + { + return _candidatesTokensDetails ?? new List(); + } } - } - private readonly IReadOnlyList _toolUsePromptTokensDetails; - /// - /// The breakdown, by modality, of how many tokens were consumed by the tools used to process - /// the request. - /// - public IReadOnlyList ToolUsePromptTokensDetails { - get { - return _toolUsePromptTokensDetails ?? new List(); + private readonly IReadOnlyList _toolUsePromptTokensDetails; + /// + /// The breakdown, by modality, of how many tokens were consumed by the tools used to process + /// the request. + /// + public IReadOnlyList ToolUsePromptTokensDetails + { + get + { + return _toolUsePromptTokensDetails ?? new List(); + } } - } - // Hidden constructor, users don't need to make this. - private UsageMetadata(int promptTC, int candidatesTC, int thoughtsTC, int toolUseTC, int totalTC, - List promptDetails, List candidateDetails, - List toolUseDetails) { - PromptTokenCount = promptTC; - CandidatesTokenCount = candidatesTC; - ThoughtsTokenCount = thoughtsTC; - ToolUsePromptTokenCount = toolUseTC; - TotalTokenCount = totalTC; - _promptTokensDetails = promptDetails; - _candidatesTokensDetails = candidateDetails; - _toolUsePromptTokensDetails = toolUseDetails; - } + // Hidden constructor, users don't need to make this. + private UsageMetadata(int promptTC, int candidatesTC, int thoughtsTC, int toolUseTC, int totalTC, + List promptDetails, List candidateDetails, + List toolUseDetails) + { + PromptTokenCount = promptTC; + CandidatesTokenCount = candidatesTC; + ThoughtsTokenCount = thoughtsTC; + ToolUsePromptTokenCount = toolUseTC; + TotalTokenCount = totalTC; + _promptTokensDetails = promptDetails; + _candidatesTokensDetails = candidateDetails; + _toolUsePromptTokensDetails = toolUseDetails; + } - /// - /// Intended for internal use only. - /// This method is used for deserializing JSON responses and should not be called directly. - /// - internal static UsageMetadata FromJson(Dictionary jsonDict) { - return new UsageMetadata( - jsonDict.ParseValue("promptTokenCount"), - jsonDict.ParseValue("candidatesTokenCount"), - jsonDict.ParseValue("thoughtsTokenCount"), - jsonDict.ParseValue("toolUsePromptTokenCount"), - jsonDict.ParseValue("totalTokenCount"), - jsonDict.ParseObjectList("promptTokensDetails", ModalityTokenCount.FromJson), - jsonDict.ParseObjectList("candidatesTokensDetails", ModalityTokenCount.FromJson), - jsonDict.ParseObjectList("toolUsePromptTokensDetails", ModalityTokenCount.FromJson)); + /// + /// Intended for internal use only. + /// This method is used for deserializing JSON responses and should not be called directly. + /// + internal static UsageMetadata FromJson(Dictionary jsonDict) + { + return new UsageMetadata( + jsonDict.ParseValue("promptTokenCount"), + jsonDict.ParseValue("candidatesTokenCount"), + jsonDict.ParseValue("thoughtsTokenCount"), + jsonDict.ParseValue("toolUsePromptTokenCount"), + jsonDict.ParseValue("totalTokenCount"), + jsonDict.ParseObjectList("promptTokensDetails", ModalityTokenCount.FromJson), + jsonDict.ParseObjectList("candidatesTokensDetails", ModalityTokenCount.FromJson), + jsonDict.ParseObjectList("toolUsePromptTokensDetails", ModalityTokenCount.FromJson)); + } } -} } diff --git a/firebaseai/src/GenerationConfig.cs b/firebaseai/src/GenerationConfig.cs index e4a0ca97..ba53aa2e 100644 --- a/firebaseai/src/GenerationConfig.cs +++ b/firebaseai/src/GenerationConfig.cs @@ -19,13 +19,14 @@ using System.Linq; using Firebase.AI.Internal; -namespace Firebase.AI { - +namespace Firebase.AI +{ /// /// A struct defining model parameters to be used when sending generative AI /// requests to the backend model. /// - public readonly struct GenerationConfig { + public readonly struct GenerationConfig + { private readonly float? _temperature; private readonly float? _topP; private readonly float? _topK; @@ -168,7 +169,8 @@ public GenerationConfig( string responseMimeType = null, Schema responseSchema = null, IEnumerable responseModalities = null, - ThinkingConfig? thinkingConfig = null) { + ThinkingConfig? thinkingConfig = null) + { _temperature = temperature; _topP = topP; _topK = topK; @@ -188,7 +190,8 @@ public GenerationConfig( /// Intended for internal use only. /// This method is used for serializing the object to JSON for the API request. /// - internal Dictionary ToJson() { + internal Dictionary ToJson() + { Dictionary jsonDict = new(); if (_temperature.HasValue) jsonDict["temperature"] = _temperature.Value; if (_topP.HasValue) jsonDict["topP"] = _topP.Value; @@ -200,7 +203,8 @@ internal Dictionary ToJson() { if (_stopSequences != null && _stopSequences.Length > 0) jsonDict["stopSequences"] = _stopSequences; if (!string.IsNullOrWhiteSpace(_responseMimeType)) jsonDict["responseMimeType"] = _responseMimeType; if (_responseSchema != null) jsonDict["responseSchema"] = _responseSchema.ToJson(); - if (_responseModalities != null && _responseModalities.Count > 0) { + if (_responseModalities != null && _responseModalities.Count > 0) + { jsonDict["responseModalities"] = _responseModalities.Select(EnumConverters.ResponseModalityToString).ToList(); } @@ -213,7 +217,8 @@ internal Dictionary ToJson() { /// /// Configuration options for Thinking features. /// - public readonly struct ThinkingConfig { + public readonly struct ThinkingConfig + { #if !DOXYGEN public readonly int? ThinkingBudget { get; } public readonly bool? IncludeThoughts { get; } @@ -226,7 +231,8 @@ public readonly struct ThinkingConfig { /// /// If true, summaries of the model's "thoughts" are included in responses. /// - public ThinkingConfig(int? thinkingBudget = null, bool? includeThoughts = null) { + public ThinkingConfig(int? thinkingBudget = null, bool? includeThoughts = null) + { ThinkingBudget = thinkingBudget; IncludeThoughts = includeThoughts; } @@ -235,7 +241,8 @@ public ThinkingConfig(int? thinkingBudget = null, bool? includeThoughts = null) /// Intended for internal use only. /// This method is used for serializing the object to JSON for the API request. /// - internal Dictionary ToJson() { + internal Dictionary ToJson() + { Dictionary jsonDict = new(); jsonDict.AddIfHasValue("thinkingBudget", ThinkingBudget); jsonDict.AddIfHasValue("includeThoughts", IncludeThoughts); diff --git a/firebaseai/src/GenerativeModel.cs b/firebaseai/src/GenerativeModel.cs index e45802e1..e96c891b 100644 --- a/firebaseai/src/GenerativeModel.cs +++ b/firebaseai/src/GenerativeModel.cs @@ -26,335 +26,369 @@ using Google.MiniJSON; using Firebase.AI.Internal; -namespace Firebase.AI { - -/// -/// A type that represents a remote multimodal model (like Gemini), with the ability to generate -/// content based on various input types. -/// -public class GenerativeModel { - private readonly FirebaseApp _firebaseApp; - - // Various setting fields provided by the user. - private readonly FirebaseAI.Backend _backend; - private readonly string _modelName; - private readonly GenerationConfig? _generationConfig; - private readonly SafetySetting[] _safetySettings; - private readonly Tool[] _tools; - private readonly ToolConfig? _toolConfig; - private readonly ModelContent? _systemInstruction; - private readonly RequestOptions? _requestOptions; - - private readonly HttpClient _httpClient; - // String prefix to look for when handling streaming a response. - private const string StreamPrefix = "data: "; - +namespace Firebase.AI +{ /// - /// Intended for internal use only. - /// Use `FirebaseAI.GetGenerativeModel` instead to ensure proper initialization and configuration of the `GenerativeModel`. + /// A type that represents a remote multimodal model (like Gemini), with the ability to generate + /// content based on various input types. /// - internal GenerativeModel(FirebaseApp firebaseApp, - FirebaseAI.Backend backend, - string modelName, - GenerationConfig? generationConfig = null, - SafetySetting[] safetySettings = null, - Tool[] tools = null, - ToolConfig? toolConfig = null, - ModelContent? systemInstruction = null, - RequestOptions? requestOptions = null) { - _firebaseApp = firebaseApp; - _backend = backend; - _modelName = modelName; - _generationConfig = generationConfig; - _safetySettings = safetySettings; - _tools = tools; - _toolConfig = toolConfig; - // Make sure that the system instructions have the role "system". - _systemInstruction = systemInstruction?.ConvertToSystem(); - _requestOptions = requestOptions; - - // Create a HttpClient using the timeout requested, or the default one. - _httpClient = new HttpClient() { - Timeout = requestOptions?.Timeout ?? RequestOptions.DefaultTimeout - }; - } + public class GenerativeModel + { + private readonly FirebaseApp _firebaseApp; + + // Various setting fields provided by the user. + private readonly FirebaseAI.Backend _backend; + private readonly string _modelName; + private readonly GenerationConfig? _generationConfig; + private readonly SafetySetting[] _safetySettings; + private readonly Tool[] _tools; + private readonly ToolConfig? _toolConfig; + private readonly ModelContent? _systemInstruction; + private readonly RequestOptions? _requestOptions; + + private readonly HttpClient _httpClient; + // String prefix to look for when handling streaming a response. + private const string StreamPrefix = "data: "; + + /// + /// Intended for internal use only. + /// Use `FirebaseAI.GetGenerativeModel` instead to ensure proper initialization and configuration of the `GenerativeModel`. + /// + internal GenerativeModel(FirebaseApp firebaseApp, + FirebaseAI.Backend backend, + string modelName, + GenerationConfig? generationConfig = null, + SafetySetting[] safetySettings = null, + Tool[] tools = null, + ToolConfig? toolConfig = null, + ModelContent? systemInstruction = null, + RequestOptions? requestOptions = null) + { + _firebaseApp = firebaseApp; + _backend = backend; + _modelName = modelName; + _generationConfig = generationConfig; + _safetySettings = safetySettings; + _tools = tools; + _toolConfig = toolConfig; + // Make sure that the system instructions have the role "system". + _systemInstruction = systemInstruction?.ConvertToSystem(); + _requestOptions = requestOptions; + + // Create a HttpClient using the timeout requested, or the default one. + _httpClient = new HttpClient() + { + Timeout = requestOptions?.Timeout ?? RequestOptions.DefaultTimeout + }; + } -#region Public API - /// - /// Generates new content from input `ModelContent` given to the model as a prompt. - /// - /// The input given to the model as a prompt. - /// An optional token to cancel the operation. - /// The generated content response from the model. - /// Thrown when an error occurs during content generation. - public Task GenerateContentAsync( - ModelContent content, CancellationToken cancellationToken = default) { - return GenerateContentAsync(new[] { content }, cancellationToken); - } - /// - /// Generates new content from input text given to the model as a prompt. - /// - /// The text given to the model as a prompt. - /// An optional token to cancel the operation. - /// The generated content response from the model. - /// Thrown when an error occurs during content generation. - public Task GenerateContentAsync( - string text, CancellationToken cancellationToken = default) { - return GenerateContentAsync(new[] { ModelContent.Text(text) }, cancellationToken); - } - /// - /// Generates new content from input `ModelContent` given to the model as a prompt. - /// - /// The input given to the model as a prompt. - /// An optional token to cancel the operation. - /// The generated content response from the model. - /// Thrown when an error occurs during content generation. - public Task GenerateContentAsync( - IEnumerable content, CancellationToken cancellationToken = default) { - return GenerateContentAsyncInternal(content, cancellationToken); - } + #region Public API + /// + /// Generates new content from input `ModelContent` given to the model as a prompt. + /// + /// The input given to the model as a prompt. + /// An optional token to cancel the operation. + /// The generated content response from the model. + /// Thrown when an error occurs during content generation. + public Task GenerateContentAsync( + ModelContent content, CancellationToken cancellationToken = default) + { + return GenerateContentAsync(new[] { content }, cancellationToken); + } + /// + /// Generates new content from input text given to the model as a prompt. + /// + /// The text given to the model as a prompt. + /// An optional token to cancel the operation. + /// The generated content response from the model. + /// Thrown when an error occurs during content generation. + public Task GenerateContentAsync( + string text, CancellationToken cancellationToken = default) + { + return GenerateContentAsync(new[] { ModelContent.Text(text) }, cancellationToken); + } + /// + /// Generates new content from input `ModelContent` given to the model as a prompt. + /// + /// The input given to the model as a prompt. + /// An optional token to cancel the operation. + /// The generated content response from the model. + /// Thrown when an error occurs during content generation. + public Task GenerateContentAsync( + IEnumerable content, CancellationToken cancellationToken = default) + { + return GenerateContentAsyncInternal(content, cancellationToken); + } - /// - /// Generates new content as a stream from input `ModelContent` given to the model as a prompt. - /// - /// The input given to the model as a prompt. - /// An optional token to cancel the operation. - /// A stream of generated content responses from the model. - /// Thrown when an error occurs during content generation. - public IAsyncEnumerable GenerateContentStreamAsync( - ModelContent content, CancellationToken cancellationToken = default) { - return GenerateContentStreamAsync(new[] { content }, cancellationToken); - } - /// - /// Generates new content as a stream from input text given to the model as a prompt. - /// - /// The text given to the model as a prompt. - /// An optional token to cancel the operation. - /// A stream of generated content responses from the model. - /// Thrown when an error occurs during content generation. - public IAsyncEnumerable GenerateContentStreamAsync( - string text, CancellationToken cancellationToken = default) { - return GenerateContentStreamAsync(new[] { ModelContent.Text(text) }, cancellationToken); - } - /// - /// Generates new content as a stream from input `ModelContent` given to the model as a prompt. - /// - /// The input given to the model as a prompt. - /// An optional token to cancel the operation. - /// A stream of generated content responses from the model. - /// Thrown when an error occurs during content generation. - public IAsyncEnumerable GenerateContentStreamAsync( - IEnumerable content, CancellationToken cancellationToken = default) { - return GenerateContentStreamAsyncInternal(content, cancellationToken); - } + /// + /// Generates new content as a stream from input `ModelContent` given to the model as a prompt. + /// + /// The input given to the model as a prompt. + /// An optional token to cancel the operation. + /// A stream of generated content responses from the model. + /// Thrown when an error occurs during content generation. + public IAsyncEnumerable GenerateContentStreamAsync( + ModelContent content, CancellationToken cancellationToken = default) + { + return GenerateContentStreamAsync(new[] { content }, cancellationToken); + } + /// + /// Generates new content as a stream from input text given to the model as a prompt. + /// + /// The text given to the model as a prompt. + /// An optional token to cancel the operation. + /// A stream of generated content responses from the model. + /// Thrown when an error occurs during content generation. + public IAsyncEnumerable GenerateContentStreamAsync( + string text, CancellationToken cancellationToken = default) + { + return GenerateContentStreamAsync(new[] { ModelContent.Text(text) }, cancellationToken); + } + /// + /// Generates new content as a stream from input `ModelContent` given to the model as a prompt. + /// + /// The input given to the model as a prompt. + /// An optional token to cancel the operation. + /// A stream of generated content responses from the model. + /// Thrown when an error occurs during content generation. + public IAsyncEnumerable GenerateContentStreamAsync( + IEnumerable content, CancellationToken cancellationToken = default) + { + return GenerateContentStreamAsyncInternal(content, cancellationToken); + } - /// - /// Counts the number of tokens in a prompt using the model's tokenizer. - /// - /// The input given to the model as a prompt. - /// The `CountTokensResponse` of running the model's tokenizer on the input. - /// Thrown when an error occurs during the request. - public Task CountTokensAsync( - ModelContent content, CancellationToken cancellationToken = default) { - return CountTokensAsync(new[] { content }, cancellationToken); - } - /// - /// Counts the number of tokens in a prompt using the model's tokenizer. - /// - /// The text input given to the model as a prompt. - /// An optional token to cancel the operation. - /// The `CountTokensResponse` of running the model's tokenizer on the input. - /// Thrown when an error occurs during the request. - public Task CountTokensAsync( - string text, CancellationToken cancellationToken = default) { - return CountTokensAsync(new[] { ModelContent.Text(text) }, cancellationToken); - } - /// - /// Counts the number of tokens in a prompt using the model's tokenizer. - /// - /// The input given to the model as a prompt. - /// An optional token to cancel the operation. - /// The `CountTokensResponse` of running the model's tokenizer on the input. - /// Thrown when an error occurs during the request. - public Task CountTokensAsync( - IEnumerable content, CancellationToken cancellationToken = default) { - return CountTokensAsyncInternal(content, cancellationToken); - } + /// + /// Counts the number of tokens in a prompt using the model's tokenizer. + /// + /// The input given to the model as a prompt. + /// The `CountTokensResponse` of running the model's tokenizer on the input. + /// Thrown when an error occurs during the request. + public Task CountTokensAsync( + ModelContent content, CancellationToken cancellationToken = default) + { + return CountTokensAsync(new[] { content }, cancellationToken); + } + /// + /// Counts the number of tokens in a prompt using the model's tokenizer. + /// + /// The text input given to the model as a prompt. + /// An optional token to cancel the operation. + /// The `CountTokensResponse` of running the model's tokenizer on the input. + /// Thrown when an error occurs during the request. + public Task CountTokensAsync( + string text, CancellationToken cancellationToken = default) + { + return CountTokensAsync(new[] { ModelContent.Text(text) }, cancellationToken); + } + /// + /// Counts the number of tokens in a prompt using the model's tokenizer. + /// + /// The input given to the model as a prompt. + /// An optional token to cancel the operation. + /// The `CountTokensResponse` of running the model's tokenizer on the input. + /// Thrown when an error occurs during the request. + public Task CountTokensAsync( + IEnumerable content, CancellationToken cancellationToken = default) + { + return CountTokensAsyncInternal(content, cancellationToken); + } - /// - /// Creates a new chat conversation using this model with the provided history. - /// - /// Initial content history to start with. - public Chat StartChat(params ModelContent[] history) { - return StartChat((IEnumerable)history); - } - /// - /// Creates a new chat conversation using this model with the provided history. - /// - /// Initial content history to start with. - public Chat StartChat(IEnumerable history) { - return Chat.InternalCreateChat(this, history); - } -#endregion + /// + /// Creates a new chat conversation using this model with the provided history. + /// + /// Initial content history to start with. + public Chat StartChat(params ModelContent[] history) + { + return StartChat((IEnumerable)history); + } + /// + /// Creates a new chat conversation using this model with the provided history. + /// + /// Initial content history to start with. + public Chat StartChat(IEnumerable history) + { + return Chat.InternalCreateChat(this, history); + } + #endregion - private async Task GenerateContentAsyncInternal( - IEnumerable content, - CancellationToken cancellationToken) { - HttpRequestMessage request = new(HttpMethod.Post, - HttpHelpers.GetURL(_firebaseApp, _backend, _modelName) + ":generateContent"); + private async Task GenerateContentAsyncInternal( + IEnumerable content, + CancellationToken cancellationToken) + { + HttpRequestMessage request = new(HttpMethod.Post, + HttpHelpers.GetURL(_firebaseApp, _backend, _modelName) + ":generateContent"); - // Set the request headers - await HttpHelpers.SetRequestHeaders(request, _firebaseApp); + // Set the request headers + await HttpHelpers.SetRequestHeaders(request, _firebaseApp); - // Set the content - string bodyJson = MakeGenerateContentRequest(content); - request.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json"); + // Set the content + string bodyJson = MakeGenerateContentRequest(content); + request.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json"); #if FIREBASE_LOG_REST_CALLS - UnityEngine.Debug.Log("Request:\n" + bodyJson); + UnityEngine.Debug.Log("Request:\n" + bodyJson); #endif - var response = await _httpClient.SendAsync(request, cancellationToken); - await HttpHelpers.ValidateHttpResponse(response); + var response = await _httpClient.SendAsync(request, cancellationToken); + await HttpHelpers.ValidateHttpResponse(response); - string result = await response.Content.ReadAsStringAsync(); + string result = await response.Content.ReadAsStringAsync(); #if FIREBASE_LOG_REST_CALLS - UnityEngine.Debug.Log("Response:\n" + result); + UnityEngine.Debug.Log("Response:\n" + result); #endif - return GenerateContentResponse.FromJson(result, _backend.Provider); - } + return GenerateContentResponse.FromJson(result, _backend.Provider); + } - private async IAsyncEnumerable GenerateContentStreamAsyncInternal( - IEnumerable content, - [EnumeratorCancellation] CancellationToken cancellationToken) { - HttpRequestMessage request = new(HttpMethod.Post, - HttpHelpers.GetURL(_firebaseApp, _backend, _modelName) + ":streamGenerateContent?alt=sse"); + private async IAsyncEnumerable GenerateContentStreamAsyncInternal( + IEnumerable content, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + HttpRequestMessage request = new(HttpMethod.Post, + HttpHelpers.GetURL(_firebaseApp, _backend, _modelName) + ":streamGenerateContent?alt=sse"); - // Set the request headers - await HttpHelpers.SetRequestHeaders(request, _firebaseApp); + // Set the request headers + await HttpHelpers.SetRequestHeaders(request, _firebaseApp); - // Set the content - string bodyJson = MakeGenerateContentRequest(content); - request.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json"); + // Set the content + string bodyJson = MakeGenerateContentRequest(content); + request.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json"); #if FIREBASE_LOG_REST_CALLS - UnityEngine.Debug.Log("Request:\n" + bodyJson); + UnityEngine.Debug.Log("Request:\n" + bodyJson); #endif - var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - await HttpHelpers.ValidateHttpResponse(response); + var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + await HttpHelpers.ValidateHttpResponse(response); - // We are expecting a Stream as the response, so handle that. - using var stream = await response.Content.ReadAsStreamAsync(); - using var reader = new StreamReader(stream); + // We are expecting a Stream as the response, so handle that. + using var stream = await response.Content.ReadAsStreamAsync(); + using var reader = new StreamReader(stream); - string line; - while ((line = await reader.ReadLineAsync()) != null) { - // Only pass along strings that begin with the expected prefix. - if (line.StartsWith(StreamPrefix)) { + string line; + while ((line = await reader.ReadLineAsync()) != null) + { + // Only pass along strings that begin with the expected prefix. + if (line.StartsWith(StreamPrefix)) + { #if FIREBASE_LOG_REST_CALLS - UnityEngine.Debug.Log("Streaming Response:\n" + line); + UnityEngine.Debug.Log("Streaming Response:\n" + line); #endif - yield return GenerateContentResponse.FromJson(line[StreamPrefix.Length..], _backend.Provider); + yield return GenerateContentResponse.FromJson(line[StreamPrefix.Length..], _backend.Provider); + } } } - } - private async Task CountTokensAsyncInternal( - IEnumerable content, - CancellationToken cancellationToken) { - HttpRequestMessage request = new(HttpMethod.Post, - HttpHelpers.GetURL(_firebaseApp, _backend, _modelName) + ":countTokens"); + private async Task CountTokensAsyncInternal( + IEnumerable content, + CancellationToken cancellationToken) + { + HttpRequestMessage request = new(HttpMethod.Post, + HttpHelpers.GetURL(_firebaseApp, _backend, _modelName) + ":countTokens"); - // Set the request headers - await HttpHelpers.SetRequestHeaders(request, _firebaseApp); + // Set the request headers + await HttpHelpers.SetRequestHeaders(request, _firebaseApp); - // Set the content - string bodyJson = MakeCountTokensRequest(content); - request.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json"); + // Set the content + string bodyJson = MakeCountTokensRequest(content); + request.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json"); #if FIREBASE_LOG_REST_CALLS - UnityEngine.Debug.Log("CountTokensRequest:\n" + bodyJson); + UnityEngine.Debug.Log("CountTokensRequest:\n" + bodyJson); #endif - var response = await _httpClient.SendAsync(request, cancellationToken); - await HttpHelpers.ValidateHttpResponse(response); + var response = await _httpClient.SendAsync(request, cancellationToken); + await HttpHelpers.ValidateHttpResponse(response); - string result = await response.Content.ReadAsStringAsync(); + string result = await response.Content.ReadAsStringAsync(); #if FIREBASE_LOG_REST_CALLS - UnityEngine.Debug.Log("CountTokensResponse:\n" + result); + UnityEngine.Debug.Log("CountTokensResponse:\n" + result); #endif - return CountTokensResponse.FromJson(result); - } - - private string MakeGenerateContentRequest(IEnumerable contents) { - Dictionary jsonDict = MakeGenerateContentRequestAsDictionary(contents); - return Json.Serialize(jsonDict); - } - - private Dictionary MakeGenerateContentRequestAsDictionary( - IEnumerable contents) { - Dictionary jsonDict = new() { - // Convert the Contents into a list of Json dictionaries - ["contents"] = contents.Select(c => c.ToJson()).ToList() - }; - if (_generationConfig.HasValue) { - jsonDict["generationConfig"] = _generationConfig?.ToJson(); - } - if (_safetySettings != null && _safetySettings.Length > 0) { - jsonDict["safetySettings"] = _safetySettings.Select(s => s.ToJson(_backend.Provider)).ToList(); - } - if (_tools != null && _tools.Length > 0) { - jsonDict["tools"] = _tools.Select(t => t.ToJson()).ToList(); + return CountTokensResponse.FromJson(result); } - if (_toolConfig.HasValue) { - jsonDict["toolConfig"] = _toolConfig?.ToJson(); - } - if (_systemInstruction.HasValue) { - jsonDict["systemInstruction"] = _systemInstruction?.ToJson(); + + private string MakeGenerateContentRequest(IEnumerable contents) + { + Dictionary jsonDict = MakeGenerateContentRequestAsDictionary(contents); + return Json.Serialize(jsonDict); } - return jsonDict; - } + private Dictionary MakeGenerateContentRequestAsDictionary( + IEnumerable contents) + { + Dictionary jsonDict = new() + { + // Convert the Contents into a list of Json dictionaries + ["contents"] = contents.Select(c => c.ToJson()).ToList() + }; + if (_generationConfig.HasValue) + { + jsonDict["generationConfig"] = _generationConfig?.ToJson(); + } + if (_safetySettings != null && _safetySettings.Length > 0) + { + jsonDict["safetySettings"] = _safetySettings.Select(s => s.ToJson(_backend.Provider)).ToList(); + } + if (_tools != null && _tools.Length > 0) + { + jsonDict["tools"] = _tools.Select(t => t.ToJson()).ToList(); + } + if (_toolConfig.HasValue) + { + jsonDict["toolConfig"] = _toolConfig?.ToJson(); + } + if (_systemInstruction.HasValue) + { + jsonDict["systemInstruction"] = _systemInstruction?.ToJson(); + } - // CountTokensRequest is a subset of the full info needed for GenerateContent - private string MakeCountTokensRequest(IEnumerable contents) { - Dictionary jsonDict; - switch (_backend.Provider) { - case FirebaseAI.Backend.InternalProvider.GoogleAI: - jsonDict = new() { - ["generateContentRequest"] = MakeGenerateContentRequestAsDictionary(contents) - }; - // GoogleAI wants the model name included as well. - ((Dictionary)jsonDict["generateContentRequest"])["model"] = - $"models/{_modelName}"; - break; - case FirebaseAI.Backend.InternalProvider.VertexAI: - jsonDict = new() { - // Convert the Contents into a list of Json dictionaries - ["contents"] = contents.Select(c => c.ToJson()).ToList() - }; - if (_generationConfig.HasValue) { - jsonDict["generationConfig"] = _generationConfig?.ToJson(); - } - if (_tools != null && _tools.Length > 0) { - jsonDict["tools"] = _tools.Select(t => t.ToJson()).ToList(); - } - if (_systemInstruction.HasValue) { - jsonDict["systemInstruction"] = _systemInstruction?.ToJson(); - } - break; - default: - throw new NotSupportedException($"Missing support for backend: {_backend.Provider}"); + return jsonDict; } - return Json.Serialize(jsonDict); + // CountTokensRequest is a subset of the full info needed for GenerateContent + private string MakeCountTokensRequest(IEnumerable contents) + { + Dictionary jsonDict; + switch (_backend.Provider) + { + case FirebaseAI.Backend.InternalProvider.GoogleAI: + jsonDict = new() + { + ["generateContentRequest"] = MakeGenerateContentRequestAsDictionary(contents) + }; + // GoogleAI wants the model name included as well. + ((Dictionary)jsonDict["generateContentRequest"])["model"] = + $"models/{_modelName}"; + break; + case FirebaseAI.Backend.InternalProvider.VertexAI: + jsonDict = new() + { + // Convert the Contents into a list of Json dictionaries + ["contents"] = contents.Select(c => c.ToJson()).ToList() + }; + if (_generationConfig.HasValue) + { + jsonDict["generationConfig"] = _generationConfig?.ToJson(); + } + if (_tools != null && _tools.Length > 0) + { + jsonDict["tools"] = _tools.Select(t => t.ToJson()).ToList(); + } + if (_systemInstruction.HasValue) + { + jsonDict["systemInstruction"] = _systemInstruction?.ToJson(); + } + break; + default: + throw new NotSupportedException($"Missing support for backend: {_backend.Provider}"); + } + + return Json.Serialize(jsonDict); + } } -} } diff --git a/firebaseai/src/Imagen/ImagenConfig.cs b/firebaseai/src/Imagen/ImagenConfig.cs index d5ee11df..47f790d1 100644 --- a/firebaseai/src/Imagen/ImagenConfig.cs +++ b/firebaseai/src/Imagen/ImagenConfig.cs @@ -16,7 +16,8 @@ using System.Collections.Generic; -namespace Firebase.AI { +namespace Firebase.AI +{ /// /// An aspect ratio for images generated by Imagen. /// @@ -25,7 +26,8 @@ namespace Firebase.AI { /// documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/image/generate-images#aspect-ratio) /// for more details and examples of the supported aspect ratios. /// - public enum ImagenAspectRatio { + public enum ImagenAspectRatio + { /// /// Square (1:1) aspect ratio. /// @@ -74,13 +76,15 @@ public enum ImagenAspectRatio { /// documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/imagen-api#output-options) /// for more details. /// - public readonly struct ImagenImageFormat { + public readonly struct ImagenImageFormat + { #if !DOXYGEN public string MimeType { get; } public int? CompressionQuality { get; } #endif - private ImagenImageFormat(string mimeType, int? compressionQuality = null) { + private ImagenImageFormat(string mimeType, int? compressionQuality = null) + { MimeType = mimeType; CompressionQuality = compressionQuality; } @@ -92,7 +96,8 @@ private ImagenImageFormat(string mimeType, int? compressionQuality = null) { /// during compression. Images in PNG format are *typically* larger than JPEG images, though this /// depends on the image content and JPEG compression quality. /// - public static ImagenImageFormat Png() { + public static ImagenImageFormat Png() + { return new ImagenImageFormat("image/png"); } @@ -106,7 +111,8 @@ public static ImagenImageFormat Png() { /// The JPEG quality setting from 0 to 100, where `0` is highest level of /// compression (lowest image quality, smallest file size) and `100` is the lowest level of /// compression (highest image quality, largest file size); defaults to `75`. - public static ImagenImageFormat Jpeg(int? compressionQuality = null) { + public static ImagenImageFormat Jpeg(int? compressionQuality = null) + { return new ImagenImageFormat("image/jpeg", compressionQuality); } @@ -114,11 +120,14 @@ public static ImagenImageFormat Jpeg(int? compressionQuality = null) { /// Intended for internal use only. /// This method is used for serializing the object to JSON for the API request. /// - internal Dictionary ToJson() { - Dictionary jsonDict = new() { + internal Dictionary ToJson() + { + Dictionary jsonDict = new() + { ["mimeType"] = MimeType }; - if (CompressionQuality != null) { + if (CompressionQuality != null) + { jsonDict["compressionQuality"] = CompressionQuality.Value; } return jsonDict; @@ -132,7 +141,8 @@ internal Dictionary ToJson() { /// models](https://firebase.google.com/docs/vertex-ai/model-parameters?platform=unity#imagen) to /// learn about parameters available for use with Imagen models, including how to configure them. /// - public readonly struct ImagenGenerationConfig { + public readonly struct ImagenGenerationConfig + { #if !DOXYGEN public string NegativePrompt { get; } public int? NumberOfImages { get; } @@ -159,7 +169,8 @@ public ImagenGenerationConfig( int? numberOfImages = null, ImagenAspectRatio? aspectRatio = null, ImagenImageFormat? imageFormat = null, - bool? addWatermark = null) { + bool? addWatermark = null) + { NegativePrompt = negativePrompt; NumberOfImages = numberOfImages; AspectRatio = aspectRatio; @@ -167,8 +178,10 @@ public ImagenGenerationConfig( AddWatermark = addWatermark; } - private static string ConvertAspectRatio(ImagenAspectRatio aspectRatio) { - return aspectRatio switch { + private static string ConvertAspectRatio(ImagenAspectRatio aspectRatio) + { + return aspectRatio switch + { ImagenAspectRatio.Square1x1 => "1:1", ImagenAspectRatio.Portrait9x16 => "9:16", ImagenAspectRatio.Landscape16x9 => "16:9", @@ -182,20 +195,26 @@ private static string ConvertAspectRatio(ImagenAspectRatio aspectRatio) { /// Intended for internal use only. /// This method is used for serializing the object to JSON for the API request. /// - internal Dictionary ToJson() { - Dictionary jsonDict = new() { + internal Dictionary ToJson() + { + Dictionary jsonDict = new() + { ["sampleCount"] = NumberOfImages ?? 1 }; - if (!string.IsNullOrEmpty(NegativePrompt)) { + if (!string.IsNullOrEmpty(NegativePrompt)) + { jsonDict["negativePrompt"] = NegativePrompt; } - if (AspectRatio != null) { + if (AspectRatio != null) + { jsonDict["aspectRatio"] = ConvertAspectRatio(AspectRatio.Value); } - if (ImageFormat != null) { + if (ImageFormat != null) + { jsonDict["outputOptions"] = ImageFormat?.ToJson(); } - if (AddWatermark != null) { + if (AddWatermark != null) + { jsonDict["addWatermark"] = AddWatermark.Value; } diff --git a/firebaseai/src/Imagen/ImagenModel.cs b/firebaseai/src/Imagen/ImagenModel.cs index 9504c3a6..a240c2f5 100644 --- a/firebaseai/src/Imagen/ImagenModel.cs +++ b/firebaseai/src/Imagen/ImagenModel.cs @@ -24,8 +24,8 @@ using Firebase.AI.Internal; using Google.MiniJSON; -namespace Firebase.AI { - +namespace Firebase.AI +{ /// /// Represents a remote Imagen model with the ability to generate images using text prompts. /// @@ -38,7 +38,8 @@ namespace Firebase.AI { /// Preview, which means that the feature is not subject to any SLA or deprecation policy and /// could change in backwards-incompatible ways. /// - public class ImagenModel { + public class ImagenModel + { private readonly FirebaseApp _firebaseApp; private readonly FirebaseAI.Backend _backend; private readonly string _modelName; @@ -53,7 +54,8 @@ internal ImagenModel(FirebaseApp firebaseApp, string modelName, ImagenGenerationConfig? generationConfig = null, ImagenSafetySettings? safetySettings = null, - RequestOptions? requestOptions = null) { + RequestOptions? requestOptions = null) + { _firebaseApp = firebaseApp; _backend = backend; _modelName = modelName; @@ -62,7 +64,8 @@ internal ImagenModel(FirebaseApp firebaseApp, _requestOptions = requestOptions; // Create a HttpClient using the timeout requested, or the default one. - _httpClient = new HttpClient() { + _httpClient = new HttpClient() + { Timeout = requestOptions?.Timeout ?? RequestOptions.DefaultTimeout }; } @@ -79,12 +82,14 @@ internal ImagenModel(FirebaseApp firebaseApp, /// The generated content response from the model. /// Thrown when an error occurs during content generation. public Task> GenerateImagesAsync( - string prompt, CancellationToken cancellationToken = default) { + string prompt, CancellationToken cancellationToken = default) + { return GenerateImagesAsyncInternal(prompt, cancellationToken); } private async Task> GenerateImagesAsyncInternal( - string prompt, CancellationToken cancellationToken) { + string prompt, CancellationToken cancellationToken) + { HttpRequestMessage request = new(HttpMethod.Post, HttpHelpers.GetURL(_firebaseApp, _backend, _modelName) + ":predict"); @@ -111,33 +116,42 @@ private async Task> GenerateImagesAs return ImagenGenerationResponse.FromJson(result); } - private string MakeGenerateImagenRequest(string prompt) { + private string MakeGenerateImagenRequest(string prompt) + { Dictionary jsonDict = MakeGenerateImagenRequestAsDictionary(prompt); return Json.Serialize(jsonDict); } private Dictionary MakeGenerateImagenRequestAsDictionary( - string prompt) { - Dictionary parameters = new() { + string prompt) + { + Dictionary parameters = new() + { // These values are hardcoded to true for AI Monitoring. ["includeRaiReason"] = true, ["includeSafetyAttributes"] = true, }; // Merge the settings into a single parameter dictionary - if (_generationConfig != null) { + if (_generationConfig != null) + { _generationConfig?.ToJson().ToList() .ForEach(x => parameters.Add(x.Key, x.Value)); - } else { + } + else + { // We want the change the default behavior for sampleCount to return 1. parameters["sampleCount"] = 1; } - if (_safetySettings != null) { + if (_safetySettings != null) + { _safetySettings?.ToJson().ToList() .ForEach(x => parameters.Add(x.Key, x.Value)); } - Dictionary jsonDict = new() { - ["instances"] = new Dictionary() { + Dictionary jsonDict = new() + { + ["instances"] = new Dictionary() + { ["prompt"] = prompt, }, ["parameters"] = parameters, diff --git a/firebaseai/src/Imagen/ImagenResponse.cs b/firebaseai/src/Imagen/ImagenResponse.cs index 4f7729ba..50218b9b 100644 --- a/firebaseai/src/Imagen/ImagenResponse.cs +++ b/firebaseai/src/Imagen/ImagenResponse.cs @@ -21,11 +21,13 @@ using Google.MiniJSON; using UnityEngine; -namespace Firebase.AI { +namespace Firebase.AI +{ /// /// An image generated by Imagen. /// - public interface IImagenImage { + public interface IImagenImage + { /// /// The IANA standard MIME type of the image file; either `"image/png"` or `"image/jpeg"`. /// @@ -38,7 +40,8 @@ public interface IImagenImage { /// /// An image generated by Imagen, represented as inline data. /// - public readonly struct ImagenInlineImage : IImagenImage { + public readonly struct ImagenInlineImage : IImagenImage + { /// /// The IANA standard MIME type of the image file; either `"image/png"` or `"image/jpeg"`. /// @@ -55,13 +58,15 @@ public interface IImagenImage { /// Convert the image data into a `UnityEngine.Texture2D`. /// /// - public UnityEngine.Texture2D AsTexture2D() { + public UnityEngine.Texture2D AsTexture2D() + { var texture = new Texture2D(1, 1); texture.LoadImage(Data); return texture; } - private ImagenInlineImage(string mimeType, byte[] data) { + private ImagenInlineImage(string mimeType, byte[] data) + { MimeType = mimeType; Data = data; } @@ -70,7 +75,8 @@ private ImagenInlineImage(string mimeType, byte[] data) { /// Intended for internal use only. /// This method is used for deserializing JSON responses and should not be called directly. /// - internal static IImagenImage FromJson(Dictionary jsonDict) { + internal static IImagenImage FromJson(Dictionary jsonDict) + { return new ImagenInlineImage( jsonDict.ParseValue("mimeType", JsonParseOptions.ThrowEverything), Convert.FromBase64String(jsonDict.ParseValue("bytesBase64Encoded", JsonParseOptions.ThrowEverything))); @@ -83,7 +89,8 @@ internal static IImagenImage FromJson(Dictionary jsonDict) { /// This type is returned from: /// - `ImagenModel.GenerateImagesAsync(prompt)` where `T` is `ImagenInlineImage` /// - public readonly struct ImagenGenerationResponse where T : IImagenImage { + public readonly struct ImagenGenerationResponse where T : IImagenImage + { /// /// The images generated by Imagen; see `ImagenInlineImage`. /// @@ -101,7 +108,8 @@ internal static IImagenImage FromJson(Dictionary jsonDict) { /// for more details. public string FilteredReason { get; } - private ImagenGenerationResponse(List images, string filteredReason) { + private ImagenGenerationResponse(List images, string filteredReason) + { Images = images ?? new List(); FilteredReason = filteredReason; } @@ -110,7 +118,8 @@ private ImagenGenerationResponse(List images, string filteredReason) { /// Intended for internal use only. /// This method is used for deserializing JSON responses and should not be called directly. /// - internal static ImagenGenerationResponse FromJson(string jsonString) { + internal static ImagenGenerationResponse FromJson(string jsonString) + { return FromJson(Json.Deserialize(jsonString) as Dictionary); } @@ -118,8 +127,10 @@ internal static ImagenGenerationResponse FromJson(string jsonString) { /// Intended for internal use only. /// This method is used for deserializing JSON responses and should not be called directly. /// - internal static ImagenGenerationResponse FromJson(Dictionary jsonDict) { - if (!jsonDict.ContainsKey("predictions") || jsonDict["predictions"] is not List) { + internal static ImagenGenerationResponse FromJson(Dictionary jsonDict) + { + if (!jsonDict.ContainsKey("predictions") || jsonDict["predictions"] is not List) + { return new ImagenGenerationResponse(null, "Model response missing predictions"); } @@ -130,10 +141,14 @@ internal static ImagenGenerationResponse FromJson(Dictionary List images = new(); string filteredReason = null; - foreach (var pred in predictions) { - if (pred.ContainsKey("bytesBase64Encoded") && typeof(T) == typeof(ImagenInlineImage)) { + foreach (var pred in predictions) + { + if (pred.ContainsKey("bytesBase64Encoded") && typeof(T) == typeof(ImagenInlineImage)) + { images.Add((T)ImagenInlineImage.FromJson(pred)); - } else if (pred.ContainsKey("raiFilteredReason")) { + } + else if (pred.ContainsKey("raiFilteredReason")) + { filteredReason = pred.ParseValue("raiFilteredReason", JsonParseOptions.ThrowEverything); } } diff --git a/firebaseai/src/Imagen/ImagenSafety.cs b/firebaseai/src/Imagen/ImagenSafety.cs index 22e9c1c3..0f55d7d6 100644 --- a/firebaseai/src/Imagen/ImagenSafety.cs +++ b/firebaseai/src/Imagen/ImagenSafety.cs @@ -16,14 +16,15 @@ using System.Collections.Generic; -namespace Firebase.AI { - +namespace Firebase.AI +{ /// Settings for controlling the aggressiveness of filtering out sensitive content. /// /// See the [Responsible AI and usage /// guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#config-safety-filters) /// for more details. - public readonly struct ImagenSafetySettings { + public readonly struct ImagenSafetySettings + { /// /// A filter level controlling how aggressively to filter sensitive content. @@ -37,7 +38,8 @@ public readonly struct ImagenSafetySettings { /// guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters) /// for more details. /// - public enum SafetyFilterLevel { + public enum SafetyFilterLevel + { /// /// The most aggressive filtering level; most strict blocking. /// @@ -68,7 +70,8 @@ public enum SafetyFilterLevel { /// [`personGeneration`](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/imagen-api#parameter_list) /// documentation for more details. /// - public enum PersonFilterLevel { + public enum PersonFilterLevel + { /// /// Disallow generation of images containing people or faces; images of people are filtered out. /// @@ -107,13 +110,16 @@ public enum PersonFilterLevel { /// of images containing people or faces is allowed. public ImagenSafetySettings( SafetyFilterLevel? safetyFilterLevel = null, - PersonFilterLevel? personFilterLevel = null) { + PersonFilterLevel? personFilterLevel = null) + { SafetyFilter = safetyFilterLevel; PersonFilter = personFilterLevel; } - private static string ConvertSafetyFilter(SafetyFilterLevel safetyFilter) { - return safetyFilter switch { + private static string ConvertSafetyFilter(SafetyFilterLevel safetyFilter) + { + return safetyFilter switch + { SafetyFilterLevel.BlockLowAndAbove => "block_low_and_above", SafetyFilterLevel.BlockMediumAndAbove => "block_medium_and_above", SafetyFilterLevel.BlockOnlyHigh => "block_only_high", @@ -122,8 +128,10 @@ private static string ConvertSafetyFilter(SafetyFilterLevel safetyFilter) { }; } - private static string ConvertPersonFilter(PersonFilterLevel safetyFilter) { - return safetyFilter switch { + private static string ConvertPersonFilter(PersonFilterLevel safetyFilter) + { + return safetyFilter switch + { PersonFilterLevel.BlockAll => "dont_allow", PersonFilterLevel.AllowAdult => "allow_adult", PersonFilterLevel.AllowAll => "allow_all", @@ -135,13 +143,16 @@ private static string ConvertPersonFilter(PersonFilterLevel safetyFilter) { /// Intended for internal use only. /// This method is used for serializing the object to JSON for the API request. /// - internal Dictionary ToJson() { + internal Dictionary ToJson() + { Dictionary jsonDict = new(); - if (PersonFilter != null) { + if (PersonFilter != null) + { jsonDict["personGeneration"] = ConvertPersonFilter(PersonFilter.Value); } - if (SafetyFilter != null) { + if (SafetyFilter != null) + { jsonDict["safetySetting"] = ConvertSafetyFilter(SafetyFilter.Value); } diff --git a/firebaseai/src/Internal/EnumConverters.cs b/firebaseai/src/Internal/EnumConverters.cs index 9a2ab8c6..51df71e4 100644 --- a/firebaseai/src/Internal/EnumConverters.cs +++ b/firebaseai/src/Internal/EnumConverters.cs @@ -14,19 +14,21 @@ * limitations under the License. */ -namespace Firebase.AI.Internal { - -// Contains extension methods for converting shared enums to strings. -internal static class EnumConverters { - - public static string ResponseModalityToString(this ResponseModality modality) { - return modality switch { - ResponseModality.Text => "TEXT", - ResponseModality.Image => "IMAGE", - ResponseModality.Audio => "AUDIO", - _ => throw new System.ArgumentOutOfRangeException(nameof(modality), "Unsupported Modality type") - }; +namespace Firebase.AI.Internal +{ + // Contains extension methods for converting shared enums to strings. + internal static class EnumConverters + { + public static string ResponseModalityToString(this ResponseModality modality) + { + return modality switch + { + ResponseModality.Text => "TEXT", + ResponseModality.Image => "IMAGE", + ResponseModality.Audio => "AUDIO", + _ => throw new System.ArgumentOutOfRangeException(nameof(modality), "Unsupported Modality type") + }; + } } -} } diff --git a/firebaseai/src/Internal/FirebaseInterops.cs b/firebaseai/src/Internal/FirebaseInterops.cs index ad5d3f40..d7981fc8 100644 --- a/firebaseai/src/Internal/FirebaseInterops.cs +++ b/firebaseai/src/Internal/FirebaseInterops.cs @@ -20,356 +20,420 @@ using System.Reflection; using System.Threading.Tasks; -namespace Firebase.AI.Internal { - -// Contains internal helper methods for interacting with other Firebase libraries. -internal static class FirebaseInterops { - // The cached fields for FirebaseApp reflection. - private static PropertyInfo _dataCollectionProperty = null; - - // The various App Check types needed to retrieve the token, cached via reflection on startup. - private static Type _appCheckType; - private static MethodInfo _appCheckGetInstanceMethod; - private static MethodInfo _appCheckGetTokenMethod; - private static PropertyInfo _appCheckTokenResultProperty; - private static PropertyInfo _appCheckTokenTokenProperty; - // Used to determine if the App Check reflection initialized successfully, and should work. - private static bool _appCheckReflectionInitialized = false; - // The header used by the AppCheck token. - private const string appCheckHeader = "X-Firebase-AppCheck"; - - // The various Auth types needed to retrieve the token, cached via reflection on startup. - private static Type _authType; - private static MethodInfo _authGetAuthMethod; - private static PropertyInfo _authCurrentUserProperty; - private static MethodInfo _userTokenAsyncMethod; - private static PropertyInfo _userTokenTaskResultProperty; - // Used to determine if the Auth reflection initialized successfully, and should work. - private static bool _authReflectionInitialized = false; - // The header used by the AppCheck token. - private const string authHeader = "Authorization"; - - static FirebaseInterops() { - InitializeAppReflection(); - InitializeAppCheckReflection(); - InitializeAuthReflection(); - } +namespace Firebase.AI.Internal +{ + // Contains internal helper methods for interacting with other Firebase libraries. + internal static class FirebaseInterops + { + // The cached fields for FirebaseApp reflection. + private static PropertyInfo _dataCollectionProperty = null; + + // The various App Check types needed to retrieve the token, cached via reflection on startup. + private static Type _appCheckType; + private static MethodInfo _appCheckGetInstanceMethod; + private static MethodInfo _appCheckGetTokenMethod; + private static PropertyInfo _appCheckTokenResultProperty; + private static PropertyInfo _appCheckTokenTokenProperty; + // Used to determine if the App Check reflection initialized successfully, and should work. + private static bool _appCheckReflectionInitialized = false; + // The header used by the AppCheck token. + private const string appCheckHeader = "X-Firebase-AppCheck"; + + // The various Auth types needed to retrieve the token, cached via reflection on startup. + private static Type _authType; + private static MethodInfo _authGetAuthMethod; + private static PropertyInfo _authCurrentUserProperty; + private static MethodInfo _userTokenAsyncMethod; + private static PropertyInfo _userTokenTaskResultProperty; + // Used to determine if the Auth reflection initialized successfully, and should work. + private static bool _authReflectionInitialized = false; + // The header used by the AppCheck token. + private const string authHeader = "Authorization"; + + static FirebaseInterops() + { + InitializeAppReflection(); + InitializeAppCheckReflection(); + InitializeAuthReflection(); + } - private static void LogError(string message) { + private static void LogError(string message) + { #if FIREBASEAI_DEBUG_LOGGING - UnityEngine.Debug.LogError(message); + UnityEngine.Debug.LogError(message); #endif - } + } - // Cache the methods needed for FirebaseApp reflection. - private static void InitializeAppReflection() { - try { - _dataCollectionProperty = typeof(FirebaseApp).GetProperty( - "IsDataCollectionDefaultEnabled", - BindingFlags.Instance | BindingFlags.NonPublic); - if (_dataCollectionProperty == null) { - LogError("Could not find FirebaseApp.IsDataCollectionDefaultEnabled property via reflection."); - return; + // Cache the methods needed for FirebaseApp reflection. + private static void InitializeAppReflection() + { + try + { + _dataCollectionProperty = typeof(FirebaseApp).GetProperty( + "IsDataCollectionDefaultEnabled", + BindingFlags.Instance | BindingFlags.NonPublic); + if (_dataCollectionProperty == null) + { + LogError("Could not find FirebaseApp.IsDataCollectionDefaultEnabled property via reflection."); + return; + } + if (_dataCollectionProperty.PropertyType != typeof(bool)) + { + LogError("FirebaseApp.IsDataCollectionDefaultEnabled is not a bool, " + + $"but is {_dataCollectionProperty.PropertyType}"); + return; + } } - if (_dataCollectionProperty.PropertyType != typeof(bool)) { - LogError("FirebaseApp.IsDataCollectionDefaultEnabled is not a bool, " + - $"but is {_dataCollectionProperty.PropertyType}"); - return; + catch (Exception e) + { + LogError($"Failed to initialize FirebaseApp reflection: {e}"); } - } catch (Exception e) { - LogError($"Failed to initialize FirebaseApp reflection: {e}"); } - } - // Gets the property FirebaseApp.IsDataCollectionDefaultEnabled. - public static bool GetIsDataCollectionDefaultEnabled(FirebaseApp firebaseApp) { - if (firebaseApp == null || _dataCollectionProperty == null) { - return false; - } + // Gets the property FirebaseApp.IsDataCollectionDefaultEnabled. + public static bool GetIsDataCollectionDefaultEnabled(FirebaseApp firebaseApp) + { + if (firebaseApp == null || _dataCollectionProperty == null) + { + return false; + } - try { - return (bool)_dataCollectionProperty.GetValue(firebaseApp); - } catch (Exception e) { - LogError($"Error accessing 'IsDataCollectionDefaultEnabled': {e}"); - return false; + try + { + return (bool)_dataCollectionProperty.GetValue(firebaseApp); + } + catch (Exception e) + { + LogError($"Error accessing 'IsDataCollectionDefaultEnabled': {e}"); + return false; + } } - } - // SDK version to use if unable to find it. - private const string _unknownSdkVersion = "unknown"; - private static readonly Lazy _sdkVersionFetcher = new(() => { - try { - // Get the type Firebase.VersionInfo from the assembly that defines FirebaseApp. - Type versionInfoType = typeof(FirebaseApp).Assembly.GetType("Firebase.VersionInfo"); - if (versionInfoType == null) { - LogError("Firebase.VersionInfo type not found via reflection"); - return _unknownSdkVersion; + // SDK version to use if unable to find it. + private const string _unknownSdkVersion = "unknown"; + private static readonly Lazy _sdkVersionFetcher = new(() => + { + try + { + // Get the type Firebase.VersionInfo from the assembly that defines FirebaseApp. + Type versionInfoType = typeof(FirebaseApp).Assembly.GetType("Firebase.VersionInfo"); + if (versionInfoType == null) + { + LogError("Firebase.VersionInfo type not found via reflection"); + return _unknownSdkVersion; + } + + // Firebase.VersionInfo.SdkVersion + PropertyInfo sdkVersionProperty = versionInfoType.GetProperty( + "SdkVersion", + BindingFlags.Static | BindingFlags.NonPublic); + if (sdkVersionProperty == null) + { + LogError("Firebase.VersionInfo.SdkVersion property not found via reflection."); + return _unknownSdkVersion; + } + + return sdkVersionProperty.GetValue(null) as string ?? _unknownSdkVersion; } - - // Firebase.VersionInfo.SdkVersion - PropertyInfo sdkVersionProperty = versionInfoType.GetProperty( - "SdkVersion", - BindingFlags.Static | BindingFlags.NonPublic); - if (sdkVersionProperty == null) { - LogError("Firebase.VersionInfo.SdkVersion property not found via reflection."); + catch (Exception e) + { + LogError($"Error accessing SdkVersion via reflection: {e}"); return _unknownSdkVersion; } + }); - return sdkVersionProperty.GetValue(null) as string ?? _unknownSdkVersion; - } catch (Exception e) { - LogError($"Error accessing SdkVersion via reflection: {e}"); - return _unknownSdkVersion; + // Gets the internal property Firebase.VersionInfo.SdkVersion + internal static string GetVersionInfoSdkVersion() + { + return _sdkVersionFetcher.Value; } - }); - // Gets the internal property Firebase.VersionInfo.SdkVersion - internal static string GetVersionInfoSdkVersion() { - return _sdkVersionFetcher.Value; - } + // Cache the various types and methods needed for AppCheck token retrieval. + private static void InitializeAppCheckReflection() + { + const string firebaseAppCheckTypeName = "Firebase.AppCheck.FirebaseAppCheck, Firebase.AppCheck"; + const string getAppCheckTokenMethodName = "GetAppCheckTokenAsync"; - // Cache the various types and methods needed for AppCheck token retrieval. - private static void InitializeAppCheckReflection() { - const string firebaseAppCheckTypeName = "Firebase.AppCheck.FirebaseAppCheck, Firebase.AppCheck"; - const string getAppCheckTokenMethodName = "GetAppCheckTokenAsync"; + try + { + // Set this to false, to allow easy failing out via return. + _appCheckReflectionInitialized = false; - try { - // Set this to false, to allow easy failing out via return. - _appCheckReflectionInitialized = false; + _appCheckType = Type.GetType(firebaseAppCheckTypeName); + if (_appCheckType == null) + { + return; + } + + // Get the static method GetInstance(FirebaseApp app) + _appCheckGetInstanceMethod = _appCheckType.GetMethod( + "GetInstance", BindingFlags.Static | BindingFlags.Public, null, + new Type[] { typeof(FirebaseApp) }, null); + if (_appCheckGetInstanceMethod == null) + { + LogError("Could not find FirebaseAppCheck.GetInstance method via reflection."); + return; + } + + // Get the instance method GetAppCheckTokenAsync(bool forceRefresh) + _appCheckGetTokenMethod = _appCheckType.GetMethod( + getAppCheckTokenMethodName, BindingFlags.Instance | BindingFlags.Public, null, + new Type[] { typeof(bool) }, null); + if (_appCheckGetTokenMethod == null) + { + LogError($"Could not find {getAppCheckTokenMethodName} method via reflection."); + return; + } - _appCheckType = Type.GetType(firebaseAppCheckTypeName); - if (_appCheckType == null) { - return; - } + // Should be Task + Type appCheckTokenTaskType = _appCheckGetTokenMethod.ReturnType; - // Get the static method GetInstance(FirebaseApp app) - _appCheckGetInstanceMethod = _appCheckType.GetMethod( - "GetInstance", BindingFlags.Static | BindingFlags.Public, null, - new Type[] { typeof(FirebaseApp) }, null); - if (_appCheckGetInstanceMethod == null) { - LogError("Could not find FirebaseAppCheck.GetInstance method via reflection."); - return; - } + // Get the Result property from the Task + _appCheckTokenResultProperty = appCheckTokenTaskType.GetProperty("Result"); + if (_appCheckTokenResultProperty == null) + { + LogError("Could not find Result property on App Check token Task."); + return; + } - // Get the instance method GetAppCheckTokenAsync(bool forceRefresh) - _appCheckGetTokenMethod = _appCheckType.GetMethod( - getAppCheckTokenMethodName, BindingFlags.Instance | BindingFlags.Public, null, - new Type[] { typeof(bool) }, null); - if (_appCheckGetTokenMethod == null) { - LogError($"Could not find {getAppCheckTokenMethodName} method via reflection."); - return; - } + // Should be AppCheckToken + Type appCheckTokenType = _appCheckTokenResultProperty.PropertyType; - // Should be Task - Type appCheckTokenTaskType = _appCheckGetTokenMethod.ReturnType; + _appCheckTokenTokenProperty = appCheckTokenType.GetProperty("Token"); + if (_appCheckTokenTokenProperty == null) + { + LogError($"Could not find Token property on AppCheckToken."); + return; + } - // Get the Result property from the Task - _appCheckTokenResultProperty = appCheckTokenTaskType.GetProperty("Result"); - if (_appCheckTokenResultProperty == null) { - LogError("Could not find Result property on App Check token Task."); - return; + _appCheckReflectionInitialized = true; } - - // Should be AppCheckToken - Type appCheckTokenType = _appCheckTokenResultProperty.PropertyType; - - _appCheckTokenTokenProperty = appCheckTokenType.GetProperty("Token"); - if (_appCheckTokenTokenProperty == null) { - LogError($"Could not find Token property on AppCheckToken."); - return; + catch (Exception e) + { + LogError($"Exception during static initialization of FirebaseInterops: {e}"); } - - _appCheckReflectionInitialized = true; - } catch (Exception e) { - LogError($"Exception during static initialization of FirebaseInterops: {e}"); - } - } - - // Gets the AppCheck Token, assuming there is one. Otherwise, returns null. - internal static async Task GetAppCheckTokenAsync(FirebaseApp firebaseApp) { - // If AppCheck reflection failed for any reason, nothing to do. - if (!_appCheckReflectionInitialized) { - return null; } - try { - // Get the FirebaseAppCheck instance for the current FirebaseApp - object appCheckInstance = _appCheckGetInstanceMethod.Invoke(null, new object[] { firebaseApp }); - if (appCheckInstance == null) { - LogError("Failed to get FirebaseAppCheck instance via reflection."); + // Gets the AppCheck Token, assuming there is one. Otherwise, returns null. + internal static async Task GetAppCheckTokenAsync(FirebaseApp firebaseApp) + { + // If AppCheck reflection failed for any reason, nothing to do. + if (!_appCheckReflectionInitialized) + { return null; } - // Invoke GetAppCheckTokenAsync(false) - returns a Task - object taskObject = _appCheckGetTokenMethod.Invoke(appCheckInstance, new object[] { false }); - if (taskObject is not Task appCheckTokenTask) { - LogError($"Invoking GetToken did not return a Task."); - return null; - } - - // Await the task to get the AppCheckToken result - await appCheckTokenTask; - - // Check for exceptions in the task - if (appCheckTokenTask.IsFaulted) { - LogError($"Error getting App Check token: {appCheckTokenTask.Exception}"); - return null; + try + { + // Get the FirebaseAppCheck instance for the current FirebaseApp + object appCheckInstance = _appCheckGetInstanceMethod.Invoke(null, new object[] { firebaseApp }); + if (appCheckInstance == null) + { + LogError("Failed to get FirebaseAppCheck instance via reflection."); + return null; + } + + // Invoke GetAppCheckTokenAsync(false) - returns a Task + object taskObject = _appCheckGetTokenMethod.Invoke(appCheckInstance, new object[] { false }); + if (taskObject is not Task appCheckTokenTask) + { + LogError($"Invoking GetToken did not return a Task."); + return null; + } + + // Await the task to get the AppCheckToken result + await appCheckTokenTask; + + // Check for exceptions in the task + if (appCheckTokenTask.IsFaulted) + { + LogError($"Error getting App Check token: {appCheckTokenTask.Exception}"); + return null; + } + + // Get the Result property from the Task + object tokenResult = _appCheckTokenResultProperty.GetValue(appCheckTokenTask); // This is the AppCheckToken struct + if (tokenResult == null) + { + LogError("App Check token result was null."); + return null; + } + + // Get the Token property from the AppCheckToken struct + return _appCheckTokenTokenProperty.GetValue(tokenResult) as string; } - - // Get the Result property from the Task - object tokenResult = _appCheckTokenResultProperty.GetValue(appCheckTokenTask); // This is the AppCheckToken struct - if (tokenResult == null) { - LogError("App Check token result was null."); - return null; + catch (Exception e) + { + // Log any exceptions during the reflection/invocation process + LogError($"An error occurred while trying to fetch App Check token: {e}"); } - - // Get the Token property from the AppCheckToken struct - return _appCheckTokenTokenProperty.GetValue(tokenResult) as string; - } catch (Exception e) { - // Log any exceptions during the reflection/invocation process - LogError($"An error occurred while trying to fetch App Check token: {e}"); + return null; } - return null; - } - // Cache the various types and methods needed for Auth token retrieval. - private static void InitializeAuthReflection() { - const string firebaseAuthTypeName = "Firebase.Auth.FirebaseAuth, Firebase.Auth"; - const string getTokenMethodName = "TokenAsync"; - - try { - // Set this to false, to allow easy failing out via return. - _authReflectionInitialized = false; - - _authType = Type.GetType(firebaseAuthTypeName); - if (_authType == null) { - // Auth assembly likely not present, fine to skip - return; - } - - // Get the static method GetAuth(FirebaseApp app): - _authGetAuthMethod = _authType.GetMethod( - "GetAuth", BindingFlags.Static | BindingFlags.Public, null, - new Type[] { typeof(FirebaseApp) }, null); - if (_authGetAuthMethod == null) { - LogError("Could not find FirebaseAuth.GetAuth method via reflection."); - return; - } - - // Get the CurrentUser property from FirebaseAuth instance - _authCurrentUserProperty = _authType.GetProperty("CurrentUser", BindingFlags.Instance | BindingFlags.Public); - if (_authCurrentUserProperty == null) { - LogError("Could not find FirebaseAuth.CurrentUser property via reflection."); - return; - } - - // This should be FirebaseUser type - Type userType = _authCurrentUserProperty.PropertyType; + // Cache the various types and methods needed for Auth token retrieval. + private static void InitializeAuthReflection() + { + const string firebaseAuthTypeName = "Firebase.Auth.FirebaseAuth, Firebase.Auth"; + const string getTokenMethodName = "TokenAsync"; + + try + { + // Set this to false, to allow easy failing out via return. + _authReflectionInitialized = false; + + _authType = Type.GetType(firebaseAuthTypeName); + if (_authType == null) + { + // Auth assembly likely not present, fine to skip + return; + } + + // Get the static method GetAuth(FirebaseApp app): + _authGetAuthMethod = _authType.GetMethod( + "GetAuth", BindingFlags.Static | BindingFlags.Public, null, + new Type[] { typeof(FirebaseApp) }, null); + if (_authGetAuthMethod == null) + { + LogError("Could not find FirebaseAuth.GetAuth method via reflection."); + return; + } - // Get the TokenAsync(bool) method from FirebaseUser - _userTokenAsyncMethod = userType.GetMethod( - getTokenMethodName, BindingFlags.Instance | BindingFlags.Public, null, - new Type[] { typeof(bool) }, null); - if (_userTokenAsyncMethod == null) { - LogError($"Could not find FirebaseUser.{getTokenMethodName}(bool) method via reflection."); - return; - } + // Get the CurrentUser property from FirebaseAuth instance + _authCurrentUserProperty = _authType.GetProperty("CurrentUser", BindingFlags.Instance | BindingFlags.Public); + if (_authCurrentUserProperty == null) + { + LogError("Could not find FirebaseAuth.CurrentUser property via reflection."); + return; + } + + // This should be FirebaseUser type + Type userType = _authCurrentUserProperty.PropertyType; + + // Get the TokenAsync(bool) method from FirebaseUser + _userTokenAsyncMethod = userType.GetMethod( + getTokenMethodName, BindingFlags.Instance | BindingFlags.Public, null, + new Type[] { typeof(bool) }, null); + if (_userTokenAsyncMethod == null) + { + LogError($"Could not find FirebaseUser.{getTokenMethodName}(bool) method via reflection."); + return; + } - // The return type is Task - Type tokenTaskType = _userTokenAsyncMethod.ReturnType; + // The return type is Task + Type tokenTaskType = _userTokenAsyncMethod.ReturnType; - // Get the Result property from Task - _userTokenTaskResultProperty = tokenTaskType.GetProperty("Result"); - if (_userTokenTaskResultProperty == null) { - LogError("Could not find Result property on Auth token Task."); - return; - } + // Get the Result property from Task + _userTokenTaskResultProperty = tokenTaskType.GetProperty("Result"); + if (_userTokenTaskResultProperty == null) + { + LogError("Could not find Result property on Auth token Task."); + return; + } - // Check if Result property is actually a string - if (_userTokenTaskResultProperty.PropertyType != typeof(string)) { + // Check if Result property is actually a string + if (_userTokenTaskResultProperty.PropertyType != typeof(string)) + { LogError("Auth token Task's Result property is not a string, " + $"but is {_userTokenTaskResultProperty.PropertyType}"); return; - } - - _authReflectionInitialized = true; - } catch (Exception e) { - LogError($"Exception during static initialization of Auth reflection in FirebaseInterops: {e}"); - _authReflectionInitialized = false; - } - } - - // Gets the Auth Token, assuming there is one. Otherwise, returns null. - internal static async Task GetAuthTokenAsync(FirebaseApp firebaseApp) { - // If Auth reflection failed for any reason, nothing to do. - if (!_authReflectionInitialized) { - return null; - } + } - try { - // Get the FirebaseAuth instance for the given FirebaseApp. - object authInstance = _authGetAuthMethod.Invoke(null, new object[] { firebaseApp }); - if (authInstance == null) { - LogError("Failed to get FirebaseAuth instance via reflection."); - return null; + _authReflectionInitialized = true; } - - // Get the CurrentUser property - object currentUser = _authCurrentUserProperty.GetValue(authInstance); - if (currentUser == null) { - // No user logged in, so no token - return null; + catch (Exception e) + { + LogError($"Exception during static initialization of Auth reflection in FirebaseInterops: {e}"); + _authReflectionInitialized = false; } + } - // Invoke TokenAsync(false) - returns a Task - object taskObject = _userTokenAsyncMethod.Invoke(currentUser, new object[] { false }); - if (taskObject is not Task tokenTask) { - LogError("Invoking TokenAsync did not return a Task."); + // Gets the Auth Token, assuming there is one. Otherwise, returns null. + internal static async Task GetAuthTokenAsync(FirebaseApp firebaseApp) + { + // If Auth reflection failed for any reason, nothing to do. + if (!_authReflectionInitialized) + { return null; } - // Await the task to get the token result - await tokenTask; - - // Check for exceptions in the task - if (tokenTask.IsFaulted) { - LogError($"Error getting Auth token: {tokenTask.Exception}"); - return null; + try + { + // Get the FirebaseAuth instance for the given FirebaseApp. + object authInstance = _authGetAuthMethod.Invoke(null, new object[] { firebaseApp }); + if (authInstance == null) + { + LogError("Failed to get FirebaseAuth instance via reflection."); + return null; + } + + // Get the CurrentUser property + object currentUser = _authCurrentUserProperty.GetValue(authInstance); + if (currentUser == null) + { + // No user logged in, so no token + return null; + } + + // Invoke TokenAsync(false) - returns a Task + object taskObject = _userTokenAsyncMethod.Invoke(currentUser, new object[] { false }); + if (taskObject is not Task tokenTask) + { + LogError("Invoking TokenAsync did not return a Task."); + return null; + } + + // Await the task to get the token result + await tokenTask; + + // Check for exceptions in the task + if (tokenTask.IsFaulted) + { + LogError($"Error getting Auth token: {tokenTask.Exception}"); + return null; + } + + // Get the Result property (which is the string token) + return _userTokenTaskResultProperty.GetValue(tokenTask) as string; } - - // Get the Result property (which is the string token) - return _userTokenTaskResultProperty.GetValue(tokenTask) as string; - } catch (Exception e) { - // Log any exceptions during the reflection/invocation process - LogError($"An error occurred while trying to fetch Auth token: {e}"); + catch (Exception e) + { + // Log any exceptions during the reflection/invocation process + LogError($"An error occurred while trying to fetch Auth token: {e}"); + } + return null; } - return null; - } - // Adds the other Firebase tokens to the HttpRequest, as available. - internal static async Task AddFirebaseTokensAsync(HttpRequestMessage request, FirebaseApp firebaseApp) { - string appCheckToken = await GetAppCheckTokenAsync(firebaseApp); - if (!string.IsNullOrEmpty(appCheckToken)) { - request.Headers.Add(appCheckHeader, appCheckToken); - } + // Adds the other Firebase tokens to the HttpRequest, as available. + internal static async Task AddFirebaseTokensAsync(HttpRequestMessage request, FirebaseApp firebaseApp) + { + string appCheckToken = await GetAppCheckTokenAsync(firebaseApp); + if (!string.IsNullOrEmpty(appCheckToken)) + { + request.Headers.Add(appCheckHeader, appCheckToken); + } - string authToken = await GetAuthTokenAsync(firebaseApp); - if (!string.IsNullOrEmpty(authToken)) { - request.Headers.Add(authHeader, $"Firebase {authToken}"); + string authToken = await GetAuthTokenAsync(firebaseApp); + if (!string.IsNullOrEmpty(authToken)) + { + request.Headers.Add(authHeader, $"Firebase {authToken}"); + } } - } - // Adds the other Firebase tokens to the WebSocket, as available. - internal static async Task AddFirebaseTokensAsync(ClientWebSocket socket, FirebaseApp firebaseApp) { - string appCheckToken = await GetAppCheckTokenAsync(firebaseApp); - if (!string.IsNullOrEmpty(appCheckToken)) { - socket.Options.SetRequestHeader(appCheckHeader, appCheckToken); - } + // Adds the other Firebase tokens to the WebSocket, as available. + internal static async Task AddFirebaseTokensAsync(ClientWebSocket socket, FirebaseApp firebaseApp) + { + string appCheckToken = await GetAppCheckTokenAsync(firebaseApp); + if (!string.IsNullOrEmpty(appCheckToken)) + { + socket.Options.SetRequestHeader(appCheckHeader, appCheckToken); + } - string authToken = await GetAuthTokenAsync(firebaseApp); - if (!string.IsNullOrEmpty(authToken)) { - socket.Options.SetRequestHeader(authHeader, $"Firebase {authToken}"); + string authToken = await GetAuthTokenAsync(firebaseApp); + if (!string.IsNullOrEmpty(authToken)) + { + socket.Options.SetRequestHeader(authHeader, $"Firebase {authToken}"); + } } } -} } diff --git a/firebaseai/src/Internal/HttpHelpers.cs b/firebaseai/src/Internal/HttpHelpers.cs index 370ca165..4c34252d 100644 --- a/firebaseai/src/Internal/HttpHelpers.cs +++ b/firebaseai/src/Internal/HttpHelpers.cs @@ -18,31 +18,41 @@ using System.Net.Http; using System.Threading.Tasks; -namespace Firebase.AI.Internal { +namespace Firebase.AI.Internal +{ // Helper functions to help handling the Http calls. - internal static class HttpHelpers { + internal static class HttpHelpers + { // Get the URL to use for the rest calls based on the backend. internal static string GetURL(FirebaseApp firebaseApp, - FirebaseAI.Backend backend, string modelName) { - if (backend.Provider == FirebaseAI.Backend.InternalProvider.VertexAI) { + FirebaseAI.Backend backend, string modelName) + { + if (backend.Provider == FirebaseAI.Backend.InternalProvider.VertexAI) + { return "https://firebasevertexai.googleapis.com/v1beta" + "/projects/" + firebaseApp.Options.ProjectId + "/locations/" + backend.Location + "/publishers/google/models/" + modelName; - } else if (backend.Provider == FirebaseAI.Backend.InternalProvider.GoogleAI) { + } + else if (backend.Provider == FirebaseAI.Backend.InternalProvider.GoogleAI) + { return "https://firebasevertexai.googleapis.com/v1beta" + "/projects/" + firebaseApp.Options.ProjectId + "/models/" + modelName; - } else { + } + else + { throw new NotSupportedException($"Missing support for backend: {backend.Provider}"); } } - internal static async Task SetRequestHeaders(HttpRequestMessage request, FirebaseApp firebaseApp) { + internal static async Task SetRequestHeaders(HttpRequestMessage request, FirebaseApp firebaseApp) + { request.Headers.Add("x-goog-api-key", firebaseApp.Options.ApiKey); string version = FirebaseInterops.GetVersionInfoSdkVersion(); request.Headers.Add("x-goog-api-client", $"gl-csharp/8.0 fire/{version}"); - if (FirebaseInterops.GetIsDataCollectionDefaultEnabled(firebaseApp)) { + if (FirebaseInterops.GetIsDataCollectionDefaultEnabled(firebaseApp)) + { request.Headers.Add("X-Firebase-AppId", firebaseApp.Options.AppId); request.Headers.Add("X-Firebase-AppVersion", UnityEngine.Application.version); } @@ -52,17 +62,23 @@ internal static async Task SetRequestHeaders(HttpRequestMessage request, Firebas // Helper function to throw an exception if the Http Response indicates failure. // Useful as EnsureSuccessStatusCode can leave out relevant information. - internal static async Task ValidateHttpResponse(HttpResponseMessage response) { - if (response.IsSuccessStatusCode) { + internal static async Task ValidateHttpResponse(HttpResponseMessage response) + { + if (response.IsSuccessStatusCode) + { return; } // Status code indicates failure, try to read the content for more details string errorContent = "No error content available."; - if (response.Content != null) { - try { + if (response.Content != null) + { + try + { errorContent = await response.Content.ReadAsStringAsync(); - } catch (Exception readEx) { + } + catch (Exception readEx) + { // Handle being unable to read the content errorContent = $"Failed to read error content: {readEx.Message}"; } diff --git a/firebaseai/src/Internal/InternalHelpers.cs b/firebaseai/src/Internal/InternalHelpers.cs index 7b9e9d75..1a475a0f 100644 --- a/firebaseai/src/Internal/InternalHelpers.cs +++ b/firebaseai/src/Internal/InternalHelpers.cs @@ -18,236 +18,303 @@ using System.Collections.Generic; using System.Linq; -namespace Firebase.AI.Internal { - -// Include this to just shorthand it in this file -using JsonDict = Dictionary; - -// Options used by the extensions below to modify logic, mostly around throwing exceptions. -[Flags] -internal enum JsonParseOptions { - None = 0, - // Throw if the key is missing from the dictionary, useful for required fields. - ThrowMissingKey = 1 << 0, - // Throw if the key was found, but it failed to cast to the expected type. - ThrowInvalidCast = 1 << 1, - // Combination of the above. - ThrowEverything = ThrowMissingKey | ThrowInvalidCast, -} +namespace Firebase.AI.Internal +{ + // Include this to just shorthand it in this file + using JsonDict = Dictionary; -// Contains extension methods for commonly done logic. -internal static class FirebaseAIExtensions { + // Options used by the extensions below to modify logic, mostly around throwing exceptions. + [Flags] + internal enum JsonParseOptions + { + None = 0, + // Throw if the key is missing from the dictionary, useful for required fields. + ThrowMissingKey = 1 << 0, + // Throw if the key was found, but it failed to cast to the expected type. + ThrowInvalidCast = 1 << 1, + // Combination of the above. + ThrowEverything = ThrowMissingKey | ThrowInvalidCast, + } - // Tries to find the object with the key, and cast it to the type T - public static bool TryParseValue(this JsonDict jsonDict, string key, - out T value, JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) { - if (jsonDict.TryGetValue(key, out object obj)) { - if (obj is T tObj) { - value = tObj; - return true; - } else if (obj is long asLong && - (typeof(T) == typeof(int) || - typeof(T) == typeof(float) || - typeof(T) == typeof(double))) { - // MiniJson puts all ints as longs, so special case it - // Also, if the number didn't have a decimal point, we still want to - // allow it to be a float or double. - value = (T)Convert.ChangeType(asLong, typeof(T)); - return true; - } else if (typeof(T) == typeof(float) && obj is double asDouble) { - // Similarly, floats are stored as doubles. - value = (T)(object)(float)asDouble; - return true; - } else if (options.HasFlag(JsonParseOptions.ThrowInvalidCast)) { - throw new InvalidCastException( - $"Invalid JSON format: '{key}' is not of type '{typeof(T).Name}', " + - $"Actual type: {obj.GetType().FullName}."); + // Contains extension methods for commonly done logic. + internal static class FirebaseAIExtensions + { + + // Tries to find the object with the key, and cast it to the type T + public static bool TryParseValue(this JsonDict jsonDict, string key, + out T value, JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) + { + if (jsonDict.TryGetValue(key, out object obj)) + { + if (obj is T tObj) + { + value = tObj; + return true; + } + else if (obj is long asLong && + (typeof(T) == typeof(int) || + typeof(T) == typeof(float) || + typeof(T) == typeof(double))) + { + // MiniJson puts all ints as longs, so special case it + // Also, if the number didn't have a decimal point, we still want to + // allow it to be a float or double. + value = (T)Convert.ChangeType(asLong, typeof(T)); + return true; + } + else if (typeof(T) == typeof(float) && obj is double asDouble) + { + // Similarly, floats are stored as doubles. + value = (T)(object)(float)asDouble; + return true; + } + else if (options.HasFlag(JsonParseOptions.ThrowInvalidCast)) + { + throw new InvalidCastException( + $"Invalid JSON format: '{key}' is not of type '{typeof(T).Name}', " + + $"Actual type: {obj.GetType().FullName}."); + } + } + else if (options.HasFlag(JsonParseOptions.ThrowMissingKey)) + { + throw new KeyNotFoundException( + $"Invalid JSON format: Unable to locate expected key {key}"); } - } else if (options.HasFlag(JsonParseOptions.ThrowMissingKey)) { - throw new KeyNotFoundException( - $"Invalid JSON format: Unable to locate expected key {key}"); + value = default; + return false; } - value = default; - return false; - } - // Casts the found object to type T, otherwise returns default (or throws) - public static T ParseValue(this JsonDict jsonDict, string key, - JsonParseOptions options = JsonParseOptions.ThrowInvalidCast, - T defaultValue = default) { - if (TryParseValue(jsonDict, key, out T value, options)) { - return value; - } else { - return defaultValue; + // Casts the found object to type T, otherwise returns default (or throws) + public static T ParseValue(this JsonDict jsonDict, string key, + JsonParseOptions options = JsonParseOptions.ThrowInvalidCast, + T defaultValue = default) + { + if (TryParseValue(jsonDict, key, out T value, options)) + { + return value; + } + else + { + return defaultValue; + } } - } - // Casts the found object to type T, otherwise returns null (or throws) - public static T? ParseNullableValue(this JsonDict jsonDict, string key, - JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) where T : struct { - if (TryParseValue(jsonDict, key, out T value, options)) { - return value; - } else { - return null; + // Casts the found object to type T, otherwise returns null (or throws) + public static T? ParseNullableValue(this JsonDict jsonDict, string key, + JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) where T : struct + { + if (TryParseValue(jsonDict, key, out T value, options)) + { + return value; + } + else + { + return null; + } } - } - // Tries to convert the string found with the key to an enum, using the given function. - public static bool TryParseEnum(this JsonDict jsonDict, string key, - Func parseFunc, out T value, - JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) { - if (jsonDict.TryParseValue(key, out string enumStr, options)) { - value = parseFunc(enumStr); - return true; - } else { - value = default; - return false; + // Tries to convert the string found with the key to an enum, using the given function. + public static bool TryParseEnum(this JsonDict jsonDict, string key, + Func parseFunc, out T value, + JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) + { + if (jsonDict.TryParseValue(key, out string enumStr, options)) + { + value = parseFunc(enumStr); + return true; + } + else + { + value = default; + return false; + } } - } - // Casts the found string to an enum, otherwise returns default (or throws) - public static T ParseEnum(this JsonDict jsonDict, string key, - Func parseFunc, - JsonParseOptions options = JsonParseOptions.ThrowInvalidCast, - T defaultValue = default) { - if (TryParseEnum(jsonDict, key, parseFunc, out T value, options)) { - return value; - } else { - return defaultValue; + // Casts the found string to an enum, otherwise returns default (or throws) + public static T ParseEnum(this JsonDict jsonDict, string key, + Func parseFunc, + JsonParseOptions options = JsonParseOptions.ThrowInvalidCast, + T defaultValue = default) + { + if (TryParseEnum(jsonDict, key, parseFunc, out T value, options)) + { + return value; + } + else + { + return defaultValue; + } } - } - // Casts the found string to an enum, otherwise returns null (or throws) - public static T? ParseNullableEnum(this JsonDict jsonDict, string key, - Func parseFunc, - JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) where T : struct, Enum { - if (TryParseEnum(jsonDict, key, parseFunc, out T value, options)) { - return value; - } else { - return null; + // Casts the found string to an enum, otherwise returns null (or throws) + public static T? ParseNullableEnum(this JsonDict jsonDict, string key, + Func parseFunc, + JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) where T : struct, Enum + { + if (TryParseEnum(jsonDict, key, parseFunc, out T value, options)) + { + return value; + } + else + { + return null; + } } - } - // Tries to convert the given Dictionary found at the key with the given function. - public static bool TryParseObject(this JsonDict jsonDict, string key, - Func parseFunc, out T value, - JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) { - if (TryParseValue(jsonDict, key, out JsonDict innerDict, options)) { - value = parseFunc(innerDict); - return true; - } else { - value = default; - return false; + // Tries to convert the given Dictionary found at the key with the given function. + public static bool TryParseObject(this JsonDict jsonDict, string key, + Func parseFunc, out T value, + JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) + { + if (TryParseValue(jsonDict, key, out JsonDict innerDict, options)) + { + value = parseFunc(innerDict); + return true; + } + else + { + value = default; + return false; + } + } + + // Casts the found Dictionary to an object, otherwise returns default (or throws) + public static T ParseObject(this JsonDict jsonDict, string key, + Func parseFunc, + JsonParseOptions options = JsonParseOptions.ThrowInvalidCast, + T defaultValue = default) + { + if (TryParseObject(jsonDict, key, parseFunc, out T value, options)) + { + return value; + } + else + { + return defaultValue; + } } - } - // Casts the found Dictionary to an object, otherwise returns default (or throws) - public static T ParseObject(this JsonDict jsonDict, string key, - Func parseFunc, - JsonParseOptions options = JsonParseOptions.ThrowInvalidCast, - T defaultValue = default) { - if (TryParseObject(jsonDict, key, parseFunc, out T value, options)) { - return value; - } else { - return defaultValue; + // Casts the found Dictionary to an object, otherwise returns null (or throws) + public static T? ParseNullableObject(this JsonDict jsonDict, string key, + Func parseFunc, + JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) where T : struct + { + if (TryParseObject(jsonDict, key, parseFunc, out T value, options)) + { + return value; + } + else + { + return null; + } } - } - // Casts the found Dictionary to an object, otherwise returns null (or throws) - public static T? ParseNullableObject(this JsonDict jsonDict, string key, - Func parseFunc, - JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) where T : struct { - if (TryParseObject(jsonDict, key, parseFunc, out T value, options)) { - return value; - } else { - return null; + // Tries to convert the found List of objects to a List of strings. + public static bool TryParseStringList(this JsonDict jsonDict, string key, + out List values, + JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) + { + if (jsonDict.TryParseValue(key, out List list, options)) + { + values = list.OfType().ToList(); + return true; + } + else + { + values = null; + return false; + } } - } - // Tries to convert the found List of objects to a List of strings. - public static bool TryParseStringList(this JsonDict jsonDict, string key, - out List values, - JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) { - if (jsonDict.TryParseValue(key, out List list, options)) { - values = list.OfType().ToList(); - return true; - } else { - values = null; - return false; + // Casts the found List to a string List, otherwise returns default (or throws) + public static List ParseStringList(this JsonDict jsonDict, string key, + JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) + { + TryParseStringList(jsonDict, key, out List values, options); + return values; } - } - // Casts the found List to a string List, otherwise returns default (or throws) - public static List ParseStringList(this JsonDict jsonDict, string key, - JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) { - TryParseStringList(jsonDict, key, out List values, options); - return values; - } + // Tries to convert the found List of Dictionaries, using the given function. + public static bool TryParseObjectList(this JsonDict jsonDict, string key, + Func parseFunc, out List values, + JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) + { + if (jsonDict.TryParseValue(key, out List list, options)) + { + values = list.ConvertJsonList(parseFunc); + return true; + } + else + { + values = null; + return false; + } + } - // Tries to convert the found List of Dictionaries, using the given function. - public static bool TryParseObjectList(this JsonDict jsonDict, string key, - Func parseFunc, out List values, - JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) { - if (jsonDict.TryParseValue(key, out List list, options)) { - values = list.ConvertJsonList(parseFunc); - return true; - } else { - values = null; - return false; + // Casts the found List to an object List, otherwise returns default (or throws) + public static List ParseObjectList(this JsonDict jsonDict, string key, + Func parseFunc, + JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) + { + TryParseObjectList(jsonDict, key, parseFunc, out List values, options); + return values; } - } - // Casts the found List to an object List, otherwise returns default (or throws) - public static List ParseObjectList(this JsonDict jsonDict, string key, - Func parseFunc, - JsonParseOptions options = JsonParseOptions.ThrowInvalidCast) { - TryParseObjectList(jsonDict, key, parseFunc, out List values, options); - return values; - } + // Converts the given List of Dictionaries into a list of T, using the converter. + public static List ConvertJsonList(this List list, + Func converter) + { + return list + .Select(o => o as JsonDict) + .Where(dict => dict != null) + .Select(converter) + .ToList(); + } - // Converts the given List of Dictionaries into a list of T, using the converter. - public static List ConvertJsonList(this List list, - Func converter) { - return list - .Select(o => o as JsonDict) - .Where(dict => dict != null) - .Select(converter) - .ToList(); - } + public static ModelContent ConvertRole(this ModelContent content, string role) + { + if (content.Role == role) + { + return content; + } + else + { + return new ModelContent(role, content.Parts); + } + } - public static ModelContent ConvertRole(this ModelContent content, string role) { - if (content.Role == role) { - return content; - } else { - return new ModelContent(role, content.Parts); + public static ModelContent ConvertToUser(this ModelContent content) + { + return content.ConvertRole("user"); } - } - public static ModelContent ConvertToUser(this ModelContent content) { - return content.ConvertRole("user"); - } + public static ModelContent ConvertToModel(this ModelContent content) + { + return content.ConvertRole("model"); + } - public static ModelContent ConvertToModel(this ModelContent content) { - return content.ConvertRole("model"); - } + public static ModelContent ConvertToSystem(this ModelContent content) + { + return content.ConvertRole("system"); + } - public static ModelContent ConvertToSystem(this ModelContent content) { - return content.ConvertRole("system"); - } - - public static void AddIfHasValue(this JsonDict jsonDict, string key, - T? value) where T : struct { - if (value.HasValue) { - jsonDict.Add(key, value.Value); + public static void AddIfHasValue(this JsonDict jsonDict, string key, + T? value) where T : struct + { + if (value.HasValue) + { + jsonDict.Add(key, value.Value); + } } - } - - public static void AddIfHasValue(this JsonDict jsonDict, string key, - T value) where T : class { - if (value != null) { - jsonDict.Add(key, value); + + public static void AddIfHasValue(this JsonDict jsonDict, string key, + T value) where T : class + { + if (value != null) + { + jsonDict.Add(key, value); + } } } -} - + } diff --git a/firebaseai/src/LiveGenerationConfig.cs b/firebaseai/src/LiveGenerationConfig.cs index f4418f6d..d70b93ce 100644 --- a/firebaseai/src/LiveGenerationConfig.cs +++ b/firebaseai/src/LiveGenerationConfig.cs @@ -18,182 +18,191 @@ using System.Linq; using Firebase.AI.Internal; -namespace Firebase.AI { - -/// -/// A struct used to configure speech generation settings. -/// -public readonly struct SpeechConfig { - internal readonly string voice; - - private SpeechConfig(string voice) { - this.voice = voice; - } - +namespace Firebase.AI +{ /// - /// See https://cloud.google.com/text-to-speech/docs/chirp3-hd for the list of available voices. + /// A struct used to configure speech generation settings. /// - /// - /// - public static SpeechConfig UsePrebuiltVoice(string voice) { - return new SpeechConfig(voice); - } + public readonly struct SpeechConfig + { + internal readonly string voice; - /// - /// Intended for internal use only. - /// This method is used for serializing the object to JSON for the API request. - /// - internal Dictionary ToJson() { - Dictionary dict = new(); + private SpeechConfig(string voice) + { + this.voice = voice; + } + + /// + /// See https://cloud.google.com/text-to-speech/docs/chirp3-hd for the list of available voices. + /// + /// + /// + public static SpeechConfig UsePrebuiltVoice(string voice) + { + return new SpeechConfig(voice); + } + + /// + /// Intended for internal use only. + /// This method is used for serializing the object to JSON for the API request. + /// + internal Dictionary ToJson() + { + Dictionary dict = new(); - if (!string.IsNullOrWhiteSpace(voice)) { - dict["voiceConfig"] = new Dictionary() { + if (!string.IsNullOrWhiteSpace(voice)) + { + dict["voiceConfig"] = new Dictionary() { { "prebuiltVoiceConfig" , new Dictionary() { { "voiceName", voice } } } }; - } + } - return dict; + return dict; + } } -} - -/// -/// A struct defining model parameters to be used when generating live session content. -/// -public readonly struct LiveGenerationConfig { - private readonly SpeechConfig? _speechConfig; - private readonly List _responseModalities; - private readonly float? _temperature; - private readonly float? _topP; - private readonly float? _topK; - private readonly int? _maxOutputTokens; - private readonly float? _presencePenalty; - private readonly float? _frequencyPenalty; /// - /// Creates a new `LiveGenerationConfig` value. - /// - /// See the - /// [Configure model parameters](https://firebase.google.com/docs/vertex-ai/model-parameters) - /// guide and the - /// [Cloud documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#generationconfig) - /// for more details. + /// A struct defining model parameters to be used when generating live session content. /// - /// - /// The speech configuration to use if generating audio output. - /// - /// A list of response types to receive from the model. - /// Note: Currently only supports being provided one type, despite being a list. - /// - /// Controls the randomness of the language model's output. Higher values (for - /// example, 1.0) make the text more random and creative, while lower values (for example, - /// 0.1) make it more focused and deterministic. - /// - /// > Note: A temperature of 0 means that the highest probability tokens are always selected. - /// > In this case, responses for a given prompt are mostly deterministic, but a small amount - /// > of variation is still possible. - /// - /// > Important: The range of supported temperature values depends on the model; see the - /// > [Cloud documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#generationconfig) - /// > for more details. - /// - /// Controls diversity of generated text. Higher values (e.g., 0.9) produce more diverse - /// text, while lower values (e.g., 0.5) make the output more focused. - /// - /// The supported range is 0.0 to 1.0. - /// - /// > Important: The default `topP` value depends on the model; see the - /// [Cloud documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#generationconfig) - /// for more details. - /// - /// Limits the number of highest probability words the model considers when generating - /// text. For example, a topK of 40 means only the 40 most likely words are considered for the - /// next token. A higher value increases diversity, while a lower value makes the output more - /// deterministic. - /// - /// The supported range is 1 to 40. - /// - /// > Important: Support for `topK` and the default value depends on the model; see the - /// [Cloud documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#generationconfig) - /// for more details. - /// - /// Maximum number of tokens that can be generated in the response. - /// See the configure model parameters [documentation](https://firebase.google.com/docs/vertex-ai/model-parameters?platform=ios#max-output-tokens) - /// for more details. - /// - /// Controls the likelihood of repeating the same words or phrases already - /// generated in the text. Higher values increase the penalty of repetition, resulting in more - /// diverse output. - /// - /// > Note: While both `presencePenalty` and `frequencyPenalty` discourage repetition, - /// > `presencePenalty` applies the same penalty regardless of how many times the word/phrase - /// > has already appeared, whereas `frequencyPenalty` increases the penalty for *each* - /// > repetition of a word/phrase. - /// - /// > Important: The range of supported `presencePenalty` values depends on the model; see the - /// > [Cloud documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#generationconfig) - /// > for more details. - /// - /// Controls the likelihood of repeating words or phrases, with the penalty - /// increasing for each repetition. Higher values increase the penalty of repetition, - /// resulting in more diverse output. - /// - /// > Note: While both `frequencyPenalty` and `presencePenalty` discourage repetition, - /// > `frequencyPenalty` increases the penalty for *each* repetition of a word/phrase, whereas - /// > `presencePenalty` applies the same penalty regardless of how many times the word/phrase - /// > has already appeared. - /// - /// > Important: The range of supported `frequencyPenalty` values depends on the model; see - /// > the - /// > [Cloud documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#generationconfig) - /// > for more details. - /// - /// A set of up to 5 `String`s that will stop output generation. If specified, - /// the API will stop at the first appearance of a stop sequence. The stop sequence will not - /// be included as part of the response. See the - /// [Cloud documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#generationconfig) - /// for more details. - public LiveGenerationConfig( - SpeechConfig? speechConfig = null, - IEnumerable responseModalities = null, - float? temperature = null, - float? topP = null, - float? topK = null, - int? maxOutputTokens = null, - float? presencePenalty = null, - float? frequencyPenalty = null) { - _speechConfig = speechConfig; - _responseModalities = responseModalities != null ? - new List(responseModalities) : new List(); - _temperature = temperature; - _topP = topP; - _topK = topK; - _maxOutputTokens = maxOutputTokens; - _presencePenalty = presencePenalty; - _frequencyPenalty = frequencyPenalty; - } + public readonly struct LiveGenerationConfig + { + private readonly SpeechConfig? _speechConfig; + private readonly List _responseModalities; + private readonly float? _temperature; + private readonly float? _topP; + private readonly float? _topK; + private readonly int? _maxOutputTokens; + private readonly float? _presencePenalty; + private readonly float? _frequencyPenalty; - /// - /// Intended for internal use only. - /// This method is used for serializing the object to JSON for the API request. - /// - internal Dictionary ToJson() { - Dictionary jsonDict = new(); - if (_speechConfig.HasValue) jsonDict["speechConfig"] = _speechConfig?.ToJson(); - if (_responseModalities != null && _responseModalities.Any()) { - jsonDict["responseModalities"] = - _responseModalities.Select(EnumConverters.ResponseModalityToString).ToList(); + /// + /// Creates a new `LiveGenerationConfig` value. + /// + /// See the + /// [Configure model parameters](https://firebase.google.com/docs/vertex-ai/model-parameters) + /// guide and the + /// [Cloud documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#generationconfig) + /// for more details. + /// + /// + /// The speech configuration to use if generating audio output. + /// + /// A list of response types to receive from the model. + /// Note: Currently only supports being provided one type, despite being a list. + /// + /// Controls the randomness of the language model's output. Higher values (for + /// example, 1.0) make the text more random and creative, while lower values (for example, + /// 0.1) make it more focused and deterministic. + /// + /// > Note: A temperature of 0 means that the highest probability tokens are always selected. + /// > In this case, responses for a given prompt are mostly deterministic, but a small amount + /// > of variation is still possible. + /// + /// > Important: The range of supported temperature values depends on the model; see the + /// > [Cloud documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#generationconfig) + /// > for more details. + /// + /// Controls diversity of generated text. Higher values (e.g., 0.9) produce more diverse + /// text, while lower values (e.g., 0.5) make the output more focused. + /// + /// The supported range is 0.0 to 1.0. + /// + /// > Important: The default `topP` value depends on the model; see the + /// [Cloud documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#generationconfig) + /// for more details. + /// + /// Limits the number of highest probability words the model considers when generating + /// text. For example, a topK of 40 means only the 40 most likely words are considered for the + /// next token. A higher value increases diversity, while a lower value makes the output more + /// deterministic. + /// + /// The supported range is 1 to 40. + /// + /// > Important: Support for `topK` and the default value depends on the model; see the + /// [Cloud documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#generationconfig) + /// for more details. + /// + /// Maximum number of tokens that can be generated in the response. + /// See the configure model parameters [documentation](https://firebase.google.com/docs/vertex-ai/model-parameters?platform=ios#max-output-tokens) + /// for more details. + /// + /// Controls the likelihood of repeating the same words or phrases already + /// generated in the text. Higher values increase the penalty of repetition, resulting in more + /// diverse output. + /// + /// > Note: While both `presencePenalty` and `frequencyPenalty` discourage repetition, + /// > `presencePenalty` applies the same penalty regardless of how many times the word/phrase + /// > has already appeared, whereas `frequencyPenalty` increases the penalty for *each* + /// > repetition of a word/phrase. + /// + /// > Important: The range of supported `presencePenalty` values depends on the model; see the + /// > [Cloud documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#generationconfig) + /// > for more details. + /// + /// Controls the likelihood of repeating words or phrases, with the penalty + /// increasing for each repetition. Higher values increase the penalty of repetition, + /// resulting in more diverse output. + /// + /// > Note: While both `frequencyPenalty` and `presencePenalty` discourage repetition, + /// > `frequencyPenalty` increases the penalty for *each* repetition of a word/phrase, whereas + /// > `presencePenalty` applies the same penalty regardless of how many times the word/phrase + /// > has already appeared. + /// + /// > Important: The range of supported `frequencyPenalty` values depends on the model; see + /// > the + /// > [Cloud documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#generationconfig) + /// > for more details. + /// + /// A set of up to 5 `String`s that will stop output generation. If specified, + /// the API will stop at the first appearance of a stop sequence. The stop sequence will not + /// be included as part of the response. See the + /// [Cloud documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#generationconfig) + /// for more details. + public LiveGenerationConfig( + SpeechConfig? speechConfig = null, + IEnumerable responseModalities = null, + float? temperature = null, + float? topP = null, + float? topK = null, + int? maxOutputTokens = null, + float? presencePenalty = null, + float? frequencyPenalty = null) + { + _speechConfig = speechConfig; + _responseModalities = responseModalities != null ? + new List(responseModalities) : new List(); + _temperature = temperature; + _topP = topP; + _topK = topK; + _maxOutputTokens = maxOutputTokens; + _presencePenalty = presencePenalty; + _frequencyPenalty = frequencyPenalty; } - if (_temperature.HasValue) jsonDict["temperature"] = _temperature.Value; - if (_topP.HasValue) jsonDict["topP"] = _topP.Value; - if (_topK.HasValue) jsonDict["topK"] = _topK.Value; - if (_maxOutputTokens.HasValue) jsonDict["maxOutputTokens"] = _maxOutputTokens.Value; - if (_presencePenalty.HasValue) jsonDict["presencePenalty"] = _presencePenalty.Value; - if (_frequencyPenalty.HasValue) jsonDict["frequencyPenalty"] = _frequencyPenalty.Value; - return jsonDict; + /// + /// Intended for internal use only. + /// This method is used for serializing the object to JSON for the API request. + /// + internal Dictionary ToJson() + { + Dictionary jsonDict = new(); + if (_speechConfig.HasValue) jsonDict["speechConfig"] = _speechConfig?.ToJson(); + if (_responseModalities != null && _responseModalities.Any()) + { + jsonDict["responseModalities"] = + _responseModalities.Select(EnumConverters.ResponseModalityToString).ToList(); + } + if (_temperature.HasValue) jsonDict["temperature"] = _temperature.Value; + if (_topP.HasValue) jsonDict["topP"] = _topP.Value; + if (_topK.HasValue) jsonDict["topK"] = _topK.Value; + if (_maxOutputTokens.HasValue) jsonDict["maxOutputTokens"] = _maxOutputTokens.Value; + if (_presencePenalty.HasValue) jsonDict["presencePenalty"] = _presencePenalty.Value; + if (_frequencyPenalty.HasValue) jsonDict["frequencyPenalty"] = _frequencyPenalty.Value; + + return jsonDict; + } } -} } diff --git a/firebaseai/src/LiveGenerativeModel.cs b/firebaseai/src/LiveGenerativeModel.cs index f5f31826..d550ed0f 100644 --- a/firebaseai/src/LiveGenerativeModel.cs +++ b/firebaseai/src/LiveGenerativeModel.cs @@ -24,143 +24,167 @@ using Firebase.AI.Internal; using Google.MiniJSON; -namespace Firebase.AI { - -/// -/// A live, generative AI model for real-time interaction. -/// -/// See the [Cloud -/// documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/multimodal-live) -/// for more details about the low-latency, two-way interactions that use text, -/// audio, and video input, with audio and text output. -/// -/// > Warning: For Firebase AI, Live Model -/// is in Public Preview, which means that the feature is not subject to any SLA -/// or deprecation policy and could change in backwards-incompatible ways. -/// -public class LiveGenerativeModel { - private readonly FirebaseApp _firebaseApp; - - // Various setting fields provided by the user. - private readonly FirebaseAI.Backend _backend; - private readonly string _modelName; - private readonly LiveGenerationConfig? _liveConfig; - private readonly Tool[] _tools; - private readonly ModelContent? _systemInstruction; - private readonly RequestOptions? _requestOptions; - +namespace Firebase.AI +{ /// - /// Intended for internal use only. - /// Use `FirebaseAI.GetLiveModel` instead to ensure proper initialization and configuration of the `LiveGenerativeModel`. + /// A live, generative AI model for real-time interaction. + /// + /// See the [Cloud + /// documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/multimodal-live) + /// for more details about the low-latency, two-way interactions that use text, + /// audio, and video input, with audio and text output. + /// + /// > Warning: For Firebase AI, Live Model + /// is in Public Preview, which means that the feature is not subject to any SLA + /// or deprecation policy and could change in backwards-incompatible ways. /// - internal LiveGenerativeModel(FirebaseApp firebaseApp, - FirebaseAI.Backend backend, - string modelName, - LiveGenerationConfig? liveConfig = null, - Tool[] tools = null, - ModelContent? systemInstruction = null, - RequestOptions? requestOptions = null) { - _firebaseApp = firebaseApp; - _backend = backend; - _modelName = modelName; - _liveConfig = liveConfig; - _tools = tools; - _systemInstruction = systemInstruction; - _requestOptions = requestOptions; - } + public class LiveGenerativeModel + { + private readonly FirebaseApp _firebaseApp; - private string GetURL() { - if (_backend.Provider == FirebaseAI.Backend.InternalProvider.VertexAI) { - return "wss://firebasevertexai.googleapis.com/ws" + - "/google.firebase.vertexai.v1beta.LlmBidiService/BidiGenerateContent" + - $"/locations/{_backend.Location}" + - $"?key={_firebaseApp.Options.ApiKey}"; - } else if (_backend.Provider == FirebaseAI.Backend.InternalProvider.GoogleAI) { - return "wss://firebasevertexai.googleapis.com/ws" + - "/google.firebase.vertexai.v1beta.GenerativeService/BidiGenerateContent" + - $"?key={_firebaseApp.Options.ApiKey}"; - } else { - throw new NotSupportedException($"Missing support for backend: {_backend.Provider}"); + // Various setting fields provided by the user. + private readonly FirebaseAI.Backend _backend; + private readonly string _modelName; + private readonly LiveGenerationConfig? _liveConfig; + private readonly Tool[] _tools; + private readonly ModelContent? _systemInstruction; + private readonly RequestOptions? _requestOptions; + + /// + /// Intended for internal use only. + /// Use `FirebaseAI.GetLiveModel` instead to ensure proper initialization and configuration of the `LiveGenerativeModel`. + /// + internal LiveGenerativeModel(FirebaseApp firebaseApp, + FirebaseAI.Backend backend, + string modelName, + LiveGenerationConfig? liveConfig = null, + Tool[] tools = null, + ModelContent? systemInstruction = null, + RequestOptions? requestOptions = null) + { + _firebaseApp = firebaseApp; + _backend = backend; + _modelName = modelName; + _liveConfig = liveConfig; + _tools = tools; + _systemInstruction = systemInstruction; + _requestOptions = requestOptions; } - } - - private string GetModelName() { - if (_backend.Provider == FirebaseAI.Backend.InternalProvider.VertexAI) { - return $"projects/{_firebaseApp.Options.ProjectId}/locations/{_backend.Location}" + - $"/publishers/google/models/{_modelName}"; - } else if (_backend.Provider == FirebaseAI.Backend.InternalProvider.GoogleAI) { - return $"projects/{_firebaseApp.Options.ProjectId}" + - $"/models/{_modelName}"; - } else { - throw new NotSupportedException($"Missing support for backend: {_backend.Provider}"); + + private string GetURL() + { + if (_backend.Provider == FirebaseAI.Backend.InternalProvider.VertexAI) + { + return "wss://firebasevertexai.googleapis.com/ws" + + "/google.firebase.vertexai.v1beta.LlmBidiService/BidiGenerateContent" + + $"/locations/{_backend.Location}" + + $"?key={_firebaseApp.Options.ApiKey}"; + } + else if (_backend.Provider == FirebaseAI.Backend.InternalProvider.GoogleAI) + { + return "wss://firebasevertexai.googleapis.com/ws" + + "/google.firebase.vertexai.v1beta.GenerativeService/BidiGenerateContent" + + $"?key={_firebaseApp.Options.ApiKey}"; + } + else + { + throw new NotSupportedException($"Missing support for backend: {_backend.Provider}"); + } } - } - /// - /// Establishes a connection to a live generation service. - /// - /// This function handles the WebSocket connection setup and returns an `LiveSession` - /// object that can be used to communicate with the service. - /// - /// The token that can be used to cancel the creation of the session. - /// The LiveSession, once it is established. - public async Task ConnectAsync(CancellationToken cancellationToken = default) { - ClientWebSocket clientWebSocket = new(); - - string endpoint = GetURL(); - - // Set initial headers - string version = FirebaseInterops.GetVersionInfoSdkVersion(); - clientWebSocket.Options.SetRequestHeader("x-goog-api-client", $"gl-csharp/8.0 fire/{version}"); - if (FirebaseInterops.GetIsDataCollectionDefaultEnabled(_firebaseApp)) { - clientWebSocket.Options.SetRequestHeader("X-Firebase-AppId", _firebaseApp.Options.AppId); - clientWebSocket.Options.SetRequestHeader("X-Firebase-AppVersion", UnityEngine.Application.version); + private string GetModelName() + { + if (_backend.Provider == FirebaseAI.Backend.InternalProvider.VertexAI) + { + return $"projects/{_firebaseApp.Options.ProjectId}/locations/{_backend.Location}" + + $"/publishers/google/models/{_modelName}"; + } + else if (_backend.Provider == FirebaseAI.Backend.InternalProvider.GoogleAI) + { + return $"projects/{_firebaseApp.Options.ProjectId}" + + $"/models/{_modelName}"; + } + else + { + throw new NotSupportedException($"Missing support for backend: {_backend.Provider}"); + } } - // Add additional Firebase tokens to the header. - await FirebaseInterops.AddFirebaseTokensAsync(clientWebSocket, _firebaseApp); - // Add a timeout to the initial connection, using the RequestOptions. - using var connectionCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - TimeSpan connectionTimeout = _requestOptions?.Timeout ?? RequestOptions.DefaultTimeout; - connectionCts.CancelAfter(connectionTimeout); + /// + /// Establishes a connection to a live generation service. + /// + /// This function handles the WebSocket connection setup and returns an `LiveSession` + /// object that can be used to communicate with the service. + /// + /// The token that can be used to cancel the creation of the session. + /// The LiveSession, once it is established. + public async Task ConnectAsync(CancellationToken cancellationToken = default) + { + ClientWebSocket clientWebSocket = new(); - await clientWebSocket.ConnectAsync(new Uri(endpoint), connectionCts.Token); + string endpoint = GetURL(); - if (clientWebSocket.State != WebSocketState.Open) { - throw new WebSocketException("ClientWebSocket failed to connect, can't create LiveSession."); - } - - try { - // Send the initial setup message - Dictionary setupDict = new() { - { "model", $"projects/{_firebaseApp.Options.ProjectId}/locations/{_backend.Location}/publishers/google/models/{_modelName}" } - }; - if (_liveConfig != null) { - setupDict["generationConfig"] = _liveConfig?.ToJson(); - } - if (_systemInstruction.HasValue) { - setupDict["systemInstruction"] = _systemInstruction?.ToJson(); + // Set initial headers + string version = FirebaseInterops.GetVersionInfoSdkVersion(); + clientWebSocket.Options.SetRequestHeader("x-goog-api-client", $"gl-csharp/8.0 fire/{version}"); + if (FirebaseInterops.GetIsDataCollectionDefaultEnabled(_firebaseApp)) + { + clientWebSocket.Options.SetRequestHeader("X-Firebase-AppId", _firebaseApp.Options.AppId); + clientWebSocket.Options.SetRequestHeader("X-Firebase-AppVersion", UnityEngine.Application.version); } - if (_tools != null && _tools.Length > 0) { - setupDict["tools"] = _tools.Select(t => t.ToJson()).ToList(); + // Add additional Firebase tokens to the header. + await FirebaseInterops.AddFirebaseTokensAsync(clientWebSocket, _firebaseApp); + + // Add a timeout to the initial connection, using the RequestOptions. + using var connectionCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + TimeSpan connectionTimeout = _requestOptions?.Timeout ?? RequestOptions.DefaultTimeout; + connectionCts.CancelAfter(connectionTimeout); + + await clientWebSocket.ConnectAsync(new Uri(endpoint), connectionCts.Token); + + if (clientWebSocket.State != WebSocketState.Open) + { + throw new WebSocketException("ClientWebSocket failed to connect, can't create LiveSession."); } - Dictionary jsonDict = new() { + + try + { + // Send the initial setup message + Dictionary setupDict = new() { + { "model", $"projects/{_firebaseApp.Options.ProjectId}/locations/{_backend.Location}/publishers/google/models/{_modelName}" } + }; + if (_liveConfig != null) + { + setupDict["generationConfig"] = _liveConfig?.ToJson(); + } + if (_systemInstruction.HasValue) + { + setupDict["systemInstruction"] = _systemInstruction?.ToJson(); + } + if (_tools != null && _tools.Length > 0) + { + setupDict["tools"] = _tools.Select(t => t.ToJson()).ToList(); + } + Dictionary jsonDict = new() { { "setup", setupDict } }; - var byteArray = Encoding.UTF8.GetBytes(Json.Serialize(jsonDict)); - await clientWebSocket.SendAsync(new ArraySegment(byteArray), WebSocketMessageType.Binary, true, cancellationToken); + var byteArray = Encoding.UTF8.GetBytes(Json.Serialize(jsonDict)); + await clientWebSocket.SendAsync(new ArraySegment(byteArray), WebSocketMessageType.Binary, true, cancellationToken); - return new LiveSession(clientWebSocket); - } catch (Exception) { - if (clientWebSocket.State == WebSocketState.Open) { - // Try to clean up the WebSocket, to avoid leaking connections. - await clientWebSocket.CloseAsync(WebSocketCloseStatus.EndpointUnavailable, - "Failed to send initial setup message.", CancellationToken.None); + return new LiveSession(clientWebSocket); + } + catch (Exception) + { + if (clientWebSocket.State == WebSocketState.Open) + { + // Try to clean up the WebSocket, to avoid leaking connections. + await clientWebSocket.CloseAsync(WebSocketCloseStatus.EndpointUnavailable, + "Failed to send initial setup message.", CancellationToken.None); + } + throw; } - throw; } } -} } diff --git a/firebaseai/src/LiveSession.cs b/firebaseai/src/LiveSession.cs index 52030a7d..730fa043 100644 --- a/firebaseai/src/LiveSession.cs +++ b/firebaseai/src/LiveSession.cs @@ -24,134 +24,156 @@ using System.Threading.Tasks; using Google.MiniJSON; -namespace Firebase.AI { +namespace Firebase.AI +{ + /// + /// Manages asynchronous communication with Gemini model over a WebSocket + /// connection. + /// + public class LiveSession : IDisposable + { -/// -/// Manages asynchronous communication with Gemini model over a WebSocket -/// connection. -/// -public class LiveSession : IDisposable { + private readonly ClientWebSocket _clientWebSocket; - private readonly ClientWebSocket _clientWebSocket; + private readonly SemaphoreSlim _sendLock = new(1, 1); - private readonly SemaphoreSlim _sendLock = new(1, 1); + private bool _disposed = false; + private readonly object _disposeLock = new(); - private bool _disposed = false; - private readonly object _disposeLock = new(); + /// + /// Intended for internal use only. + /// Use `LiveGenerativeModel.ConnectAsync` instead to ensure proper initialization. + /// + internal LiveSession(ClientWebSocket clientWebSocket) + { + if (clientWebSocket.State != WebSocketState.Open) + { + throw new InvalidOperationException( + $"ClientWebSocket failed to connect, can't create LiveSession. Current state: {clientWebSocket.State}"); + } - /// - /// Intended for internal use only. - /// Use `LiveGenerativeModel.ConnectAsync` instead to ensure proper initialization. - /// - internal LiveSession(ClientWebSocket clientWebSocket) { - if (clientWebSocket.State != WebSocketState.Open) { - throw new InvalidOperationException( - $"ClientWebSocket failed to connect, can't create LiveSession. Current state: {clientWebSocket.State}"); + _clientWebSocket = clientWebSocket; } - _clientWebSocket = clientWebSocket; - } + protected virtual void Dispose(bool disposing) + { + lock (_disposeLock) + { + if (!_disposed) + { + if (_clientWebSocket.State == WebSocketState.Open) + { + _clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "LiveSession disposed", CancellationToken.None); + } - protected virtual void Dispose(bool disposing) { - lock (_disposeLock) { - if (!_disposed) { - if (_clientWebSocket.State == WebSocketState.Open) { - _clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "LiveSession disposed", CancellationToken.None); + _disposed = true; } - - _disposed = true; } } - } - public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); - } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - ~LiveSession() { - Dispose(false); - } + ~LiveSession() + { + Dispose(false); + } - private async Task InternalSendBytesAsync( - ArraySegment bytes, - CancellationToken cancellationToken) { - // WebSockets should only have a single Send active at once, so lock around it. - await _sendLock.WaitAsync(cancellationToken); - try { - cancellationToken.ThrowIfCancellationRequested(); + private async Task InternalSendBytesAsync( + ArraySegment bytes, + CancellationToken cancellationToken) + { + // WebSockets should only have a single Send active at once, so lock around it. + await _sendLock.WaitAsync(cancellationToken); + try + { + cancellationToken.ThrowIfCancellationRequested(); - if (_clientWebSocket.State != WebSocketState.Open) { - throw new InvalidOperationException("WebSocket is not open, cannot send message."); - } + if (_clientWebSocket.State != WebSocketState.Open) + { + throw new InvalidOperationException("WebSocket is not open, cannot send message."); + } - await _clientWebSocket.SendAsync(bytes, - WebSocketMessageType.Binary, true, - cancellationToken); - } finally { - _sendLock.Release(); + await _clientWebSocket.SendAsync(bytes, + WebSocketMessageType.Binary, true, + cancellationToken); + } + finally + { + _sendLock.Release(); + } } - } - /// - /// Sends a single piece of content to the server. - /// - /// The content to send. - /// Indicates to the server that the client's turn is complete. - /// A token to cancel the send operation. - public async Task SendAsync( - ModelContent? content = null, - bool turnComplete = false, - CancellationToken cancellationToken = default) { - // If the content has FunctionResponseParts, we handle those separately. - if (content.HasValue) { - var functionParts = content?.Parts.OfType().ToList(); - if (functionParts.Count > 0) { - Dictionary toolResponse = new() { + /// + /// Sends a single piece of content to the server. + /// + /// The content to send. + /// Indicates to the server that the client's turn is complete. + /// A token to cancel the send operation. + public async Task SendAsync( + ModelContent? content = null, + bool turnComplete = false, + CancellationToken cancellationToken = default) + { + // If the content has FunctionResponseParts, we handle those separately. + if (content.HasValue) + { + var functionParts = content?.Parts.OfType().ToList(); + if (functionParts.Count > 0) + { + Dictionary toolResponse = new() { { "toolResponse", new Dictionary() { { "functionResponses", functionParts.Select(frPart => (frPart as ModelContent.Part).ToJson()["functionResponse"]).ToList() } }} }; - var toolResponseBytes = Encoding.UTF8.GetBytes(Json.Serialize(toolResponse)); - - await InternalSendBytesAsync(new ArraySegment(toolResponseBytes), cancellationToken); - if (functionParts.Count < content?.Parts.Count) { - // There are other parts to send, so send them with the other method. - content = new ModelContent(role: content?.Role, - parts: content?.Parts.Where(p => p is not ModelContent.FunctionResponsePart)); - } else { - return; + var toolResponseBytes = Encoding.UTF8.GetBytes(Json.Serialize(toolResponse)); + + await InternalSendBytesAsync(new ArraySegment(toolResponseBytes), cancellationToken); + if (functionParts.Count < content?.Parts.Count) + { + // There are other parts to send, so send them with the other method. + content = new ModelContent(role: content?.Role, + parts: content?.Parts.Where(p => p is not ModelContent.FunctionResponsePart)); + } + else + { + return; + } } } - } - // Prepare the message payload - Dictionary contentDict = new() { + // Prepare the message payload + Dictionary contentDict = new() { { "turnComplete", turnComplete } }; - if (content.HasValue) { - contentDict["turns"] = new List(new [] { content?.ToJson() }); - } - Dictionary jsonDict = new() { + if (content.HasValue) + { + contentDict["turns"] = new List(new[] { content?.ToJson() }); + } + Dictionary jsonDict = new() { { "clientContent", contentDict } }; - var byteArray = Encoding.UTF8.GetBytes(Json.Serialize(jsonDict)); + var byteArray = Encoding.UTF8.GetBytes(Json.Serialize(jsonDict)); - await InternalSendBytesAsync(new ArraySegment(byteArray), cancellationToken); - } + await InternalSendBytesAsync(new ArraySegment(byteArray), cancellationToken); + } - /// - /// Send realtime input to the server. - /// - /// A list of media chunks to send. - /// A token to cancel the send operation. - public async Task SendMediaChunksAsync( - List mediaChunks, - CancellationToken cancellationToken = default) { - if (mediaChunks == null) return; - - // Prepare the message payload. - Dictionary jsonDict = new() { + /// + /// Send realtime input to the server. + /// + /// A list of media chunks to send. + /// A token to cancel the send operation. + public async Task SendMediaChunksAsync( + List mediaChunks, + CancellationToken cancellationToken = default) + { + if (mediaChunks == null) return; + + // Prepare the message payload. + Dictionary jsonDict = new() { { "realtimeInput", new Dictionary() { { @@ -161,94 +183,109 @@ public async Task SendMediaChunksAsync( } } }; - var byteArray = Encoding.UTF8.GetBytes(Json.Serialize(jsonDict)); + var byteArray = Encoding.UTF8.GetBytes(Json.Serialize(jsonDict)); - await InternalSendBytesAsync(new ArraySegment(byteArray), cancellationToken); - } + await InternalSendBytesAsync(new ArraySegment(byteArray), cancellationToken); + } - private static byte[] ConvertTo16BitPCM(float[] samples) { - short[] shortBuffer = new short[samples.Length]; - byte[] pcmBytes = new byte[samples.Length * 2]; + private static byte[] ConvertTo16BitPCM(float[] samples) + { + short[] shortBuffer = new short[samples.Length]; + byte[] pcmBytes = new byte[samples.Length * 2]; - for (int i = 0; i < samples.Length; i++) { - float sample = samples[i] * 32767.0f; - sample = Math.Clamp(sample, -32768.0f, 32767.0f); - shortBuffer[i] = (short)sample; + for (int i = 0; i < samples.Length; i++) + { + float sample = samples[i] * 32767.0f; + sample = Math.Clamp(sample, -32768.0f, 32767.0f); + shortBuffer[i] = (short)sample; + } + + // Efficiently copy short array to byte array (respects system endianness - usually little) + Buffer.BlockCopy(shortBuffer, 0, pcmBytes, 0, pcmBytes.Length); + + return pcmBytes; } - // Efficiently copy short array to byte array (respects system endianness - usually little) - Buffer.BlockCopy(shortBuffer, 0, pcmBytes, 0, pcmBytes.Length); + /// + /// Convenience function for sending audio data in a float[] to the server. + /// + /// The audio data to send. Expected format: 16 bit PCM audio at 16kHz little-endian. + /// A token to cancel the send operation. + public Task SendAudioAsync(float[] audioData, CancellationToken cancellationToken = default) + { + ModelContent.InlineDataPart inlineDataPart = new("audio/pcm", ConvertTo16BitPCM(audioData)); + return SendMediaChunksAsync(new List(new[] { inlineDataPart }), cancellationToken); + } - return pcmBytes; - } + /// + /// Receives a stream of responses from the server. Having multiple of these ongoing will result in unexpected behavior. + /// Closes upon receiving a TurnComplete from the server. + /// + /// A token to cancel the operation. + /// A stream of `LiveContentResponse`s from the backend. + public async IAsyncEnumerable ReceiveAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + if (_clientWebSocket.State != WebSocketState.Open) + { + throw new InvalidOperationException("WebSocket is not open. Cannot start receiving."); + } - /// - /// Convenience function for sending audio data in a float[] to the server. - /// - /// The audio data to send. Expected format: 16 bit PCM audio at 16kHz little-endian. - /// A token to cancel the send operation. - public Task SendAudioAsync(float[] audioData, CancellationToken cancellationToken = default) { - ModelContent.InlineDataPart inlineDataPart = new("audio/pcm", ConvertTo16BitPCM(audioData)); - return SendMediaChunksAsync(new List(new []{inlineDataPart}), cancellationToken); - } + StringBuilder messageBuilder = new(); + byte[] receiveBuffer = new byte[4096]; + Memory buffer = new(receiveBuffer); + while (!cancellationToken.IsCancellationRequested) + { + ValueWebSocketReceiveResult result = await _clientWebSocket.ReceiveAsync(buffer, cancellationToken); - /// - /// Receives a stream of responses from the server. Having multiple of these ongoing will result in unexpected behavior. - /// Closes upon receiving a TurnComplete from the server. - /// - /// A token to cancel the operation. - /// A stream of `LiveContentResponse`s from the backend. - public async IAsyncEnumerable ReceiveAsync( - [EnumeratorCancellation] CancellationToken cancellationToken = default) { - if (_clientWebSocket.State != WebSocketState.Open) { - throw new InvalidOperationException("WebSocket is not open. Cannot start receiving."); - } + if (result.MessageType == WebSocketMessageType.Close) + { + // Close initiated by the server + // TODO: Should this just close without logging anything? + break; + } + else if (result.MessageType == WebSocketMessageType.Text) + { + // We shouldn't get a Text response from the backend + throw new NotSupportedException("Text responses from the backend are not supported."); + } + else if (result.MessageType == WebSocketMessageType.Binary) + { + messageBuilder.Append(Encoding.UTF8.GetString(receiveBuffer, 0, result.Count)); - StringBuilder messageBuilder = new(); - byte[] receiveBuffer = new byte[4096]; - Memory buffer = new(receiveBuffer); - while (!cancellationToken.IsCancellationRequested) { - ValueWebSocketReceiveResult result = await _clientWebSocket.ReceiveAsync(buffer, cancellationToken); - - if (result.MessageType == WebSocketMessageType.Close) { - // Close initiated by the server - // TODO: Should this just close without logging anything? - break; - } else if (result.MessageType == WebSocketMessageType.Text) { - // We shouldn't get a Text response from the backend - throw new NotSupportedException("Text responses from the backend are not supported."); - } else if (result.MessageType == WebSocketMessageType.Binary) { - messageBuilder.Append(Encoding.UTF8.GetString(receiveBuffer, 0, result.Count)); - - if (result.EndOfMessage) { - LiveSessionResponse? response = LiveSessionResponse.FromJson(messageBuilder.ToString()); - // Reset for the next message. - messageBuilder.Clear(); - - if (response != null) { - yield return response.Value; - - // On receiving TurnComplete we close the ongoing connection. - if (response?.Message is LiveSessionContent serverContent && - serverContent.TurnComplete) { - break; + if (result.EndOfMessage) + { + LiveSessionResponse? response = LiveSessionResponse.FromJson(messageBuilder.ToString()); + // Reset for the next message. + messageBuilder.Clear(); + + if (response != null) + { + yield return response.Value; + + // On receiving TurnComplete we close the ongoing connection. + if (response?.Message is LiveSessionContent serverContent && + serverContent.TurnComplete) + { + break; + } } } } } + // Check cancellation again, in case that is why it is finished. + cancellationToken.ThrowIfCancellationRequested(); } - // Check cancellation again, in case that is why it is finished. - cancellationToken.ThrowIfCancellationRequested(); - } - /// - /// Close the `LiveSession`. - /// - /// A token to cancel the operation. - public Task CloseAsync(CancellationToken cancellationToken = default) { - return _clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, - "LiveSession CloseAsync called.", cancellationToken); + /// + /// Close the `LiveSession`. + /// + /// A token to cancel the operation. + public Task CloseAsync(CancellationToken cancellationToken = default) + { + return _clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, + "LiveSession CloseAsync called.", cancellationToken); + } } -} } diff --git a/firebaseai/src/LiveSessionResponse.cs b/firebaseai/src/LiveSessionResponse.cs index 5751698e..5db58779 100644 --- a/firebaseai/src/LiveSessionResponse.cs +++ b/firebaseai/src/LiveSessionResponse.cs @@ -21,216 +21,254 @@ using System; using System.Text; -namespace Firebase.AI { - -/// -/// Represents the response from the model for live content updates. -/// -public readonly struct LiveSessionResponse { - +namespace Firebase.AI +{ /// - /// The detailed message from the live session. + /// Represents the response from the model for live content updates. /// - public readonly ILiveSessionMessage Message { get; } + public readonly struct LiveSessionResponse + { - /// - /// The response's content as text, if it exists. - /// - public string Text { - get { - StringBuilder stringBuilder = new(); - if (Message is LiveSessionContent content && content.Content != null) { - foreach (var part in content.Content?.Parts) { - if (part is ModelContent.TextPart textPart) { - stringBuilder.Append(textPart.Text); + /// + /// The detailed message from the live session. + /// + public readonly ILiveSessionMessage Message { get; } + + /// + /// The response's content as text, if it exists. + /// + public string Text + { + get + { + StringBuilder stringBuilder = new(); + if (Message is LiveSessionContent content && content.Content != null) + { + foreach (var part in content.Content?.Parts) + { + if (part is ModelContent.TextPart textPart) + { + stringBuilder.Append(textPart.Text); + } } } + return stringBuilder.ToString(); } - return stringBuilder.ToString(); } - } - /// - /// The response's content that was audio, if it exists. - /// - public IReadOnlyList Audio { - get { - if (Message is LiveSessionContent content) { - return content.Content?.Parts - .OfType() - .Where(part => part.MimeType.StartsWith("audio/pcm")) - .Select(part => part.Data.ToArray()) - .ToList(); + /// + /// The response's content that was audio, if it exists. + /// + public IReadOnlyList Audio + { + get + { + if (Message is LiveSessionContent content) + { + return content.Content?.Parts + .OfType() + .Where(part => part.MimeType.StartsWith("audio/pcm")) + .Select(part => part.Data.ToArray()) + .ToList(); + } + return null; } - return null; } - } - /// - /// The response's content that was audio, if it exists, converted into floats. - /// - public IReadOnlyList AudioAsFloat { - get { - return Audio?.Select(ConvertBytesToFloat).ToArray(); + /// + /// The response's content that was audio, if it exists, converted into floats. + /// + public IReadOnlyList AudioAsFloat + { + get + { + return Audio?.Select(ConvertBytesToFloat).ToArray(); + } } - } - // Helper function to convert a byte array representing a 16-bit encoded - // Audio snippit into a float array, which Unity's built in libraries supports. - private float[] ConvertBytesToFloat(byte[] byteArray) { - // Assumes 16 bit encoding, which would be two bytes per sample. - int sampleCount = byteArray.Length / 2; - float[] floatArray = new float[sampleCount]; + // Helper function to convert a byte array representing a 16-bit encoded + // Audio snippit into a float array, which Unity's built in libraries supports. + private float[] ConvertBytesToFloat(byte[] byteArray) + { + // Assumes 16 bit encoding, which would be two bytes per sample. + int sampleCount = byteArray.Length / 2; + float[] floatArray = new float[sampleCount]; - for (int i = 0; i < sampleCount; i++) { - float sample = (short)(byteArray[i * 2] | (byteArray[i * 2 + 1] << 8)) / 32768f; - floatArray[i] = Math.Clamp(sample, -1f, 1f); // Ensure values are within the valid range - } + for (int i = 0; i < sampleCount; i++) + { + float sample = (short)(byteArray[i * 2] | (byteArray[i * 2 + 1] << 8)) / 32768f; + floatArray[i] = Math.Clamp(sample, -1f, 1f); // Ensure values are within the valid range + } - return floatArray; - } + return floatArray; + } - private LiveSessionResponse(ILiveSessionMessage liveSessionMessage) { - Message = liveSessionMessage; - } + private LiveSessionResponse(ILiveSessionMessage liveSessionMessage) + { + Message = liveSessionMessage; + } - /// - /// Intended for internal use only. - /// This method is used for deserializing JSON responses and should not be called directly. - /// - internal static LiveSessionResponse? FromJson(string jsonString) { - return FromJson(Json.Deserialize(jsonString) as Dictionary); - } + /// + /// Intended for internal use only. + /// This method is used for deserializing JSON responses and should not be called directly. + /// + internal static LiveSessionResponse? FromJson(string jsonString) + { + return FromJson(Json.Deserialize(jsonString) as Dictionary); + } - /// - /// Intended for internal use only. - /// This method is used for deserializing JSON responses and should not be called directly. - /// - internal static LiveSessionResponse? FromJson(Dictionary jsonDict) { - if (jsonDict.ContainsKey("setupComplete")) { - // We don't want to pass this along to the user, so return null instead. - return null; - } else if (jsonDict.TryParseValue("serverContent", out Dictionary serverContent)) { - // TODO: Other fields - return new LiveSessionResponse(LiveSessionContent.FromJson(serverContent)); - } else if (jsonDict.TryParseValue("toolCall", out Dictionary toolCall)) { - return new LiveSessionResponse(LiveSessionToolCall.FromJson(toolCall)); - } else if (jsonDict.TryParseValue("toolCallCancellation", out Dictionary toolCallCancellation)) { - return new LiveSessionResponse(LiveSessionToolCallCancellation.FromJson(toolCallCancellation)); - } else { - // TODO: Determine if we want to log this, or just ignore it? + /// + /// Intended for internal use only. + /// This method is used for deserializing JSON responses and should not be called directly. + /// + internal static LiveSessionResponse? FromJson(Dictionary jsonDict) + { + if (jsonDict.ContainsKey("setupComplete")) + { + // We don't want to pass this along to the user, so return null instead. + return null; + } + else if (jsonDict.TryParseValue("serverContent", out Dictionary serverContent)) + { + // TODO: Other fields + return new LiveSessionResponse(LiveSessionContent.FromJson(serverContent)); + } + else if (jsonDict.TryParseValue("toolCall", out Dictionary toolCall)) + { + return new LiveSessionResponse(LiveSessionToolCall.FromJson(toolCall)); + } + else if (jsonDict.TryParseValue("toolCallCancellation", out Dictionary toolCallCancellation)) + { + return new LiveSessionResponse(LiveSessionToolCallCancellation.FromJson(toolCallCancellation)); + } + else + { + // TODO: Determine if we want to log this, or just ignore it? #if FIREBASE_LOG_REST_CALLS - UnityEngine.Debug.Log($"Failed to parse LiveSessionResponse from JSON, with keys: {string.Join(',', jsonDict.Keys)}"); + UnityEngine.Debug.Log($"Failed to parse LiveSessionResponse from JSON, with keys: {string.Join(',', jsonDict.Keys)}"); #endif - return null; + return null; + } } } -} -/// -/// Represents a message received from a live session. -/// -public interface ILiveSessionMessage { } - -/// -/// Content generated by the model in a live session. -/// -public readonly struct LiveSessionContent : ILiveSessionMessage { /// - /// The main content data of the response. This can be `null` if there was no content. + /// Represents a message received from a live session. /// - public readonly ModelContent? Content { get; } + public interface ILiveSessionMessage { } /// - /// Whether the turn is complete. If true, indicates that the model is done - /// generating. + /// Content generated by the model in a live session. /// - public readonly bool TurnComplete { get; } + public readonly struct LiveSessionContent : ILiveSessionMessage + { + /// + /// The main content data of the response. This can be `null` if there was no content. + /// + public readonly ModelContent? Content { get; } - /// - /// Whether generation was interrupted. If true, indicates that a - /// client message has interrupted current model. - /// - public readonly bool Interrupted { get; } + /// + /// Whether the turn is complete. If true, indicates that the model is done + /// generating. + /// + public readonly bool TurnComplete { get; } + + /// + /// Whether generation was interrupted. If true, indicates that a + /// client message has interrupted current model. + /// + public readonly bool Interrupted { get; } + + private LiveSessionContent(ModelContent? content, bool turnComplete, bool interrupted) + { + Content = content; + TurnComplete = turnComplete; + Interrupted = interrupted; + } - private LiveSessionContent(ModelContent? content, bool turnComplete, bool interrupted) { - Content = content; - TurnComplete = turnComplete; - Interrupted = interrupted; + /// + /// Intended for internal use only. + /// This method is used for deserializing JSON responses and should not be called directly. + /// + internal static LiveSessionContent FromJson(Dictionary jsonDict) + { + return new LiveSessionContent( + jsonDict.ParseNullableObject("modelTurn", ModelContent.FromJson), + jsonDict.ParseValue("turnComplete"), + jsonDict.ParseValue("interrupted") + ); + } } /// - /// Intended for internal use only. - /// This method is used for deserializing JSON responses and should not be called directly. + /// A request to use a tool from the live session. /// - internal static LiveSessionContent FromJson(Dictionary jsonDict) { - return new LiveSessionContent( - jsonDict.ParseNullableObject("modelTurn", ModelContent.FromJson), - jsonDict.ParseValue("turnComplete"), - jsonDict.ParseValue("interrupted") - ); - } -} + public readonly struct LiveSessionToolCall : ILiveSessionMessage + { + private readonly IReadOnlyList _functionCalls; -/// -/// A request to use a tool from the live session. -/// -public readonly struct LiveSessionToolCall : ILiveSessionMessage { - private readonly IReadOnlyList _functionCalls; + /// + /// A list of `ModelContent.FunctionCallPart` included in the response, if any. + /// + /// This will be empty if no function calls are present. + /// + public IReadOnlyList FunctionCalls + { + get + { + return _functionCalls ?? new List(); + } + } - /// - /// A list of `ModelContent.FunctionCallPart` included in the response, if any. - /// - /// This will be empty if no function calls are present. - /// - public IReadOnlyList FunctionCalls { - get { - return _functionCalls ?? new List(); + private LiveSessionToolCall(List functionCalls) + { + _functionCalls = functionCalls; } - } - private LiveSessionToolCall(List functionCalls) { - _functionCalls = functionCalls; + /// + /// Intended for internal use only. + /// This method is used for deserializing JSON responses and should not be called directly. + /// + internal static LiveSessionToolCall FromJson(Dictionary jsonDict) + { + return new LiveSessionToolCall( + jsonDict.ParseObjectList("functionCalls", + innerDict => ModelContentJsonParsers.FunctionCallPartFromJson(innerDict, null, null))); + } } /// - /// Intended for internal use only. - /// This method is used for deserializing JSON responses and should not be called directly. + /// A request to cancel using a tool from the live session. /// - internal static LiveSessionToolCall FromJson(Dictionary jsonDict) { - return new LiveSessionToolCall( - jsonDict.ParseObjectList("functionCalls", - innerDict => ModelContentJsonParsers.FunctionCallPartFromJson(innerDict, null, null))); - } -} + public readonly struct LiveSessionToolCallCancellation : ILiveSessionMessage + { + private readonly IReadOnlyList _functionIds; -/// -/// A request to cancel using a tool from the live session. -/// -public readonly struct LiveSessionToolCallCancellation : ILiveSessionMessage { - private readonly IReadOnlyList _functionIds; - - /// - /// The list of Function IDs to cancel. - /// - public IReadOnlyList FunctionIds { - get { - return _functionIds ?? new List(); + /// + /// The list of Function IDs to cancel. + /// + public IReadOnlyList FunctionIds + { + get + { + return _functionIds ?? new List(); + } } - } - private LiveSessionToolCallCancellation(List functionIds) { - _functionIds = functionIds; - } + private LiveSessionToolCallCancellation(List functionIds) + { + _functionIds = functionIds; + } - /// - /// Intended for internal use only. - /// This method is used for deserializing JSON responses and should not be called directly. - /// - internal static LiveSessionToolCallCancellation FromJson(Dictionary jsonDict) { - return new LiveSessionToolCallCancellation( - jsonDict.ParseStringList("ids")); + /// + /// Intended for internal use only. + /// This method is used for deserializing JSON responses and should not be called directly. + /// + internal static LiveSessionToolCallCancellation FromJson(Dictionary jsonDict) + { + return new LiveSessionToolCallCancellation( + jsonDict.ParseStringList("ids")); + } } -} } diff --git a/firebaseai/src/ModalityTokenCount.cs b/firebaseai/src/ModalityTokenCount.cs index 8b230d7c..4aa0b5cb 100644 --- a/firebaseai/src/ModalityTokenCount.cs +++ b/firebaseai/src/ModalityTokenCount.cs @@ -17,77 +17,83 @@ using System.Collections.Generic; using Firebase.AI.Internal; -namespace Firebase.AI { - -/// -/// Content part modality. -/// -public enum ContentModality { - /// - /// A new and not yet supported value. - /// - Unknown = 0, - /// - /// Plain text. - /// - Text, - /// - /// Image. - /// - Image, - /// - /// Video. - /// - Video, - /// - /// Audio. - /// - Audio, +namespace Firebase.AI +{ /// - /// Document, e.g. PDF. + /// Content part modality. /// - Document, -} + public enum ContentModality + { + /// + /// A new and not yet supported value. + /// + Unknown = 0, + /// + /// Plain text. + /// + Text, + /// + /// Image. + /// + Image, + /// + /// Video. + /// + Video, + /// + /// Audio. + /// + Audio, + /// + /// Document, e.g. PDF. + /// + Document, + } -/// -/// Represents token counting info for a single modality. -/// -public readonly struct ModalityTokenCount { - /// - /// The modality associated with this token count. - /// - public ContentModality Modality { get; } /// - /// The number of tokens counted. + /// Represents token counting info for a single modality. /// - public int TokenCount { get; } + public readonly struct ModalityTokenCount + { + /// + /// The modality associated with this token count. + /// + public ContentModality Modality { get; } + /// + /// The number of tokens counted. + /// + public int TokenCount { get; } - // Hidden constructor, users don't need to make this - private ModalityTokenCount(ContentModality modality, int tokenCount) { - Modality = modality; - TokenCount = tokenCount; - } + // Hidden constructor, users don't need to make this + private ModalityTokenCount(ContentModality modality, int tokenCount) + { + Modality = modality; + TokenCount = tokenCount; + } - private static ContentModality ParseModality(string str) { - return str switch { - "TEXT" => ContentModality.Text, - "IMAGE" => ContentModality.Image, - "VIDEO" => ContentModality.Video, - "AUDIO" => ContentModality.Audio, - "DOCUMENT" => ContentModality.Document, - _ => ContentModality.Unknown, - }; - } + private static ContentModality ParseModality(string str) + { + return str switch + { + "TEXT" => ContentModality.Text, + "IMAGE" => ContentModality.Image, + "VIDEO" => ContentModality.Video, + "AUDIO" => ContentModality.Audio, + "DOCUMENT" => ContentModality.Document, + _ => ContentModality.Unknown, + }; + } - /// - /// Intended for internal use only. - /// This method is used for deserializing JSON responses and should not be called directly. - /// - internal static ModalityTokenCount FromJson(Dictionary jsonDict) { - return new ModalityTokenCount( - jsonDict.ParseEnum("modality", ParseModality), - jsonDict.ParseValue("tokenCount")); + /// + /// Intended for internal use only. + /// This method is used for deserializing JSON responses and should not be called directly. + /// + internal static ModalityTokenCount FromJson(Dictionary jsonDict) + { + return new ModalityTokenCount( + jsonDict.ParseEnum("modality", ParseModality), + jsonDict.ParseValue("tokenCount")); + } } -} } diff --git a/firebaseai/src/ModelContent.cs b/firebaseai/src/ModelContent.cs index 7fa0f2bb..a96f79b8 100644 --- a/firebaseai/src/ModelContent.cs +++ b/firebaseai/src/ModelContent.cs @@ -19,611 +19,678 @@ using System.Linq; using Firebase.AI.Internal; -namespace Firebase.AI { - -/// -/// A type describing data in media formats interpretable by an AI model. Each generative AI -/// request or response contains a list of `ModelContent`s, and each `ModelContent` value -/// may comprise multiple heterogeneous `ModelContent.Part`s. -/// -public readonly struct ModelContent { - private readonly string _role; - private readonly IReadOnlyList _parts; - +namespace Firebase.AI +{ /// - /// The role of the entity creating the `ModelContent`. For user-generated client requests, - /// for example, the role is `user`. + /// A type describing data in media formats interpretable by an AI model. Each generative AI + /// request or response contains a list of `ModelContent`s, and each `ModelContent` value + /// may comprise multiple heterogeneous `ModelContent.Part`s. /// - public string Role { - get { - return string.IsNullOrWhiteSpace(_role) ? "user" : _role; + public readonly struct ModelContent + { + private readonly string _role; + private readonly IReadOnlyList _parts; + + /// + /// The role of the entity creating the `ModelContent`. For user-generated client requests, + /// for example, the role is `user`. + /// + public string Role + { + get + { + return string.IsNullOrWhiteSpace(_role) ? "user" : _role; + } } - } - /// - /// The data parts comprising this `ModelContent` value. - /// - public IReadOnlyList Parts { - get { - return _parts ?? new List(); + /// + /// The data parts comprising this `ModelContent` value. + /// + public IReadOnlyList Parts + { + get + { + return _parts ?? new List(); + } } - } - /// - /// Creates a `ModelContent` with the given `Part`s, using the default `user` role. - /// - public ModelContent(params Part[] parts) : this("user", parts) { } + /// + /// Creates a `ModelContent` with the given `Part`s, using the default `user` role. + /// + public ModelContent(params Part[] parts) : this("user", parts) { } - /// - /// Creates a `ModelContent` with the given `Part`s, using the default `user` role. - /// - public ModelContent(IEnumerable parts) : this("user", parts) { } + /// + /// Creates a `ModelContent` with the given `Part`s, using the default `user` role. + /// + public ModelContent(IEnumerable parts) : this("user", parts) { } - /// - /// Creates a `ModelContent` with the given role and `Part`s. - /// - public ModelContent(string role, params Part[] parts) : this(role, (IEnumerable)parts) { } + /// + /// Creates a `ModelContent` with the given role and `Part`s. + /// + public ModelContent(string role, params Part[] parts) : this(role, (IEnumerable)parts) { } - /// - /// Creates a `ModelContent` with the given role and `Part`s. - /// - public ModelContent(string role, IEnumerable parts) { - _role = role; - _parts = parts?.ToList(); - } + /// + /// Creates a `ModelContent` with the given role and `Part`s. + /// + public ModelContent(string role, IEnumerable parts) + { + _role = role; + _parts = parts?.ToList(); + } -#region Helper Factories + #region Helper Factories - /// - /// Creates a new `ModelContent` with the default `user` role, and a - /// `TextPart` containing the given text. - /// - public static ModelContent Text(string text) { - return new ModelContent(new TextPart(text)); - } + /// + /// Creates a new `ModelContent` with the default `user` role, and a + /// `TextPart` containing the given text. + /// + public static ModelContent Text(string text) + { + return new ModelContent(new TextPart(text)); + } - /// - /// Creates a new `ModelContent` with the default `user` role, and an - /// `InlineDataPart` containing the given mimeType and data. - /// - public static ModelContent InlineData(string mimeType, byte[] data) { - return new ModelContent(new InlineDataPart(mimeType, data)); - } + /// + /// Creates a new `ModelContent` with the default `user` role, and an + /// `InlineDataPart` containing the given mimeType and data. + /// + public static ModelContent InlineData(string mimeType, byte[] data) + { + return new ModelContent(new InlineDataPart(mimeType, data)); + } - /// - /// Creates a new `ModelContent` with the default `user` role, and a - /// `FileDataPart` containing the given mimeType and data. - /// - public static ModelContent FileData(string mimeType, System.Uri uri) { - return new ModelContent(new FileDataPart(mimeType, uri)); - } + /// + /// Creates a new `ModelContent` with the default `user` role, and a + /// `FileDataPart` containing the given mimeType and data. + /// + public static ModelContent FileData(string mimeType, System.Uri uri) + { + return new ModelContent(new FileDataPart(mimeType, uri)); + } - /// - /// Creates a new `ModelContent` with the default `user` role, and a - /// `FunctionResponsePart` containing the given name and args. - /// - public static ModelContent FunctionResponse( - string name, IDictionary response, string id = null) { - return new ModelContent(new FunctionResponsePart(name, response, id)); - } + /// + /// Creates a new `ModelContent` with the default `user` role, and a + /// `FunctionResponsePart` containing the given name and args. + /// + public static ModelContent FunctionResponse( + string name, IDictionary response, string id = null) + { + return new ModelContent(new FunctionResponsePart(name, response, id)); + } - // TODO: Possibly more, like Multi, Model, FunctionResponses, System (only on Dart?) + // TODO: Possibly more, like Multi, Model, FunctionResponses, System (only on Dart?) - // TODO: Do we want to include helper factories for common C# or Unity types? -#endregion + // TODO: Do we want to include helper factories for common C# or Unity types? + #endregion -#region Parts + #region Parts - /// - /// A discrete piece of data in a media format interpretable by an AI model. Within a - /// single value of `Part`, different data types may not mix. - /// - public interface Part { /// - /// Indicates whether this `Part` is a summary of the model's internal thinking process. - /// - /// When `IncludeThoughts` is set to `true` in `ThinkingConfig`, the model may return one or - /// more "thought" parts that provide insight into how it reasoned through the prompt to arrive - /// at the final answer. These parts will have `IsThought` set to `true`. + /// A discrete piece of data in a media format interpretable by an AI model. Within a + /// single value of `Part`, different data types may not mix. /// - public bool IsThought { get; } + public interface Part + { + /// + /// Indicates whether this `Part` is a summary of the model's internal thinking process. + /// + /// When `IncludeThoughts` is set to `true` in `ThinkingConfig`, the model may return one or + /// more "thought" parts that provide insight into how it reasoned through the prompt to arrive + /// at the final answer. These parts will have `IsThought` set to `true`. + /// + public bool IsThought { get; } #if !DOXYGEN - /// - /// Intended for internal use only. - /// This method is used for serializing the object to JSON for the API request. - /// - Dictionary ToJson(); + /// + /// Intended for internal use only. + /// This method is used for serializing the object to JSON for the API request. + /// + Dictionary ToJson(); #endif - } + } - /// - /// A text part containing a string value. - /// - public readonly struct TextPart : Part { /// - /// Text value. + /// A text part containing a string value. /// - public string Text { get; } - - private readonly bool? _isThought; - public bool IsThought { get { return _isThought ?? false; } } - - private readonly string _thoughtSignature; + public readonly struct TextPart : Part + { + /// + /// Text value. + /// + public string Text { get; } - /// - /// Creates a `TextPart` with the given text. - /// - /// The text value to use. - public TextPart(string text) { - Text = text; - _isThought = null; - _thoughtSignature = null; - } + private readonly bool? _isThought; + public bool IsThought { get { return _isThought ?? false; } } - /// - /// Intended for internal use only. - /// - internal TextPart(string text, bool? isThought, string thoughtSignature) { - Text = text; - _isThought = isThought; - _thoughtSignature = thoughtSignature; - } + private readonly string _thoughtSignature; + + /// + /// Creates a `TextPart` with the given text. + /// + /// The text value to use. + public TextPart(string text) + { + Text = text; + _isThought = null; + _thoughtSignature = null; + } - Dictionary Part.ToJson() { - var jsonDict = new Dictionary() { + /// + /// Intended for internal use only. + /// + internal TextPart(string text, bool? isThought, string thoughtSignature) + { + Text = text; + _isThought = isThought; + _thoughtSignature = thoughtSignature; + } + + Dictionary Part.ToJson() + { + var jsonDict = new Dictionary() { { "text", Text } }; - jsonDict.AddIfHasValue("thought", _isThought); - jsonDict.AddIfHasValue("thoughtSignature", _thoughtSignature); - return jsonDict; + jsonDict.AddIfHasValue("thought", _isThought); + jsonDict.AddIfHasValue("thoughtSignature", _thoughtSignature); + return jsonDict; + } } - } - /// - /// Data with a specified media type. - /// Note: Not all media types may be supported by the AI model. - /// - public readonly struct InlineDataPart : Part { /// - /// The IANA standard MIME type of the data. + /// Data with a specified media type. + /// Note: Not all media types may be supported by the AI model. /// - public string MimeType { get; } - /// - /// The data provided in the inline data part. - /// - public byte[] Data { get; } - - private readonly bool? _isThought; - public bool IsThought { get { return _isThought ?? false; } } - - private readonly string _thoughtSignature; + public readonly struct InlineDataPart : Part + { + /// + /// The IANA standard MIME type of the data. + /// + public string MimeType { get; } + /// + /// The data provided in the inline data part. + /// + public byte[] Data { get; } - /// - /// Creates an `InlineDataPart` from data and a MIME type. - /// - /// > Important: Supported input types depend on the model on the model being used; see [input - /// files and requirements](https://firebase.google.com/docs/vertex-ai/input-file-requirements) - /// for more details. - /// - /// The IANA standard MIME type of the data, for example, `"image/jpeg"` or - /// `"video/mp4"`; see [input files and - /// requirements](https://firebase.google.com/docs/vertex-ai/input-file-requirements) for - /// supported values. - /// The data representation of an image, video, audio or document; see [input files and - /// requirements](https://firebase.google.com/docs/vertex-ai/input-file-requirements) for - /// supported media types. - public InlineDataPart(string mimeType, byte[] data) { - MimeType = mimeType; - Data = data; - _isThought = null; - _thoughtSignature = null; - } + private readonly bool? _isThought; + public bool IsThought { get { return _isThought ?? false; } } - internal InlineDataPart(string mimeType, byte[] data, bool? isThought, string thoughtSignature) { - MimeType = mimeType; - Data = data; - _isThought = isThought; - _thoughtSignature = thoughtSignature; - } + private readonly string _thoughtSignature; - Dictionary Part.ToJson() { - var jsonDict = new Dictionary() { + /// + /// Creates an `InlineDataPart` from data and a MIME type. + /// + /// > Important: Supported input types depend on the model on the model being used; see [input + /// files and requirements](https://firebase.google.com/docs/vertex-ai/input-file-requirements) + /// for more details. + /// + /// The IANA standard MIME type of the data, for example, `"image/jpeg"` or + /// `"video/mp4"`; see [input files and + /// requirements](https://firebase.google.com/docs/vertex-ai/input-file-requirements) for + /// supported values. + /// The data representation of an image, video, audio or document; see [input files and + /// requirements](https://firebase.google.com/docs/vertex-ai/input-file-requirements) for + /// supported media types. + public InlineDataPart(string mimeType, byte[] data) + { + MimeType = mimeType; + Data = data; + _isThought = null; + _thoughtSignature = null; + } + + internal InlineDataPart(string mimeType, byte[] data, bool? isThought, string thoughtSignature) + { + MimeType = mimeType; + Data = data; + _isThought = isThought; + _thoughtSignature = thoughtSignature; + } + + Dictionary Part.ToJson() + { + var jsonDict = new Dictionary() { { "inlineData", new Dictionary() { { "mimeType", MimeType }, { "data", Convert.ToBase64String(Data) } } } }; - jsonDict.AddIfHasValue("thought", _isThought); - jsonDict.AddIfHasValue("thoughtSignature", _thoughtSignature); - return jsonDict; + jsonDict.AddIfHasValue("thought", _isThought); + jsonDict.AddIfHasValue("thoughtSignature", _thoughtSignature); + return jsonDict; + } } - } - /// - /// File data stored in Cloud Storage for Firebase, referenced by a URI. - /// - public readonly struct FileDataPart : Part { - /// - /// The IANA standard MIME type of the data. - /// - public string MimeType { get; } /// - /// The URI of the file. + /// File data stored in Cloud Storage for Firebase, referenced by a URI. /// - public System.Uri Uri { get; } - - // This Part can only come from the user, and thus will never be a thought. - public bool IsThought { get { return false; } } + public readonly struct FileDataPart : Part + { + /// + /// The IANA standard MIME type of the data. + /// + public string MimeType { get; } + /// + /// The URI of the file. + /// + public System.Uri Uri { get; } - /// - /// Constructs a new file data part. - /// - /// The IANA standard MIME type of the uploaded file, for example, `"image/jpeg"` - /// or `"video/mp4"`; see [media requirements - /// ](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/send-multimodal-prompts#media_requirements) - /// for supported values. - /// The `"gs://"`-prefixed URI of the file in Cloud Storage for Firebase, for example, - /// `"gs://bucket-name/path/image.jpg"` - public FileDataPart(string mimeType, System.Uri uri) { MimeType = mimeType; Uri = uri; } - - Dictionary Part.ToJson() { - return new Dictionary() { + // This Part can only come from the user, and thus will never be a thought. + public bool IsThought { get { return false; } } + + /// + /// Constructs a new file data part. + /// + /// The IANA standard MIME type of the uploaded file, for example, `"image/jpeg"` + /// or `"video/mp4"`; see [media requirements + /// ](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/send-multimodal-prompts#media_requirements) + /// for supported values. + /// The `"gs://"`-prefixed URI of the file in Cloud Storage for Firebase, for example, + /// `"gs://bucket-name/path/image.jpg"` + public FileDataPart(string mimeType, System.Uri uri) { MimeType = mimeType; Uri = uri; } + + Dictionary Part.ToJson() + { + return new Dictionary() { { "fileData", new Dictionary() { { "mimeType", MimeType }, { "fileUri", Uri.AbsoluteUri } } } }; + } } - } - /// - /// A predicted function call returned from the model. - /// - public readonly struct FunctionCallPart : Part { - /// - /// The name of the registered function to call. - /// - public string Name { get; } - /// - /// The function parameters and values, matching the registered schema. - /// - public IReadOnlyDictionary Args { get; } /// - /// An identifier that should be passed along in the FunctionResponsePart. + /// A predicted function call returned from the model. /// - public string Id { get; } - - private readonly bool? _isThought; - public bool IsThought { get { return _isThought ?? false; } } - - private readonly string _thoughtSignature; - - /// - /// Intended for internal use only. - /// - internal FunctionCallPart(string name, IDictionary args, string id, - bool? isThought, string thoughtSignature) { - Name = name; - Args = new Dictionary(args); - Id = id; - _isThought = isThought; - _thoughtSignature = thoughtSignature; - } + public readonly struct FunctionCallPart : Part + { + /// + /// The name of the registered function to call. + /// + public string Name { get; } + /// + /// The function parameters and values, matching the registered schema. + /// + public IReadOnlyDictionary Args { get; } + /// + /// An identifier that should be passed along in the FunctionResponsePart. + /// + public string Id { get; } + + private readonly bool? _isThought; + public bool IsThought { get { return _isThought ?? false; } } + + private readonly string _thoughtSignature; + + /// + /// Intended for internal use only. + /// + internal FunctionCallPart(string name, IDictionary args, string id, + bool? isThought, string thoughtSignature) + { + Name = name; + Args = new Dictionary(args); + Id = id; + _isThought = isThought; + _thoughtSignature = thoughtSignature; + } - Dictionary Part.ToJson() { - var innerDict = new Dictionary() { + Dictionary Part.ToJson() + { + var innerDict = new Dictionary() { { "name", Name }, { "args", Args } }; - innerDict.AddIfHasValue("id", Id); + innerDict.AddIfHasValue("id", Id); - var jsonDict = new Dictionary() { + var jsonDict = new Dictionary() { { "functionCall", innerDict } }; - jsonDict.AddIfHasValue("thought", _isThought); - jsonDict.AddIfHasValue("thoughtSignature", _thoughtSignature); - return jsonDict; + jsonDict.AddIfHasValue("thought", _isThought); + jsonDict.AddIfHasValue("thoughtSignature", _thoughtSignature); + return jsonDict; + } } - } - /// - /// Result output from a function call. - /// - /// Contains a string representing the `FunctionDeclaration.name` and a structured JSON object - /// containing any output from the function is used as context to the model. This should contain the - /// result of a `FunctionCallPart` made based on model prediction. - /// - public readonly struct FunctionResponsePart : Part { - /// - /// The name of the function that was called. - /// - public string Name { get; } /// - /// The function's response or return value. - /// - public IReadOnlyDictionary Response { get; } - /// - /// The id from the FunctionCallPart this is in response to. + /// Result output from a function call. + /// + /// Contains a string representing the `FunctionDeclaration.name` and a structured JSON object + /// containing any output from the function is used as context to the model. This should contain the + /// result of a `FunctionCallPart` made based on model prediction. /// - public string Id { get; } - - // This Part can only come from the user, and thus will never be a thought. - public bool IsThought { get { return false; } } + public readonly struct FunctionResponsePart : Part + { + /// + /// The name of the function that was called. + /// + public string Name { get; } + /// + /// The function's response or return value. + /// + public IReadOnlyDictionary Response { get; } + /// + /// The id from the FunctionCallPart this is in response to. + /// + public string Id { get; } - /// - /// Constructs a new `FunctionResponsePart`. - /// - /// The name of the function that was called. - /// The function's response. - /// The id from the FunctionCallPart this is in response to. - public FunctionResponsePart(string name, IDictionary response, string id = null) { - Name = name; - Response = new Dictionary(response); - Id = id; - } + // This Part can only come from the user, and thus will never be a thought. + public bool IsThought { get { return false; } } - Dictionary Part.ToJson() { - var result = new Dictionary() { + /// + /// Constructs a new `FunctionResponsePart`. + /// + /// The name of the function that was called. + /// The function's response. + /// The id from the FunctionCallPart this is in response to. + public FunctionResponsePart(string name, IDictionary response, string id = null) + { + Name = name; + Response = new Dictionary(response); + Id = id; + } + + Dictionary Part.ToJson() + { + var result = new Dictionary() { { "name", Name }, { "response", Response } }; - if (!string.IsNullOrEmpty(Id)) { - result["id"] = Id; - } - return new Dictionary() { + if (!string.IsNullOrEmpty(Id)) + { + result["id"] = Id; + } + return new Dictionary() { { "functionResponse", result } }; - } - } - - /// - /// A part containing code that was executed by the model. - /// - public readonly struct ExecutableCodePart : Part { - public enum CodeLanguage { - Unspecified = 0, - Python + } } /// - /// The language - /// - public CodeLanguage Language { get; } - /// - /// The code that was executed. + /// A part containing code that was executed by the model. /// - public string Code { get; } + public readonly struct ExecutableCodePart : Part + { + public enum CodeLanguage + { + Unspecified = 0, + Python + } - private readonly bool? _isThought; - public bool IsThought { get { return _isThought ?? false; } } + /// + /// The language + /// + public CodeLanguage Language { get; } + /// + /// The code that was executed. + /// + public string Code { get; } - private readonly string _thoughtSignature; + private readonly bool? _isThought; + public bool IsThought { get { return _isThought ?? false; } } - private static CodeLanguage ParseLanguage(string str) { - return str switch { - "PYTHON" => CodeLanguage.Python, - _ => CodeLanguage.Unspecified, - }; - } + private readonly string _thoughtSignature; - private string LanguageAsString { - get { - return Language switch { - CodeLanguage.Python => "PYTHON", - _ => "LANGUAGE_UNSPECIFIED" + private static CodeLanguage ParseLanguage(string str) + { + return str switch + { + "PYTHON" => CodeLanguage.Python, + _ => CodeLanguage.Unspecified, }; } - } - /// - /// Intended for internal use only. - /// - internal ExecutableCodePart(string language, string code, - bool? isThought, string thoughtSignature) { - Language = ParseLanguage(language); - Code = code; - _isThought = isThought; - _thoughtSignature = thoughtSignature; - } + private string LanguageAsString + { + get + { + return Language switch + { + CodeLanguage.Python => "PYTHON", + _ => "LANGUAGE_UNSPECIFIED" + }; + } + } - Dictionary Part.ToJson() { - var jsonDict = new Dictionary() { + /// + /// Intended for internal use only. + /// + internal ExecutableCodePart(string language, string code, + bool? isThought, string thoughtSignature) + { + Language = ParseLanguage(language); + Code = code; + _isThought = isThought; + _thoughtSignature = thoughtSignature; + } + + Dictionary Part.ToJson() + { + var jsonDict = new Dictionary() { { "executableCode", new Dictionary() { { "language", LanguageAsString }, { "code", Code } } } }; - jsonDict.AddIfHasValue("thought", _isThought); - jsonDict.AddIfHasValue("thoughtSignature", _thoughtSignature); - return jsonDict; + jsonDict.AddIfHasValue("thought", _isThought); + jsonDict.AddIfHasValue("thoughtSignature", _thoughtSignature); + return jsonDict; + } } - } - - /// - /// A part containing the result of executing code. - /// - public readonly struct CodeExecutionResultPart : Part { + /// - /// The outcome of a code execution. + /// A part containing the result of executing code. /// - public enum ExecutionOutcome { - Unspecified = 0, + public readonly struct CodeExecutionResultPart : Part + { /// - /// The code executed without errors. + /// The outcome of a code execution. /// - Ok, + public enum ExecutionOutcome + { + Unspecified = 0, + /// + /// The code executed without errors. + /// + Ok, + /// + /// The code failed to execute. + /// + Failed, + /// + /// The code took too long to execute. + /// + DeadlineExceeded + } + /// - /// The code failed to execute. + /// The outcome of the code execution. /// - Failed, + public ExecutionOutcome Outcome { get; } /// - /// The code took too long to execute. + /// The output of the code execution. /// - DeadlineExceeded - } - - /// - /// The outcome of the code execution. - /// - public ExecutionOutcome Outcome { get; } - /// - /// The output of the code execution. - /// - public string Output { get; } - - private readonly bool? _isThought; - public bool IsThought { get { return _isThought ?? false; } } - - private readonly string _thoughtSignature; - - private static ExecutionOutcome ParseOutcome(string str) { - return str switch { - "OUTCOME_UNSPECIFIED" => ExecutionOutcome.Unspecified, - "OUTCOME_OK" => ExecutionOutcome.Ok, - "OUTCOME_FAILED" => ExecutionOutcome.Failed, - "OUTCOME_DEADLINE_EXCEEDED" => ExecutionOutcome.DeadlineExceeded, - _ => ExecutionOutcome.Unspecified, - }; - } - - private string OutcomeAsString { - get { - return Outcome switch { - ExecutionOutcome.Ok => "OUTCOME_OK", - ExecutionOutcome.Failed => "OUTCOME_FAILED", - ExecutionOutcome.DeadlineExceeded => "OUTCOME_DEADLINE_EXCEEDED", - _ => "OUTCOME_UNSPECIFIED" + public string Output { get; } + + private readonly bool? _isThought; + public bool IsThought { get { return _isThought ?? false; } } + + private readonly string _thoughtSignature; + + private static ExecutionOutcome ParseOutcome(string str) + { + return str switch + { + "OUTCOME_UNSPECIFIED" => ExecutionOutcome.Unspecified, + "OUTCOME_OK" => ExecutionOutcome.Ok, + "OUTCOME_FAILED" => ExecutionOutcome.Failed, + "OUTCOME_DEADLINE_EXCEEDED" => ExecutionOutcome.DeadlineExceeded, + _ => ExecutionOutcome.Unspecified, }; } - } - /// - /// Intended for internal use only. - /// - internal CodeExecutionResultPart(string outcome, string output, - bool? isThought, string thoughtSignature) { + private string OutcomeAsString + { + get + { + return Outcome switch + { + ExecutionOutcome.Ok => "OUTCOME_OK", + ExecutionOutcome.Failed => "OUTCOME_FAILED", + ExecutionOutcome.DeadlineExceeded => "OUTCOME_DEADLINE_EXCEEDED", + _ => "OUTCOME_UNSPECIFIED" + }; + } + } + + /// + /// Intended for internal use only. + /// + internal CodeExecutionResultPart(string outcome, string output, + bool? isThought, string thoughtSignature) + { Outcome = ParseOutcome(outcome); Output = output; _isThought = isThought; _thoughtSignature = thoughtSignature; } - - Dictionary Part.ToJson() { - var jsonDict = new Dictionary() { + + Dictionary Part.ToJson() + { + var jsonDict = new Dictionary() { { "codeExecutionResult", new Dictionary() { { "outcome", OutcomeAsString }, { "output", Output } } } }; - jsonDict.AddIfHasValue("thought", _isThought); - jsonDict.AddIfHasValue("thoughtSignature", _thoughtSignature); - return jsonDict; + jsonDict.AddIfHasValue("thought", _isThought); + jsonDict.AddIfHasValue("thoughtSignature", _thoughtSignature); + return jsonDict; + } } - } -#endregion + #endregion - /// - /// Intended for internal use only. - /// This method is used for serializing the object to JSON for the API request. - /// - internal Dictionary ToJson() { - return new Dictionary() { - ["role"] = Role, - ["parts"] = Parts.Select(p => p.ToJson()).ToList() - }; - } + /// + /// Intended for internal use only. + /// This method is used for serializing the object to JSON for the API request. + /// + internal Dictionary ToJson() + { + return new Dictionary() + { + ["role"] = Role, + ["parts"] = Parts.Select(p => p.ToJson()).ToList() + }; + } - /// - /// Intended for internal use only. - /// This method is used for deserializing JSON responses and should not be called directly. - /// - internal static ModelContent FromJson(Dictionary jsonDict) { - return new ModelContent( - // If the role is missing, default to model since this is likely coming from the backend. - jsonDict.ParseValue("role", defaultValue: "model"), - // Unknown parts are converted to null, which we then want to filter out here - jsonDict.ParseObjectList("parts", PartFromJson)?.Where(p => p is not null)); - } + /// + /// Intended for internal use only. + /// This method is used for deserializing JSON responses and should not be called directly. + /// + internal static ModelContent FromJson(Dictionary jsonDict) + { + return new ModelContent( + // If the role is missing, default to model since this is likely coming from the backend. + jsonDict.ParseValue("role", defaultValue: "model"), + // Unknown parts are converted to null, which we then want to filter out here + jsonDict.ParseObjectList("parts", PartFromJson)?.Where(p => p is not null)); + } - private static InlineDataPart InlineDataPartFromJson(Dictionary jsonDict, - bool? isThought, string thoughtSignature) { - return new InlineDataPart( - jsonDict.ParseValue("mimeType", JsonParseOptions.ThrowEverything), - Convert.FromBase64String(jsonDict.ParseValue("data", JsonParseOptions.ThrowEverything)), - isThought, - thoughtSignature); - } - - private static ExecutableCodePart ExecutableCodePartFromJson(Dictionary jsonDict, - bool? isThought, string thoughtSignature) { - return new ExecutableCodePart( - jsonDict.ParseValue("language", JsonParseOptions.ThrowEverything), - jsonDict.ParseValue("code", JsonParseOptions.ThrowEverything), - isThought, - thoughtSignature); - } - - private static CodeExecutionResultPart CodeExecutionResultPartFromJson(Dictionary jsonDict, - bool? isThought, string thoughtSignature) { - return new CodeExecutionResultPart( - jsonDict.ParseValue("outcome", JsonParseOptions.ThrowEverything), - jsonDict.ParseValue("output", JsonParseOptions.ThrowEverything), - isThought, - thoughtSignature); - } + private static InlineDataPart InlineDataPartFromJson(Dictionary jsonDict, + bool? isThought, string thoughtSignature) + { + return new InlineDataPart( + jsonDict.ParseValue("mimeType", JsonParseOptions.ThrowEverything), + Convert.FromBase64String(jsonDict.ParseValue("data", JsonParseOptions.ThrowEverything)), + isThought, + thoughtSignature); + } - private static Part PartFromJson(Dictionary jsonDict) { - bool? isThought = jsonDict.ParseNullableValue("thought"); - string thoughtSignature = jsonDict.ParseValue("thoughtSignature"); - if (jsonDict.TryParseValue("text", out string text)) { - return new TextPart(text, isThought, thoughtSignature); - } else if (jsonDict.TryParseObject("functionCall", - innerDict => ModelContentJsonParsers.FunctionCallPartFromJson(innerDict, isThought, thoughtSignature), - out var fcPart)) { - return fcPart; - } else if (jsonDict.TryParseObject("inlineData", - innerDict => InlineDataPartFromJson(innerDict, isThought, thoughtSignature), - out var inlineDataPart)) { - return inlineDataPart; - } else if (jsonDict.TryParseObject("executableCode", - innerDict => ExecutableCodePartFromJson(innerDict, isThought, thoughtSignature), - out var executableCodePart)) { - return executableCodePart; - } else if (jsonDict.TryParseObject("codeExecutionResult", - innerDict => CodeExecutionResultPartFromJson(innerDict, isThought, thoughtSignature), - out var codeExecutionResultPart)) { - return codeExecutionResultPart; - } else { + private static ExecutableCodePart ExecutableCodePartFromJson(Dictionary jsonDict, + bool? isThought, string thoughtSignature) + { + return new ExecutableCodePart( + jsonDict.ParseValue("language", JsonParseOptions.ThrowEverything), + jsonDict.ParseValue("code", JsonParseOptions.ThrowEverything), + isThought, + thoughtSignature); + } + + private static CodeExecutionResultPart CodeExecutionResultPartFromJson(Dictionary jsonDict, + bool? isThought, string thoughtSignature) + { + return new CodeExecutionResultPart( + jsonDict.ParseValue("outcome", JsonParseOptions.ThrowEverything), + jsonDict.ParseValue("output", JsonParseOptions.ThrowEverything), + isThought, + thoughtSignature); + } + + private static Part PartFromJson(Dictionary jsonDict) + { + bool? isThought = jsonDict.ParseNullableValue("thought"); + string thoughtSignature = jsonDict.ParseValue("thoughtSignature"); + if (jsonDict.TryParseValue("text", out string text)) + { + return new TextPart(text, isThought, thoughtSignature); + } + else if (jsonDict.TryParseObject("functionCall", + innerDict => ModelContentJsonParsers.FunctionCallPartFromJson(innerDict, isThought, thoughtSignature), + out var fcPart)) + { + return fcPart; + } + else if (jsonDict.TryParseObject("inlineData", + innerDict => InlineDataPartFromJson(innerDict, isThought, thoughtSignature), + out var inlineDataPart)) + { + return inlineDataPart; + } + else if (jsonDict.TryParseObject("executableCode", + innerDict => ExecutableCodePartFromJson(innerDict, isThought, thoughtSignature), + out var executableCodePart)) + { + return executableCodePart; + } + else if (jsonDict.TryParseObject("codeExecutionResult", + innerDict => CodeExecutionResultPartFromJson(innerDict, isThought, thoughtSignature), + out var codeExecutionResultPart)) + { + return codeExecutionResultPart; + } + else + { #if FIREBASEAI_DEBUG_LOGGING - UnityEngine.Debug.LogWarning($"Received unknown part, with keys: {string.Join(',', jsonDict.Keys)}"); + UnityEngine.Debug.LogWarning($"Received unknown part, with keys: {string.Join(',', jsonDict.Keys)}"); #endif - return null; + return null; + } } } -} -namespace Internal { - -// Class for parsing Parts that need to be called from other files as well. -internal static class ModelContentJsonParsers { - internal static ModelContent.FunctionCallPart FunctionCallPartFromJson(Dictionary jsonDict, - bool? isThought, string thoughtSignature) { - return new ModelContent.FunctionCallPart( - jsonDict.ParseValue("name", JsonParseOptions.ThrowEverything), - jsonDict.ParseValue>("args", JsonParseOptions.ThrowEverything), - jsonDict.ParseValue("id"), - isThought, - thoughtSignature); - } -} + namespace Internal + { + + // Class for parsing Parts that need to be called from other files as well. + internal static class ModelContentJsonParsers + { + internal static ModelContent.FunctionCallPart FunctionCallPartFromJson(Dictionary jsonDict, + bool? isThought, string thoughtSignature) + { + return new ModelContent.FunctionCallPart( + jsonDict.ParseValue("name", JsonParseOptions.ThrowEverything), + jsonDict.ParseValue>("args", JsonParseOptions.ThrowEverything), + jsonDict.ParseValue("id"), + isThought, + thoughtSignature); + } + } -} + } } diff --git a/firebaseai/src/RequestOptions.cs b/firebaseai/src/RequestOptions.cs index 9ed545fa..d6f25b19 100644 --- a/firebaseai/src/RequestOptions.cs +++ b/firebaseai/src/RequestOptions.cs @@ -16,36 +16,38 @@ using System; -namespace Firebase.AI { - -/// -/// Configuration parameters for sending requests to the backend. -/// -public readonly struct RequestOptions { - // Since the user could create `RequestOptions` with the default constructor, - // which isn't hidable in our C# version, we default to null, and use that - // to determine if it should be 180. - private readonly TimeSpan? _timeout; - +namespace Firebase.AI +{ /// - /// Intended for internal use only. - /// This provides access to the default timeout value. + /// Configuration parameters for sending requests to the backend. /// - internal static TimeSpan DefaultTimeout => TimeSpan.FromSeconds(180); + public readonly struct RequestOptions + { + // Since the user could create `RequestOptions` with the default constructor, + // which isn't hidable in our C# version, we default to null, and use that + // to determine if it should be 180. + private readonly TimeSpan? _timeout; - /// - /// Intended for internal use only. - /// This provides access to the timeout value used for API requests. - /// - internal TimeSpan Timeout => _timeout ?? DefaultTimeout; + /// + /// Intended for internal use only. + /// This provides access to the default timeout value. + /// + internal static TimeSpan DefaultTimeout => TimeSpan.FromSeconds(180); - /// - /// Initialize a `RequestOptions` object. - /// - /// The request's timeout interval. Defaults to 180 seconds if given null. - public RequestOptions(TimeSpan? timeout = null) { - _timeout = timeout; + /// + /// Intended for internal use only. + /// This provides access to the timeout value used for API requests. + /// + internal TimeSpan Timeout => _timeout ?? DefaultTimeout; + + /// + /// Initialize a `RequestOptions` object. + /// + /// The request's timeout interval. Defaults to 180 seconds if given null. + public RequestOptions(TimeSpan? timeout = null) + { + _timeout = timeout; + } } -} } diff --git a/firebaseai/src/ResponseModality.cs b/firebaseai/src/ResponseModality.cs index 65f4eccb..4f0400dc 100644 --- a/firebaseai/src/ResponseModality.cs +++ b/firebaseai/src/ResponseModality.cs @@ -14,39 +14,40 @@ * limitations under the License. */ -namespace Firebase.AI { - -/// -/// The response type the model should return with. -/// -public enum ResponseModality { - /// - /// Specifies that the model should generate textual content. - /// - /// Use this modality when you need the model to produce written language, such as answers to - /// questions, summaries, creative writing, code snippets, or structured data formats like JSON. - /// - Text, +namespace Firebase.AI +{ /// - /// **Public Experimental**: Specifies that the model should generate image data. - /// - /// Use this modality when you want the model to create visual content based on the provided input - /// or prompts. The response might contain one or more generated images. See the [image - /// generation](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal-response-generation#image-generation) - /// documentation for more details. - /// - /// > Warning: Image generation using Gemini 2.0 Flash is a **Public Experimental** feature, which - /// > means that it is not subject to any SLA or deprecation policy and could change in - /// > backwards-incompatible ways. + /// The response type the model should return with. /// - Image, - /// - /// **Public Experimental**: Specifies that the model should generate audio data. - /// - /// Use this modality with a `LiveGenerationConfig` to create audio content based on the - /// provided input or prompts with a `LiveGenerativeModel`. - /// - Audio, -} + public enum ResponseModality + { + /// + /// Specifies that the model should generate textual content. + /// + /// Use this modality when you need the model to produce written language, such as answers to + /// questions, summaries, creative writing, code snippets, or structured data formats like JSON. + /// + Text, + /// + /// **Public Experimental**: Specifies that the model should generate image data. + /// + /// Use this modality when you want the model to create visual content based on the provided input + /// or prompts. The response might contain one or more generated images. See the [image + /// generation](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal-response-generation#image-generation) + /// documentation for more details. + /// + /// > Warning: Image generation using Gemini 2.0 Flash is a **Public Experimental** feature, which + /// > means that it is not subject to any SLA or deprecation policy and could change in + /// > backwards-incompatible ways. + /// + Image, + /// + /// **Public Experimental**: Specifies that the model should generate audio data. + /// + /// Use this modality with a `LiveGenerationConfig` to create audio content based on the + /// provided input or prompts with a `LiveGenerativeModel`. + /// + Audio, + } } diff --git a/firebaseai/src/Safety.cs b/firebaseai/src/Safety.cs index d1281aa3..3e640f52 100644 --- a/firebaseai/src/Safety.cs +++ b/firebaseai/src/Safety.cs @@ -18,311 +18,336 @@ using System.Collections.Generic; using Firebase.AI.Internal; -namespace Firebase.AI { - -/// -/// Categories describing the potential harm a piece of content may pose. -/// -public enum HarmCategory { - /// - /// A new and not yet supported value. - /// - Unknown = 0, - /// - /// Harassment content. - /// - Harassment, - /// - /// Negative or harmful comments targeting identity and/or protected attributes. - /// - HateSpeech, - /// - /// Contains references to sexual acts or other lewd content. - /// - SexuallyExplicit, +namespace Firebase.AI +{ /// - /// Promotes or enables access to harmful goods, services, or activities. + /// Categories describing the potential harm a piece of content may pose. /// - DangerousContent, - /// - /// Content that may be used to harm civic integrity. - /// - CivicIntegrity, -} - -/// -/// A type used to specify a threshold for harmful content, beyond which the model will return a -/// fallback response instead of generated content. -/// -public readonly struct SafetySetting { - - /// - /// Block at and beyond a specified threshold. - /// - public enum HarmBlockThreshold { + public enum HarmCategory + { /// - /// Content with negligible harm is allowed. + /// A new and not yet supported value. /// - LowAndAbove, + Unknown = 0, /// - /// Content with negligible to low harm is allowed. + /// Harassment content. /// - MediumAndAbove, + Harassment, /// - /// Content with negligible to medium harm is allowed. + /// Negative or harmful comments targeting identity and/or protected attributes. /// - OnlyHigh, + HateSpeech, /// - /// All content is allowed regardless of harm. + /// Contains references to sexual acts or other lewd content. /// - None, + SexuallyExplicit, /// - /// All content is allowed regardless of harm, and metadata will not be included in the response. + /// Promotes or enables access to harmful goods, services, or activities. /// - Off, + DangerousContent, + /// + /// Content that may be used to harm civic integrity. + /// + CivicIntegrity, } /// - /// The method of computing whether the threshold has been exceeded. + /// A type used to specify a threshold for harmful content, beyond which the model will return a + /// fallback response instead of generated content. /// - public enum HarmBlockMethod { + public readonly struct SafetySetting + { + /// - /// Use only the probability score. + /// Block at and beyond a specified threshold. /// - Probability, + public enum HarmBlockThreshold + { + /// + /// Content with negligible harm is allowed. + /// + LowAndAbove, + /// + /// Content with negligible to low harm is allowed. + /// + MediumAndAbove, + /// + /// Content with negligible to medium harm is allowed. + /// + OnlyHigh, + /// + /// All content is allowed regardless of harm. + /// + None, + /// + /// All content is allowed regardless of harm, and metadata will not be included in the response. + /// + Off, + } + /// - /// Use both probability and severity scores. + /// The method of computing whether the threshold has been exceeded. /// - Severity, - } + public enum HarmBlockMethod + { + /// + /// Use only the probability score. + /// + Probability, + /// + /// Use both probability and severity scores. + /// + Severity, + } - private readonly HarmCategory _category; - private readonly HarmBlockThreshold _threshold; - private readonly HarmBlockMethod? _method; + private readonly HarmCategory _category; + private readonly HarmBlockThreshold _threshold; + private readonly HarmBlockMethod? _method; - /// - /// Initializes a new safety setting with the given category and threshold. - /// - /// The category this safety setting should be applied to. - /// The threshold describing what content should be blocked. - /// The method of computing whether the threshold has been exceeded; if not specified, - /// the default method is `Severity` for most models. This parameter is unused in the GoogleAI backend. - public SafetySetting(HarmCategory category, HarmBlockThreshold threshold, - HarmBlockMethod? method = null) { - _category = category; - _threshold = threshold; - _method = method; - } + /// + /// Initializes a new safety setting with the given category and threshold. + /// + /// The category this safety setting should be applied to. + /// The threshold describing what content should be blocked. + /// The method of computing whether the threshold has been exceeded; if not specified, + /// the default method is `Severity` for most models. This parameter is unused in the GoogleAI backend. + public SafetySetting(HarmCategory category, HarmBlockThreshold threshold, + HarmBlockMethod? method = null) + { + _category = category; + _threshold = threshold; + _method = method; + } - private string ConvertCategory(HarmCategory category) { - return category switch { - HarmCategory.Unknown => "UNKNOWN", - HarmCategory.Harassment => "HARM_CATEGORY_HARASSMENT", - HarmCategory.HateSpeech => "HARM_CATEGORY_HATE_SPEECH", - HarmCategory.SexuallyExplicit => "HARM_CATEGORY_SEXUALLY_EXPLICIT", - HarmCategory.DangerousContent => "HARM_CATEGORY_DANGEROUS_CONTENT", - HarmCategory.CivicIntegrity => "HARM_CATEGORY_CIVIC_INTEGRITY", - _ => category.ToString(), // Fallback - }; - } + private string ConvertCategory(HarmCategory category) + { + return category switch + { + HarmCategory.Unknown => "UNKNOWN", + HarmCategory.Harassment => "HARM_CATEGORY_HARASSMENT", + HarmCategory.HateSpeech => "HARM_CATEGORY_HATE_SPEECH", + HarmCategory.SexuallyExplicit => "HARM_CATEGORY_SEXUALLY_EXPLICIT", + HarmCategory.DangerousContent => "HARM_CATEGORY_DANGEROUS_CONTENT", + HarmCategory.CivicIntegrity => "HARM_CATEGORY_CIVIC_INTEGRITY", + _ => category.ToString(), // Fallback + }; + } - private string ConvertThreshold(HarmBlockThreshold threshold) { - return threshold switch { - HarmBlockThreshold.LowAndAbove => "BLOCK_LOW_AND_ABOVE", - HarmBlockThreshold.MediumAndAbove => "BLOCK_MEDIUM_AND_ABOVE", - HarmBlockThreshold.OnlyHigh => "BLOCK_ONLY_HIGH", - HarmBlockThreshold.None => "BLOCK_NONE", - HarmBlockThreshold.Off => "OFF", - _ => threshold.ToString(), // Fallback - }; - } + private string ConvertThreshold(HarmBlockThreshold threshold) + { + return threshold switch + { + HarmBlockThreshold.LowAndAbove => "BLOCK_LOW_AND_ABOVE", + HarmBlockThreshold.MediumAndAbove => "BLOCK_MEDIUM_AND_ABOVE", + HarmBlockThreshold.OnlyHigh => "BLOCK_ONLY_HIGH", + HarmBlockThreshold.None => "BLOCK_NONE", + HarmBlockThreshold.Off => "OFF", + _ => threshold.ToString(), // Fallback + }; + } - private string ConvertMethod(HarmBlockMethod method) { - return method switch { - HarmBlockMethod.Probability => "PROBABILITY", - HarmBlockMethod.Severity => "SEVERITY", - _ => method.ToString(), // Fallback - }; - } + private string ConvertMethod(HarmBlockMethod method) + { + return method switch + { + HarmBlockMethod.Probability => "PROBABILITY", + HarmBlockMethod.Severity => "SEVERITY", + _ => method.ToString(), // Fallback + }; + } - /// - /// Intended for internal use only. - /// This method is used for serializing the object to JSON for the API request. - /// - internal Dictionary ToJson(FirebaseAI.Backend.InternalProvider backend) { - Dictionary jsonDict = new () { - ["category"] = ConvertCategory(_category), - ["threshold"] = ConvertThreshold(_threshold), - }; - // GoogleAI doesn't support HarmBlockMethod. - if (backend != FirebaseAI.Backend.InternalProvider.GoogleAI) { - if (_method.HasValue) jsonDict["method"] = ConvertMethod(_method.Value); + /// + /// Intended for internal use only. + /// This method is used for serializing the object to JSON for the API request. + /// + internal Dictionary ToJson(FirebaseAI.Backend.InternalProvider backend) + { + Dictionary jsonDict = new() + { + ["category"] = ConvertCategory(_category), + ["threshold"] = ConvertThreshold(_threshold), + }; + // GoogleAI doesn't support HarmBlockMethod. + if (backend != FirebaseAI.Backend.InternalProvider.GoogleAI) + { + if (_method.HasValue) jsonDict["method"] = ConvertMethod(_method.Value); + } + return jsonDict; } - return jsonDict; } -} - -/// -/// A type defining potentially harmful media categories and their model-assigned ratings. A value -/// of this type may be assigned to a category for every model-generated response, not just -/// responses that exceed a certain threshold. -/// -public readonly struct SafetyRating { /// - /// The probability that a given model output falls under a harmful content category. - /// - /// > Note: This does not indicate the severity of harm for a piece of content. + /// A type defining potentially harmful media categories and their model-assigned ratings. A value + /// of this type may be assigned to a category for every model-generated response, not just + /// responses that exceed a certain threshold. /// - public enum HarmProbability { - /// - /// A new and not yet supported value. - /// - Unknown = 0, + public readonly struct SafetyRating + { + /// - /// The probability is zero or close to zero. + /// The probability that a given model output falls under a harmful content category. /// - /// For benign content, the probability across all categories will be this value. + /// > Note: This does not indicate the severity of harm for a piece of content. /// - Negligible, + public enum HarmProbability + { + /// + /// A new and not yet supported value. + /// + Unknown = 0, + /// + /// The probability is zero or close to zero. + /// + /// For benign content, the probability across all categories will be this value. + /// + Negligible, + /// + /// The probability is small but non-zero. + /// + Low, + /// + /// The probability is moderate. + /// + Medium, + /// + /// The probability is high. + /// + /// The content described is very likely harmful. + /// + High, + } + /// - /// The probability is small but non-zero. + /// The magnitude of how harmful a model response might be for the respective `HarmCategory`. /// - Low, + public enum HarmSeverity + { + /// + /// A new and not yet supported value. + /// + Unknown = 0, + /// + /// Negligible level of harm severity. + /// + Negligible, + /// + /// Low level of harm severity. + /// + Low, + /// + /// Medium level of harm severity. + /// + Medium, + /// + /// High level of harm severity. + /// + High, + } + /// - /// The probability is moderate. + /// The category describing the potential harm a piece of content may pose. /// - Medium, + public HarmCategory Category { get; } /// - /// The probability is high. + /// The model-generated probability that the content falls under the specified HarmCategory. /// - /// The content described is very likely harmful. - /// - High, - } - - /// - /// The magnitude of how harmful a model response might be for the respective `HarmCategory`. - /// - public enum HarmSeverity { - /// - /// A new and not yet supported value. + /// This is a discretized representation of the `ProbabilityScore`. + /// + /// > Important: This does not indicate the severity of harm for a piece of content. /// - Unknown = 0, + public HarmProbability Probability { get; } /// - /// Negligible level of harm severity. + /// The confidence score that the response is associated with the corresponding HarmCategory. + /// + /// The probability safety score is a confidence score between 0.0 and 1.0, rounded to one decimal + /// place; it is discretized into a `HarmProbability` in `Probability`. See [probability + /// scores](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/configure-safety-filters#comparison_of_probability_scores_and_severity_scores) + /// in the Google Cloud documentation for more details. /// - Negligible, + public float ProbabilityScore { get; } /// - /// Low level of harm severity. + /// If true, the response was blocked. /// - Low, + public bool Blocked { get; } /// - /// Medium level of harm severity. + /// The severity reflects the magnitude of how harmful a model response might be. + /// + /// This is a discretized representation of the `SeverityScore`. /// - Medium, + public HarmSeverity Severity { get; } /// - /// High level of harm severity. + /// The severity score is the magnitude of how harmful a model response might be. + /// + /// The severity score ranges from 0.0 to 1.0, rounded to one decimal place; it is discretized + /// into a `HarmSeverity` in `Severity`. See [severity scores](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/configure-safety-filters#comparison_of_probability_scores_and_severity_scores) + /// in the Google Cloud documentation for more details. /// - High, - } - - /// - /// The category describing the potential harm a piece of content may pose. - /// - public HarmCategory Category { get; } - /// - /// The model-generated probability that the content falls under the specified HarmCategory. - /// - /// This is a discretized representation of the `ProbabilityScore`. - /// - /// > Important: This does not indicate the severity of harm for a piece of content. - /// - public HarmProbability Probability { get; } - /// - /// The confidence score that the response is associated with the corresponding HarmCategory. - /// - /// The probability safety score is a confidence score between 0.0 and 1.0, rounded to one decimal - /// place; it is discretized into a `HarmProbability` in `Probability`. See [probability - /// scores](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/configure-safety-filters#comparison_of_probability_scores_and_severity_scores) - /// in the Google Cloud documentation for more details. - /// - public float ProbabilityScore { get; } - /// - /// If true, the response was blocked. - /// - public bool Blocked { get; } - /// - /// The severity reflects the magnitude of how harmful a model response might be. - /// - /// This is a discretized representation of the `SeverityScore`. - /// - public HarmSeverity Severity { get; } - /// - /// The severity score is the magnitude of how harmful a model response might be. - /// - /// The severity score ranges from 0.0 to 1.0, rounded to one decimal place; it is discretized - /// into a `HarmSeverity` in `Severity`. See [severity scores](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/configure-safety-filters#comparison_of_probability_scores_and_severity_scores) - /// in the Google Cloud documentation for more details. - /// - public float SeverityScore { get; } + public float SeverityScore { get; } - // Hidden constructor, users don't need to make this. - private SafetyRating(HarmCategory category, HarmProbability probability, - float probabilityScore, bool blocked, HarmSeverity severity, float severityScore) { - Category = category; - Probability = probability; - ProbabilityScore = probabilityScore; - Blocked = blocked; - Severity = severity; - SeverityScore = severityScore; - } + // Hidden constructor, users don't need to make this. + private SafetyRating(HarmCategory category, HarmProbability probability, + float probabilityScore, bool blocked, HarmSeverity severity, float severityScore) + { + Category = category; + Probability = probability; + ProbabilityScore = probabilityScore; + Blocked = blocked; + Severity = severity; + SeverityScore = severityScore; + } - private static HarmCategory ParseCategory(string str) { - return str switch { - "HARM_CATEGORY_HARASSMENT" => HarmCategory.Harassment, - "HARM_CATEGORY_HATE_SPEECH" => HarmCategory.HateSpeech, - "HARM_CATEGORY_SEXUALLY_EXPLICIT" => HarmCategory.SexuallyExplicit, - "HARM_CATEGORY_DANGEROUS_CONTENT" => HarmCategory.DangerousContent, - "HARM_CATEGORY_CIVIC_INTEGRITY" => HarmCategory.CivicIntegrity, - _ => HarmCategory.Unknown, - }; - } + private static HarmCategory ParseCategory(string str) + { + return str switch + { + "HARM_CATEGORY_HARASSMENT" => HarmCategory.Harassment, + "HARM_CATEGORY_HATE_SPEECH" => HarmCategory.HateSpeech, + "HARM_CATEGORY_SEXUALLY_EXPLICIT" => HarmCategory.SexuallyExplicit, + "HARM_CATEGORY_DANGEROUS_CONTENT" => HarmCategory.DangerousContent, + "HARM_CATEGORY_CIVIC_INTEGRITY" => HarmCategory.CivicIntegrity, + _ => HarmCategory.Unknown, + }; + } - private static HarmProbability ParseProbability(string str) { - return str switch { - "NEGLIGIBLE" => HarmProbability.Negligible, - "LOW" => HarmProbability.Low, - "MEDIUM" => HarmProbability.Medium, - "HIGH" => HarmProbability.High, - _ => HarmProbability.Unknown, - }; - } + private static HarmProbability ParseProbability(string str) + { + return str switch + { + "NEGLIGIBLE" => HarmProbability.Negligible, + "LOW" => HarmProbability.Low, + "MEDIUM" => HarmProbability.Medium, + "HIGH" => HarmProbability.High, + _ => HarmProbability.Unknown, + }; + } - private static HarmSeverity ParseSeverity(string str) { - return str switch { - "HARM_SEVERITY_NEGLIGIBLE" => HarmSeverity.Negligible, - "HARM_SEVERITY_LOW" => HarmSeverity.Low, - "HARM_SEVERITY_MEDIUM" => HarmSeverity.Medium, - "HARM_SEVERITY_HIGH" => HarmSeverity.High, - _ => HarmSeverity.Unknown, - }; - } + private static HarmSeverity ParseSeverity(string str) + { + return str switch + { + "HARM_SEVERITY_NEGLIGIBLE" => HarmSeverity.Negligible, + "HARM_SEVERITY_LOW" => HarmSeverity.Low, + "HARM_SEVERITY_MEDIUM" => HarmSeverity.Medium, + "HARM_SEVERITY_HIGH" => HarmSeverity.High, + _ => HarmSeverity.Unknown, + }; + } - /// - /// Intended for internal use only. - /// This method is used for deserializing JSON responses and should not be called directly. - /// - internal static SafetyRating FromJson(Dictionary jsonDict) { - return new SafetyRating( - jsonDict.ParseEnum("category", ParseCategory), - jsonDict.ParseEnum("probability", ParseProbability), - jsonDict.ParseValue("probabilityScore"), - jsonDict.ParseValue("blocked"), - jsonDict.ParseEnum("severity", ParseSeverity), - jsonDict.ParseValue("severityScore") - ); + /// + /// Intended for internal use only. + /// This method is used for deserializing JSON responses and should not be called directly. + /// + internal static SafetyRating FromJson(Dictionary jsonDict) + { + return new SafetyRating( + jsonDict.ParseEnum("category", ParseCategory), + jsonDict.ParseEnum("probability", ParseProbability), + jsonDict.ParseValue("probabilityScore"), + jsonDict.ParseValue("blocked"), + jsonDict.ParseEnum("severity", ParseSeverity), + jsonDict.ParseValue("severityScore") + ); + } } -} } diff --git a/firebaseai/src/Schema.cs b/firebaseai/src/Schema.cs index 8017041e..1363e48b 100644 --- a/firebaseai/src/Schema.cs +++ b/firebaseai/src/Schema.cs @@ -18,493 +18,531 @@ using System.Collections.Generic; using System.Linq; -namespace Firebase.AI { - -/// -/// A `Schema` object allows the definition of input and output data types. -/// -/// These types can be objects, but also primitives and arrays. Represents a select subset of an -/// [OpenAPI 3.0 schema object](https://spec.openapis.org/oas/v3.0.3#schema). -/// -public class Schema { - - /// - /// The value type of a `Schema`. - /// - public enum SchemaType { - String, - Number, - Integer, - Boolean, - Array, - Object, - } - - private string TypeAsString => - Type switch { - SchemaType.String => "STRING", - SchemaType.Number => "NUMBER", - SchemaType.Integer => "INTEGER", - SchemaType.Boolean => "BOOLEAN", - SchemaType.Array => "ARRAY", - SchemaType.Object => "OBJECT", - null => null, - _ => throw new ArgumentOutOfRangeException(nameof(Type), Type, "Invalid SchemaType value") - }; - +namespace Firebase.AI +{ /// - /// Modifiers describing the expected format of a string `Schema`. + /// A `Schema` object allows the definition of input and output data types. + /// + /// These types can be objects, but also primitives and arrays. Represents a select subset of an + /// [OpenAPI 3.0 schema object](https://spec.openapis.org/oas/v3.0.3#schema). /// - public readonly struct StringFormat { - internal string Format { get; } + public class Schema + { - private StringFormat(string format) { - Format = format; + /// + /// The value type of a `Schema`. + /// + public enum SchemaType + { + String, + Number, + Integer, + Boolean, + Array, + Object, } + private string TypeAsString => + Type switch + { + SchemaType.String => "STRING", + SchemaType.Number => "NUMBER", + SchemaType.Integer => "INTEGER", + SchemaType.Boolean => "BOOLEAN", + SchemaType.Array => "ARRAY", + SchemaType.Object => "OBJECT", + null => null, + _ => throw new ArgumentOutOfRangeException(nameof(Type), Type, "Invalid SchemaType value") + }; + /// - /// A custom string format. + /// Modifiers describing the expected format of a string `Schema`. /// - public static StringFormat Custom(string format) { - return new StringFormat(format); - } - } + public readonly struct StringFormat + { + internal string Format { get; } - /// - /// The data type. - /// - public SchemaType? Type { get; } - /// - /// A human-readable explanation of the purpose of the schema or property. While not strictly - /// enforced on the value itself, good descriptions significantly help the model understand the - /// context and generate more relevant and accurate output. - /// - public string Description { get; } - /// - /// A human-readable name/summary for the schema or a specific property. This helps document the - /// schema's purpose but doesn't typically constrain the generated value. It can subtly guide the - /// model by clarifying the intent of a field. - /// - public string Title { get; } - /// - /// Indicates if the value may be null. - /// - public bool? Nullable { get; } - /// - /// The format of the data. - /// - public string Format { get; } + private StringFormat(string format) + { + Format = format; + } - /// - /// Possible values of the element of type "String" with "enum" format. - /// - public IReadOnlyList EnumValues { get; } - /// - /// Schema of the elements of type "Array". - /// - public Schema Items { get; } - /// - /// An integer specifying the minimum number of items the generated "Array" must contain. - /// - public int? MinItems { get; } - /// - /// An integer specifying the maximum number of items the generated "Array" must contain. - /// - public int? MaxItems { get; } + /// + /// A custom string format. + /// + public static StringFormat Custom(string format) + { + return new StringFormat(format); + } + } - /// - /// The minimum value of a numeric type. - /// - public double? Minimum { get; } - /// - /// The maximum value of a numeric type. - /// - public double? Maximum { get; } + /// + /// The data type. + /// + public SchemaType? Type { get; } + /// + /// A human-readable explanation of the purpose of the schema or property. While not strictly + /// enforced on the value itself, good descriptions significantly help the model understand the + /// context and generate more relevant and accurate output. + /// + public string Description { get; } + /// + /// A human-readable name/summary for the schema or a specific property. This helps document the + /// schema's purpose but doesn't typically constrain the generated value. It can subtly guide the + /// model by clarifying the intent of a field. + /// + public string Title { get; } + /// + /// Indicates if the value may be null. + /// + public bool? Nullable { get; } + /// + /// The format of the data. + /// + public string Format { get; } - /// - /// Properties of type "Object". - /// - public IReadOnlyDictionary Properties { get; } + /// + /// Possible values of the element of type "String" with "enum" format. + /// + public IReadOnlyList EnumValues { get; } + /// + /// Schema of the elements of type "Array". + /// + public Schema Items { get; } + /// + /// An integer specifying the minimum number of items the generated "Array" must contain. + /// + public int? MinItems { get; } + /// + /// An integer specifying the maximum number of items the generated "Array" must contain. + /// + public int? MaxItems { get; } - /// - /// Required properties of type "Object". - /// - public IReadOnlyList RequiredProperties { get; } + /// + /// The minimum value of a numeric type. + /// + public double? Minimum { get; } + /// + /// The maximum value of a numeric type. + /// + public double? Maximum { get; } - /// - /// A specific hint provided to the Gemini model, suggesting the order in which the keys should - /// appear in the generated JSON string. Important: Standard JSON objects are inherently unordered - /// collections of key-value pairs. While the model will try to respect PropertyOrdering in its - /// textual JSON output, subsequent parsing into native C# objects (like Dictionaries) might - /// not preserve this order. This parameter primarily affects the raw JSON string - /// serialization. - /// - public IReadOnlyList PropertyOrdering { get; } + /// + /// Properties of type "Object". + /// + public IReadOnlyDictionary Properties { get; } - /// - /// An array of `Schema` objects. The generated data must be valid against *any* (one or more) - /// of the schemas listed in this array. This allows specifying multiple possible structures or - /// types for a single field. - /// - /// For example, a value could be either a `String` or an `Int`: - /// ``` - /// Schema.AnyOf(new [] { Schema.String(), Schema.Int() }) - /// ``` - /// - public IReadOnlyList AnyOfSchemas { get; } - - private Schema( - SchemaType? type, - string description = null, - string title = null, - bool? nullable = null, - string format = null, - IEnumerable enumValues = null, - Schema items = null, - int? minItems = null, - int? maxItems = null, - double? minimum = null, - double? maximum = null, - IDictionary properties = null, - IEnumerable requiredProperties = null, - IEnumerable propertyOrdering = null, - IEnumerable anyOf = null) { - Type = type; - Description = description; - Title = title; - Nullable = nullable; - Format = format; - EnumValues = enumValues?.ToList(); - Items = items; - MinItems = minItems; - MaxItems = maxItems; - Minimum = minimum; - Maximum = maximum; - Properties = (properties == null) ? null : new Dictionary(properties); - RequiredProperties = requiredProperties?.ToList(); - PropertyOrdering = propertyOrdering?.ToList(); - AnyOfSchemas = anyOf?.ToList(); - } + /// + /// Required properties of type "Object". + /// + public IReadOnlyList RequiredProperties { get; } - /// - /// Returns a `Schema` representing a boolean value. - /// - /// An optional description of what the boolean should contain or represent. - /// Indicates whether the value can be `null`. Defaults to `false`. - public static Schema Boolean( - string description = null, - bool nullable = false) { - return new Schema(SchemaType.Boolean, - description: description, - nullable: nullable - ); - } + /// + /// A specific hint provided to the Gemini model, suggesting the order in which the keys should + /// appear in the generated JSON string. Important: Standard JSON objects are inherently unordered + /// collections of key-value pairs. While the model will try to respect PropertyOrdering in its + /// textual JSON output, subsequent parsing into native C# objects (like Dictionaries) might + /// not preserve this order. This parameter primarily affects the raw JSON string + /// serialization. + /// + public IReadOnlyList PropertyOrdering { get; } - /// - /// Returns a `Schema` for a 32-bit signed integer number. - /// - /// **Important:** This `Schema` provides a hint to the model that it should generate a 32-bit - /// integer, but only guarantees that the value will be an integer. Therefore it's *possible* - /// that decoding it as an `int` could overflow. - /// - /// An optional description of what the integer should contain or represent. - /// Indicates whether the value can be `null`. Defaults to `false`. - /// If specified, instructs the model that the value should be greater than or - /// equal to the specified minimum. - /// If specified, instructs the model that the value should be less than or - /// equal to the specified maximum. - public static Schema Int( - string description = null, - bool nullable = false, - int? minimum = null, - int? maximum = null) { - return new Schema(SchemaType.Integer, - description: description, - nullable: nullable, - format: "int32", - minimum: minimum, - maximum: maximum - ); - } + /// + /// An array of `Schema` objects. The generated data must be valid against *any* (one or more) + /// of the schemas listed in this array. This allows specifying multiple possible structures or + /// types for a single field. + /// + /// For example, a value could be either a `String` or an `Int`: + /// ``` + /// Schema.AnyOf(new [] { Schema.String(), Schema.Int() }) + /// ``` + /// + public IReadOnlyList AnyOfSchemas { get; } + + private Schema( + SchemaType? type, + string description = null, + string title = null, + bool? nullable = null, + string format = null, + IEnumerable enumValues = null, + Schema items = null, + int? minItems = null, + int? maxItems = null, + double? minimum = null, + double? maximum = null, + IDictionary properties = null, + IEnumerable requiredProperties = null, + IEnumerable propertyOrdering = null, + IEnumerable anyOf = null) + { + Type = type; + Description = description; + Title = title; + Nullable = nullable; + Format = format; + EnumValues = enumValues?.ToList(); + Items = items; + MinItems = minItems; + MaxItems = maxItems; + Minimum = minimum; + Maximum = maximum; + Properties = (properties == null) ? null : new Dictionary(properties); + RequiredProperties = requiredProperties?.ToList(); + PropertyOrdering = propertyOrdering?.ToList(); + AnyOfSchemas = anyOf?.ToList(); + } - /// - /// Returns a `Schema` for a 64-bit signed integer number. - /// - /// An optional description of what the number should contain or represent. - /// Indicates whether the value can be `null`. Defaults to `false`. - /// If specified, instructs the model that the value should be greater than or - /// equal to the specified minimum. - /// If specified, instructs the model that the value should be less than or - /// equal to the specified maximum. - public static Schema Long( - string description = null, - bool nullable = false, - long? minimum = null, - long? maximum = null) { - return new Schema(SchemaType.Integer, - description: description, - nullable: nullable, - minimum: minimum, - maximum: maximum - ); - } + /// + /// Returns a `Schema` representing a boolean value. + /// + /// An optional description of what the boolean should contain or represent. + /// Indicates whether the value can be `null`. Defaults to `false`. + public static Schema Boolean( + string description = null, + bool nullable = false) + { + return new Schema(SchemaType.Boolean, + description: description, + nullable: nullable + ); + } - /// - /// Returns a `Schema` for a double-precision floating-point number. - /// - /// An optional description of what the number should contain or represent. - /// Indicates whether the value can be `null`. Defaults to `false`. - /// If specified, instructs the model that the value should be greater than or - /// equal to the specified minimum. - /// If specified, instructs the model that the value should be less than or - /// equal to the specified maximum. - public static Schema Double( - string description = null, - bool nullable = false, - double? minimum = null, - double? maximum = null) { - return new Schema(SchemaType.Number, - description: description, - nullable: nullable, - minimum: minimum, - maximum: maximum - ); - } + /// + /// Returns a `Schema` for a 32-bit signed integer number. + /// + /// **Important:** This `Schema` provides a hint to the model that it should generate a 32-bit + /// integer, but only guarantees that the value will be an integer. Therefore it's *possible* + /// that decoding it as an `int` could overflow. + /// + /// An optional description of what the integer should contain or represent. + /// Indicates whether the value can be `null`. Defaults to `false`. + /// If specified, instructs the model that the value should be greater than or + /// equal to the specified minimum. + /// If specified, instructs the model that the value should be less than or + /// equal to the specified maximum. + public static Schema Int( + string description = null, + bool nullable = false, + int? minimum = null, + int? maximum = null) + { + return new Schema(SchemaType.Integer, + description: description, + nullable: nullable, + format: "int32", + minimum: minimum, + maximum: maximum + ); + } - /// - /// Returns a `Schema` for a single-precision floating-point number. - /// - /// **Important:** This `Schema` provides a hint to the model that it should generate a - /// single-precision floating-point number, but only guarantees that the value will be a number. - /// Therefore it's *possible* that decoding it as a `float` could overflow. - /// - /// An optional description of what the number should contain or represent. - /// Indicates whether the value can be `null`. Defaults to `false`. - /// If specified, instructs the model that the value should be greater than or - /// equal to the specified minimum. - /// If specified, instructs the model that the value should be less than or - /// equal to the specified maximum. - public static Schema Float( - string description = null, - bool nullable = false, - float? minimum = null, - float? maximum = null) { - return new Schema(SchemaType.Number, - description: description, - nullable: nullable, - format: "float", - minimum: minimum, - maximum: maximum - ); - } + /// + /// Returns a `Schema` for a 64-bit signed integer number. + /// + /// An optional description of what the number should contain or represent. + /// Indicates whether the value can be `null`. Defaults to `false`. + /// If specified, instructs the model that the value should be greater than or + /// equal to the specified minimum. + /// If specified, instructs the model that the value should be less than or + /// equal to the specified maximum. + public static Schema Long( + string description = null, + bool nullable = false, + long? minimum = null, + long? maximum = null) + { + return new Schema(SchemaType.Integer, + description: description, + nullable: nullable, + minimum: minimum, + maximum: maximum + ); + } - /// - /// Returns a `Schema` for a string. - /// - /// An optional description of what the string should contain or represent. - /// Indicates whether the value can be `null`. Defaults to `false`. - /// An optional pattern that values need to adhere to. - public static Schema String( - string description = null, - bool nullable = false, - StringFormat? format = null) { - return new Schema(SchemaType.String, - description: description, - nullable: nullable, - format: format?.Format - ); - } + /// + /// Returns a `Schema` for a double-precision floating-point number. + /// + /// An optional description of what the number should contain or represent. + /// Indicates whether the value can be `null`. Defaults to `false`. + /// If specified, instructs the model that the value should be greater than or + /// equal to the specified minimum. + /// If specified, instructs the model that the value should be less than or + /// equal to the specified maximum. + public static Schema Double( + string description = null, + bool nullable = false, + double? minimum = null, + double? maximum = null) + { + return new Schema(SchemaType.Number, + description: description, + nullable: nullable, + minimum: minimum, + maximum: maximum + ); + } - /// - /// Returns a `Schema` representing an object. - /// - /// This schema instructs the model to produce data of type "Object", which has keys of type - /// "String" and values of any other data type (including nested "Objects"s). - /// - /// **Example:** A `City` could be represented with the following object `Schema`. - /// ``` - /// Schema.Object(properties: new Dictionary() { - /// { "name", Schema.String() }, - /// { "population", Schema.Integer() } - /// }) - /// ``` - /// - /// The map of the object's property names to their `Schema`s. - /// The list of optional properties. They must correspond to the keys - /// provided in the `properties` map. By default it's empty, signaling the model that all - /// properties are to be included. - /// An optional hint to the model suggesting the order for keys in the - /// generated JSON string. - /// An optional description of what the object represents. - /// An optional human-readable name/summary for the object schema. - /// Indicates whether the value can be `null`. Defaults to `false`. - public static Schema Object( - IDictionary properties, - IEnumerable optionalProperties = null, - IEnumerable propertyOrdering = null, - string description = null, - string title = null, - bool nullable = false) { - if (properties == null) { - throw new ArgumentNullException(nameof(properties)); + /// + /// Returns a `Schema` for a single-precision floating-point number. + /// + /// **Important:** This `Schema` provides a hint to the model that it should generate a + /// single-precision floating-point number, but only guarantees that the value will be a number. + /// Therefore it's *possible* that decoding it as a `float` could overflow. + /// + /// An optional description of what the number should contain or represent. + /// Indicates whether the value can be `null`. Defaults to `false`. + /// If specified, instructs the model that the value should be greater than or + /// equal to the specified minimum. + /// If specified, instructs the model that the value should be less than or + /// equal to the specified maximum. + public static Schema Float( + string description = null, + bool nullable = false, + float? minimum = null, + float? maximum = null) + { + return new Schema(SchemaType.Number, + description: description, + nullable: nullable, + format: "float", + minimum: minimum, + maximum: maximum + ); } - if (optionalProperties != null) { - // Check for invalid optional properties - var invalidOptionalProperties = optionalProperties.Except(properties.Keys).ToList(); - if (invalidOptionalProperties.Any()) { - throw new ArgumentException( - "The following optional properties are not present in the properties dictionary: " + - $"{string.Join(", ", invalidOptionalProperties)}", - nameof(optionalProperties)); - } + /// + /// Returns a `Schema` for a string. + /// + /// An optional description of what the string should contain or represent. + /// Indicates whether the value can be `null`. Defaults to `false`. + /// An optional pattern that values need to adhere to. + public static Schema String( + string description = null, + bool nullable = false, + StringFormat? format = null) + { + return new Schema(SchemaType.String, + description: description, + nullable: nullable, + format: format?.Format + ); } - // Determine required properties - var required = - properties.Keys.Except(optionalProperties ?? Enumerable.Empty()).ToList(); + /// + /// Returns a `Schema` representing an object. + /// + /// This schema instructs the model to produce data of type "Object", which has keys of type + /// "String" and values of any other data type (including nested "Objects"s). + /// + /// **Example:** A `City` could be represented with the following object `Schema`. + /// ``` + /// Schema.Object(properties: new Dictionary() { + /// { "name", Schema.String() }, + /// { "population", Schema.Integer() } + /// }) + /// ``` + /// + /// The map of the object's property names to their `Schema`s. + /// The list of optional properties. They must correspond to the keys + /// provided in the `properties` map. By default it's empty, signaling the model that all + /// properties are to be included. + /// An optional hint to the model suggesting the order for keys in the + /// generated JSON string. + /// An optional description of what the object represents. + /// An optional human-readable name/summary for the object schema. + /// Indicates whether the value can be `null`. Defaults to `false`. + public static Schema Object( + IDictionary properties, + IEnumerable optionalProperties = null, + IEnumerable propertyOrdering = null, + string description = null, + string title = null, + bool nullable = false) + { + if (properties == null) + { + throw new ArgumentNullException(nameof(properties)); + } - foreach (string key in properties.Keys) { - if (optionalProperties == null || !optionalProperties.Contains(key)) { - required.Add(key); + if (optionalProperties != null) + { + // Check for invalid optional properties + var invalidOptionalProperties = optionalProperties.Except(properties.Keys).ToList(); + if (invalidOptionalProperties.Any()) + { + throw new ArgumentException( + "The following optional properties are not present in the properties dictionary: " + + $"{string.Join(", ", invalidOptionalProperties)}", + nameof(optionalProperties)); + } } - } - return new Schema(SchemaType.Object, - description: description, - title: title, - nullable: nullable, - properties: properties, - requiredProperties: required, - propertyOrdering: propertyOrdering - ); - } + // Determine required properties + var required = + properties.Keys.Except(optionalProperties ?? Enumerable.Empty()).ToList(); - /// - /// Returns a `Schema` for an array. - /// - /// The `Schema` of the elements stored in the array. - /// An optional description of what the array represents. - /// Indicates whether the value can be `null`. Defaults to `false`. - /// Instructs the model to produce at least the specified minimum number of elements - /// in the array. - /// Instructs the model to produce at most the specified minimum number of elements - /// in the array. - public static Schema Array( - Schema items, - string description = null, - bool nullable = false, - int? minItems = null, - int? maxItems = null) { - return new Schema(SchemaType.Array, - description: description, - nullable: nullable, - items: items, - minItems: minItems, - maxItems: maxItems - ); - } + foreach (string key in properties.Keys) + { + if (optionalProperties == null || !optionalProperties.Contains(key)) + { + required.Add(key); + } + } - /// - /// Returns a `Schema` for an enumeration. - /// - /// For example, the cardinal directions can be represented as: - /// ``` - /// Schema.Enum(new string[]{ "North", "East", "South", "West" }, "Cardinal directions") - /// ``` - /// - /// The list of valid values for this enumeration. - /// An optional description of what the enum represents. - /// Indicates whether the value can be `null`. Defaults to `false`. - public static Schema Enum( - IEnumerable values, - string description = null, - bool nullable = false) { - return new Schema(SchemaType.String, - description: description, - nullable: nullable, - enumValues: values, - format: "enum" - ); - } + return new Schema(SchemaType.Object, + description: description, + title: title, + nullable: nullable, + properties: properties, + requiredProperties: required, + propertyOrdering: propertyOrdering + ); + } - /// - /// Returns a `Schema` representing a value that must conform to *any* (one or more) of the - /// provided sub-schemas. - /// - /// This schema instructs the model to produce data that is valid against at least one of the - /// schemas listed in the `schemas` array. This is useful when a field can accept multiple - /// distinct types or structures. - /// - /// An array of `Schema` objects. The generated data must be valid against at least - /// one of these schemas. The array must not be empty. - public static Schema AnyOf( - IEnumerable schemas) { - if (schemas == null || !schemas.Any()) { - throw new ArgumentException("The `AnyOf` schemas array cannot be empty."); + /// + /// Returns a `Schema` for an array. + /// + /// The `Schema` of the elements stored in the array. + /// An optional description of what the array represents. + /// Indicates whether the value can be `null`. Defaults to `false`. + /// Instructs the model to produce at least the specified minimum number of elements + /// in the array. + /// Instructs the model to produce at most the specified minimum number of elements + /// in the array. + public static Schema Array( + Schema items, + string description = null, + bool nullable = false, + int? minItems = null, + int? maxItems = null) + { + return new Schema(SchemaType.Array, + description: description, + nullable: nullable, + items: items, + minItems: minItems, + maxItems: maxItems + ); } - // The backend doesn't define a SchemaType for AnyOf, instead using the existence of 'anyOf'. - return new Schema(null, - anyOf: schemas - ); - } + /// + /// Returns a `Schema` for an enumeration. + /// + /// For example, the cardinal directions can be represented as: + /// ``` + /// Schema.Enum(new string[]{ "North", "East", "South", "West" }, "Cardinal directions") + /// ``` + /// + /// The list of valid values for this enumeration. + /// An optional description of what the enum represents. + /// Indicates whether the value can be `null`. Defaults to `false`. + public static Schema Enum( + IEnumerable values, + string description = null, + bool nullable = false) + { + return new Schema(SchemaType.String, + description: description, + nullable: nullable, + enumValues: values, + format: "enum" + ); + } - /// - /// Intended for internal use only. - /// This method is used for serializing the object to JSON for the API request. - /// - internal Dictionary ToJson() { - Dictionary json = new() { + /// + /// Returns a `Schema` representing a value that must conform to *any* (one or more) of the + /// provided sub-schemas. + /// + /// This schema instructs the model to produce data that is valid against at least one of the + /// schemas listed in the `schemas` array. This is useful when a field can accept multiple + /// distinct types or structures. + /// + /// An array of `Schema` objects. The generated data must be valid against at least + /// one of these schemas. The array must not be empty. + public static Schema AnyOf( + IEnumerable schemas) + { + if (schemas == null || !schemas.Any()) + { + throw new ArgumentException("The `AnyOf` schemas array cannot be empty."); + } + + // The backend doesn't define a SchemaType for AnyOf, instead using the existence of 'anyOf'. + return new Schema(null, + anyOf: schemas + ); + } + + /// + /// Intended for internal use only. + /// This method is used for serializing the object to JSON for the API request. + /// + internal Dictionary ToJson() + { + Dictionary json = new() { { "type", TypeAsString } }; - if (!string.IsNullOrWhiteSpace(Description)) { - json["description"] = Description; - } - if (!string.IsNullOrWhiteSpace(Title)) { - json["title"] = Title; - } - if (Nullable.HasValue) { - json["nullable"] = Nullable.Value; - } - if (EnumValues != null && EnumValues.Any()) { - json["enum"] = EnumValues.ToList(); - } - if (Items != null) { - json["items"] = Items.ToJson(); - } - if (MinItems != null) { - json["minItems"] = MinItems.Value; - } - if (MaxItems != null) { - json["maxItems"] = MaxItems.Value; - } - if (Minimum != null) { - json["minimum"] = Minimum.Value; - } - if (Maximum != null) { - json["maximum"] = Maximum.Value; - } - if (Properties != null && Properties.Any()) { - Dictionary propertiesJson = new(); - foreach (var kvp in Properties) { - propertiesJson[kvp.Key] = kvp.Value.ToJson(); + if (!string.IsNullOrWhiteSpace(Description)) + { + json["description"] = Description; } - json["properties"] = propertiesJson; - - } - if (RequiredProperties != null && RequiredProperties.Any()) { - json["required"] = RequiredProperties.ToList(); - } - if (PropertyOrdering != null && PropertyOrdering.Any()) { - json["propertyOrdering"] = PropertyOrdering.ToList(); - } - if (AnyOfSchemas != null && AnyOfSchemas.Any()) { - json["anyOf"] = AnyOfSchemas.Select(s => s.ToJson()).ToList(); - } + if (!string.IsNullOrWhiteSpace(Title)) + { + json["title"] = Title; + } + if (Nullable.HasValue) + { + json["nullable"] = Nullable.Value; + } + if (EnumValues != null && EnumValues.Any()) + { + json["enum"] = EnumValues.ToList(); + } + if (Items != null) + { + json["items"] = Items.ToJson(); + } + if (MinItems != null) + { + json["minItems"] = MinItems.Value; + } + if (MaxItems != null) + { + json["maxItems"] = MaxItems.Value; + } + if (Minimum != null) + { + json["minimum"] = Minimum.Value; + } + if (Maximum != null) + { + json["maximum"] = Maximum.Value; + } + if (Properties != null && Properties.Any()) + { + Dictionary propertiesJson = new(); + foreach (var kvp in Properties) + { + propertiesJson[kvp.Key] = kvp.Value.ToJson(); + } + json["properties"] = propertiesJson; - return json; + } + if (RequiredProperties != null && RequiredProperties.Any()) + { + json["required"] = RequiredProperties.ToList(); + } + if (PropertyOrdering != null && PropertyOrdering.Any()) + { + json["propertyOrdering"] = PropertyOrdering.ToList(); + } + if (AnyOfSchemas != null && AnyOfSchemas.Any()) + { + json["anyOf"] = AnyOfSchemas.Select(s => s.ToJson()).ToList(); + } + + return json; + } } -} } diff --git a/firebaseai/src/URLContext.cs b/firebaseai/src/URLContext.cs index 5fec083a..d025f3fd 100644 --- a/firebaseai/src/URLContext.cs +++ b/firebaseai/src/URLContext.cs @@ -18,8 +18,8 @@ using System.Collections.Generic; using Firebase.AI.Internal; -namespace Firebase.AI { - +namespace Firebase.AI +{ /// /// A tool that allows you to provide additional context to the models in the form of /// public web URLs. @@ -32,11 +32,13 @@ public readonly struct UrlContext { } /// /// Metadata for a single URL retrieved by the UrlContext tool. /// - public readonly struct UrlMetadata { + public readonly struct UrlMetadata + { /// /// Status of the URL retrieval. /// - public enum UrlRetrievalStatus { + public enum UrlRetrievalStatus + { /// /// Unspecified retrieval status /// @@ -68,18 +70,23 @@ public enum UrlRetrievalStatus { /// public UrlRetrievalStatus RetrievalStatus { get; } - private UrlMetadata(string urlString, UrlRetrievalStatus? retrievalStatus) { - if (string.IsNullOrEmpty(urlString)) { + private UrlMetadata(string urlString, UrlRetrievalStatus? retrievalStatus) + { + if (string.IsNullOrEmpty(urlString)) + { Url = null; } - else { + else + { Url = new Uri(urlString); } RetrievalStatus = retrievalStatus ?? UrlRetrievalStatus.Unspecified; } - private static UrlRetrievalStatus ParseUrlRetrievalStatus(string str) { - return str switch { + private static UrlRetrievalStatus ParseUrlRetrievalStatus(string str) + { + return str switch + { "URL_RETRIEVAL_STATUS_SUCCESS" => UrlRetrievalStatus.Success, "URL_RETRIEVAL_STATUS_ERROR" => UrlRetrievalStatus.Error, "URL_RETRIEVAL_STATUS_PAYWALL" => UrlRetrievalStatus.Paywall, @@ -92,7 +99,8 @@ private static UrlRetrievalStatus ParseUrlRetrievalStatus(string str) { /// Intended for internal use only. /// This method is used for deserializing JSON responses and should not be called directly. /// - internal static UrlMetadata FromJson(Dictionary jsonDict) { + internal static UrlMetadata FromJson(Dictionary jsonDict) + { return new UrlMetadata( jsonDict.ParseValue("retrievedUrl"), jsonDict.ParseNullableEnum("urlRetrievalStatus", ParseUrlRetrievalStatus) @@ -103,18 +111,22 @@ internal static UrlMetadata FromJson(Dictionary jsonDict) { /// /// Metadata related to the UrlContext tool. /// - public readonly struct UrlContextMetadata { + public readonly struct UrlContextMetadata + { private readonly IReadOnlyList _urlMetadata; /// /// List of URL metadata used to provide context to the Gemini model. /// - public IReadOnlyList UrlMetadata { - get { + public IReadOnlyList UrlMetadata + { + get + { return _urlMetadata ?? new List(); } } - private UrlContextMetadata(List urlMetadata) { + private UrlContextMetadata(List urlMetadata) + { _urlMetadata = urlMetadata; } @@ -122,7 +134,8 @@ private UrlContextMetadata(List urlMetadata) { /// Intended for internal use only. /// This method is used for deserializing JSON responses and should not be called directly. /// - internal static UrlContextMetadata FromJson(Dictionary jsonDict) { + internal static UrlContextMetadata FromJson(Dictionary jsonDict) + { return new UrlContextMetadata( jsonDict.ParseObjectList("urlMetadata", Firebase.AI.UrlMetadata.FromJson) );