From 9baf469a55ad375e846df6b25ae2d6ca06f21900 Mon Sep 17 00:00:00 2001 From: Vatsal Manot Date: Sat, 11 Jan 2025 01:23:16 -0800 Subject: [PATCH] Update package --- Package.resolved | 8 +- .../CoreMI._ServiceClientProtocol.swift | 5 + .../Service/CoreMI._ServiceFileDrive.swift | 338 ++++++++++++++++++ ...nLabs.APISpecification.RequestBodies.swift | 319 +++++++++-------- .../LLMs/Chat/AbstractLLM.ChatPrompt.swift | 2 +- .../Intramodular/Models/OpenAI.File.swift | 22 +- .../Intramodular/Models/_Gemini.File.swift | 30 +- .../Models/_Gemini.GenerationConfig.swift | 16 +- .../_Gemini.Client+ContentGeneration.swift | 1 - ...I._ServiceClientProvisionedFileSpace.swift | 125 +++++++ .../Intramodular/_Gemini.Client+Files.swift | 17 +- .../_Gemini/Intramodular/_Gemini.Client.swift | 29 +- Sources/_Gemini/module.swift | 3 +- 13 files changed, 721 insertions(+), 194 deletions(-) create mode 100644 Sources/CoreMI/Intramodular/Service/CoreMI._ServiceFileDrive.swift create mode 100644 Sources/_Gemini/Intramodular/_Gemini.Client+CoreMI._ServiceClientProvisionedFileSpace.swift diff --git a/Package.resolved b/Package.resolved index 44c6cdec..1aaa8500 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "094840915419b625ed8a43083bdf164ab8d3f6bbb7fda2dcec07cb5e55a2b736", + "originHash" : "24eb1da3856f86e40bb6b87db2d4707a7cf68fd47d732dcbb9ec4d0aa614626d", "pins" : [ { "identity" : "corepersistence", @@ -16,7 +16,7 @@ "location" : "https://github.com/vmanot/Merge.git", "state" : { "branch" : "master", - "revision" : "4bc71ce650b79b3dbe1a26acf7e54b29d750e0b6" + "revision" : "cfa56e9a6af4206ec9055bdb29d59def5ee16297" } }, { @@ -34,7 +34,7 @@ "location" : "https://github.com/vmanot/Swallow.git", "state" : { "branch" : "master", - "revision" : "4c05166cf644846199fb734bbc47d74f87610945" + "revision" : "2ac7c7f06110bc3b397677e82d3a232980c20617" } }, { @@ -70,7 +70,7 @@ "location" : "https://github.com/SwiftUIX/SwiftUIX.git", "state" : { "branch" : "master", - "revision" : "50e2aacd7b124ffb5d06b4bfa5a4f255052a559b" + "revision" : "2313c0890dd6a01c7a82618b9e505b1b00e5cdf7" } } ], diff --git a/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceClientProtocol.swift b/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceClientProtocol.swift index 9fd23037..94d20ffc 100644 --- a/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceClientProtocol.swift +++ b/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceClientProtocol.swift @@ -8,6 +8,11 @@ import Swallow extension CoreMI { /// A client for an AI/ML service. public protocol _ServiceClientProtocol: PersistentlyRepresentableType { + /// A global filespace as provisioned by the service. + /// + /// For e.g. to represent all the files in an OpenAI/Gemini project. + func _globalFileSpace() -> any CoreMI._ServiceProvisionedFileSpace + init(account: (any CoreMI._ServiceAccountProtocol)?) async throws } diff --git a/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceFileDrive.swift b/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceFileDrive.swift new file mode 100644 index 00000000..6a3b8719 --- /dev/null +++ b/Sources/CoreMI/Intramodular/Service/CoreMI._ServiceFileDrive.swift @@ -0,0 +1,338 @@ +// +// Copyright (c) Vatsal Manot +// + +import CorePersistence +import FoundationX +import Merge +import Swallow + +extension CoreMI { + public struct _ServiceProvisionedFileStatus: Codable, Hashable, Sendable { + public let isReady: Bool + + public init(isReady: Bool) { + self.isReady = isReady + } + } + + public struct _ServiceProvisionedFile: Codable, Hashable, Identifiable, Sendable { + public struct ID: Codable, Hashable, Sendable { + public let rawValue: AnyPersistentIdentifier + + public init( + erasing x: T + ) { + self.rawValue = AnyPersistentIdentifier(erasing: x) + } + + public func `as`(_ type: T.Type) throws -> T { + try rawValue.as(type) + } + } + + public struct Metadata: Codable, Hashable, Initiable, Sendable { + public var displayName: String? + + public init(displayName: String?) { + self.displayName = displayName + } + + public init() { + self.init(displayName: nil) + } + } + + public let id: ID + public let metadata: Metadata + + public init(id: ID, metadata: Metadata) { + self.id = id + self.metadata = metadata + } + } + + public protocol _ServiceProvisionedFileSpace { + func listFiles() async throws -> AnyAsyncSequence + func file(for id: CoreMI._ServiceProvisionedFile.ID) async throws -> CoreMI._ServiceProvisionedFile + + func uploadFile( + contents: T, + metadata: CoreMI._ServiceProvisionedFile.Metadata + ) async throws -> CoreMI._ServiceProvisionedFile + + func status( + ofFile file: CoreMI._ServiceProvisionedFile.ID + ) async throws -> CoreMI._ServiceProvisionedFileStatus + + func requestDeletion( + ofFile _: CoreMI._ServiceProvisionedFile.ID + ) async throws + + func delete( + file: CoreMI._ServiceProvisionedFile.ID + ) async throws + } +} + +extension CoreMI { + fileprivate enum _ServiceProvisionedFilePollingError: Error { + case fileNotReady + } +} + +extension CoreMI._ServiceProvisionedFileSpace { + func _pollUntilReady( + for file: CoreMI._ServiceProvisionedFile.ID, + maxRetryCount: Int? = nil, + retryDelay: DispatchTimeInterval = .seconds(1) + ) async throws -> CoreMI._ServiceProvisionedFile { + try await Task.retrying( + priority: nil, + maxRetryCount: maxRetryCount ?? Int.max, + retryDelay: retryDelay + ) { + let fileStatus = try await self.status(ofFile: file) + + guard fileStatus.isReady else { + throw CoreMI._ServiceProvisionedFilePollingError.fileNotReady + } + }.value + + return try await self.file(for: file) + } +} + +extension CoreMI._ServiceProvisionedFileSpace { + public func requestDeletion( + ofFile file: CoreMI._ServiceProvisionedFile.ID + ) async throws { + try await delete(file: file) + } +} + +extension CoreMI { + public struct _ServiceProvisionedFileConversionContext { + public let client: any CoreMI._ServiceClientProtocol + + public init(client: any CoreMI._ServiceClientProtocol) { + self.client = client + } + } + + public protocol _ServiceProvisionedFileConvertible { + init( + from file: CoreMI._ServiceProvisionedFile, + context: CoreMI._ServiceProvisionedFileConversionContext + ) async throws + + func __conversion( + context: CoreMI._ServiceProvisionedFileConversionContext + ) async throws -> CoreMI._ServiceProvisionedFile + } +} + +extension CoreMI { + public struct _AnyServiceFileDriveConfiguration { + public let storageDirectory: URL? + public let indexFileURL: URL + } + + public class _AnyServiceFileDrive { + let configuration: _AnyServiceFileDriveConfiguration + let owner: any CoreMI._ServiceClientProtocol + let remote: any _ServiceProvisionedFileSpace + + init( + configuration: _AnyServiceFileDriveConfiguration, + owner: any CoreMI._ServiceClientProtocol, + remote: any CoreMI._ServiceProvisionedFileSpace + ) throws { + self.owner = owner + self.configuration = configuration + self.remote = remote + } + } + + /// This class binds a remote-service managed file space (for e.g. OpenAI's files/Gemini's files for your project) that you control partially to a local file storage that you control fully. + public final class ServiceFileDrive: CoreMI._AnyServiceFileDrive, ObjectDidChangeObservableObject { + public struct Item: Codable, Hashable, Identifiable, Sendable { + public struct ID: Codable, Hashable, Sendable { + public let key: Key + public let remoteFileID: _ServiceProvisionedFile.ID + } + + public let id: ID + public let metadata: _ServiceProvisionedFile.Metadata + public let fileURL: URL? + } + + public struct _IndexData: Codable, Hashable, Initiable, Sendable { + public var items: [Key: Item] + + public init() { + self.items = [:] + } + } + + @FileStorage( + location: .temporaryDirectory.appending(.file(UUID().uuidString)), + coder: HadeanTopLevelCoder(coder: .json) + ) + var _indexData: _IndexData = .init() + + public var items: [Item] { + Array(_indexData.items.values) + } + + override init( + configuration: _AnyServiceFileDriveConfiguration, + owner: any CoreMI._ServiceClientProtocol, + remote: any CoreMI._ServiceProvisionedFileSpace + ) throws { + try self.__indexData.setLocation(configuration.indexFileURL) + + try super.init( + configuration: configuration, + owner: owner, + remote: remote + ) + } + + public func uploadFile( + withContents contents: T, + metadata: CoreMI._ServiceProvisionedFile.Metadata, + forKey key: Key + ) async throws -> Item { + assert(configuration.storageDirectory == nil) + + let file: CoreMI._ServiceProvisionedFile = try await remote.uploadFile(contents: contents, metadata: metadata) + let item: Item = .init( + id: .init( + key: key, + remoteFileID: file.id + ), + metadata: metadata, + fileURL: nil + ) + + self._indexData.items[key] = item + + return item + } + + func _getReadyItem(forKey key: Key) async throws -> Item { + let result: Item = try self._indexData.items[key].unwrap() + let id = result.id + + let latestFile = try await remote._pollUntilReady(for: id.remoteFileID) + + _ = latestFile + + return result + } + + public subscript( + key: Key + ) -> Item { + get async throws { + try await _getReadyItem(forKey: key) + } + } + + public subscript( + item id: Item.ID + ) -> Item { + get async throws { + let key: Key = try self._indexData.items.values.first(where: { $0.id == id }).unwrap().id.key + + return try await self[key] + } + } + + public func items( + forKeys keys: [Key] + ) async throws -> [Item] { + try await keys.asyncMap({ key in + try await self[key] + }) + } + + public func erase() async throws { + let itemsByKey = _indexData.items + + for (_, item) in itemsByKey { + let fileID: CoreMI._ServiceProvisionedFile.ID = item.id.remoteFileID + + try await remote.delete(file: fileID) + } + + self._indexData.items = [:] + } + } +} + +extension CoreMI.ServiceFileDrive { + public func uploadFile( + withContents contents: T, + displayName: String, + forKey key: Key + ) async throws -> Item { + try await self.uploadFile( + withContents: contents, + metadata: .init(displayName: displayName), + forKey: key + ) + } +} + +extension CoreMI._ServiceClientProtocol { + public func _globalFileSpace() -> any CoreMI._ServiceProvisionedFileSpace { + fatalError(.unimplemented) + } + + public func fileDrive( + storingFilesInDirectory storageDirectory: URL?, + indexFileURL: URL, + keyedBy: Key.Type + ) throws -> CoreMI.ServiceFileDrive { + assert(storageDirectory == nil, "storageDirectory is currently unsupported") + + return try CoreMI.ServiceFileDrive( + configuration: .init( + storageDirectory: storageDirectory, + indexFileURL: indexFileURL + ), + owner: self, + remote: _globalFileSpace() + ) + } +} + +extension CoreMI._ServiceClientProtocol { + public func _convert( + _ x: T + ) async throws -> CoreMI._ServiceProvisionedFile { + let context = CoreMI._ServiceProvisionedFileConversionContext(client: self) + + return try await x.__conversion(context: context) + } +} +/*// file drive's purpose is to manage files that you give it and keep syncing it to Gemini/OpenAI + // file drive will keep a copy of the files that you give it locally, and also upload it to Gemini/OpenAI + let drive: ServiceFileDrive = try await geminiClient.fileDrive( + storingFilesInDirectory: , + indexFileURL: .automatic, + keyedBy: WWDCSession.self + ) + + let item: ServiceFileDrive.Item = drive.upload(data: myData, forKey: wwdcSession) + + try drive.data(forKey: wwdcSession) + + try await drive.eraseLocalCopiesOnly() + try await drive.erase() + try await drive.delete(forKey: wwdcSession) + + */ + diff --git a/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift index 3404e48e..67b4f4e9 100644 --- a/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift +++ b/Sources/ElevenLabs/Intramodular/API/ElevenLabs.APISpecification.RequestBodies.swift @@ -10,196 +10,199 @@ import SwiftAPI import Merge extension ElevenLabs.APISpecification { + public enum RequestBodies { + + } +} + +extension ElevenLabs.APISpecification.RequestBodies { + public struct SpeechRequest: Codable, Hashable, Equatable { + public let text: String + public let voiceSettings: ElevenLabs.VoiceSettings + public let model: ElevenLabs.Model + + private enum CodingKeys: String, CodingKey { + case text + case voiceSettings = "voice_settings" + case model = "model_id" + } + + public init( + text: String, + voiceSettings: ElevenLabs.VoiceSettings, + model: ElevenLabs.Model + ) { + self.text = text + self.voiceSettings = voiceSettings + self.model = model + } + } - enum RequestBodies { - public struct SpeechRequest: Codable, Hashable, Equatable { - public let text: String - public let voiceSettings: ElevenLabs.VoiceSettings - public let model: ElevenLabs.Model - - private enum CodingKeys: String, CodingKey { - case text - case voiceSettings = "voice_settings" - case model = "model_id" - } - - public init( - text: String, - voiceSettings: ElevenLabs.VoiceSettings, - model: ElevenLabs.Model - ) { - self.text = text - self.voiceSettings = voiceSettings - self.model = model - } + public struct TextToSpeechInput: Codable, Hashable { + public let voiceId: String + public let requestBody: SpeechRequest + + public init(voiceId: String, requestBody: SpeechRequest) { + self.voiceId = voiceId + self.requestBody = requestBody } + } + + public struct SpeechToSpeechInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { + public let voiceId: String + public let audioURL: URL + public let model: ElevenLabs.Model + public let voiceSettings: ElevenLabs.VoiceSettings - public struct TextToSpeechInput: Codable, Hashable { - public let voiceId: String - public let requestBody: SpeechRequest - - public init(voiceId: String, requestBody: SpeechRequest) { - self.voiceId = voiceId - self.requestBody = requestBody - } + public init( + voiceId: String, + audioURL: URL, + model: ElevenLabs.Model, + voiceSettings: ElevenLabs.VoiceSettings + ) { + self.voiceId = voiceId + self.audioURL = audioURL + self.model = model + self.voiceSettings = voiceSettings } - public struct SpeechToSpeechInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { - public let voiceId: String - public let audioURL: URL - public let model: ElevenLabs.Model - public let voiceSettings: ElevenLabs.VoiceSettings + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result = HTTPRequest.Multipart.Content() - public init( - voiceId: String, - audioURL: URL, - model: ElevenLabs.Model, - voiceSettings: ElevenLabs.VoiceSettings - ) { - self.voiceId = voiceId - self.audioURL = audioURL - self.model = model - self.voiceSettings = voiceSettings - } + result.append( + .text( + named: "model_id", + value: model.rawValue + ) + ) - public func __conversion() throws -> HTTPRequest.Multipart.Content { - var result = HTTPRequest.Multipart.Content() - + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + if let voiceSettingsData = try? encoder.encode(voiceSettings), + let voiceSettingsString = String( + data: voiceSettingsData, + encoding: .utf8 + ) { result.append( .text( - named: "model_id", - value: model.rawValue + named: "voice_settings", + value: voiceSettingsString ) ) - - let encoder = JSONEncoder() - encoder.keyEncodingStrategy = .convertToSnakeCase - if let voiceSettingsData = try? encoder.encode(voiceSettings), - let voiceSettingsString = String( - data: voiceSettingsData, - encoding: .utf8 - ) { - result.append( - .text( - named: "voice_settings", - value: voiceSettingsString - ) - ) - } - - if let fileData = try? Data(contentsOf: audioURL) { - result.append( - .file( - named: "audio", - data: fileData, - filename: audioURL.lastPathComponent, - contentType: .mpeg - ) + } + + if let fileData = try? Data(contentsOf: audioURL) { + result.append( + .file( + named: "audio", + data: fileData, + filename: audioURL.lastPathComponent, + contentType: .mpeg ) - } - - return result + ) } + + return result } + } + + public struct AddVoiceInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { + public let name: String + public let description: String + public let fileURL: URL - public struct AddVoiceInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { - public let name: String - public let description: String - public let fileURL: URL + public init( + name: String, + description: String, + fileURL: URL + ) { + self.name = name + self.description = description + self.fileURL = fileURL + } + + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result = HTTPRequest.Multipart.Content() - public init( - name: String, - description: String, - fileURL: URL - ) { - self.name = name - self.description = description - self.fileURL = fileURL - } + result.append( + .text( + named: "name", + value: name + ) + ) - public func __conversion() throws -> HTTPRequest.Multipart.Content { - var result = HTTPRequest.Multipart.Content() - - result.append( - .text( - named: "name", - value: name - ) + result.append( + .text( + named: "description", + value: description ) - + ) + + if let fileData = try? Data(contentsOf: fileURL) { result.append( - .text( - named: "description", - value: description + .file( + named: "files", + data: fileData, + filename: fileURL.lastPathComponent, + contentType: .m4a ) ) - - if let fileData = try? Data(contentsOf: fileURL) { - result.append( - .file( - named: "files", - data: fileData, - filename: fileURL.lastPathComponent, - contentType: .m4a - ) - ) - } - - return result } + + return result + } + } + + public struct EditVoiceInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { + public let voiceId: String + public let name: String + public let description: String? + public let fileURL: URL? + + public init( + voiceId: String, + name: String, + description: String? = nil, + fileURL: URL? = nil + ) { + self.voiceId = voiceId + self.name = name + self.description = description + self.fileURL = fileURL } - public struct EditVoiceInput: Codable, Hashable, HTTPRequest.Multipart.ContentConvertible, Equatable { - public let voiceId: String - public let name: String - public let description: String? - public let fileURL: URL? + public func __conversion() throws -> HTTPRequest.Multipart.Content { + var result = HTTPRequest.Multipart.Content() - public init( - voiceId: String, - name: String, - description: String? = nil, - fileURL: URL? = nil - ) { - self.voiceId = voiceId - self.name = name - self.description = description - self.fileURL = fileURL - } + result.append( + .text( + named: "name", + value: name + ) + ) - public func __conversion() throws -> HTTPRequest.Multipart.Content { - var result = HTTPRequest.Multipart.Content() - + if let description = description { result.append( .text( - named: "name", - value: name + named: "description", + value: description ) ) - - if let description = description { - result.append( - .text( - named: "description", - value: description - ) - ) - } - - if let fileURL = fileURL, - let fileData = try? Data(contentsOf: fileURL) { - result.append( - .file( - named: "files", - data: fileData, - filename: fileURL.lastPathComponent, - contentType: .m4a - ) + } + + if let fileURL = fileURL, + let fileData = try? Data(contentsOf: fileURL) { + result.append( + .file( + named: "files", + data: fileData, + filename: fileURL.lastPathComponent, + contentType: .m4a ) - } - - return result + ) } + + return result } } } diff --git a/Sources/LargeLanguageModels/Intramodular/LLMs/Chat/AbstractLLM.ChatPrompt.swift b/Sources/LargeLanguageModels/Intramodular/LLMs/Chat/AbstractLLM.ChatPrompt.swift index 21d5e30e..4c6717b1 100644 --- a/Sources/LargeLanguageModels/Intramodular/LLMs/Chat/AbstractLLM.ChatPrompt.swift +++ b/Sources/LargeLanguageModels/Intramodular/LLMs/Chat/AbstractLLM.ChatPrompt.swift @@ -26,7 +26,7 @@ extension AbstractLLM { } } } - + public init( messages: [AbstractLLM.ChatMessage], context: PromptContextValues = PromptContextValues.current diff --git a/Sources/OpenAI/Intramodular/Models/OpenAI.File.swift b/Sources/OpenAI/Intramodular/Models/OpenAI.File.swift index 125f0e4a..57f6d7e0 100644 --- a/Sources/OpenAI/Intramodular/Models/OpenAI.File.swift +++ b/Sources/OpenAI/Intramodular/Models/OpenAI.File.swift @@ -2,13 +2,31 @@ // Copyright (c) Vatsal Manot // +import CorePersistence import NetworkKit import Swift +extension OpenAI.File { + @HadeanIdentifier("sihim-nosam-dujaz-zafuj") + public struct ID: Codable, RawRepresentable, Hashable, Sendable { + public let rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + public init(from decoder: any Decoder) throws { + rawValue = try String(from: decoder) + } + + public func encode(to encoder: any Encoder) throws { + try rawValue.encode(to: encoder) + } + } +} + extension OpenAI { public final class File: OpenAI.Object, Identifiable { - public typealias ID = _TypeAssociatedID - private enum CodingKeys: String, CodingKey { case id case bytes diff --git a/Sources/_Gemini/Intramodular/Models/_Gemini.File.swift b/Sources/_Gemini/Intramodular/Models/_Gemini.File.swift index 7797e368..f354818e 100644 --- a/Sources/_Gemini/Intramodular/Models/_Gemini.File.swift +++ b/Sources/_Gemini/Intramodular/Models/_Gemini.File.swift @@ -5,11 +5,17 @@ // Created by Jared Davidson on 12/11/24. // +import CorePersistence import Foundation extension _Gemini { - - public struct File: Codable, Hashable { + public struct File: Codable, Hashable, Identifiable { + @HadeanIdentifier("gupuj-nutuh-fabom-luhub") + public struct ID: Codable, Hashable, Sendable { + public let name: _Gemini.File.Name + public let uri: URL + } + public let createTime: String? public let expirationTime: String? public let mimeType: String? @@ -21,13 +27,8 @@ extension _Gemini { public let uri: URL public let videoMetadata: VideoMetadata? - public enum State: String, Codable { - case processing = "PROCESSING" - case active = "ACTIVE" - } - - public struct VideoMetadata: Codable, Hashable { - public let videoDuration: String + public var id: _Gemini.File.ID { + _Gemini.File.ID(name: name, uri: uri) } } @@ -39,7 +40,16 @@ extension _Gemini { } extension _Gemini.File { - public struct Name: Codable, RawRepresentable, Hashable { + public enum State: String, Codable { + case processing = "PROCESSING" + case active = "ACTIVE" + } + + public struct VideoMetadata: Codable, Hashable { + public let videoDuration: String + } + + public struct Name: Codable, RawRepresentable, Hashable, Sendable { public let rawValue: String public init(rawValue: String) { diff --git a/Sources/_Gemini/Intramodular/Models/_Gemini.GenerationConfig.swift b/Sources/_Gemini/Intramodular/Models/_Gemini.GenerationConfig.swift index 93d054fa..e352e195 100644 --- a/Sources/_Gemini/Intramodular/Models/_Gemini.GenerationConfig.swift +++ b/Sources/_Gemini/Intramodular/Models/_Gemini.GenerationConfig.swift @@ -39,6 +39,14 @@ extension _Gemini { } } + public enum SchemaType: String, Codable { + case array = "ARRAY" + case object = "OBJECT" + case string = "STRING" + case number = "NUMBER" + case boolean = "BOOLEAN" + } + public indirect enum SchemaObject { case object(properties: [String: SchemaObject]) case array(items: SchemaObject) @@ -61,14 +69,6 @@ extension _Gemini { } } } - - public enum SchemaType: String, Codable { - case array = "ARRAY" - case object = "OBJECT" - case string = "STRING" - case number = "NUMBER" - case boolean = "BOOLEAN" - } } // MARK: - Conformances diff --git a/Sources/_Gemini/Intramodular/_Gemini.Client+ContentGeneration.swift b/Sources/_Gemini/Intramodular/_Gemini.Client+ContentGeneration.swift index 2a05fbae..174b0e1e 100644 --- a/Sources/_Gemini/Intramodular/_Gemini.Client+ContentGeneration.swift +++ b/Sources/_Gemini/Intramodular/_Gemini.Client+ContentGeneration.swift @@ -69,7 +69,6 @@ extension _Gemini.Client { model: _Gemini.Model, configuration: _Gemini.GenerationConfiguration = configDefault ) async throws -> _Gemini.Content { - let systemInstruction = extractSystemInstruction(from: messages) let messages: [_Gemini.Message] = messages.filter({ $0.role != .system }) var contents: [_Gemini.APISpecification.RequestBodies.Content] = [] diff --git a/Sources/_Gemini/Intramodular/_Gemini.Client+CoreMI._ServiceClientProvisionedFileSpace.swift b/Sources/_Gemini/Intramodular/_Gemini.Client+CoreMI._ServiceClientProvisionedFileSpace.swift new file mode 100644 index 00000000..9eb82eba --- /dev/null +++ b/Sources/_Gemini/Intramodular/_Gemini.Client+CoreMI._ServiceClientProvisionedFileSpace.swift @@ -0,0 +1,125 @@ +// +// File.swift +// AI +// +// Created by Vatsal Manot on 12/27/24. +// + +import CoreMI +import FoundationX +import Swallow + +extension _Gemini.Client { + public final class FileSpace: CoreMI._ServiceProvisionedFileSpace { + let client: _Gemini.Client + + init(client: _Gemini.Client) { + self.client = client + } + + public func listFiles() async throws -> AnyAsyncSequence { + let files: [_Gemini.File] = try await client.listFiles(pageSize: 100).files ?? []; // FIXME!!!: (@vmanot) + + return try await AnyAsyncSequence(files.asyncMap({ try await client._convert($0).id })) + } + + public func file( + for fileID: CoreMI._ServiceProvisionedFile.ID + ) async throws -> CoreMI._ServiceProvisionedFile { + let fileID = try fileID.as(_Gemini.File.ID.self) + + return try await client._convert(client.getFile(name: fileID.name)) + } + + public func status( + ofFile file: CoreMI._ServiceProvisionedFile.ID + ) async throws -> CoreMI._ServiceProvisionedFileStatus { + let fileID = try file.as(_Gemini.File.ID.self) + let file: _Gemini.File = try await client.getFile(name: fileID.name) + + return CoreMI._ServiceProvisionedFileStatus( + isReady: file.state == .active + ) + } + + public func uploadFile( + contents: T, + metadata: CoreMI._ServiceProvisionedFile.Metadata + ) async throws -> CoreMI._ServiceProvisionedFile { + let serializedContents: Data = try cast(contents, to: Data.self) + + let file: _Gemini.File = try await Task.retrying(maxRetryCount: 5) { + try await self.client.uploadFile( + from: serializedContents, + ofSwiftType: Swift.type(of: contents), + mimeType: nil, + displayName: metadata.displayName ?? "File" + ) + }.value + + return try await client._convert(file) + } + + public func delete( + file: CoreMI._ServiceProvisionedFile.ID + ) async throws { + let fileID = try file.as(_Gemini.File.ID.self) + + try await client.deleteFile(fileURL: fileID.uri) + } + } +} + +extension _Gemini.Client { + public func file( + for id: CoreMI.ServiceFileDrive.Item.ID + ) async throws -> _Gemini.File { + let fileID: _Gemini.File.ID = try id.remoteFileID.as(_Gemini.File.ID.self) + + return try await getFile(name: fileID.name) + } + + public func file( + for file: CoreMI.ServiceFileDrive.Item + ) async throws -> _Gemini.File { + try await self.file(for: file.id) + } + + public func files( + for identifiers: [CoreMI.ServiceFileDrive.Item.ID] + ) async throws -> [_Gemini.File] { + try await identifiers.asyncMap({ id in + try await file(for: id) + }) + } + + public func file( + for items: [CoreMI.ServiceFileDrive.Item] + ) async throws -> [_Gemini.File] { + try await files(for: items.map(\.id)) + } +} + +extension _Gemini.File: CoreMI._ServiceProvisionedFileConvertible { + public init( + from file: CoreMI._ServiceProvisionedFile, + context: CoreMI._ServiceProvisionedFileConversionContext + ) async throws { + let client: _Gemini.Client = try cast(context.client) + let fileID: _Gemini.File.ID = try file.id.as(_Gemini.File.ID.self) + + self = try await client.getFile(name: fileID.name) + } + + public func __conversion( + context: CoreMI._ServiceProvisionedFileConversionContext + ) async throws -> CoreMI._ServiceProvisionedFile { + let fileID = CoreMI._ServiceProvisionedFile.ID(erasing: self.id) + let fileMetadata = CoreMI._ServiceProvisionedFile.Metadata(displayName: self.name.rawValue) + + return CoreMI._ServiceProvisionedFile( + id: fileID, + metadata: fileMetadata + ) + } +} diff --git a/Sources/_Gemini/Intramodular/_Gemini.Client+Files.swift b/Sources/_Gemini/Intramodular/_Gemini.Client+Files.swift index 2665580f..b6fa298c 100644 --- a/Sources/_Gemini/Intramodular/_Gemini.Client+Files.swift +++ b/Sources/_Gemini/Intramodular/_Gemini.Client+Files.swift @@ -2,17 +2,18 @@ // Copyright (c) Vatsal Manot // +import CoreMI import Dispatch -import Foundation +import FoundationX import Merge import NetworkKit import Swallow extension _Gemini.Client { - public func uploadFile( from data: Data, - mimeType: HTTPMediaType, + ofSwiftType swiftType: Any.Type? = nil, + mimeType: HTTPMediaType?, displayName: String ) async throws -> _Gemini.File { guard !displayName.isEmpty else { @@ -20,9 +21,15 @@ extension _Gemini.Client { } do { + var mimeType: String? = mimeType?.rawValue ?? _MediaAssetFileType(data)?.mimeType + + if mimeType == nil, let swiftType { + mimeType = HTTPMediaType(_swiftType: swiftType)?.rawValue + } + let input = _Gemini.APISpecification.RequestBodies.FileUploadInput( fileData: data, - mimeType: mimeType.rawValue, + mimeType: try mimeType.unwrap(), displayName: displayName ) @@ -36,7 +43,7 @@ extension _Gemini.Client { public func uploadFile( from url: URL, - mimeType: HTTPMediaType, + mimeType: HTTPMediaType?, displayName: String? ) async throws -> _Gemini.File { let data: Data diff --git a/Sources/_Gemini/Intramodular/_Gemini.Client.swift b/Sources/_Gemini/Intramodular/_Gemini.Client.swift index 89cb515a..2e996578 100644 --- a/Sources/_Gemini/Intramodular/_Gemini.Client.swift +++ b/Sources/_Gemini/Intramodular/_Gemini.Client.swift @@ -5,14 +5,16 @@ // Created by Jared Davidson on 12/11/24. // +import CoreMI +import CorePersistence import Diagnostics import NetworkKit -import Foundation -import Merge import FoundationX +import Merge import Swallow extension _Gemini { + @HadeanIdentifier("nagik-didah-dufak-nipav") @RuntimeDiscoverable public final class Client: HTTPClient, _StaticSwift.Namespace { public typealias API = _Gemini.APISpecification @@ -34,6 +36,25 @@ extension _Gemini { } } -extension _Gemini.Client { - +extension _Gemini.Client: CoreMI._ServiceClientProtocol { + public func _globalFileSpace() -> any CoreMI._ServiceProvisionedFileSpace { + _Gemini.Client.FileSpace(client: self) + } + + public convenience init( + account: (any CoreMI._ServiceAccountProtocol)? + ) async throws { + let account: any CoreMI._ServiceAccountProtocol = try account.unwrap() + let serviceVendorIdentifier: CoreMI._ServiceVendorIdentifier = try account.serviceVendorIdentifier.unwrap() + + guard serviceVendorIdentifier == CoreMI._ServiceVendorIdentifier._OpenAI else { + throw CoreMI._ServiceClientError.incompatibleVendor(serviceVendorIdentifier) + } + + guard let credential = try account.credential as? CoreMI._ServiceCredentialTypes.APIKeyCredential else { + throw CoreMI._ServiceClientError.invalidCredential(try account.credential) + } + + self.init(apiKey: credential.apiKey) + } } diff --git a/Sources/_Gemini/module.swift b/Sources/_Gemini/module.swift index f3ecd1e3..581b3922 100644 --- a/Sources/_Gemini/module.swift +++ b/Sources/_Gemini/module.swift @@ -2,4 +2,5 @@ // Copyright (c) Vatsal Manot // -import LargeLanguageModels +@_exported import CoreMI +@_exported import LargeLanguageModels