Sources/GoogleAI/GenerateContentResponse.swift (267 lines of code) (raw):

// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import Foundation /// The model's response to a generate content request. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) public struct GenerateContentResponse { /// Token usage metadata for processing the generate content request. public struct UsageMetadata { /// The number of tokens in the request prompt. public let promptTokenCount: Int /// The total number of tokens across the generated response candidates. public let candidatesTokenCount: Int /// The total number of tokens in both the request and response. public let totalTokenCount: Int } /// A list of candidate response content, ordered from best to worst. public let candidates: [CandidateResponse] /// A value containing the safety ratings for the response, or, if the request was blocked, a /// reason for blocking the request. public let promptFeedback: PromptFeedback? /// Token usage metadata for processing the generate content request. public let usageMetadata: UsageMetadata? /// The response's content as text, if it exists. public var text: String? { guard let candidate = candidates.first else { Logging.default.error("Could not get text from a response that had no candidates.") return nil } let textValues: [String] = candidate.content.parts.compactMap { part in switch part { case let .text(text): return text case let .executableCode(executableCode): let codeBlockLanguage: String if executableCode.language == "LANGUAGE_UNSPECIFIED" { codeBlockLanguage = "" } else { codeBlockLanguage = executableCode.language.lowercased() } return "```\(codeBlockLanguage)\n\(executableCode.code)\n```" case let .codeExecutionResult(codeExecutionResult): if codeExecutionResult.output.isEmpty { return nil } return "```\n\(codeExecutionResult.output)\n```" case .data, .fileData, .functionCall, .functionResponse: return nil } } guard textValues.count > 0 else { Logging.default.error("Could not get a text part from the first candidate.") return nil } return textValues.joined(separator: "\n") } /// Returns function calls found in any `Part`s of the first candidate of the response, if any. public var functionCalls: [FunctionCall] { guard let candidate = candidates.first else { return [] } return candidate.content.parts.compactMap { part in guard case let .functionCall(functionCall) = part else { return nil } return functionCall } } /// Initializer for SwiftUI previews or tests. public init(candidates: [CandidateResponse], promptFeedback: PromptFeedback? = nil, usageMetadata: UsageMetadata? = nil) { self.candidates = candidates self.promptFeedback = promptFeedback self.usageMetadata = usageMetadata } } /// A struct representing a possible reply to a content generation prompt. Each content generation /// prompt may produce multiple candidate responses. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) public struct CandidateResponse { /// The response's content. public let content: ModelContent /// The safety rating of the response content. public let safetyRatings: [SafetyRating] /// The reason the model stopped generating content, if it exists; for example, if the model /// generated a predefined stop sequence. public let finishReason: FinishReason? /// Cited works in the model's response content, if it exists. public let citationMetadata: CitationMetadata? /// Initializer for SwiftUI previews or tests. public init(content: ModelContent, safetyRatings: [SafetyRating], finishReason: FinishReason?, citationMetadata: CitationMetadata?) { self.content = content self.safetyRatings = safetyRatings self.finishReason = finishReason self.citationMetadata = citationMetadata } } /// A collection of source attributions for a piece of content. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) public struct CitationMetadata { /// A list of individual cited sources and the parts of the content to which they apply. public let citationSources: [Citation] } /// A struct describing a source attribution. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) public struct Citation { /// The inclusive beginning of a sequence in a model response that derives from a cited source. public let startIndex: Int /// The exclusive end of a sequence in a model response that derives from a cited source. public let endIndex: Int /// A link to the cited source. public let uri: String /// The license the cited source work is distributed under, if specified. public let license: String? } /// A value enumerating possible reasons for a model to terminate a content generation request. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) public enum FinishReason: String { case unknown = "FINISH_REASON_UNKNOWN" case unspecified = "FINISH_REASON_UNSPECIFIED" /// Natural stop point of the model or provided stop sequence. case stop = "STOP" /// The maximum number of tokens as specified in the request was reached. case maxTokens = "MAX_TOKENS" /// The token generation was stopped because the response was flagged for safety reasons. /// NOTE: When streaming, the Candidate.content will be empty if content filters blocked the /// output. case safety = "SAFETY" /// The token generation was stopped because the response was flagged for unauthorized citations. case recitation = "RECITATION" /// All other reasons that stopped token generation. case other = "OTHER" } /// A metadata struct containing any feedback the model had on the prompt it was provided. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) public struct PromptFeedback { /// A type describing possible reasons to block a prompt. public enum BlockReason: String { /// The block reason is unknown. case unknown = "UNKNOWN" /// The block reason was not specified in the server response. case unspecified = "BLOCK_REASON_UNSPECIFIED" /// The prompt was blocked because it was deemed unsafe. case safety = "SAFETY" /// All other block reasons. case other = "OTHER" } /// The reason a prompt was blocked, if it was blocked. public let blockReason: BlockReason? /// The safety ratings of the prompt. public let safetyRatings: [SafetyRating] /// Initializer for SwiftUI previews or tests. public init(blockReason: BlockReason?, safetyRatings: [SafetyRating]) { self.blockReason = blockReason self.safetyRatings = safetyRatings } } // MARK: - Codable Conformances @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) extension GenerateContentResponse: Decodable { enum CodingKeys: CodingKey { case candidates case promptFeedback case usageMetadata } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) guard container.contains(CodingKeys.candidates) || container .contains(CodingKeys.promptFeedback) else { let context = DecodingError.Context( codingPath: [], debugDescription: "Failed to decode GenerateContentResponse;" + " missing keys 'candidates' and 'promptFeedback'." ) throw DecodingError.dataCorrupted(context) } if let candidates = try container.decodeIfPresent( [CandidateResponse].self, forKey: .candidates ) { self.candidates = candidates } else { candidates = [] } promptFeedback = try container.decodeIfPresent(PromptFeedback.self, forKey: .promptFeedback) usageMetadata = try container.decodeIfPresent(UsageMetadata.self, forKey: .usageMetadata) } } @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) extension GenerateContentResponse.UsageMetadata: Decodable { enum CodingKeys: CodingKey { case promptTokenCount case candidatesTokenCount case totalTokenCount } public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) promptTokenCount = try container.decodeIfPresent(Int.self, forKey: .promptTokenCount) ?? 0 candidatesTokenCount = try container .decodeIfPresent(Int.self, forKey: .candidatesTokenCount) ?? 0 totalTokenCount = try container.decodeIfPresent(Int.self, forKey: .totalTokenCount) ?? 0 } } @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) extension CandidateResponse: Decodable { enum CodingKeys: CodingKey { case content case safetyRatings case finishReason case finishMessage case citationMetadata } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) do { if let content = try container.decodeIfPresent(ModelContent.self, forKey: .content) { self.content = content } else { content = ModelContent(parts: []) } } catch { // Check if `content` can be decoded as an empty dictionary to detect the `"content": {}` bug. if let content = try? container.decode([String: String].self, forKey: .content), content.isEmpty { throw InvalidCandidateError.emptyContent(underlyingError: error) } else { throw InvalidCandidateError.malformedContent(underlyingError: error) } } if let safetyRatings = try container.decodeIfPresent( [SafetyRating].self, forKey: .safetyRatings ) { self.safetyRatings = safetyRatings } else { safetyRatings = [] } finishReason = try container.decodeIfPresent(FinishReason.self, forKey: .finishReason) citationMetadata = try container.decodeIfPresent( CitationMetadata.self, forKey: .citationMetadata ) } } @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) extension CitationMetadata: Decodable {} @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) extension Citation: Decodable { enum CodingKeys: CodingKey { case startIndex case endIndex case uri case license } public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) startIndex = try container.decodeIfPresent(Int.self, forKey: .startIndex) ?? 0 endIndex = try container.decode(Int.self, forKey: .endIndex) uri = try container.decode(String.self, forKey: .uri) if let license = try container.decodeIfPresent(String.self, forKey: .license), !license.isEmpty { self.license = license } else { license = nil } } } @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) extension FinishReason: Decodable { public init(from decoder: Decoder) throws { let value = try decoder.singleValueContainer().decode(String.self) guard let decodedFinishReason = FinishReason(rawValue: value) else { Logging.default .error("[GoogleGenerativeAI] Unrecognized FinishReason with value \"\(value)\".") self = .unknown return } self = decodedFinishReason } } @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) extension PromptFeedback.BlockReason: Decodable { public init(from decoder: Decoder) throws { let value = try decoder.singleValueContainer().decode(String.self) guard let decodedBlockReason = PromptFeedback.BlockReason(rawValue: value) else { Logging.default .error("[GoogleGenerativeAI] Unrecognized BlockReason with value \"\(value)\".") self = .unknown return } self = decodedBlockReason } } @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) extension PromptFeedback: Decodable { enum CodingKeys: CodingKey { case blockReason case safetyRatings } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) blockReason = try container.decodeIfPresent( PromptFeedback.BlockReason.self, forKey: .blockReason ) if let safetyRatings = try container.decodeIfPresent( [SafetyRating].self, forKey: .safetyRatings ) { self.safetyRatings = safetyRatings } else { safetyRatings = [] } } }