mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-09 22:36:01 +05:45
274 lines
9.6 KiB
Swift
274 lines
9.6 KiB
Swift
// 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
|
|
|
|
extension [ModelContent] {
|
|
// TODO: Rename and refactor this.
|
|
func throwIfError() throws {
|
|
for content in self {
|
|
for part in content.parts {
|
|
switch part {
|
|
case let errorPart as ErrorPart:
|
|
throw errorPart.error
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct InternalPart: Equatable, Sendable {
|
|
enum OneOfData: Equatable, Sendable {
|
|
case text(String)
|
|
case inlineData(InlineData)
|
|
case fileData(FileData)
|
|
case functionCall(FunctionCall)
|
|
case functionResponse(FunctionResponse)
|
|
case executableCode(ExecutableCode)
|
|
case codeExecutionResult(CodeExecutionResult)
|
|
|
|
struct UnsupportedDataError: Error {
|
|
let decodingError: DecodingError
|
|
|
|
var localizedDescription: String {
|
|
decodingError.localizedDescription
|
|
}
|
|
}
|
|
}
|
|
|
|
let data: OneOfData?
|
|
|
|
let isThought: Bool?
|
|
|
|
let thoughtSignature: String?
|
|
|
|
init(_ data: OneOfData, isThought: Bool?, thoughtSignature: String?) {
|
|
self.data = data
|
|
self.isThought = isThought
|
|
self.thoughtSignature = thoughtSignature
|
|
}
|
|
}
|
|
|
|
/// A type describing data in media formats interpretable by an AI model. Each generative AI
|
|
/// request or response contains an `Array` of ``ModelContent``s, and each ``ModelContent`` value
|
|
/// may comprise multiple heterogeneous ``Part``s.
|
|
public struct ModelContent: Equatable, Sendable {
|
|
/// The role of the entity creating the ``ModelContent``. For user-generated client requests,
|
|
/// for example, the role is `user`.
|
|
public let role: String?
|
|
|
|
/// The data parts comprising this ``ModelContent`` value.
|
|
public var parts: [any Part] {
|
|
return internalParts.compactMap { part -> (any Part)? in
|
|
switch part.data {
|
|
case let .text(text):
|
|
return TextPart(text, isThought: part.isThought, thoughtSignature: part.thoughtSignature)
|
|
case let .inlineData(inlineData):
|
|
return InlineDataPart(
|
|
inlineData, isThought: part.isThought, thoughtSignature: part.thoughtSignature
|
|
)
|
|
case let .fileData(fileData):
|
|
return FileDataPart(
|
|
fileData, isThought: part.isThought, thoughtSignature: part.thoughtSignature
|
|
)
|
|
case let .functionCall(functionCall):
|
|
return FunctionCallPart(
|
|
functionCall, isThought: part.isThought, thoughtSignature: part.thoughtSignature
|
|
)
|
|
case let .functionResponse(functionResponse):
|
|
return FunctionResponsePart(
|
|
functionResponse, isThought: part.isThought, thoughtSignature: part.thoughtSignature
|
|
)
|
|
case let .executableCode(executableCode):
|
|
return ExecutableCodePart(
|
|
executableCode, isThought: part.isThought, thoughtSignature: part.thoughtSignature
|
|
)
|
|
case let .codeExecutionResult(codeExecutionResult):
|
|
return CodeExecutionResultPart(
|
|
codeExecutionResult: codeExecutionResult,
|
|
isThought: part.isThought,
|
|
thoughtSignature: part.thoughtSignature
|
|
)
|
|
case .none:
|
|
// Filter out parts that contain missing or unrecognized data
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Refactor this
|
|
let internalParts: [InternalPart]
|
|
|
|
/// Creates a new value from a list of ``Part``s.
|
|
public init(role: String? = "user", parts: [any Part]) {
|
|
self.role = role
|
|
var convertedParts = [InternalPart]()
|
|
for part in parts {
|
|
switch part {
|
|
case let textPart as TextPart:
|
|
convertedParts.append(InternalPart(
|
|
.text(textPart.text),
|
|
isThought: textPart._isThought,
|
|
thoughtSignature: textPart.thoughtSignature
|
|
))
|
|
case let inlineDataPart as InlineDataPart:
|
|
convertedParts.append(InternalPart(
|
|
.inlineData(inlineDataPart.inlineData),
|
|
isThought: inlineDataPart._isThought,
|
|
thoughtSignature: inlineDataPart.thoughtSignature
|
|
))
|
|
case let fileDataPart as FileDataPart:
|
|
convertedParts.append(InternalPart(
|
|
.fileData(fileDataPart.fileData),
|
|
isThought: fileDataPart._isThought,
|
|
thoughtSignature: fileDataPart.thoughtSignature
|
|
))
|
|
case let functionCallPart as FunctionCallPart:
|
|
convertedParts.append(InternalPart(
|
|
.functionCall(functionCallPart.functionCall),
|
|
isThought: functionCallPart._isThought,
|
|
thoughtSignature: functionCallPart.thoughtSignature
|
|
))
|
|
case let functionResponsePart as FunctionResponsePart:
|
|
convertedParts.append(InternalPart(
|
|
.functionResponse(functionResponsePart.functionResponse),
|
|
isThought: functionResponsePart._isThought,
|
|
thoughtSignature: functionResponsePart.thoughtSignature
|
|
))
|
|
default:
|
|
fatalError()
|
|
}
|
|
}
|
|
internalParts = convertedParts
|
|
}
|
|
|
|
/// Creates a new value from any data interpretable as a ``Part``.
|
|
/// See ``PartsRepresentable`` for types that can be interpreted as `Part`s.
|
|
public init(role: String? = "user", parts: any PartsRepresentable...) {
|
|
let content = parts.flatMap { $0.partsValue }
|
|
self.init(role: role, parts: content)
|
|
}
|
|
|
|
init(role: String?, parts: [InternalPart]) {
|
|
self.role = role
|
|
internalParts = parts
|
|
}
|
|
}
|
|
|
|
// MARK: Codable Conformances
|
|
|
|
extension ModelContent: Codable {
|
|
enum CodingKeys: String, CodingKey {
|
|
case role
|
|
case internalParts = "parts"
|
|
}
|
|
|
|
public init(from decoder: any Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
role = try container.decodeIfPresent(String.self, forKey: .role)
|
|
internalParts = try container.decodeIfPresent([InternalPart].self, forKey: .internalParts) ?? []
|
|
}
|
|
}
|
|
|
|
extension InternalPart: Codable {
|
|
enum CodingKeys: String, CodingKey {
|
|
case isThought = "thought"
|
|
case thoughtSignature
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
try data.encode(to: encoder)
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encodeIfPresent(isThought, forKey: .isThought)
|
|
try container.encodeIfPresent(thoughtSignature, forKey: .thoughtSignature)
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
do {
|
|
data = try OneOfData(from: decoder)
|
|
} catch let error as OneOfData.UnsupportedDataError {
|
|
AILog.error(code: .decodedUnsupportedPartData, error.localizedDescription)
|
|
data = nil
|
|
} catch { // Re-throw any other error types
|
|
throw error
|
|
}
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
isThought = try container.decodeIfPresent(Bool.self, forKey: .isThought)
|
|
thoughtSignature = try container.decodeIfPresent(String.self, forKey: .thoughtSignature)
|
|
}
|
|
}
|
|
|
|
extension InternalPart.OneOfData: Codable {
|
|
enum CodingKeys: String, CodingKey {
|
|
case text
|
|
case inlineData
|
|
case fileData
|
|
case functionCall
|
|
case functionResponse
|
|
case executableCode
|
|
case codeExecutionResult
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
switch self {
|
|
case let .text(text):
|
|
try container.encode(text, forKey: .text)
|
|
case let .inlineData(inlineData):
|
|
try container.encode(inlineData, forKey: .inlineData)
|
|
case let .fileData(fileData):
|
|
try container.encode(fileData, forKey: .fileData)
|
|
case let .functionCall(functionCall):
|
|
try container.encode(functionCall, forKey: .functionCall)
|
|
case let .functionResponse(functionResponse):
|
|
try container.encode(functionResponse, forKey: .functionResponse)
|
|
case let .executableCode(executableCode):
|
|
try container.encode(executableCode, forKey: .executableCode)
|
|
case let .codeExecutionResult(codeExecutionResult):
|
|
try container.encode(codeExecutionResult, forKey: .codeExecutionResult)
|
|
}
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
if values.contains(.text) {
|
|
self = try .text(values.decode(String.self, forKey: .text))
|
|
} else if values.contains(.inlineData) {
|
|
self = try .inlineData(values.decode(InlineData.self, forKey: .inlineData))
|
|
} else if values.contains(.fileData) {
|
|
self = try .fileData(values.decode(FileData.self, forKey: .fileData))
|
|
} else if values.contains(.functionCall) {
|
|
self = try .functionCall(values.decode(FunctionCall.self, forKey: .functionCall))
|
|
} else if values.contains(.functionResponse) {
|
|
self = try .functionResponse(values.decode(FunctionResponse.self, forKey: .functionResponse))
|
|
} else if values.contains(.executableCode) {
|
|
self = try .executableCode(values.decode(ExecutableCode.self, forKey: .executableCode))
|
|
} else if values.contains(.codeExecutionResult) {
|
|
self = try .codeExecutionResult(
|
|
values.decode(CodeExecutionResult.self, forKey: .codeExecutionResult)
|
|
)
|
|
} else {
|
|
let unexpectedKeys = values.allKeys.map { $0.stringValue }
|
|
throw UnsupportedDataError(decodingError: DecodingError.dataCorrupted(
|
|
DecodingError.Context(
|
|
codingPath: values.codingPath,
|
|
debugDescription: "Unexpected Part type(s): \(unexpectedKeys)"
|
|
)
|
|
))
|
|
}
|
|
}
|
|
}
|