diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/AISwiftAssist.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/AISwiftAssist.xcscheme new file mode 100644 index 0000000..a1b43d2 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/AISwiftAssist.xcscheme @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Package.swift b/Package.swift index 970cd47..6242aee 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -7,6 +7,7 @@ let package = Package( name: "AISwiftAssist", platforms: [ .iOS(.v13), + .macOS(.v12), .watchOS(.v8) ], products: [ @@ -24,5 +25,6 @@ let package = Package( name: "AISwiftAssistTests", dependencies: ["AISwiftAssist"] ), - ] + ], + swiftLanguageModes: [.v6] ) diff --git a/Sources/AISwiftAssist/APIs/AssistantsAPI.swift b/Sources/AISwiftAssist/APIs/AssistantsAPI.swift index 98bd620..1b54eab 100644 --- a/Sources/AISwiftAssist/APIs/AssistantsAPI.swift +++ b/Sources/AISwiftAssist/APIs/AssistantsAPI.swift @@ -8,7 +8,7 @@ import Foundation /// Build assistants that can call models and use tools to perform tasks. [Link for Assistants](https://platform.openai.com/docs/api-reference/assistants) -public protocol IAssistantsAPI: AnyObject { +public protocol IAssistantsAPI: AnyObject, Sendable { /// Returns a list of assistants. /// - Parameter parameters: Parameters for the list of assistants. @@ -36,49 +36,20 @@ public protocol IAssistantsAPI: AnyObject { /// - Parameter assistantId: The ID of the assistant to delete. /// - Returns: Deletion status func delete(by assistantId: String) async throws -> ASADeleteModelResponse - - /// Create an assistant file by attaching a File to an assistant. - /// - Parameters: - /// - assistantId: The ID of the assistant for which to create a File. - /// - request: The request object containing the File ID. - /// - Returns: An assistant file object. - func createFile(for assistantId: String, with request: ASACreateAssistantFileRequest) async throws -> ASAAssistantFile - - /// Retrieves an assistant file. - /// - Parameters: - /// - assistantId: The ID of the assistant who the file belongs to. - /// - fileId: The ID of the file to retrieve. - /// - Returns: The assistant file object matching the specified ID. - func retrieveFile(for assistantId: String, fileId: String) async throws -> ASAAssistantFile - /// Delete an assistant file. - /// - Parameters: - /// - assistantId: The ID of the assistant that the file belongs to. - /// - fileId: The ID of the file to delete. - /// - Returns: Deletion status. - func deleteFile(for assistantId: String, fileId: String) async throws -> ASADeleteModelResponse - - /// Returns a list of assistant files. - /// - Parameters: - /// - assistantId: The ID of the assistant the file belongs to. - /// - parameters: Parameters for the list of assistant files. - /// - Returns: A list of assistant file objects. - func listFiles(for assistantId: String, with parameters: ASAListAssistantsParameters?) async throws -> ASAAssistantFilesListResponse } -public final class AssistantsAPI: HTTPClient, IAssistantsAPI { +public actor AssistantsAPI: HTTPClient, IAssistantsAPI { let urlSession: URLSession - public init(apiKey: String, - baseScheme: String = Constants.baseScheme, - baseHost: String = Constants.baseHost, - path: String = Constants.path, - urlSession: URLSession = .shared) { - Constants.apiKey = apiKey - Constants.baseScheme = baseScheme - Constants.baseHost = baseHost - Constants.path = path + public init( + config: AISwiftAssistConfig, + constants: AISwiftAssistConstants = .default, + urlSession: URLSession = .shared + ) { + Constants.config = config + Constants.constants = constants self.urlSession = urlSession } @@ -110,25 +81,5 @@ public final class AssistantsAPI: HTTPClient, IAssistantsAPI { let endpoint = AssistantEndpoint.deleteAssistant(assistantId) return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASADeleteModelResponse.self) } - - public func createFile(for assistantId: String, with request: ASACreateAssistantFileRequest) async throws -> ASAAssistantFile { - let endpoint = AssistantEndpoint.createFile(assistantId, request) - return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAAssistantFile.self) - } - - public func retrieveFile(for assistantId: String, fileId: String) async throws -> ASAAssistantFile { - let endpoint = AssistantEndpoint.retrieveFile(assistantId, fileId) - return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAAssistantFile.self) - } - - public func deleteFile(for assistantId: String, fileId: String) async throws -> ASADeleteModelResponse { - let endpoint = AssistantEndpoint.deleteFile(assistantId, fileId) - return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASADeleteModelResponse.self) - } - - public func listFiles(for assistantId: String, with parameters: ASAListAssistantsParameters? = nil) async throws -> ASAAssistantFilesListResponse { - let endpoint = AssistantEndpoint.listFiles(assistantId, parameters) - return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAAssistantFilesListResponse.self) - } } diff --git a/Sources/AISwiftAssist/APIs/MessagesAPI.swift b/Sources/AISwiftAssist/APIs/MessagesAPI.swift index 6a6a967..46d4855 100644 --- a/Sources/AISwiftAssist/APIs/MessagesAPI.swift +++ b/Sources/AISwiftAssist/APIs/MessagesAPI.swift @@ -8,7 +8,14 @@ import Foundation /// Create messages within threads [Link for Messages](https://platform.openai.com/docs/api-reference/messages) -public protocol IMessagesAPI: AnyObject { +public protocol IMessagesAPI: AnyObject, Sendable { + + /// Returns a list of messages for a given thread. + /// - Parameters: + /// - threadId: The ID of the thread the messages belong to. + /// - parameters: Parameters for the list of messages. + /// - Returns: A list of message objects. + func getMessages(by threadId: String, parameters: ASAListMessagesParameters?) async throws -> ASAMessagesListResponse /// Create a message. /// - Parameters: @@ -31,44 +38,20 @@ public protocol IMessagesAPI: AnyObject { /// - modifyMessage: Object with parameters for modifying a message. /// - Returns: The modified message object. func modify(by threadId: String, messageId: String, modifyMessage: ASAModifyMessageRequest) async throws -> ASAMessage - - /// Returns a list of messages for a given thread. - /// - Parameters: - /// - threadId: The ID of the thread the messages belong to. - /// - parameters: Parameters for the list of messages. - /// - Returns: A list of message objects. - func getMessages(by threadId: String, parameters: ASAListMessagesParameters?) async throws -> ASAMessagesListResponse - - /// Retrieves a file associated with a message. - /// - Parameters: - /// - threadId: The ID of the thread to which the message and file belong. - /// - messageId: The ID of the message the file belongs to. - /// - fileId: The ID of the file being retrieved. - /// - Returns: The message file object. - func retrieveFile(by threadId: String, messageId: String, fileId: String) async throws -> ASAMessageFile - /// Returns a list of files associated with a message. - /// - Parameters: - /// - threadId: The ID of the thread that the message and files belong to. - /// - messageId: The ID of the message that the files belong to. - /// - parameters: Optional parameters for pagination and sorting. - /// - Returns: A list of message file objects. - func listFiles(by threadId: String, messageId: String, parameters: ASAListMessagesParameters?) async throws -> ASAMessageFilesListResponse } -public final class MessagesAPI: HTTPClient, IMessagesAPI { +public actor MessagesAPI: HTTPClient, IMessagesAPI { let urlSession: URLSession - public init(apiKey: String, - baseScheme: String = Constants.baseScheme, - baseHost: String = Constants.baseHost, - path: String = Constants.path, - urlSession: URLSession = .shared) { - Constants.apiKey = apiKey - Constants.baseScheme = baseScheme - Constants.baseHost = baseHost - Constants.path = path + public init( + config: AISwiftAssistConfig, + constants: AISwiftAssistConstants = .default, + urlSession: URLSession = .shared + ) { + Constants.config = config + Constants.constants = constants self.urlSession = urlSession } @@ -76,6 +59,11 @@ public final class MessagesAPI: HTTPClient, IMessagesAPI { self.urlSession = urlSession } + public func getMessages(by threadId: String, parameters: ASAListMessagesParameters?) async throws -> ASAMessagesListResponse { + let endpoint = MessagesEndpoint.listMessages(threadId, parameters) + return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAMessagesListResponse.self) + } + public func create(by threadId: String, createMessage: ASACreateMessageRequest) async throws -> ASAMessage { let endpoint = MessagesEndpoint.createMessage(threadId, createMessage) return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAMessage.self) @@ -91,18 +79,4 @@ public final class MessagesAPI: HTTPClient, IMessagesAPI { return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAMessage.self) } - public func getMessages(by threadId: String, parameters: ASAListMessagesParameters?) async throws -> ASAMessagesListResponse { - let endpoint = MessagesEndpoint.listMessages(threadId, parameters) - return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAMessagesListResponse.self) - } - - public func retrieveFile(by threadId: String, messageId: String, fileId: String) async throws -> ASAMessageFile { - let endpoint = MessagesEndpoint.retrieveFile(threadId, messageId, fileId) - return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAMessageFile.self) - } - - public func listFiles(by threadId: String, messageId: String, parameters: ASAListMessagesParameters?) async throws -> ASAMessageFilesListResponse { - let endpoint = MessagesEndpoint.listFiles(threadId, messageId, parameters) - return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAMessageFilesListResponse.self) - } } diff --git a/Sources/AISwiftAssist/APIs/ModelsAPI.swift b/Sources/AISwiftAssist/APIs/ModelsAPI.swift index 4cb0bbe..bf3e4aa 100644 --- a/Sources/AISwiftAssist/APIs/ModelsAPI.swift +++ b/Sources/AISwiftAssist/APIs/ModelsAPI.swift @@ -7,56 +7,54 @@ import Foundation -/// Describes an OpenAI model offering that can be used with the API. [Link for Models](https://platform.openai.com/docs/api-reference/models) -public protocol IModelsAPI: AnyObject { - - /// Lists the currently available models, and provides basic information about each one such as the owner and availability. - /// - Returns: A list of model objects. - func get() async throws -> ASAModelsListResponse - - /// Retrieves a model instance, providing basic information about the model such as the owner and permissioning. - /// - Parameter modelId: The ID of the model to use for this request - /// - Returns: The model object matching the specified ID. - func retrieve(by modelId: String) async throws -> ASAModel - - /// Delete a fine-tuned model. You must have the Owner role in your organization to delete a model. - /// - Parameter modelId: The model to delete - /// - Returns: Deletion status. - func delete(by modelId: String) async throws -> ASADeleteModelResponse -} - -public final class ModelsAPI: HTTPClient, IModelsAPI { - - let urlSession: URLSession - - public init(apiKey: String, - baseScheme: String = Constants.baseScheme, - baseHost: String = Constants.baseHost, - path: String = Constants.path, - urlSession: URLSession = .shared) { - Constants.apiKey = apiKey - Constants.baseScheme = baseScheme - Constants.baseHost = baseHost - Constants.path = path - self.urlSession = urlSession - } - - public init(urlSession: URLSession = .shared) { - self.urlSession = urlSession - } - - public func get() async throws -> ASAModelsListResponse { - let endpoint = ModelsEndpoint.getModels - return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAModelsListResponse.self) - } - - public func retrieve(by modelId: String) async throws -> ASAModel { - let endpoint = ModelsEndpoint.retrieveModel(modelId) - return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAModel.self) - } - - public func delete(by modelId: String) async throws -> ASADeleteModelResponse { - let endpoint = ModelsEndpoint.deleteModel(modelId) - return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASADeleteModelResponse.self) - } -} +///// Describes an OpenAI model offering that can be used with the API. [Link for Models](https://platform.openai.com/docs/api-reference/models) +//public protocol IModelsAPI: AnyObject, Sendable { +// +// /// Lists the currently available models, and provides basic information about each one such as the owner and availability. +// /// - Returns: A list of model objects. +// func get() async throws -> ASAModelsListResponse +// +// /// Retrieves a model instance, providing basic information about the model such as the owner and permissioning. +// /// - Parameter modelId: The ID of the model to use for this request +// /// - Returns: The model object matching the specified ID. +// func retrieve(by modelId: String) async throws -> ASAModel +// +// /// Delete a fine-tuned model. You must have the Owner role in your organization to delete a model. +// /// - Parameter modelId: The model to delete +// /// - Returns: Deletion status. +// func delete(by modelId: String) async throws -> ASADeleteModelResponse +//} +// +//public actor ModelsAPI: HTTPClient, IModelsAPI { +// +// let urlSession: URLSession +// +// public init( +// config: AISwiftAssistConfig, +// constants: AISwiftAssistConstants = .default, +// urlSession: URLSession = .shared +// ) { +// Constants.config = config +// Constants.constants = constants +// self.urlSession = urlSession +// } +// +// public init(urlSession: URLSession = .shared) { +// self.urlSession = urlSession +// } +// +// public func get() async throws -> ASAModelsListResponse { +// let endpoint = ModelsEndpoint.getModels +// return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAModelsListResponse.self) +// } +// +// public func retrieve(by modelId: String) async throws -> ASAModel { +// let endpoint = ModelsEndpoint.retrieveModel(modelId) +// return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAModel.self) +// } +// +// public func delete(by modelId: String) async throws -> ASADeleteModelResponse { +// let endpoint = ModelsEndpoint.deleteModel(modelId) +// return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASADeleteModelResponse.self) +// } +//} diff --git a/Sources/AISwiftAssist/APIs/RunsAPI.swift b/Sources/AISwiftAssist/APIs/RunsAPI.swift index 823dc53..5461494 100644 --- a/Sources/AISwiftAssist/APIs/RunsAPI.swift +++ b/Sources/AISwiftAssist/APIs/RunsAPI.swift @@ -8,7 +8,7 @@ import Foundation /// Represents an execution run on a thread. [Link for Runs](https://platform.openai.com/docs/api-reference/runs) -public protocol IRunsAPI: AnyObject { +public protocol IRunsAPI: AnyObject, Sendable { /// Create a run. /// - Parameters: @@ -77,19 +77,17 @@ public protocol IRunsAPI: AnyObject { func listRunSteps(by threadId: String, runId: String, parameters: ASAListRunStepsParameters?) async throws -> ASARunStepsListResponse } -public final class RunsAPI: HTTPClient, IRunsAPI { +public actor RunsAPI: HTTPClient, IRunsAPI { let urlSession: URLSession - public init(apiKey: String, - baseScheme: String = Constants.baseScheme, - baseHost: String = Constants.baseHost, - path: String = Constants.path, - urlSession: URLSession = .shared) { - Constants.apiKey = apiKey - Constants.baseScheme = baseScheme - Constants.baseHost = baseHost - Constants.path = path + public init( + config: AISwiftAssistConfig, + constants: AISwiftAssistConstants = .default, + urlSession: URLSession = .shared + ) { + Constants.config = config + Constants.constants = constants self.urlSession = urlSession } diff --git a/Sources/AISwiftAssist/APIs/ThreadsAPI.swift b/Sources/AISwiftAssist/APIs/ThreadsAPI.swift index 531f7c5..9b884e5 100644 --- a/Sources/AISwiftAssist/APIs/ThreadsAPI.swift +++ b/Sources/AISwiftAssist/APIs/ThreadsAPI.swift @@ -8,7 +8,7 @@ import Foundation /// Create threads that assistants can interact with. [Link for Threads](https://platform.openai.com/docs/api-reference/threads) -public protocol IThreadsAPI: AnyObject { +public protocol IThreadsAPI: AnyObject, Sendable { /// Create a thread. /// - Parameter createThreads: Object with parameters for creating a thread. @@ -33,19 +33,17 @@ public protocol IThreadsAPI: AnyObject { func delete(threadId: String) async throws -> ASADeleteModelResponse } -public final class ThreadsAPI: HTTPClient, IThreadsAPI { +public actor ThreadsAPI: HTTPClient, IThreadsAPI { let urlSession: URLSession - public init(apiKey: String, - baseScheme: String = Constants.baseScheme, - baseHost: String = Constants.baseHost, - path: String = Constants.path, - urlSession: URLSession = .shared) { - Constants.apiKey = apiKey - Constants.baseScheme = baseScheme - Constants.baseHost = baseHost - Constants.path = path + public init( + config: AISwiftAssistConfig, + constants: AISwiftAssistConstants = .default, + urlSession: URLSession = .shared + ) { + Constants.config = config + Constants.constants = constants self.urlSession = urlSession } diff --git a/Sources/AISwiftAssist/Base/Constants.swift b/Sources/AISwiftAssist/Base/Constants.swift index a4b4af5..9fab8ad 100644 --- a/Sources/AISwiftAssist/Base/Constants.swift +++ b/Sources/AISwiftAssist/Base/Constants.swift @@ -7,10 +7,7 @@ import Foundation -public enum Constants { - public static var baseScheme: String = "https" - public static var baseHost: String = "api.openai.com" - public static var path: String = "/v1/" - public static var apiKey: String = "" - public static var organizationId: String? +public enum Constants: Sendable { + nonisolated(unsafe) public static var config: AISwiftAssistConfig = .empty + nonisolated(unsafe) public static var constants: AISwiftAssistConstants = .default } diff --git a/Sources/AISwiftAssist/Base/HTTPClient.swift b/Sources/AISwiftAssist/Base/HTTPClient.swift index 6687cf5..c78c082 100644 --- a/Sources/AISwiftAssist/Base/HTTPClient.swift +++ b/Sources/AISwiftAssist/Base/HTTPClient.swift @@ -7,7 +7,7 @@ import Foundation -protocol HTTPClient: AnyObject { +protocol HTTPClient: AnyObject, Sendable { func sendRequest(session: URLSession, endpoint: any Endpoint, responseModel: T.Type) async throws -> T @@ -35,9 +35,9 @@ extension HTTPClient { request.httpMethod = endpoint.method.rawValue request.allHTTPHeaderFields = endpoint.header if request.allHTTPHeaderFields == nil { - request.allHTTPHeaderFields = ["Authorization": "Bearer \(Constants.apiKey)"] + request.allHTTPHeaderFields = ["Authorization": "Bearer \(Constants.config.apiKey)"] } else { - request.allHTTPHeaderFields?["Authorization"] = "Bearer \(Constants.apiKey)" + request.allHTTPHeaderFields?["Authorization"] = "Bearer \(Constants.config.apiKey)" } request.httpBody = endpoint.body?.data diff --git a/Sources/AISwiftAssist/Base/HTTPRequestError.swift b/Sources/AISwiftAssist/Base/HTTPRequestError.swift index 8a3b7b8..7c20439 100644 --- a/Sources/AISwiftAssist/Base/HTTPRequestError.swift +++ b/Sources/AISwiftAssist/Base/HTTPRequestError.swift @@ -7,13 +7,13 @@ import Foundation -public struct ValidatorErrorResponse: Codable { +public struct ValidatorErrorResponse: Codable, Sendable { public let code: Int public let desc: String } /// Types of HTTP Request Errors -public enum HTTPRequestError: Error { +public enum HTTPRequestError: Error, Sendable { /// Model decoding error case decode(String) /// URL validation error diff --git a/Sources/AISwiftAssist/Base/HTTPRequestMethods.swift b/Sources/AISwiftAssist/Base/HTTPRequestMethods.swift index 89d27a8..dae7043 100644 --- a/Sources/AISwiftAssist/Base/HTTPRequestMethods.swift +++ b/Sources/AISwiftAssist/Base/HTTPRequestMethods.swift @@ -7,7 +7,7 @@ import Foundation -enum HTTPRequestMethods: String { +enum HTTPRequestMethods: String, Sendable { case get = "GET" case post = "POST" case put = "PUT" diff --git a/Sources/AISwiftAssist/Client/AISwiftAssistClient.swift b/Sources/AISwiftAssist/Client/AISwiftAssistClient.swift index dc86b74..e1483be 100644 --- a/Sources/AISwiftAssist/Client/AISwiftAssistClient.swift +++ b/Sources/AISwiftAssist/Client/AISwiftAssistClient.swift @@ -7,52 +7,24 @@ import Foundation -public final class AISwiftAssistClient { - - public let assistantsApi: IAssistantsAPI - public let messagesApi: IMessagesAPI - public let modelsApi: IModelsAPI - public let runsApi: IRunsAPI - public let threadsApi: IThreadsAPI - - public init(config: AISwiftAssistConfig, - baseScheme: String = Constants.baseScheme, - baseHost: String = Constants.baseHost, - path: String = Constants.path) { - Constants.apiKey = config.apiKey - Constants.organizationId = config.organizationId - Constants.baseScheme = baseScheme - Constants.baseHost = baseHost - Constants.path = path - self.assistantsApi = AssistantsAPI(urlSession: .shared) - self.messagesApi = MessagesAPI(urlSession: .shared) - self.modelsApi = ModelsAPI(urlSession: .shared) - self.runsApi = RunsAPI(urlSession: .shared) - self.threadsApi = ThreadsAPI(urlSession: .shared) +public actor AISwiftAssistClient: Sendable { + + public nonisolated let assistantsApi: any IAssistantsAPI + public nonisolated let messagesApi: any IMessagesAPI + public nonisolated let runsApi: any IRunsAPI + public nonisolated let threadsApi: any IThreadsAPI + + public init( + config: AISwiftAssistConfig, + constants: AISwiftAssistConstants = .default, + urlSession: URLSession = .shared + ) { + Constants.config = config + Constants.constants = constants + self.assistantsApi = AssistantsAPI(urlSession: urlSession) + self.messagesApi = MessagesAPI(urlSession: urlSession) + self.runsApi = RunsAPI(urlSession: urlSession) + self.threadsApi = ThreadsAPI(urlSession: urlSession) } } - -extension AISwiftAssistClient { - /// Creates an assistant and thread based on the provided parameters. - public func createAssistantAndThread(with params: AssistantCreationParams) async throws -> AssistantAndThreadConfig { - let modelsResponse = try await modelsApi.get() - guard let model = modelsResponse.data.first(where: { $0.id == params.model.rawValue }) else { - throw NSError(domain: "AISwiftAssistClient", code: 0, userInfo: [NSLocalizedDescriptionKey: "Model not found"]) - } - - let createAssistantRequest = ASACreateAssistantRequest(asaModel: model, - name: params.name, - description: params.description, - instructions: params.instructions, - tools: params.tools, - fileIds: params.fileIds, - metadata: params.metadata) - let assistant = try await assistantsApi.create(by: createAssistantRequest) - - let threadRequest = ASACreateThreadRequest(messages: nil) - let thread = try await threadsApi.create(by: threadRequest) - - return AssistantAndThreadConfig(assistant: assistant, thread: thread) - } -} diff --git a/Sources/AISwiftAssist/Client/AISwiftAssistConfig.swift b/Sources/AISwiftAssist/Client/AISwiftAssistConfig.swift index 76dd776..c241dc6 100644 --- a/Sources/AISwiftAssist/Client/AISwiftAssistConfig.swift +++ b/Sources/AISwiftAssist/Client/AISwiftAssistConfig.swift @@ -7,15 +7,18 @@ import Foundation -public struct AISwiftAssistConfig { +public struct AISwiftAssistConfig: Sendable { public let apiKey: String public let organizationId: String? - public init(apiKey: String, - organizationId: String? = nil) { + public init( + apiKey: String, + organizationId: String? = nil + ) { self.apiKey = apiKey self.organizationId = organizationId } + public static let empty: AISwiftAssistConfig = .init(apiKey: "") } diff --git a/Sources/AISwiftAssist/Client/AISwiftAssistConstants.swift b/Sources/AISwiftAssist/Client/AISwiftAssistConstants.swift new file mode 100644 index 0000000..ec0a109 --- /dev/null +++ b/Sources/AISwiftAssist/Client/AISwiftAssistConstants.swift @@ -0,0 +1,34 @@ +// +// AISwiftAssistConfig 2.swift +// AISwiftAssist +// +// Created by Alexey on 3/6/25. +// + +public struct AISwiftAssistConstants: Sendable { + + public enum AssistantsVersion: String, Sendable { + case v1 = "assistants=v1" /// deprecated + case v2 = "assistants=v2" + } + + public let baseScheme: String + public let baseHost: String + public let path: String + /// The version of the Assistants API to use. + public let version: AssistantsVersion + + public init( + baseScheme: String = "https", + baseHost: String = "api.openai.com", + path: String = "/v1/", + version: AssistantsVersion = .v2 + ) { + self.baseScheme = baseScheme + self.baseHost = baseHost + self.path = path + self.version = version + } + + public static let `default`: AISwiftAssistConstants = .init() +} diff --git a/Sources/AISwiftAssist/Client/Models/ASAOpenAIModel.swift b/Sources/AISwiftAssist/Client/Models/ASAOpenAIModel.swift index f644a82..ea807e1 100644 --- a/Sources/AISwiftAssist/Client/Models/ASAOpenAIModel.swift +++ b/Sources/AISwiftAssist/Client/Models/ASAOpenAIModel.swift @@ -8,7 +8,7 @@ import Foundation /// Represents various models created and used by OpenAI and its partners. -public enum ASAOpenAIModel: String { +public enum ASAOpenAIModel: String, Sendable { /// Model "text-search-babbage-doc-001" created by openai-dev, a document search model based on the Babbage architecture. case textSearchBabbageDoc001 = "text-search-babbage-doc-001" @@ -186,5 +186,117 @@ public enum ASAOpenAIModel: String { /// Model "dall-e-3" created by system, an advanced version of the DALL-E image generation model. case dallE3 = "dall-e-3" -} + // MARK: - Additional Models + + /// Model "gpt-4.5-preview" created by system. + case gpt4_5Preview = "gpt-4.5-preview" + + /// Model "gpt-4.5-preview-2025-02-27" created by system. + case gpt4_5Preview2025_02_27 = "gpt-4.5-preview-2025-02-27" + + /// Model "gpt-4o-mini-audio-preview-2024-12-17" created by system. + case gpt4oMiniAudioPreview2024_12_17 = "gpt-4o-mini-audio-preview-2024-12-17" + + /// Model "gpt-4o-audio-preview-2024-10-01" created by system. + case gpt4oAudioPreview2024_10_01 = "gpt-4o-audio-preview-2024-10-01" + + /// Model "gpt-4o-audio-preview" created by system. + case gpt4oAudioPreview = "gpt-4o-audio-preview" + + /// Model "gpt-4o-mini-realtime-preview-2024-12-17" created by system. + case gpt4oMiniRealtimePreview2024_12_17 = "gpt-4o-mini-realtime-preview-2024-12-17" + + /// Model "gpt-4o-mini-realtime-preview" created by system. + case gpt4oMiniRealtimePreview = "gpt-4o-mini-realtime-preview" + + /// Model "o1-mini-2024-09-12" created by system. + case o1Mini2024_09_12 = "o1-mini-2024-09-12" + + /// Model "o1-mini" created by system. + case o1Mini = "o1-mini" + + /// Model "gpt-4o-mini-audio-preview" created by system. + case gpt4oMiniAudioPreview = "gpt-4o-mini-audio-preview" + + /// Model "whisper-1" created by openai-internal. + case whisper1 = "whisper-1" + + /// Model "omni-moderation-latest" created by system. + case omniModerationLatest = "omni-moderation-latest" + + /// Model "gpt-4o-2024-05-13" created by system. + case gpt4o2024_05_13 = "gpt-4o-2024-05-13" + + /// Model "omni-moderation-2024-09-26" created by system. + case omniModeration2024_09_26 = "omni-moderation-2024-09-26" + + /// Model "gpt-4o-realtime-preview-2024-10-01" created by system. + case gpt4oRealtimePreview2024_10_01 = "gpt-4o-realtime-preview-2024-10-01" + + /// Model "gpt-4o-2024-08-06" created by system. + case gpt4o2024_08_06 = "gpt-4o-2024-08-06" + + /// Model "chatgpt-4o-latest" created by system. + case chatgpt4oLatest = "chatgpt-4o-latest" + + /// Model "tts-1-hd-1106" created by system. + case tts1Hd1106 = "tts-1-hd-1106" + + /// Model "text-embedding-3-large" created by system. + case textEmbedding3Large = "text-embedding-3-large" + + /// Model "gpt-4o-audio-preview-2024-12-17" created by system. + case gpt4oAudioPreview2024_12_17 = "gpt-4o-audio-preview-2024-12-17" + + /// Model "gpt-4o" created by system. + case gpt4o = "gpt-4o" + + /// Model "o1" created by system. + case o1 = "o1" + + /// Model "gpt-4" created by openai. + case gpt4 = "gpt-4" + + /// Model "gpt-4o-2024-11-20" created by system. + case gpt4o2024_11_20 = "gpt-4o-2024-11-20" + + /// Model "o1-2024-12-17" created by system. + case o1_2024_12_17 = "o1-2024-12-17" + + /// Model "o1-preview" created by system. + case o1Preview = "o1-preview" + + /// Model "o1-preview-2024-09-12" created by system. + case o1Preview2024_09_12 = "o1-preview-2024-09-12" + + /// Model "gpt-4o-mini-2024-07-18" created by system. + case gpt4oMini2024_07_18 = "gpt-4o-mini-2024-07-18" + + /// Model "gpt-4o-mini" created by system. + case gpt4oMini = "gpt-4o-mini" + + /// Model "gpt-4-turbo" created by system. + case gpt4Turbo = "gpt-4-turbo" + + /// Model "o3-mini-2025-01-31" created by system. + case o3Mini2025_01_31 = "o3-mini-2025-01-31" + + /// Model "gpt-3.5-turbo-0125" created by system. + case gpt3_5Turbo0125 = "gpt-3.5-turbo-0125" + + /// Model "gpt-4o-realtime-preview-2024-12-17" created by system. + case gpt4oRealtimePreview2024_12_17 = "gpt-4o-realtime-preview-2024-12-17" + + /// Model "text-embedding-3-small" created by system. + case textEmbedding3Small = "text-embedding-3-small" + + /// Model "gpt-4-0125-preview" created by system. + case gpt4_0125Preview = "gpt-4-0125-preview" + + /// Model "gpt-4-turbo-preview" created by system. + case gpt4TurboPreview = "gpt-4-turbo-preview" + + /// Model "o3-mini" created by system. + case o3Mini = "o3-mini" +} diff --git a/Sources/AISwiftAssist/Client/Models/AssistantAndThreadConfig.swift b/Sources/AISwiftAssist/Client/Models/AssistantAndThreadConfig.swift index a948f79..f51111f 100644 --- a/Sources/AISwiftAssist/Client/Models/AssistantAndThreadConfig.swift +++ b/Sources/AISwiftAssist/Client/Models/AssistantAndThreadConfig.swift @@ -7,7 +7,7 @@ import Foundation -public struct AssistantAndThreadConfig { +public struct AssistantAndThreadConfig: Sendable { public let assistant: ASAAssistant public let thread: ASAThread } diff --git a/Sources/AISwiftAssist/Client/Models/AssistantCreationParams.swift b/Sources/AISwiftAssist/Client/Models/AssistantCreationParams.swift index e81a0ed..32bf9e1 100644 --- a/Sources/AISwiftAssist/Client/Models/AssistantCreationParams.swift +++ b/Sources/AISwiftAssist/Client/Models/AssistantCreationParams.swift @@ -7,23 +7,31 @@ import Foundation -public struct AssistantCreationParams { - - public let model: ASAOpenAIModel - public let name: String - public let description: String - public let instructions: String - public let tools: [ASACreateAssistantRequest.Tool]? - public let fileIds: [String]? - public let metadata: [String: String]? - - public init(model: ASAOpenAIModel, name: String, description: String, instructions: String, tools: [ASACreateAssistantRequest.Tool]? = nil, fileIds: [String]? = nil, metadata: [String : String]? = nil) { - self.model = model - self.name = name - self.description = description - self.instructions = instructions - self.tools = tools - self.fileIds = fileIds - self.metadata = metadata - } -} +//public struct AssistantCreationParams: Sendable { +// +// public let model: ASAOpenAIModel +// public let name: String +// public let description: String +// public let instructions: String +// public let tools: [ASACreateAssistantRequest.Tool]? +// public let fileIds: [String]? +// public let metadata: [String: String]? +// +// public init( +// model: ASAOpenAIModel, +// name: String, +// description: String, +// instructions: String, +// tools: [ASACreateAssistantRequest.Tool]? = nil, +// fileIds: [String]? = nil, +// metadata: [String : String]? = nil +// ) { +// self.model = model +// self.name = name +// self.description = description +// self.instructions = instructions +// self.tools = tools +// self.fileIds = fileIds +// self.metadata = metadata +// } +//} diff --git a/Sources/AISwiftAssist/Endpoints/AssistantEndpoint.swift b/Sources/AISwiftAssist/Endpoints/AssistantEndpoint.swift index 330171c..1ae6999 100644 --- a/Sources/AISwiftAssist/Endpoints/AssistantEndpoint.swift +++ b/Sources/AISwiftAssist/Endpoints/AssistantEndpoint.swift @@ -13,26 +13,21 @@ enum AssistantEndpoint { case retrieveAssistant(String) case modifyAssistant(String, ASAModifyAssistantRequest) case deleteAssistant(String) - case createFile(String, ASACreateAssistantFileRequest) - case retrieveFile(String, String) - case deleteFile(String, String) - case listFiles(String, ASAListAssistantsParameters?) } extension AssistantEndpoint: CustomEndpoint { public var url: URL? { var urlComponents: URLComponents = .default urlComponents.queryItems = queryItems - urlComponents.path = Constants.path + path + urlComponents.path = Constants.constants.path + path return urlComponents.url } public var queryItems: [URLQueryItem]? { var items: [URLQueryItem]? switch self { - case .createAssistant, .deleteAssistant, .retrieveAssistant, .modifyAssistant, .createFile, .retrieveFile, .deleteFile: items = nil + case .createAssistant, .deleteAssistant, .retrieveAssistant, .modifyAssistant: items = nil case .getAssistants(let params): items = Utils.createURLQueryItems(from: params) - case .listFiles(_, let params): items = Utils.createURLQueryItems(from: params) } return items } @@ -43,10 +38,7 @@ extension AssistantEndpoint: CustomEndpoint { case .retrieveAssistant(let assistantId): return "assistants/\(assistantId)" case .modifyAssistant(let assistantId, _): return "assistants/\(assistantId)" case .deleteAssistant(let assistantId): return "assistants/\(assistantId)" - case .createFile(let assistantId, _): return "assistants/\(assistantId)/files" - case .retrieveFile(let assistantId, let fileId): return "assistants/\(assistantId)/files/\(fileId)" - case .deleteFile(let assistantId, let fileId): return "assistants/\(assistantId)/files/\(fileId)" - case .listFiles(let assistantId, _): return "assistants/\(assistantId)/files" + } } @@ -57,15 +49,11 @@ extension AssistantEndpoint: CustomEndpoint { case .retrieveAssistant: return .get case .modifyAssistant: return .post case .deleteAssistant: return .delete - case .createFile: return .post - case .retrieveFile: return .get - case .deleteFile: return .delete - case .listFiles: return .get } } public var header: [String : String]? { - let headers: [String: String] = ["OpenAI-Beta": "assistants=v1", + var headers: [String: String] = ["OpenAI-Beta": Constants.constants.version.rawValue, "Content-Type": "application/json"] return headers } @@ -74,8 +62,7 @@ extension AssistantEndpoint: CustomEndpoint { switch self { case .createAssistant(let createAssistant): return .init(object: createAssistant) case .modifyAssistant(_, let request): return .init(object: request) - case .createFile(_, let request): return .init(object: request) - case .deleteAssistant, .retrieveAssistant, .getAssistants, .retrieveFile, .deleteFile, .listFiles: return nil + case .deleteAssistant, .retrieveAssistant, .getAssistants: return nil } } } diff --git a/Sources/AISwiftAssist/Endpoints/MessagesEndpoint.swift b/Sources/AISwiftAssist/Endpoints/MessagesEndpoint.swift index 11c446e..e6daed6 100644 --- a/Sources/AISwiftAssist/Endpoints/MessagesEndpoint.swift +++ b/Sources/AISwiftAssist/Endpoints/MessagesEndpoint.swift @@ -20,7 +20,7 @@ extension MessagesEndpoint: CustomEndpoint { public var url: URL? { var urlComponents: URLComponents = .default urlComponents.queryItems = queryItems - urlComponents.path = Constants.path + path + urlComponents.path = Constants.constants.path + path return urlComponents.url } @@ -64,7 +64,7 @@ extension MessagesEndpoint: CustomEndpoint { } public var header: [String : String]? { - let headers: [String: String] = ["OpenAI-Beta": "assistants=v1", + let headers: [String: String] = ["OpenAI-Beta": Constants.constants.version.rawValue, "Content-Type": "application/json"] return headers } diff --git a/Sources/AISwiftAssist/Endpoints/ModelsEndpoint.swift b/Sources/AISwiftAssist/Endpoints/ModelsEndpoint.swift index 51d1e98..d84f484 100644 --- a/Sources/AISwiftAssist/Endpoints/ModelsEndpoint.swift +++ b/Sources/AISwiftAssist/Endpoints/ModelsEndpoint.swift @@ -17,7 +17,7 @@ extension ModelsEndpoint: CustomEndpoint { public var url: URL? { var urlComponents: URLComponents = .default urlComponents.queryItems = queryItems - urlComponents.path = Constants.path + path + urlComponents.path = Constants.constants.path + path return urlComponents.url } diff --git a/Sources/AISwiftAssist/Endpoints/RunsEndpoint.swift b/Sources/AISwiftAssist/Endpoints/RunsEndpoint.swift index 04a55c3..dca1b38 100644 --- a/Sources/AISwiftAssist/Endpoints/RunsEndpoint.swift +++ b/Sources/AISwiftAssist/Endpoints/RunsEndpoint.swift @@ -24,7 +24,7 @@ extension RunsEndpoint: CustomEndpoint { public var url: URL? { var urlComponents: URLComponents = .default urlComponents.queryItems = queryItems - urlComponents.path = Constants.path + path + urlComponents.path = Constants.constants.path + path return urlComponents.url } @@ -85,7 +85,7 @@ extension RunsEndpoint: CustomEndpoint { } public var header: [String : String]? { - let headers: [String: String] = ["OpenAI-Beta": "assistants=v1", + let headers: [String: String] = ["OpenAI-Beta": Constants.constants.version.rawValue, "Content-Type": "application/json"] return headers } diff --git a/Sources/AISwiftAssist/Endpoints/ThreadsEndpoint.swift b/Sources/AISwiftAssist/Endpoints/ThreadsEndpoint.swift index 0886a1c..d109f46 100644 --- a/Sources/AISwiftAssist/Endpoints/ThreadsEndpoint.swift +++ b/Sources/AISwiftAssist/Endpoints/ThreadsEndpoint.swift @@ -19,7 +19,7 @@ extension ThreadsEndpoint: CustomEndpoint { public var url: URL? { var urlComponents: URLComponents = .default urlComponents.queryItems = queryItems - urlComponents.path = Constants.path + path + urlComponents.path = Constants.constants.path + path return urlComponents.url } @@ -50,7 +50,7 @@ extension ThreadsEndpoint: CustomEndpoint { } public var header: [String : String]? { - let headers: [String: String] = ["OpenAI-Beta": "assistants=v1", + let headers: [String: String] = ["OpenAI-Beta": Constants.constants.version.rawValue, "Content-Type": "application/json"] return headers } diff --git a/Sources/AISwiftAssist/Extensions/URLComponents.swift b/Sources/AISwiftAssist/Extensions/URLComponents.swift index 4e885cb..0e404ad 100644 --- a/Sources/AISwiftAssist/Extensions/URLComponents.swift +++ b/Sources/AISwiftAssist/Extensions/URLComponents.swift @@ -10,8 +10,8 @@ import Foundation extension URLComponents { static var `default`: Self { var components: Self = .init() - components.scheme = Constants.baseScheme - components.host = Constants.baseHost + components.scheme = Constants.constants.baseScheme + components.host = Constants.constants.baseHost return components } diff --git a/Sources/AISwiftAssist/Models/AnyCodable.swift b/Sources/AISwiftAssist/Models/AnyCodable.swift new file mode 100644 index 0000000..6ff5b0c --- /dev/null +++ b/Sources/AISwiftAssist/Models/AnyCodable.swift @@ -0,0 +1,67 @@ +// +// AnyCodable.swift +// AISwiftAssist +// +// Created by Alexey on 3/7/25. +// + + +import Foundation + +public struct AnyCodable: Codable, @unchecked Sendable { + public let value: Any + + public init(_ value: Any) { + self.value = value + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let int = try? container.decode(Int.self) { + self.value = int + } else if let double = try? container.decode(Double.self) { + self.value = double + } else if let bool = try? container.decode(Bool.self) { + self.value = bool + } else if let string = try? container.decode(String.self) { + self.value = string + } else if container.decodeNil() { + self.value = () + } else if let array = try? container.decode([AnyCodable].self) { + self.value = array + } else if let dict = try? container.decode([String: AnyCodable].self) { + self.value = dict + } else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Unsupported type" + ) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + switch value { + case let int as Int: + try container.encode(int) + case let double as Double: + try container.encode(double) + case let bool as Bool: + try container.encode(bool) + case let string as String: + try container.encode(string) + case Optional.none: + try container.encodeNil() + case let array as [Any]: + try container.encode(array.map { AnyCodable($0) }) + case let dict as [String: Any]: + try container.encode(dict.mapValues { AnyCodable($0) }) + default: + throw EncodingError.invalidValue( + value, + EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Unsupported type") + ) + } + } +} diff --git a/Sources/AISwiftAssist/Models/Main/ASAAssistant.swift b/Sources/AISwiftAssist/Models/Main/ASAAssistant.swift index 9d49b5a..ee34bca 100644 --- a/Sources/AISwiftAssist/Models/Main/ASAAssistant.swift +++ b/Sources/AISwiftAssist/Models/Main/ASAAssistant.swift @@ -7,54 +7,339 @@ import Foundation -/// Represents an assistant that can call the model and use tools. -public struct ASAAssistant: Codable { - /// The identifier of the assistant, which can be referenced in API endpoints. +/// Represents an assistant capable of calling the model and utilizing tools. +public struct ASAAssistant: Codable, Sendable { + + /// Unique identifier of the assistant (used in API endpoints). public let id: String - /// The object type, which is always 'assistant'. + /// Object type, always "assistant". public let objectType: String - /// The Unix timestamp (in seconds) for when the assistant was created. + /// Unix timestamp of assistant creation (in seconds). public let createdAt: Int - /// Optional: The name of the assistant. The maximum length is 256 characters. + /// Optional: Assistant's name (max 256 characters). public let name: String? - /// Optional: The description of the assistant. The maximum length is 512 characters. + /// Optional: Description of the assistant (max 512 characters). public let description: String? - /// ID of the model to use. You can use the List models API to see all of your available models. + /// ID of the model used. public let model: String - /// Optional: The system instructions that the assistant uses. The maximum length is 32768 characters. + /// Optional: System instructions used by the assistant (max 256000 characters). public let instructions: String? - /// A list of tools enabled on the assistant. There can be a maximum of 128 tools per assistant. - /// Tools can be of types code_interpreter, retrieval, or function. + /// Tools available to the assistant (max 128). public let tools: [Tool] - /// A list of file IDs attached to this assistant. There can be a maximum of 20 files attached to the assistant. - /// Files are ordered by their creation date in ascending order. - public let fileIds: [String] + /// Optional: Resources utilized by the assistant's tools. + public let toolResources: ToolResources? - /// Optional: Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information - /// about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. + /// Optional: Sampling temperature for output generation (range 0–2, default is 1). + public let temperature: Double? + + /// Optional: Nucleus sampling parameter (range 0–1, default is 1). + public let topP: Double? + + /// Optional: Format of the response generated by the model (e.g., auto, text, json_object). + /// Using this can enforce specific output formats like JSON. + public let responseFormat: ResponseFormat? + + /// Optional: Metadata (up to 16 key-value pairs). public let metadata: [String: String]? - public enum CodingKeys: String, CodingKey { - case id - case objectType = "object" - case createdAt = "created_at" - case name, description, model, instructions, tools - case fileIds = "file_ids" - case metadata + enum CodingKeys: String, CodingKey { + case id, objectType = "object", createdAt = "created_at" + case name, description, model, instructions, tools, metadata, temperature, topP = "top_p", responseFormat = "response_format", toolResources = "tool_resources" } - /// Represents a tool enabled on the assistant. - public struct Tool: Codable { - /// The type of the tool (e.g., code_interpreter, retrieval, function). - public let type: String + public init( + id: String, + objectType: String, + createdAt: Int, + name: String?, + description: String?, + model: String, + instructions: String?, + tools: [Tool], + toolResources: ToolResources?, + temperature: Double?, + topP: Double?, + responseFormat: ResponseFormat?, + metadata: [String : String]? + ) { + self.id = id + self.objectType = objectType + self.createdAt = createdAt + self.name = name + self.description = description + self.model = model + self.instructions = instructions + self.tools = tools + self.toolResources = toolResources + self.temperature = temperature + self.topP = topP + self.responseFormat = responseFormat + self.metadata = metadata + } + + /// Tool available to the assistant. + public struct Tool: Codable, Sendable { + public let type: ToolType + public let function: FunctionTool? + public let fileSearch: FileSearchTool? + + enum CodingKeys: String, CodingKey { + case type, function, fileSearch = "file_search" + } + + public struct FunctionTool: Codable, Sendable { + public let name: String + public let description: String? + public let parameters: [String: AnyCodable]? + public let strict: Bool? + } + + public struct FileSearchTool: Codable, Sendable { + public let maxNumResults: Int? + public let rankingOptions: RankingOptions? + + enum CodingKeys: String, CodingKey { + case maxNumResults = "max_num_results" + case rankingOptions = "ranking_options" + } + + public struct RankingOptions: Codable, Sendable { + public let ranker: String? + public let scoreThreshold: Double? + + enum CodingKeys: String, CodingKey { + case ranker, scoreThreshold = "score_threshold" + } + } + } + } + + /// Supported types of tools. + public enum ToolType: String, Codable, Sendable { + case codeInterpreter = "code_interpreter" + case fileSearch = "file_search" + case function + } + + /// Resources used by the assistant's tools. + public struct ToolResources: Codable, Sendable { + public let codeInterpreter: CodeInterpreterResources? + public let fileSearch: FileSearchResources? + + enum CodingKeys: String, CodingKey { + case codeInterpreter = "code_interpreter" + case fileSearch = "file_search" + } + } + + /// Resources for the code interpreter tool. + public struct CodeInterpreterResources: Codable, Sendable { + public let fileIds: [String] + + enum CodingKeys: String, CodingKey { + case fileIds = "file_ids" + } + } + + /// Resources for the file search tool. + public struct FileSearchResources: Codable, Sendable { + public let vectorStoreIds: [String]? + public let vectorStores: [VectorStore]? + + enum CodingKeys: String, CodingKey { + case vectorStoreIds = "vector_store_ids" + case vectorStores = "vector_stores" + } + + /// Vector store for file searches. + public struct VectorStore: Codable, Sendable { + public let fileIds: [String]? + public let chunkingStrategy: ChunkingStrategy? + + enum CodingKeys: String, CodingKey { + case fileIds = "file_ids" + case chunkingStrategy = "chunking_strategy" + } + + /// Strategy for chunking files. + public struct ChunkingStrategy: Codable, Sendable { + public let type: ChunkingType + public let staticChunking: StaticChunking? + + public enum ChunkingType: String, Codable, Sendable { + case auto + case `static` + } + + enum CodingKeys: String, CodingKey { + case type + case staticChunking = "static" + } + + /// Parameters for static file chunking. + public struct StaticChunking: Codable, Sendable { + public let maxChunkSizeTokens: Int + public let chunkOverlapTokens: Int + + enum CodingKeys: String, CodingKey { + case maxChunkSizeTokens = "max_chunk_size_tokens" + case chunkOverlapTokens = "chunk_overlap_tokens" + } + } + } + } + } + + public struct JSONSchema: Codable, Sendable { + public let name: String + public let description: String? + public let schema: [String: AnyCodable]? + public let strict: Bool? + } + + public enum ResponseFormat: Codable, Sendable { + case auto + case text + case jsonObject + case jsonSchema(JSONSchema) + + enum CodingKeys: String, CodingKey { + case type, jsonSchema = "json_schema" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + switch type { + case "auto": self = .auto + case "text": self = .text + case "json_object": self = .jsonObject + case "json_schema": + let schema = try container.decode(JSONSchema.self, forKey: .jsonSchema) + self = .jsonSchema(schema) + default: + self = .auto + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .auto: try container.encode("auto", forKey: .type) + case .text: try container.encode("text", forKey: .type) + case .jsonObject: try container.encode("json_object", forKey: .type) + case .jsonSchema(let schema): + try container.encode("json_schema", forKey: .type) + try container.encode(schema, forKey: .jsonSchema) + } + } } } +extension ASAAssistant { + // MARK: - Mock Data + + static let assistantMinimal: Self = .init( + id: "asst_minimal_001", + objectType: "assistant", + createdAt: Int(Date().timeIntervalSince1970), + name: nil, + description: nil, + model: "gpt-4", + instructions: nil, + tools: [], + toolResources: nil, + temperature: nil, + topP: nil, + responseFormat: nil, + metadata: nil + ) + + /// Средняя конфигурация с основными полями. + static let assistantMedium: Self = .init( + id: "asst_medium_001", + objectType: "assistant", + createdAt: Int(Date().timeIntervalSince1970), + name: "Medium Assistant", + description: "Assistant with moderate complexity", + model: "gpt-4-turbo", + instructions: "Answer general queries.", + tools: [ + Tool(type: .codeInterpreter, function: nil, fileSearch: nil) + ], + toolResources: ToolResources( + codeInterpreter: CodeInterpreterResources(fileIds: ["file_123"]), + fileSearch: nil + ), + temperature: 0.7, + topP: 0.9, + responseFormat: .auto, + metadata: ["env": "staging"] + ) + + /// Полная конфигурация с максимальным набором полей. + static let assistantFull: Self = .init( + id: "asst_full_001", + objectType: "assistant", + createdAt: Int(Date().timeIntervalSince1970), + name: "Full Assistant", + description: "Fully configured assistant for advanced tasks", + model: "gpt-4o", + instructions: "You are a comprehensive assistant handling complex tasks and queries.", + tools: [ + Tool( + type: .function, + function: Tool.FunctionTool( + name: "calculate", + description: "Performs basic arithmetic calculations.", + parameters: ["operation": AnyCodable("add"), "values": AnyCodable([1, 2])], + strict: true + ), + fileSearch: nil + ), + Tool( + type: .fileSearch, + function: nil, + fileSearch: Tool.FileSearchTool( + maxNumResults: 10, + rankingOptions: Tool.FileSearchTool.RankingOptions( + ranker: "auto", + scoreThreshold: 0.8 + ) + ) + ), + Tool( + type: .codeInterpreter, + function: nil, + fileSearch: nil + ) + ], + toolResources: ToolResources( + codeInterpreter: CodeInterpreterResources( + fileIds: ["file_456", "file_789"] + ), + fileSearch: FileSearchResources( + vectorStoreIds: ["vector_store_1"], + vectorStores: nil + ) + ), + temperature: 0.5, + topP: 0.85, + responseFormat: .jsonObject, + metadata: ["version": "1.0", "env": "production"] + ) + + static let mocks: [Self] = [ + assistantMinimal, + assistantMedium, + assistantFull + ] +} diff --git a/Sources/AISwiftAssist/Models/Main/ASAAssistantFile.swift b/Sources/AISwiftAssist/Models/Main/ASAAssistantFile.swift deleted file mode 100644 index 822f8b3..0000000 --- a/Sources/AISwiftAssist/Models/Main/ASAAssistantFile.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 12/5/23. -// - -import Foundation - -/// Represents an assistant file that can be used by the assistant. -public struct ASAAssistantFile: Codable { - /// The identifier of the assistant file. - public let id: String - - /// The object type, which is always 'assistant.file'. - public let objectType: String - - /// The Unix timestamp (in seconds) for when the assistant file was created. - public let createdAt: Int - - /// The identifier of the assistant to which this file belongs. - public let assistantId: String - - enum CodingKeys: String, CodingKey { - case id, objectType = "object", createdAt = "created_at", assistantId = "assistant_id" - } -} diff --git a/Sources/AISwiftAssist/Models/Main/ASAFile.swift b/Sources/AISwiftAssist/Models/Main/ASAFile.swift deleted file mode 100644 index 35fa7be..0000000 --- a/Sources/AISwiftAssist/Models/Main/ASAFile.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 11/15/23. -// - -import Foundation - -/// Represents a document that has been uploaded to OpenAI. -public struct ASAFile: Codable { - /// The file identifier, which can be referenced in the API endpoints. - public let id: String - - /// The size of the file, in bytes. - public let bytes: Int - - /// The Unix timestamp (in seconds) for when the file was created. - public let createdAt: Int - - /// The name of the file. - public let filename: String - - /// The object type, which is always 'file'. - public let object: String - - /// The intended purpose of the file. Supported values are 'fine-tune', 'fine-tune-results', 'assistants', and 'assistants_output'. - public let purpose: String - - enum CodingKeys: String, CodingKey { - case id, bytes - case createdAt = "created_at" - case filename, object, purpose - } -} diff --git a/Sources/AISwiftAssist/Models/Main/ASAMessage.swift b/Sources/AISwiftAssist/Models/Main/ASAMessage.swift new file mode 100644 index 0000000..2e78111 --- /dev/null +++ b/Sources/AISwiftAssist/Models/Main/ASAMessage.swift @@ -0,0 +1,203 @@ +// +// File.swift +// +// +// Created by Alexey on 11/15/23. +// + +import Foundation + +/// Represents a message within a thread. +public struct ASAMessage: Codable, Sendable { + /// Unique identifier of the message. + public let id: String + + /// Object type, always "thread.message". + public let object: String + + /// Unix timestamp (in seconds) when the message was created. + public let createdAt: Int + + /// ID of the thread this message belongs to. + public let threadId: String + + /// The status of the message (in_progress, incomplete, completed). + public let status: Status + + /// Details about why the message is incomplete (if applicable). + public let incompleteDetails: IncompleteDetails? + + /// Unix timestamp (in seconds) when the message was completed. + public let completedAt: Int? + + /// Unix timestamp (in seconds) when the message was marked incomplete. + public let incompleteAt: Int? + + /// Role of the entity that produced the message (user or assistant). + public let role: Role + + /// Content of the message (array of text and/or images). + public let content: [Content] + + /// ID of the assistant that authored this message (if applicable). + public let assistantId: String? + + /// ID of the run associated with the creation of this message. + public let runId: String? + + /// Attachments (files attached to the message and their tools). + public let attachments: [Attachment]? + + /// Metadata (max 16 key-value pairs). + public let metadata: [String: String]? + + enum CodingKeys: String, CodingKey { + case id, object, status, role, content, attachments, metadata + case createdAt = "created_at" + case threadId = "thread_id" + case incompleteDetails = "incomplete_details" + case completedAt = "completed_at" + case incompleteAt = "incomplete_at" + case assistantId = "assistant_id" + case runId = "run_id" + } + + /// Status of the message. + public enum Status: String, Codable, Sendable { + case inProgress = "in_progress" + case incomplete + case completed + } + + /// Details about why the message is incomplete. + public struct IncompleteDetails: Codable, Sendable { + /// Reason the message is incomplete. + public let reason: String + } + + /// Role type of the message sender. + public enum Role: String, Codable, Sendable { + case user + case assistant + } + + /// Content of the message. + public enum Content: Codable, Sendable { + case text(TextContent) + case imageFile(ImageFileContent) + case imageUrl(ImageURLContent) + + enum CodingKeys: String, CodingKey { + case type, text, imageFile = "image_file", imageUrl = "image_url" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + + switch type { + case "text": + self = .text(try container.decode(TextContent.self, forKey: .text)) + case "image_file": + self = .imageFile(try container.decode(ImageFileContent.self, forKey: .imageFile)) + case "image_url": + self = .imageUrl(try container.decode(ImageURLContent.self, forKey: .imageUrl)) + default: + throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown content type") + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .text(let textContent): + try container.encode("text", forKey: .type) + try container.encode(textContent, forKey: .text) + case .imageFile(let imageFileContent): + try container.encode("image_file", forKey: .type) + try container.encode(imageFileContent, forKey: .imageFile) + case .imageUrl(let imageUrlContent): + try container.encode("image_url", forKey: .type) + try container.encode(imageUrlContent, forKey: .imageUrl) + } + } + } + + /// Represents text content. + public struct TextContent: Codable, Sendable { + public let value: String + public let annotations: [Annotation]? + } + + /// Represents an image file content. + public struct ImageFileContent: Codable, Sendable { + public let fileId: String + public let detail: String? + + enum CodingKeys: String, CodingKey { + case fileId = "file_id" + case detail + } + } + + /// Represents an image URL content. + public struct ImageURLContent: Codable, Sendable { + public let url: URL + public let detail: String? + } + + /// Annotation details within text content. + public struct Annotation: Codable, Sendable { + public let type: String + public let text: String + public let startIndex: Int + public let endIndex: Int + public let fileCitation: FileCitation? + public let filePath: FilePath? + + enum CodingKeys: String, CodingKey { + case type, text + case startIndex = "start_index" + case endIndex = "end_index" + case fileCitation = "file_citation" + case filePath = "file_path" + } + } + + /// Citation pointing to a specific file quote. + public struct FileCitation: Codable, Sendable { + public let fileId: String + public let quote: String + + enum CodingKeys: String, CodingKey { + case fileId = "file_id" + case quote + } + } + + /// URL for the file generated by the assistant. + public struct FilePath: Codable, Sendable { + public let fileId: String + + enum CodingKeys: String, CodingKey { + case fileId = "file_id" + } + } + + /// Represents a file attachment. + public struct Attachment: Codable, Sendable { + public let fileId: String + public let tools: [ToolType] + + enum CodingKeys: String, CodingKey { + case fileId = "file_id" + case tools + } + } + + /// Type of tool associated with an attachment. + public enum ToolType: String, Codable, Sendable { + case codeInterpreter = "code_interpreter" + case fileSearch = "file_search" + } +} diff --git a/Sources/AISwiftAssist/Models/Main/ASAMessageFile.swift b/Sources/AISwiftAssist/Models/Main/ASAMessageFile.swift deleted file mode 100644 index dd4063f..0000000 --- a/Sources/AISwiftAssist/Models/Main/ASAMessageFile.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 12/5/23. -// - -import Foundation - -/// Represents a file attached to a message. -public struct ASAMessageFile: Codable { - /// The identifier of the file, which can be referenced in API endpoints. - public let id: String - - /// The object type, which is always 'thread.message.file'. - public let object: String - - /// The Unix timestamp (in seconds) for when the message file was created. - public let createdAt: Int - - /// The ID of the message that the file is attached to. - public let messageId: String - - enum CodingKeys: String, CodingKey { - case id, object - case createdAt = "created_at" - case messageId = "message_id" - } -} diff --git a/Sources/AISwiftAssist/Models/Main/ASAModel.swift b/Sources/AISwiftAssist/Models/Main/ASAModel.swift deleted file mode 100644 index cc90f7f..0000000 --- a/Sources/AISwiftAssist/Models/Main/ASAModel.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 11/15/23. -// - -import Foundation - -public struct ASAModel: Codable { - // The model identifier, which can be referenced in the API endpoints. - public let id: String - - // The Unix timestamp (in seconds) when the model was created. - public let created: Int - - // The object type, which is always "model". - public let object: String - - // The organization that owns the model. - public let ownedBy: String - - enum CodingKeys: String, CodingKey { - case id, created, object - case ownedBy = "owned_by" - } -} diff --git a/Sources/AISwiftAssist/Models/Main/ASARun.swift b/Sources/AISwiftAssist/Models/Main/ASARun.swift index ead7e5a..2e56145 100644 --- a/Sources/AISwiftAssist/Models/Main/ASARun.swift +++ b/Sources/AISwiftAssist/Models/Main/ASARun.swift @@ -8,131 +8,270 @@ import Foundation /// Represents an execution run on a thread. -public struct ASARun: Codable { - /// The identifier of the run, which can be referenced in API endpoints. +public struct ASARun: Codable, Sendable { + + /// The identifier, which can be referenced in API endpoints. public let id: String - /// The object type, which is always 'thread.run'. + /// The object type, always `thread.run`. public let object: String - /// The Unix timestamp (in seconds) for when the run was created. + /// The Unix timestamp (in seconds) when the run was created. public let createdAt: Int - /// The ID of the thread that was executed on as a part of this run. + /// The ID of the thread that was executed as part of this run. public let threadId: String /// The ID of the assistant used for execution of this run. public let assistantId: String - /// The status of the run, which can be either queued, in_progress, requires_action, cancelling, cancelled, failed, completed, or expired. - public let status: String + /// The status of the run. + public let status: Status - /// Details on the action required to continue the run. Will be null if no action is required. + /// Details on the action required to continue the run. Null if no action is required. public let requiredAction: RequiredAction? - /// The last error associated with this run. Will be null if there are no errors. + /// The last error associated with this run. Null if there are no errors. public let lastError: LastError? - /// The Unix timestamp (in seconds) for when the run will expire. + /// The Unix timestamp (in seconds) when the run will expire. public let expiresAt: Int? - /// The Unix timestamp (in seconds) for when the run was started. Null if not started. + /// The Unix timestamp (in seconds) when the run started. public let startedAt: Int? - /// The Unix timestamp (in seconds) for when the run was cancelled. Null if not cancelled. + /// The Unix timestamp (in seconds) when the run was cancelled. public let cancelledAt: Int? - /// The Unix timestamp (in seconds) for when the run failed. Null if not failed. + /// The Unix timestamp (in seconds) when the run failed. public let failedAt: Int? - /// The Unix timestamp (in seconds) for when the run was completed. Null if not completed. + /// The Unix timestamp (in seconds) when the run was completed. public let completedAt: Int? - /// The model that the assistant used for this run. + /// The model used by the assistant for this run. public let model: String - /// The instructions that the assistant used for this run. + /// Instructions used by the assistant for this run. public let instructions: String? - /// The list of tools that the assistant used for this run. - /// Tools can be of types code_interpreter, retrieval, or function. + /// Tools used by the assistant for this run. public let tools: [Tool] - /// The list of File IDs the assistant used for this run. - public let fileIds: [String] - - /// Set of 16 key-value pairs that can be attached to the run. Useful for storing additional information. + /// Set of key-value pairs with additional information about the object. public let metadata: [String: String]? - /// Represents the required action details for the run to continue. - public struct RequiredAction: Codable { - /// For now, this is always 'submit_tool_outputs'. - public let type: String + /// Usage statistics related to the run. Null if the run is not in a terminal state. + public let usage: Usage? + + /// The sampling temperature used for this run. Defaults to 1 if not set. + public let temperature: Double? + + /// The nucleus sampling value used for this run. Defaults to 1 if not set. + public let topP: Double? + + /// Maximum number of completion tokens allowed for this run. + public let maxCompletionTokens: Int? + + /// Maximum number of prompt tokens allowed for this run. + public let maxPromptTokens: Int? + + /// Controls how a thread will be truncated prior to the run. + public let truncationStrategy: TruncationStrategy? + + /// Specifies the format that the model must output. + public let responseFormat: ResponseFormat? + + /// Controls which (if any) tool is called by the model. + public let toolChoice: ToolChoice? - /// Details on the tool outputs needed for this run to continue. + /// Enables parallel function calling during tool use. + public let parallelToolCalls: Bool? + + /// Details on why the run is incomplete. Null if the run is not incomplete. + public let incompleteDetails: IncompleteDetails? + + public enum Status: String, Codable, Sendable { + case queued + case inProgress = "in_progress" + case requiresAction = "requires_action" + case cancelling + case cancelled + case failed + case completed + case incomplete + case expired + } + + public struct RequiredAction: Codable, Sendable { + public let type: String public let submitToolOutputs: SubmitToolOutputs + public struct SubmitToolOutputs: Codable, Sendable { + public let toolCalls: [ToolCall] + + public struct ToolCall: Codable, Sendable { + public let id: String + public let type: String + public let function: Function + + public struct Function: Codable, Sendable { + public let name: String + public let arguments: String + } + } + + enum CodingKeys: String, CodingKey { + case toolCalls = "tool_calls" + } + } + enum CodingKeys: String, CodingKey { case type case submitToolOutputs = "submit_tool_outputs" } + } - /// Represents the tool outputs needed for this run to continue. - public struct SubmitToolOutputs: Codable { - /// A list of the relevant tool calls. - public let toolCalls: [ToolCall] + public struct LastError: Codable, Sendable { + public let code: String + public let message: String + } - /// Represents a single tool call. - public struct ToolCall: Codable { - /// The ID of the tool call. - public let id: String + public struct Tool: Codable, Sendable { + public let type: ToolType + public let function: FunctionTool? + public let fileSearch: FileSearchTool? - /// The type of tool call the output is required for. For now, this is always 'function'. - public let type: String + public enum ToolType: String, Codable, Sendable { + case codeInterpreter = "code_interpreter" + case fileSearch = "file_search" + case function + } - /// The function definition. - public let function: Function + public struct FunctionTool: Codable, Sendable { + public let name: String + public let description: String? + public let parameters: [String: AnyCodable]? + public let strict: Bool? + } - /// Represents the function definition. - public struct Function: Codable { - /// The name of the function. - public let name: String + public struct FileSearchTool: Codable, Sendable { + public let maxNumResults: Int? + public let rankingOptions: RankingOptions? - /// The arguments that the model expects you to pass to the function. - public let arguments: String + public struct RankingOptions: Codable, Sendable { + public let ranker: String? + public let scoreThreshold: Double? + + enum CodingKeys: String, CodingKey { + case ranker + case scoreThreshold = "score_threshold" } } + + enum CodingKeys: String, CodingKey { + case maxNumResults = "max_num_results" + case rankingOptions = "ranking_options" + } } } - public struct LastError: Codable { - /// One of 'server_error' or 'rate_limit_exceeded'. - public let code: String + public struct Usage: Codable, Sendable { + public let completionTokens: Int + public let promptTokens: Int + public let totalTokens: Int - /// A human-readable description of the error. - public let message: String + enum CodingKeys: String, CodingKey { + case completionTokens = "completion_tokens" + case promptTokens = "prompt_tokens" + case totalTokens = "total_tokens" + } } - /// Represents a tool enabled on the assistant. - public struct Tool: Codable { - /// The type of the tool (e.g., code_interpreter, retrieval, function). + public struct TruncationStrategy: Codable, Sendable { public let type: String + public let lastMessages: Int? + + enum CodingKeys: String, CodingKey { + case type + case lastMessages = "last_messages" + } + } + + public enum ResponseFormat: Codable, Sendable { + case auto + case text + case jsonObject + case jsonSchema(JSONSchema) + + public struct JSONSchema: Codable, Sendable { + public let name: String + public let description: String? + public let schema: [String: AnyCodable]? + public let strict: Bool? + } + + enum CodingKeys: String, CodingKey { + case type + case jsonSchema = "json_schema" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + switch type { + case "auto": self = .auto + case "text": self = .text + case "json_object": self = .jsonObject + case "json_schema": + let schema = try container.decode(JSONSchema.self, forKey: .jsonSchema) + self = .jsonSchema(schema) + default: + self = .auto + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .auto: try container.encode("auto", forKey: .type) + case .text: try container.encode("text", forKey: .type) + case .jsonObject: try container.encode("json_object", forKey: .type) + case .jsonSchema(let schema): + try container.encode("json_schema", forKey: .type) + try container.encode(schema, forKey: .jsonSchema) + } + } + } + + public enum ToolChoice: Codable, Sendable { + case none + case auto + case required + case specificTool(SpecificTool) + + public struct SpecificTool: Codable, Sendable { + public let type: String + public let function: SpecificFunction? + + public struct SpecificFunction: Codable, Sendable { + public let name: String + } + } + } + + public struct IncompleteDetails: Codable, Sendable { + public let reason: String } enum CodingKeys: String, CodingKey { - case id, object - case createdAt = "created_at" - case threadId = "thread_id" - case assistantId = "assistant_id" - case status, requiredAction = "required_action" - case lastError = "last_error" - case expiresAt = "expires_at" - case startedAt = "started_at" - case cancelledAt = "cancelled_at" - case failedAt = "failed_at" - case completedAt = "completed_at" - case model, instructions, tools - case fileIds = "file_ids" - case metadata + case id, object, createdAt = "created_at", threadId = "thread_id", assistantId = "assistant_id", + status, requiredAction = "required_action", lastError = "last_error", + expiresAt = "expires_at", startedAt = "started_at", cancelledAt = "cancelled_at", + failedAt = "failed_at", completedAt = "completed_at", model, instructions, tools, metadata, + usage, temperature, topP = "top_p", maxCompletionTokens = "max_completion_tokens", + maxPromptTokens = "max_prompt_tokens", truncationStrategy = "truncation_strategy", + responseFormat = "response_format", toolChoice = "tool_choice", + parallelToolCalls = "parallel_tool_calls", incompleteDetails = "incomplete_details" } } diff --git a/Sources/AISwiftAssist/Models/Main/ASARunStep.swift b/Sources/AISwiftAssist/Models/Main/ASARunStep.swift index 2448b65..eed2b7f 100644 --- a/Sources/AISwiftAssist/Models/Main/ASARunStep.swift +++ b/Sources/AISwiftAssist/Models/Main/ASARunStep.swift @@ -8,7 +8,7 @@ import Foundation /// Represents a step in the execution of a run. -public struct ASARunStep: Codable { +public struct ASARunStep: Codable, Sendable { /// The identifier of the run step, which can be referenced in API endpoints. let id: String @@ -55,13 +55,13 @@ public struct ASARunStep: Codable { let metadata: [String: String]? /// A structure to represent the step details. - struct StepDetails: Codable { + struct StepDetails: Codable, Sendable { let type: String // This can be 'message_creation' or 'tool_calls'. let messageCreation: MessageCreation? let toolCalls: [ToolCall]? /// A structure to represent the message creation details. - struct MessageCreation: Codable { + struct MessageCreation: Codable, Sendable { let messageId: String enum CodingKeys: String, CodingKey { @@ -70,7 +70,7 @@ public struct ASARunStep: Codable { } /// A structure to represent the tool calls. - struct ToolCall: Codable { + struct ToolCall: Codable, Sendable { let id: String let type: String // This can be 'code_interpreter', 'retrieval', or 'function'. let details: ToolCallDetails? @@ -80,7 +80,7 @@ public struct ASARunStep: Codable { } /// A structure to represent the details of a tool call. - struct ToolCallDetails: Codable { + struct ToolCallDetails: Codable, Sendable { let codeInterpreter: CodeInterpreter? let retrieval: Retrieval? let function: FunctionCall? @@ -92,7 +92,7 @@ public struct ASARunStep: Codable { } /// A structure to represent the code interpreter tool call. - struct CodeInterpreter: Codable { + struct CodeInterpreter: Codable, Sendable { let id: String let type: String // This will always be 'code_interpreter'. let input: String @@ -103,7 +103,7 @@ public struct ASARunStep: Codable { } /// A structure to represent the outputs of a code interpreter tool call. - struct CodeInterpreterOutput: Codable { + struct CodeInterpreterOutput: Codable, Sendable { let type: String // Can be 'logs' or 'image'. let logs: String? let image: ImageOutput? @@ -113,7 +113,7 @@ public struct ASARunStep: Codable { } /// A structure to represent the image output. - struct ImageOutput: Codable { + struct ImageOutput: Codable, Sendable { let fileId: String enum CodingKeys: String, CodingKey { @@ -124,12 +124,12 @@ public struct ASARunStep: Codable { } /// A structure to represent the retrieval tool call. - struct Retrieval: Codable { + struct Retrieval: Codable, Sendable { // For now, it's always an empty object. } /// A structure to represent the function tool call. - struct FunctionCall: Codable { + struct FunctionCall: Codable, Sendable { let name: String let arguments: String let output: String? @@ -149,7 +149,7 @@ public struct ASARunStep: Codable { } /// A structure to represent the last error. - struct LastError: Codable { + struct LastError: Codable, Sendable { /// One of `server_error` or `rate_limit_exceeded`. let code: String diff --git a/Sources/AISwiftAssist/Models/Main/ASAThread.swift b/Sources/AISwiftAssist/Models/Main/ASAThread.swift index e44e18f..4874eee 100644 --- a/Sources/AISwiftAssist/Models/Main/ASAThread.swift +++ b/Sources/AISwiftAssist/Models/Main/ASAThread.swift @@ -6,25 +6,57 @@ // import Foundation + /// Represents a thread that contains messages. -public struct ASAThread: Codable { +public struct ASAThread: Codable, Sendable { /// The identifier of the thread, which can be referenced in API endpoints. public let id: String - /// The object type, which is always 'thread'. + /// The object type, always 'thread'. public let object: String - /// The Unix timestamp (in seconds) for when the thread was created. + /// The Unix timestamp (in seconds) when the thread was created. public let createdAt: Int - /// Optional: Set of 16 key-value pairs that can be attached to the thread. - /// This can be useful for storing additional information about the thread in a structured format. - /// Keys can be a maximum of 64 characters long, and values can be a maximum of 512 characters long. + /// Resources available to the assistant's tools in this thread. + public let toolResources: ToolResources? + + /// Optional metadata (max 16 key-value pairs). public let metadata: [String: String]? enum CodingKeys: String, CodingKey { case id, object case createdAt = "created_at" + case toolResources = "tool_resources" case metadata } + + public struct ToolResources: Codable, Sendable { + public let codeInterpreter: CodeInterpreter? + public let fileSearch: FileSearch? + + enum CodingKeys: String, CodingKey { + case codeInterpreter = "code_interpreter" + case fileSearch = "file_search" + } + } + + public struct CodeInterpreter: Codable, Sendable { + /// A list of file IDs (max 20 files). + public let fileIds: [String] + + enum CodingKeys: String, CodingKey { + case fileIds = "file_ids" + } + } + + public struct FileSearch: Codable, Sendable { + /// A list of vector store IDs (max 1). + public let vectorStoreIds: [String] + + enum CodingKeys: String, CodingKey { + case vectorStoreIds = "vector_store_ids" + } + } + } diff --git a/Sources/AISwiftAssist/Models/Main/Message/ASAImageContent.swift b/Sources/AISwiftAssist/Models/Main/Message/ASAImageContent.swift deleted file mode 100644 index 3013ccb..0000000 --- a/Sources/AISwiftAssist/Models/Main/Message/ASAImageContent.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 12/13/23. -// - -import Foundation - -/// Represents an image file in the content of a message. -public struct ASAImageContent: Codable { - /// The File ID of the image in the message content. - public let file_id: String - - enum CodingKeys: String, CodingKey { - case file_id - } -} diff --git a/Sources/AISwiftAssist/Models/Main/Message/ASAMessage.swift b/Sources/AISwiftAssist/Models/Main/Message/ASAMessage.swift deleted file mode 100644 index d6c523d..0000000 --- a/Sources/AISwiftAssist/Models/Main/Message/ASAMessage.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 11/15/23. -// - -import Foundation - -/// Represents a message within a thread. -public struct ASAMessage: Codable { - /// The identifier of the message, which can be referenced in API endpoints. - public let id: String - - /// The object type, which is always 'thread.message'. - public let object: String - - /// The Unix timestamp (in seconds) for when the message was created. - public let createdAt: Int - - /// The thread ID that this message belongs to. - public let threadId: String - - /// The entity that produced the message. One of 'user' or 'assistant'. - public let role: String - - /// The content of the message in array of text and/or images. - public let content: [ASAMessageContent] - - /// If applicable, the ID of the assistant that authored this message. - public let assistantId: String? - - /// If applicable, the ID of the run associated with the authoring of this message. - public let runId: String? - - /// A list of file IDs that the assistant should use. Useful for tools like retrieval and code_interpreter that can access files. - /// A maximum of 10 files can be attached to a message. - public let fileIds: [String] - - /// Optional: Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information - /// about the object in a structured format. Keys can be a maximum of 64 characters long, and values can be a maximum of 512 characters long. - public let metadata: [String: String]? - - enum CodingKeys: String, CodingKey { - case id, object - case createdAt = "created_at" - case threadId = "thread_id" - case role, content - case assistantId = "assistant_id" - case runId = "run_id" - case fileIds = "file_ids" - case metadata - } -} diff --git a/Sources/AISwiftAssist/Models/Main/Message/ASAMessageContent.swift b/Sources/AISwiftAssist/Models/Main/Message/ASAMessageContent.swift deleted file mode 100644 index d877733..0000000 --- a/Sources/AISwiftAssist/Models/Main/Message/ASAMessageContent.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 12/13/23. -// - -import Foundation - -public enum ASAMessageContent: Codable { - case image(ASAImageContent) - case text(ASATextContent) - - enum CodingKeys: String, CodingKey { - case type, imageFile = "image_file", text - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let type = try container.decode(String.self, forKey: .type) - - switch type { - case "image_file": - let imageContent = try container.decode(ASAImageContent.self, forKey: .imageFile) - self = .image(imageContent) - case "text": - let textContent = try container.decode(ASATextContent.self, forKey: .text) - self = .text(textContent) - default: - throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Invalid type") - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - switch self { - case .image(let imageContent): - try container.encode("image_file", forKey: .type) - try container.encode(imageContent, forKey: .imageFile) - case .text(let textContent): - try container.encode("text", forKey: .type) - try container.encode(textContent, forKey: .text) - } - } -} - - diff --git a/Sources/AISwiftAssist/Models/Main/Message/ASATextContent.swift b/Sources/AISwiftAssist/Models/Main/Message/ASATextContent.swift deleted file mode 100644 index e137d24..0000000 --- a/Sources/AISwiftAssist/Models/Main/Message/ASATextContent.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 12/13/23. -// - -import Foundation - -/// Represents the text content of a message. -public struct ASATextContent: Codable { - /// The value of the text. - public let value: String - - /// Optional: Annotations - public let annotations: [ASAAnnotation]? -} - -public struct ASAAnnotation: Codable { - let type: String - let text: String - let startIndex: Int - let endIndex: Int - let fileCitation: ASAFileCitation? - let filePath: ASAFilePath? - - enum CodingKeys: String, CodingKey { - case type, text, startIndex = "start_index", endIndex = "end_index", fileCitation = "file_citation", filePath = "file_path" - } -} - -public struct ASAFileCitation: Codable { - public let fileId: String - public let quote: String - - enum CodingKeys: String, CodingKey { - case fileId = "file_id", quote - } -} - -public struct ASAFilePath: Codable { - public let fileId: String - - enum CodingKeys: String, CodingKey { - case fileId = "file_id" - } -} diff --git a/Sources/AISwiftAssist/Models/Request/ASACreateAssistantFileRequest.swift b/Sources/AISwiftAssist/Models/Request/ASACreateAssistantFileRequest.swift deleted file mode 100644 index e285b87..0000000 --- a/Sources/AISwiftAssist/Models/Request/ASACreateAssistantFileRequest.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 12/5/23. -// - -import Foundation - -/// Represents a request to create an assistant file. -public struct ASACreateAssistantFileRequest: Codable { - /// A File ID (with purpose="assistants") that the assistant should use. - /// Useful for tools like retrieval and code_interpreter that can access files. - public let fileId: String - - public init(fileId: String) { - self.fileId = fileId - } - - enum CodingKeys: String, CodingKey { - case fileId = "file_id" - } -} diff --git a/Sources/AISwiftAssist/Models/Request/ASACreateAssistantRequest.swift b/Sources/AISwiftAssist/Models/Request/ASACreateAssistantRequest.swift deleted file mode 100644 index 5e03b99..0000000 --- a/Sources/AISwiftAssist/Models/Request/ASACreateAssistantRequest.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 11/15/23. -// - -import Foundation - -public struct ASACreateAssistantRequest: Codable { - /// ID of the model to use. You can use the List models API to see all of your available models. - public let model: String - - /// Optional: The name of the assistant. The maximum length is 256 characters. - public let name: String? - - /// Optional: The description of the assistant. The maximum length is 512 characters. - public let description: String? - - /// Optional: The system instructions that the assistant uses. The maximum length is 32768 characters. - public let instructions: String? - - /// Optional: A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant. - /// Tools can be of types code_interpreter, retrieval, or function. - public let tools: [Tool]? - - /// Optional: A list of file IDs attached to this assistant. There can be a maximum of 20 files attached to the assistant. - /// Files are ordered by their creation date in ascending order. - public let fileIds: [String]? - - /// Optional: Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information - /// about the object in a structured format. Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. - public let metadata: [String: String]? - - public enum CodingKeys: String, CodingKey { - case model, name, description, instructions, tools - case fileIds = "file_ids" - case metadata - } - - public init(model: String, name: String? = nil, description: String? = nil, instructions: String? = nil, tools: [Tool]? = nil, fileIds: [String]? = nil, metadata: [String : String]? = nil) { - self.model = model - self.name = name - self.description = description - self.instructions = instructions - self.tools = tools - self.fileIds = fileIds - self.metadata = metadata - } - - public init(asaModel: ASAModel, name: String? = nil, description: String? = nil, instructions: String? = nil, tools: [Tool]? = nil, fileIds: [String]? = nil, metadata: [String : String]? = nil) { - self.model = asaModel.id - self.name = name - self.description = description - self.instructions = instructions - self.tools = tools - self.fileIds = fileIds - self.metadata = metadata - } - - /// Represents a tool enabled on the assistant. - public struct Tool: Codable { - /// The type of the tool (e.g., code_interpreter, retrieval, function). - let type: String - } -} diff --git a/Sources/AISwiftAssist/Models/Request/ASACreateMessageRequest.swift b/Sources/AISwiftAssist/Models/Request/ASACreateMessageRequest.swift deleted file mode 100644 index f3ff362..0000000 --- a/Sources/AISwiftAssist/Models/Request/ASACreateMessageRequest.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 11/15/23. -// - -import Foundation - -/// A request structure for creating a message in a thread. -public struct ASACreateMessageRequest: Codable { - - /// The role of the entity that is creating the message. Currently only 'user' is supported. - public let role: String - - /// The content of the message. - public let content: String - - /// Optional: A list of File IDs that the message should use. A maximum of 10 files can be attached to a message. - public let fileIds: [String]? - - /// Optional: Set of 16 key-value pairs that can be attached to the message. Useful for storing additional information. - public let metadata: [String: String]? - - enum CodingKeys: String, CodingKey { - case role, content - case fileIds = "file_ids" - case metadata - } - - public init(role: String, content: String, fileIds: [String]? = nil, metadata: [String : String]? = nil) { - self.role = role - self.content = content - self.fileIds = fileIds - self.metadata = metadata - } -} diff --git a/Sources/AISwiftAssist/Models/Request/ASACreateRunRequest.swift b/Sources/AISwiftAssist/Models/Request/ASACreateRunRequest.swift deleted file mode 100644 index 0569bd5..0000000 --- a/Sources/AISwiftAssist/Models/Request/ASACreateRunRequest.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 11/16/23. -// - -import Foundation - -/// A request structure for creating a run in a thread. -public struct ASACreateRunRequest: Codable { - - /// The ID of the assistant to use to execute this run. - public let assistantId: String - - /// Optional: The ID of the Model to be used to execute this run. - public let model: String? - - /// Optional: Override the default system message of the assistant. - public let instructions: String? - - /// Optional: Override the tools the assistant can use for this run. - public let tools: [Tool]? - - /// Optional: Set of 16 key-value pairs that can be attached to the run. - public let metadata: [String: String]? - - /// Represents a tool that can be used by the assistant during the run. - public struct Tool: Codable { - /// The type of tool being defined: 'code_interpreter', 'retrieval', 'function'. - public let type: String - - /// Additional details for the 'function' tool type. - public let function: Function? - - /// Represents a function tool's details. - public struct Function: Codable { - /// Optional: A description of what the function does. - public let description: String? - - /// The name of the function to be called. - public let name: String - - /// The parameters the functions accepts, described as a JSON Schema object. - public let parameters: [String: String] - - enum CodingKeys: String, CodingKey { - case description, name, parameters - } - } - - enum CodingKeys: String, CodingKey { - case type, function - } - } - - enum CodingKeys: String, CodingKey { - case assistantId = "assistant_id" - case model, instructions, tools, metadata - } - - public init(assistantId: String, model: String? = nil, instructions: String? = nil, tools: [ASACreateRunRequest.Tool]? = nil, metadata: [String : String]? = nil) { - self.assistantId = assistantId - self.model = model - self.instructions = instructions - self.tools = tools - self.metadata = metadata - } -} diff --git a/Sources/AISwiftAssist/Models/Request/ASACreateThreadRequest.swift b/Sources/AISwiftAssist/Models/Request/ASACreateThreadRequest.swift index 4f52a77..a089d5d 100644 --- a/Sources/AISwiftAssist/Models/Request/ASACreateThreadRequest.swift +++ b/Sources/AISwiftAssist/Models/Request/ASACreateThreadRequest.swift @@ -8,31 +8,88 @@ import Foundation /// A request structure for creating a thread. -public struct ASACreateThreadRequest: Codable { +public struct ASACreateThreadRequest: Codable, Sendable { /// Optional: A list of messages to start the thread with. public let messages: [Message]? - public struct Message: Codable { - /// Required: The role of the entity that is creating the message. Currently, only 'user' is supported. + /// Optional: Resources available to the assistant's tools in this thread. + public let toolResources: ASAThread.ToolResources? + + /// Optional: A helper to create a vector store and attach it to this thread (max 1). + public let vectorStores: [VectorStore]? + + /// Optional: Metadata associated with the thread (max 16 key-value pairs). + public let metadata: [String: String]? + + public struct Message: Codable, Sendable { + /// Required: The role of the entity that is creating the message ('user' or 'assistant'). public let role: String - /// Required: The content of the message. + /// Required: The content of the message (can be string or array, simplified to string here). public let content: String - /// Optional: A list of File IDs that the message should use. A maximum of 10 files can be attached to a message. + /// Optional: A list of file attachments. + public let attachments: [Attachment]? + + /// Optional: Metadata for the message (max 16 key-value pairs). + public let metadata: [String: String]? + + enum CodingKeys: String, CodingKey { + case role, content, attachments, metadata + } + } + + public struct Attachment: Codable, Sendable { + /// The ID of the file to attach. + public let fileId: String + + /// Tools to add the file to. + public let tools: [Tool]? + + enum CodingKeys: String, CodingKey { + case fileId = "file_id" + case tools + } + } + + public struct Tool: Codable, Sendable { + /// Type of tool (e.g., 'file_search'). + public let type: String + + enum CodingKeys: String, CodingKey { + case type + } + } + + public struct VectorStore: Codable, Sendable { + /// A list of file IDs to add to the vector store (max 10000 files). public let fileIds: [String]? - /// Optional: Set of 16 key-value pairs that can be attached to the message. Useful for storing additional information. + /// Optional metadata for the vector store (max 16 key-value pairs). public let metadata: [String: String]? enum CodingKeys: String, CodingKey { - case role, content case fileIds = "file_ids" case metadata } } - public init(messages: [Message]? = nil) { + enum CodingKeys: String, CodingKey { + case messages + case toolResources = "tool_resources" + case vectorStores = "vector_stores" + case metadata + } + + public init( + messages: [Message]? = nil, + toolResources: ASAThread.ToolResources? = nil, + vectorStores: [VectorStore]? = nil, + metadata: [String: String]? = nil + ) { self.messages = messages + self.toolResources = toolResources + self.vectorStores = vectorStores + self.metadata = metadata } } diff --git a/Sources/AISwiftAssist/Models/Request/ASACreateThreadRunRequest.swift b/Sources/AISwiftAssist/Models/Request/ASACreateThreadRunRequest.swift index 35bcdc3..2ea47ce 100644 --- a/Sources/AISwiftAssist/Models/Request/ASACreateThreadRunRequest.swift +++ b/Sources/AISwiftAssist/Models/Request/ASACreateThreadRunRequest.swift @@ -8,7 +8,7 @@ import Foundation /// A request structure for creating a thread and run. -public struct ASACreateThreadRunRequest: Codable { +public struct ASACreateThreadRunRequest: Codable, Sendable { /// The ID of the assistant to use to execute this run. public let assistantId: String @@ -16,12 +16,12 @@ public struct ASACreateThreadRunRequest: Codable { public let thread: Thread /// Represents a thread containing messages and other parameters. - public struct Thread: Codable { + public struct Thread: Codable, Sendable { /// The messages to be processed in this thread. public let messages: [Message] /// Represents a single message in a thread. - public struct Message: Codable { + public struct Message: Codable, Sendable { /// The role of the message sender, e.g., 'user' or 'system'. public let role: String diff --git a/Sources/AISwiftAssist/Models/Request/ASAListRunStepsParameters.swift b/Sources/AISwiftAssist/Models/Request/ASAListRunStepsParameters.swift index 3dfafc7..03ec038 100644 --- a/Sources/AISwiftAssist/Models/Request/ASAListRunStepsParameters.swift +++ b/Sources/AISwiftAssist/Models/Request/ASAListRunStepsParameters.swift @@ -8,7 +8,7 @@ import Foundation /// Parameters for listing run steps in a thread. -public struct ASAListRunStepsParameters: Codable { +public struct ASAListRunStepsParameters: Codable, Sendable { /// A limit on the number of objects to be returned. public let limit: Int? diff --git a/Sources/AISwiftAssist/Models/Request/ASAModifyAssistantRequest.swift b/Sources/AISwiftAssist/Models/Request/ASAModifyAssistantRequest.swift deleted file mode 100644 index a5c3a57..0000000 --- a/Sources/AISwiftAssist/Models/Request/ASAModifyAssistantRequest.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 11/15/23. -// - -import Foundation -/// A request structure for modifying an existing assistant. -public struct ASAModifyAssistantRequest: Codable { - - /// Optional: ID of the model to use. - public let model: String? - - /// Optional: The name of the assistant. Maximum length is 256 characters. - public let name: String? - - /// Optional: The description of the assistant. Maximum length is 512 characters. - public let description: String? - - /// Optional: The system instructions that the assistant uses. Maximum length is 32768 characters. - public let instructions: String? - - /// Optional: A list of tools enabled on the assistant. Maximum of 128 tools per assistant. - public let tools: [ASAAssistant.Tool]? - - /// Optional: A list of file IDs attached to the assistant. Maximum of 20 files attached to the assistant. - public let fileIds: [String]? - - /// Optional: A map of key-value pairs for storing additional information. Maximum of 16 pairs. - public let metadata: [String: String]? - - enum CodingKeys: String, CodingKey { - case model, name, description, instructions, tools - case fileIds = "file_ids" - case metadata - } - - public init(model: String? = nil, name: String? = nil, description: String? = nil, instructions: String? = nil, tools: [ASAAssistant.Tool]? = nil, fileIds: [String]? = nil, metadata: [String : String]? = nil) { - self.model = model - self.name = name - self.description = description - self.instructions = instructions - self.tools = tools - self.fileIds = fileIds - self.metadata = metadata - } -} diff --git a/Sources/AISwiftAssist/Models/Request/ASAModifyRunRequest.swift b/Sources/AISwiftAssist/Models/Request/ASAModifyRunRequest.swift index 1a35b10..299db3d 100644 --- a/Sources/AISwiftAssist/Models/Request/ASAModifyRunRequest.swift +++ b/Sources/AISwiftAssist/Models/Request/ASAModifyRunRequest.swift @@ -8,7 +8,7 @@ import Foundation /// A request structure for modifying a run. -public struct ASAModifyRunRequest: Codable { +public struct ASAModifyRunRequest: Codable, Sendable { /// Optional: Set of 16 key-value pairs that can be attached to the run. public let metadata: [String: String]? diff --git a/Sources/AISwiftAssist/Models/Request/ASAModifyThreadRequest.swift b/Sources/AISwiftAssist/Models/Request/ASAModifyThreadRequest.swift index ba02df0..9cf24f3 100644 --- a/Sources/AISwiftAssist/Models/Request/ASAModifyThreadRequest.swift +++ b/Sources/AISwiftAssist/Models/Request/ASAModifyThreadRequest.swift @@ -8,11 +8,21 @@ import Foundation /// A request structure for modifying a thread. -public struct ASAModifyThreadRequest: Codable { - /// Optional: Set of 16 key-value pairs that can be attached to the thread. +public struct ASAModifyThreadRequest: Codable, Sendable { + /// Optional: Resources available to the assistant's tools in this thread. + public let toolResources: ASAThread.ToolResources? + + /// Optional: Metadata associated with the thread (max 16 key-value pairs). public let metadata: [String: String]? enum CodingKeys: String, CodingKey { + case toolResources = "tool_resources" case metadata } + + public init(toolResources: ASAThread.ToolResources? = nil, + metadata: [String: String]? = nil) { + self.toolResources = toolResources + self.metadata = metadata + } } diff --git a/Sources/AISwiftAssist/Models/Request/ASAToolOutput.swift b/Sources/AISwiftAssist/Models/Request/ASAToolOutput.swift index e91f979..00d9419 100644 --- a/Sources/AISwiftAssist/Models/Request/ASAToolOutput.swift +++ b/Sources/AISwiftAssist/Models/Request/ASAToolOutput.swift @@ -8,7 +8,7 @@ import Foundation /// Represents a tool output for submission. -public struct ASAToolOutput: Codable { +public struct ASAToolOutput: Codable, Sendable { /// The ID of the tool call. public let toolCallId: String diff --git a/Sources/AISwiftAssist/Models/Request/Assistant/ASACreateAssistantRequest.swift b/Sources/AISwiftAssist/Models/Request/Assistant/ASACreateAssistantRequest.swift new file mode 100644 index 0000000..f5acc25 --- /dev/null +++ b/Sources/AISwiftAssist/Models/Request/Assistant/ASACreateAssistantRequest.swift @@ -0,0 +1,180 @@ +// +// File.swift +// +// +// Created by Alexey on 11/15/23. +// + +import Foundation + +/// Represents an assistant capable of calling the model and utilizing tools. +public struct ASACreateAssistantRequest: Codable, Sendable { + /// Optional: Assistant's name (max 256 characters). + public let name: String? + + /// Optional: Description of the assistant (max 512 characters). + public let description: String? + + /// ID of the model used. + public let model: String + + /// Optional: System instructions used by the assistant (max 256000 characters). + public let instructions: String? + + /// Optional: Effort level for model reasoning (low, medium, high). o1 and o3-mini models only + public let reasoningEffort: String? + + /// Tools available to the assistant (max 128). + public let tools: [ASAAssistant.Tool] + + /// Optional: Resources utilized by the assistant's tools. + public let toolResources: ASAAssistant.ToolResources? + + /// Optional: Sampling temperature for output generation (range 0–2, default is 1). + public let temperature: Double? + + /// Optional: Nucleus sampling parameter (range 0–1, default is 1). + public let topP: Double? + + /// Optional: Format of the response generated by the model (e.g., auto, text, json_object). + /// Using this can enforce specific output formats like JSON. + public let responseFormat: ASAAssistant.ResponseFormat? + + /// Optional: Metadata (up to 16 key-value pairs). + public let metadata: [String: String]? + + public enum CodingKeys: String, CodingKey { + case name, description, model, instructions, tools, metadata + case reasoningEffort = "reasoning_effort" + case toolResources = "tool_resources" + case temperature + case topP = "top_p" + case responseFormat = "response_format" + } + + public init( + name: String? = nil, + description: String? = nil, + model: String, + instructions: String? = nil, + reasoningEffort: String? = nil, + tools: [ASAAssistant.Tool] = [], + toolResources: ASAAssistant.ToolResources? = nil, + temperature: Double? = nil, + topP: Double? = nil, + responseFormat: ASAAssistant.ResponseFormat? = nil, + metadata: [String : String]? = nil + ) { + self.name = name + self.description = description + self.model = model + self.instructions = instructions + self.reasoningEffort = reasoningEffort + self.tools = tools + self.toolResources = toolResources + self.temperature = temperature + self.topP = topP + self.responseFormat = responseFormat + self.metadata = metadata + } +} + +extension ASACreateAssistantRequest { + // MARK: - Mock Data + + /// Minimal mock assistant data (only required fields). + static let assistantMinimal: Self = .init( + name: nil, + description: nil, + model: "gpt-4", + instructions: nil, + reasoningEffort: nil, + tools: [], + toolResources: nil, + temperature: nil, + topP: nil, + responseFormat: nil, + metadata: nil + ) + + /// Medium complexity mock assistant data. + static let assistantMedium: Self = .init( + name: "Medium Assistant", + description: "An assistant with moderate complexity", + model: "gpt-4-turbo", + instructions: "Answer general queries.", + reasoningEffort: "medium", + tools: [ + ASAAssistant.Tool(type: .codeInterpreter, function: nil, fileSearch: nil) + ], + toolResources: ASAAssistant.ToolResources( + codeInterpreter: ASAAssistant.CodeInterpreterResources(fileIds: ["file_123"]), + fileSearch: nil + ), + temperature: 0.7, + topP: 0.9, + responseFormat: .auto, + metadata: ["env": "staging"] + ) + + /// Fully populated mock assistant data (maximum configuration). + static let assistantFull: Self = .init( + name: "Full Assistant", + description: "Fully configured assistant for advanced tasks", + model: "gpt-4o", + instructions: "You are a comprehensive assistant handling complex tasks and queries.", + reasoningEffort: "high", + tools: [ + ASAAssistant.Tool( + type: .function, + function: ASAAssistant.Tool.FunctionTool( + name: "calculate", + description: "Performs calculations", + parameters: ["operation": AnyCodable("add"), "values": AnyCodable([1, 2])], + strict: true + ), + fileSearch: nil + ), + ASAAssistant.Tool( + type: .fileSearch, + function: nil, + fileSearch: ASAAssistant.Tool.FileSearchTool( + maxNumResults: 10, + rankingOptions: ASAAssistant.Tool.FileSearchTool.RankingOptions( + ranker: "auto", + scoreThreshold: 0.8 + ) + ) + ) + ], + toolResources: ASAAssistant.ToolResources( + codeInterpreter: ASAAssistant.CodeInterpreterResources(fileIds: ["file_456", "file_789"]), + fileSearch: ASAAssistant.FileSearchResources( + vectorStoreIds: ["vector_store_1"], + vectorStores: [ + ASAAssistant.FileSearchResources.VectorStore( + fileIds: ["file_001", "file_002"], + chunkingStrategy: ASAAssistant.FileSearchResources.VectorStore.ChunkingStrategy( + type: .static, + staticChunking: ASAAssistant.FileSearchResources.VectorStore.ChunkingStrategy.StaticChunking( + maxChunkSizeTokens: 1000, + chunkOverlapTokens: 500 + ) + ) + ) + ] + ) + ), + temperature: 0.5, + topP: 0.85, + responseFormat: .jsonObject, + metadata: ["version": "1.0", "env": "production"] + ) + + /// Collection of all mock assistant configurations. + static let mocks: [Self] = [ + assistantMinimal, + assistantMedium, + assistantFull + ] +} diff --git a/Sources/AISwiftAssist/Models/Request/ASAListAssistantsParameters.swift b/Sources/AISwiftAssist/Models/Request/Assistant/ASAListAssistantsParameters.swift similarity index 81% rename from Sources/AISwiftAssist/Models/Request/ASAListAssistantsParameters.swift rename to Sources/AISwiftAssist/Models/Request/Assistant/ASAListAssistantsParameters.swift index 147ad37..cb3154a 100644 --- a/Sources/AISwiftAssist/Models/Request/ASAListAssistantsParameters.swift +++ b/Sources/AISwiftAssist/Models/Request/Assistant/ASAListAssistantsParameters.swift @@ -8,7 +8,7 @@ import Foundation /// Parameters for listing assistants. -public struct ASAListAssistantsParameters: Encodable { +public struct ASAListAssistantsParameters: Encodable, Sendable { /// Optional: A limit on the number of objects to be returned. Can range between 1 and 100. Defaults to 20. public let limit: Int? @@ -22,7 +22,12 @@ public struct ASAListAssistantsParameters: Encodable { /// Optional: A cursor for use in pagination. 'before' is an object ID that defines your place in the list, to fetch the previous page of the list. public let before: String? - public init(limit: Int? = nil, order: String? = nil, after: String? = nil, before: String? = nil) { + public init( + limit: Int? = nil, + order: String? = nil, + after: String? = nil, + before: String? = nil + ) { self.limit = limit self.order = order self.after = after diff --git a/Sources/AISwiftAssist/Models/Request/Assistant/ASAModifyAssistantRequest.swift b/Sources/AISwiftAssist/Models/Request/Assistant/ASAModifyAssistantRequest.swift new file mode 100644 index 0000000..d86ff1b --- /dev/null +++ b/Sources/AISwiftAssist/Models/Request/Assistant/ASAModifyAssistantRequest.swift @@ -0,0 +1,82 @@ +// +// File.swift +// +// +// Created by Alexey on 11/15/23. +// + +import Foundation + +/// A request structure for modifying an existing assistant. +public struct ASAModifyAssistantRequest: Codable, Sendable { + + /// Optional: ID of the model to use. + public let model: String? + + /// Optional: Effort level for model reasoning (low, medium, high). o1 and o3-mini models only + public let reasoningEffort: String? + + /// Optional: The name of the assistant. Maximum length is 256 characters. + public let name: String? + + /// Optional: The description of the assistant. Maximum length is 512 characters. + public let description: String? + + /// Optional: The system instructions that the assistant uses. Maximum length is 32768 characters. + public let instructions: String? + + /// Optional: A list of tools enabled on the assistant. Maximum of 128 tools per assistant. + public let tools: [ASAAssistant.Tool]? + + /// Optional: Resources utilized by the assistant's tools. + public let toolResources: ASAAssistant.ToolResources? + + /// Optional: A map of key-value pairs for storing additional information. Maximum of 16 pairs. + public let metadata: [String: String]? + + /// Optional: Sampling temperature for output generation (range 0–2, default is 1). + public let temperature: Double? + + /// Optional: Nucleus sampling parameter (range 0–1, default is 1). + public let topP: Double? + + /// Optional: Format of the response generated by the model (e.g., auto, text, json_object). + /// Using this can enforce specific output formats like JSON. + public let responseFormat: ASAAssistant.ResponseFormat? + + enum CodingKeys: String, CodingKey { + case model, name, description, instructions, tools + case reasoningEffort = "reasoning_effort" + case toolResources = "tool_resources" + case temperature + case topP = "top_p" + case responseFormat = "response_format" + case metadata + } + + public init( + model: String? = nil, + reasoningEffort: String? = nil, + name: String? = nil, + description: String? = nil, + instructions: String? = nil, + tools: [ASAAssistant.Tool]? = nil, + toolResources: ASAAssistant.ToolResources? = nil, + metadata: [String : String]? = nil, + temperature: Double? = nil, + topP: Double? = nil, + responseFormat: ASAAssistant.ResponseFormat? = nil + ) { + self.model = model + self.reasoningEffort = reasoningEffort + self.name = name + self.description = description + self.instructions = instructions + self.tools = tools + self.toolResources = toolResources + self.metadata = metadata + self.temperature = temperature + self.topP = topP + self.responseFormat = responseFormat + } +} diff --git a/Sources/AISwiftAssist/Models/Request/Messages/ASACreateMessageRequest.swift b/Sources/AISwiftAssist/Models/Request/Messages/ASACreateMessageRequest.swift new file mode 100644 index 0000000..4c344c3 --- /dev/null +++ b/Sources/AISwiftAssist/Models/Request/Messages/ASACreateMessageRequest.swift @@ -0,0 +1,37 @@ +// +// File.swift +// +// +// Created by Alexey on 11/15/23. +// + +import Foundation + +/// Represents a request to create a message within a thread. +public struct ASACreateMessageRequest: Codable, Sendable { + /// Role of the entity creating the message (user or assistant). + public let role: ASAMessage.Role + + /// Content of the message (text, image URLs, or image files). + public let content: [ASAMessage.Content] + + /// Optional attachments associated with the message. + public let attachments: [ASAMessage.Attachment]? + + /// Optional metadata (max 16 key-value pairs). + public let metadata: [String: String]? + + enum CodingKeys: String, CodingKey { + case role, content, attachments, metadata + } + + public init(role: ASAMessage.Role, + content: [ASAMessage.Content], + attachments: [ASAMessage.Attachment]? = nil, + metadata: [String: String]? = nil) { + self.role = role + self.content = content + self.attachments = attachments + self.metadata = metadata + } +} diff --git a/Sources/AISwiftAssist/Models/Request/ASAListMessagesParameters.swift b/Sources/AISwiftAssist/Models/Request/Messages/ASAListMessagesParameters.swift similarity index 93% rename from Sources/AISwiftAssist/Models/Request/ASAListMessagesParameters.swift rename to Sources/AISwiftAssist/Models/Request/Messages/ASAListMessagesParameters.swift index 00ae785..c84446a 100644 --- a/Sources/AISwiftAssist/Models/Request/ASAListMessagesParameters.swift +++ b/Sources/AISwiftAssist/Models/Request/Messages/ASAListMessagesParameters.swift @@ -8,7 +8,7 @@ import Foundation /// Parameters for listing messages in a thread. -public struct ASAListMessagesParameters: Codable { +public struct ASAListMessagesParameters: Codable, Sendable { /// Optional: A limit on the number of objects to be returned. Limit can range between 1 and 100. public let limit: Int? diff --git a/Sources/AISwiftAssist/Models/Request/ASAModifyMessageRequest.swift b/Sources/AISwiftAssist/Models/Request/Messages/ASAModifyMessageRequest.swift similarity index 88% rename from Sources/AISwiftAssist/Models/Request/ASAModifyMessageRequest.swift rename to Sources/AISwiftAssist/Models/Request/Messages/ASAModifyMessageRequest.swift index 5c290e0..124f0a2 100644 --- a/Sources/AISwiftAssist/Models/Request/ASAModifyMessageRequest.swift +++ b/Sources/AISwiftAssist/Models/Request/Messages/ASAModifyMessageRequest.swift @@ -8,7 +8,7 @@ import Foundation /// A request structure for modifying a message in a thread. -public struct ASAModifyMessageRequest: Codable { +public struct ASAModifyMessageRequest: Codable, Sendable { /// Optional: Set of 16 key-value pairs that can be attached to the message. public let metadata: [String: String]? diff --git a/Sources/AISwiftAssist/Models/Request/Run/ASACreateRunRequest.swift b/Sources/AISwiftAssist/Models/Request/Run/ASACreateRunRequest.swift new file mode 100644 index 0000000..3b6f32e --- /dev/null +++ b/Sources/AISwiftAssist/Models/Request/Run/ASACreateRunRequest.swift @@ -0,0 +1,265 @@ +// +// File.swift +// +// +// Created by Alexey on 11/16/23. +// + +import Foundation +import Foundation + +/// Represents a request for creating a run on a thread. +public struct ASACreateRunRequest: Codable, Sendable { + + /// The ID of the assistant to use to execute this run. + public let assistantId: String + + /// Optional. The model ID to override the assistant's default model. + public let model: String? + + /// Optional. Overrides the assistant's instructions for this run. + public let instructions: String? + + /// Optional. Appends additional instructions at the end of the run's instructions. + public let additionalInstructions: String? + + /// Optional. Additional messages added to the thread before creating the run. + public let additionalMessages: [AdditionalMessage]? + + /// Optional. Overrides the tools available to the assistant for this run. + public let tools: [ASARun.Tool]? + + /// Optional. Controls which (if any) tool is called by the model. + public let toolChoice: ASARun.ToolChoice? + + /// Optional. Enables parallel function calling during tool use. Defaults to `true`. + public let parallelToolCalls: Bool? + + /// Optional. The sampling temperature (0–2). Defaults to `1`. + public let temperature: Double? + + /// Optional. Alternative to temperature sampling, controlling nucleus sampling (0–1). Defaults to `1`. + public let topP: Double? + + /// Optional. Maximum number of completion tokens allowed for this run. + public let maxCompletionTokens: Int? + + /// Optional. Maximum number of prompt tokens allowed for this run. + public let maxPromptTokens: Int? + + /// Optional. Controls how the thread is truncated before running. + public let truncationStrategy: ASARun.TruncationStrategy? + + /// Optional. Constrains reasoning effort. Supported values: `low`, `medium`, `high`. Defaults to `medium`. (o-series models only) + public let reasoningEffort: String? + + /// Optional. Specifies the format of the model's output. + public let responseFormat: ASARun.ResponseFormat? + + /// Optional. Streams events during the run as server-sent events if set to `true`. + public let stream: Bool? + + /// Optional. Additional structured data (up to 16 key-value pairs) attached to the run. + public let metadata: [String: String]? + + // MARK: - Additional Types + + /// Represents an additional message to add to the thread. + public struct AdditionalMessage: Codable, Sendable { + + /// The role of the message sender (`user` or `assistant`). + public let role: String + + /// The message content (text or content parts). + public let content: Content + + /// Optional. Files attached to the message. + public let attachments: [Attachment]? + + // MARK: - Nested types + + /// Represents content of the message. + public enum Content: Codable, Sendable { + case text(String) + case parts([ContentPart]) + + public init(from decoder: Decoder) throws { + if let text = try? decoder.singleValueContainer().decode(String.self) { + self = .text(text) + } else { + let parts = try decoder.singleValueContainer().decode([ContentPart].self) + self = .parts(parts) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .text(let text): + try container.encode(text) + case .parts(let parts): + try container.encode(parts) + } + } + } + + /// Represents a content part (text or image). + public enum ContentPart: Codable, Sendable { + case text(TextContent) + case imageFile(ImageFileContent) + case imageURL(ImageURLContent) + + // Nested structures + public struct TextContent: Codable, Sendable { + public let type: String // Always "text" + public let text: String + } + + public struct ImageFileContent: Codable, Sendable { + public let type: String // Always "image_file" + public let imageFile: ImageFile + + public struct ImageFile: Codable, Sendable { + public let fileId: String + + enum CodingKeys: String, CodingKey { + case fileId = "file_id" + } + } + + enum CodingKeys: String, CodingKey { + case type + case imageFile = "image_file" + } + } + + public struct ImageURLContent: Codable, Sendable { + public let type: String // Always "image_url" + public let imageURL: ImageURL + + public struct ImageURL: Codable, Sendable { + public let url: String + public let detail: String? + + enum CodingKeys: String, CodingKey { + case url, detail + } + } + + enum CodingKeys: String, CodingKey { + case type + case imageURL = "image_url" + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + switch type { + case "text": + self = .text(try TextContent(from: decoder)) + case "image_file": + self = .imageFile(try ImageFileContent(from: decoder)) + case "image_url": + self = .imageURL(try ImageURLContent(from: decoder)) + default: + throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown type") + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .text(let content): + try content.encode(to: encoder) + case .imageFile(let content): + try content.encode(to: encoder) + case .imageURL(let content): + try content.encode(to: encoder) + } + } + + enum CodingKeys: String, CodingKey { + case type + } + } + + /// Represents an attachment of a file to the message. + public struct Attachment: Codable, Sendable { + /// ID of the attached file. + public let fileId: String + + /// Tools associated with this attachment. + public let tools: [AttachmentTool]? + + public struct AttachmentTool: Codable, Sendable { + /// Type of tool: `code_interpreter`, `file_search`. + public let type: String + } + + enum CodingKeys: String, CodingKey { + case fileId = "file_id" + case tools + } + } + } + + // MARK: - Initializer + + public init( + assistantId: String, + model: String? = nil, + instructions: String? = nil, + additionalInstructions: String? = nil, + additionalMessages: [AdditionalMessage]? = nil, + tools: [ASARun.Tool]? = nil, + toolChoice: ASARun.ToolChoice? = nil, + parallelToolCalls: Bool? = true, + temperature: Double? = 1.0, + topP: Double? = 1.0, + maxCompletionTokens: Int? = nil, + maxPromptTokens: Int? = nil, + truncationStrategy: ASARun.TruncationStrategy? = nil, + reasoningEffort: String? = "medium", + responseFormat: ASARun.ResponseFormat? = .auto, + stream: Bool? = nil, + metadata: [String: String]? = nil + ) { + self.assistantId = assistantId + self.model = model + self.instructions = instructions + self.additionalInstructions = additionalInstructions + self.additionalMessages = additionalMessages + self.tools = tools + self.toolChoice = toolChoice + self.parallelToolCalls = parallelToolCalls + self.temperature = temperature + self.topP = topP + self.maxCompletionTokens = maxCompletionTokens + self.maxPromptTokens = maxPromptTokens + self.truncationStrategy = truncationStrategy + self.reasoningEffort = reasoningEffort + self.responseFormat = responseFormat + self.stream = stream + self.metadata = metadata + } + + enum CodingKeys: String, CodingKey { + case assistantId = "assistant_id" + case model + case instructions + case additionalInstructions = "additional_instructions" + case additionalMessages = "additional_messages" + case tools + case toolChoice = "tool_choice" + case parallelToolCalls = "parallel_tool_calls" + case temperature + case topP = "top_p" + case maxCompletionTokens = "max_completion_tokens" + case maxPromptTokens = "max_prompt_tokens" + case truncationStrategy = "truncation_strategy" + case reasoningEffort = "reasoning_effort" + case responseFormat = "response_format" + case stream + case metadata + } +} diff --git a/Sources/AISwiftAssist/Models/Request/ASAListRunsParameters.swift b/Sources/AISwiftAssist/Models/Request/Run/ASAListRunsParameters.swift similarity index 92% rename from Sources/AISwiftAssist/Models/Request/ASAListRunsParameters.swift rename to Sources/AISwiftAssist/Models/Request/Run/ASAListRunsParameters.swift index f47a263..7755c8b 100644 --- a/Sources/AISwiftAssist/Models/Request/ASAListRunsParameters.swift +++ b/Sources/AISwiftAssist/Models/Request/Run/ASAListRunsParameters.swift @@ -8,7 +8,7 @@ import Foundation /// Parameters for listing runs in a thread. -public struct ASAListRunsParameters: Codable { +public struct ASAListRunsParameters: Codable, Sendable { /// Optional: A limit on the number of objects to be returned. public let limit: Int? diff --git a/Sources/AISwiftAssist/Models/Response/ASAAssistantFilesListResponse.swift b/Sources/AISwiftAssist/Models/Response/ASAAssistantFilesListResponse.swift deleted file mode 100644 index 0cbc0a1..0000000 --- a/Sources/AISwiftAssist/Models/Response/ASAAssistantFilesListResponse.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 12/5/23. -// - -import Foundation - -/// Represents a response containing a list of assistant files. -public struct ASAAssistantFilesListResponse: Codable { - /// The object type, which is always 'list'. - public let object: String - - /// The list of assistant files. - public let data: [ASAAssistantFile] - - /// The ID of the first file in the list. - public let firstId: String - - /// The ID of the last file in the list. - public let lastId: String - - /// Boolean indicating if there are more files available. - public let hasMore: Bool - - enum CodingKeys: String, CodingKey { - case data, firstId = "first_id", lastId = "last_id", hasMore = "has_more", object - } -} diff --git a/Sources/AISwiftAssist/Models/Response/ASADeleteModelResponse.swift b/Sources/AISwiftAssist/Models/Response/ASADeleteModelResponse.swift index cde3920..a4ec4b1 100644 --- a/Sources/AISwiftAssist/Models/Response/ASADeleteModelResponse.swift +++ b/Sources/AISwiftAssist/Models/Response/ASADeleteModelResponse.swift @@ -7,7 +7,7 @@ import Foundation -public struct ASADeleteModelResponse: Codable { +public struct ASADeleteModelResponse: Codable, Sendable { /// The model identifier, which can be referenced in the API endpoints. public let id: String diff --git a/Sources/AISwiftAssist/Models/Response/ASAMessageFilesListResponse.swift b/Sources/AISwiftAssist/Models/Response/ASAMessageFilesListResponse.swift deleted file mode 100644 index 7128909..0000000 --- a/Sources/AISwiftAssist/Models/Response/ASAMessageFilesListResponse.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 12/5/23. -// - -import Foundation - -/// Represents a response containing a list of assistant files. -public struct ASAMessageFilesListResponse: Codable { - /// The object type, which is always 'list'. - public let object: String - - /// The list of assistant files. - public let data: [ASAMessageFile] - - /// The ID of the first file in the list. - public let firstId: String - - /// The ID of the last file in the list. - public let lastId: String - - /// Boolean indicating if there are more files available. - public let hasMore: Bool - - enum CodingKeys: String, CodingKey { - case data, firstId = "first_id", lastId = "last_id", hasMore = "has_more", object - } -} diff --git a/Sources/AISwiftAssist/Models/Response/ASAModelsListResponse.swift b/Sources/AISwiftAssist/Models/Response/ASAModelsListResponse.swift deleted file mode 100644 index 54cac26..0000000 --- a/Sources/AISwiftAssist/Models/Response/ASAModelsListResponse.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 11/17/23. -// - -import Foundation - -public struct ASAModelsListResponse: Codable { - - /// The object type, which is always "list". - public let object: String - - /// The deletion status of the model. - public let data: [ASAModel] - - enum CodingKeys: String, CodingKey { - case object, data - } -} diff --git a/Sources/AISwiftAssist/Models/Response/ASARunStepsListResponse.swift b/Sources/AISwiftAssist/Models/Response/ASARunStepsListResponse.swift index b6f725b..1648d79 100644 --- a/Sources/AISwiftAssist/Models/Response/ASARunStepsListResponse.swift +++ b/Sources/AISwiftAssist/Models/Response/ASARunStepsListResponse.swift @@ -8,7 +8,7 @@ import Foundation /// Represents a response containing a list of run steps. -public struct ASARunStepsListResponse: Codable { +public struct ASARunStepsListResponse: Codable, Sendable { /// The object type, which is always 'list'. public let object: String diff --git a/Sources/AISwiftAssist/Models/Response/ASAAssistantsListResponse.swift b/Sources/AISwiftAssist/Models/Response/Assistant/ASAAssistantsListResponse.swift similarity index 92% rename from Sources/AISwiftAssist/Models/Response/ASAAssistantsListResponse.swift rename to Sources/AISwiftAssist/Models/Response/Assistant/ASAAssistantsListResponse.swift index b81619e..64eed4c 100644 --- a/Sources/AISwiftAssist/Models/Response/ASAAssistantsListResponse.swift +++ b/Sources/AISwiftAssist/Models/Response/Assistant/ASAAssistantsListResponse.swift @@ -8,7 +8,7 @@ import Foundation /// A response structure for listing assistants. -public struct ASAAssistantsListResponse: Codable { +public struct ASAAssistantsListResponse: Codable, Sendable { /// The object type, which is always 'list'. public let object: String diff --git a/Sources/AISwiftAssist/Models/Response/ASAMessagesListResponse.swift b/Sources/AISwiftAssist/Models/Response/Messages/ASAMessagesListResponse.swift similarity index 92% rename from Sources/AISwiftAssist/Models/Response/ASAMessagesListResponse.swift rename to Sources/AISwiftAssist/Models/Response/Messages/ASAMessagesListResponse.swift index 6bdebe7..87ae6a0 100644 --- a/Sources/AISwiftAssist/Models/Response/ASAMessagesListResponse.swift +++ b/Sources/AISwiftAssist/Models/Response/Messages/ASAMessagesListResponse.swift @@ -8,7 +8,7 @@ import Foundation /// Represents a response containing a list of messages. -public struct ASAMessagesListResponse: Codable { +public struct ASAMessagesListResponse: Codable, Sendable { /// The object type, which is always 'list'. public let object: String diff --git a/Sources/AISwiftAssist/Models/Response/ASARunsListResponse.swift b/Sources/AISwiftAssist/Models/Response/Runs/ASARunsListResponse.swift similarity index 92% rename from Sources/AISwiftAssist/Models/Response/ASARunsListResponse.swift rename to Sources/AISwiftAssist/Models/Response/Runs/ASARunsListResponse.swift index 9e6a066..e5daed1 100644 --- a/Sources/AISwiftAssist/Models/Response/ASARunsListResponse.swift +++ b/Sources/AISwiftAssist/Models/Response/Runs/ASARunsListResponse.swift @@ -8,7 +8,7 @@ import Foundation /// Represents a response containing a list of runs. -public struct ASARunsListResponse: Codable { +public struct ASARunsListResponse: Codable, Sendable { /// The object type, which is always 'list'. public let object: String diff --git a/Tests/AISwiftAssistTests/APIs/AssistantsAPITests.swift b/Tests/AISwiftAssistTests/APIs/AssistantsAPITests.swift index d0962dc..93b7b2b 100644 --- a/Tests/AISwiftAssistTests/APIs/AssistantsAPITests.swift +++ b/Tests/AISwiftAssistTests/APIs/AssistantsAPITests.swift @@ -5,278 +5,98 @@ // Created by Alexey on 11/19/23. // -import XCTest +import Foundation +import Testing @testable import AISwiftAssist -final class AssistantsAPITests: XCTestCase { +struct AssistantsAPITests { - var assistantsAPI: IAssistantsAPI! - - override func setUp() { - super.setUp() + private let api: any IAssistantsAPI = { let configuration = URLSessionConfiguration.default configuration.protocolClasses = [MockURLProtocol.self] - let mockURLSession = URLSession(configuration: configuration) - assistantsAPI = AssistantsAPI(urlSession: mockURLSession) - } - - override func tearDown() { - assistantsAPI = nil - super.tearDown() - } - - func testGetAssistants() async { - do { - // Simulate server response - let mockData = AssistantsAPITests.list.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let response: ASAAssistantsListResponse = try await assistantsAPI.get(with: nil) - - XCTAssertNotNil(response) - XCTAssertEqual(response.object, "list") - XCTAssertEqual(response.data.count, 3) - XCTAssertEqual(response.firstId, "asst_abc123") - XCTAssertEqual(response.lastId, "asst_abc789") - XCTAssertFalse(response.hasMore) - - // Checks for specific assistants - XCTAssertEqual(response.data[0].id, "asst_abc123") - XCTAssertEqual(response.data[0].objectType, "assistant") - XCTAssertEqual(response.data[0].createdAt, 1698982736) - XCTAssertEqual(response.data[0].name, "Coding Tutor") - XCTAssertEqual(response.data[0].model, "gpt-4") - XCTAssertEqual(response.data[0].instructions, "You are a helpful assistant designed to make me better at coding!") - - XCTAssertEqual(response.data[1].id, "asst_abc456") - XCTAssertEqual(response.data[1].objectType, "assistant") - XCTAssertEqual(response.data[1].createdAt, 1698982718) - XCTAssertEqual(response.data[1].name, "My Assistant") - XCTAssertEqual(response.data[1].model, "gpt-4") - XCTAssertEqual(response.data[1].instructions, "You are a helpful assistant designed to make me better at coding!") - - XCTAssertEqual(response.data[2].id, "asst_abc789") - XCTAssertEqual(response.data[2].objectType, "assistant") - XCTAssertEqual(response.data[2].createdAt, 1698982643) - XCTAssertNil(response.data[2].name) - XCTAssertEqual(response.data[2].model, "gpt-4") - XCTAssertNil(response.data[2].instructions) - } catch { - XCTFail("Error: \(error)") - } - } - - func testCreateAssistant() async { - do { - // Simulate server response - let mockData = AssistantsAPITests.create.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let createRequest = ASACreateAssistantRequest(model: "gpt-4", - name: "Math Tutor", - instructions: "You are a personal math tutor. When asked a question, write and run Python code to answer the question.") - - let assistant: ASAAssistant = try await assistantsAPI.create(by: createRequest) - - // Checks - XCTAssertEqual(assistant.id, "asst_abc123") - XCTAssertEqual(assistant.objectType, "assistant") - XCTAssertEqual(assistant.createdAt, 1698984975) - XCTAssertEqual(assistant.name, "Math Tutor") - XCTAssertEqual(assistant.model, "gpt-4") - XCTAssertEqual(assistant.instructions, "You are a personal math tutor. When asked a question, write and run Python code to answer the question.") - XCTAssertEqual(assistant.tools.count, 1) - XCTAssertEqual(assistant.tools.first?.type, "code_interpreter") - XCTAssertTrue(assistant.fileIds.isEmpty) - XCTAssertTrue(assistant.metadata?.isEmpty ?? true) - } catch { - XCTFail("Error: \(error)") - } - } - - func testRetrieveAssistant() async { - do { - // Simulate server response - let mockData = AssistantsAPITests.retrieve.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let assistantId = "asst_abc123" - let assistant: ASAAssistant = try await assistantsAPI.retrieve(by: assistantId) - - // Checks - XCTAssertEqual(assistant.id, assistantId) - XCTAssertEqual(assistant.objectType, "assistant") - XCTAssertEqual(assistant.createdAt, 1699009709) - XCTAssertEqual(assistant.name, "HR Helper") - XCTAssertEqual(assistant.model, "gpt-4") - XCTAssertEqual(assistant.instructions, "You are an HR bot, and you have access to files to answer employee questions about company policies.") - XCTAssertEqual(assistant.tools.count, 1) - XCTAssertEqual(assistant.tools.first?.type, "retrieval") - XCTAssertEqual(assistant.fileIds, ["file-abc123"]) - XCTAssertTrue(assistant.metadata?.isEmpty ?? true) - } catch { - XCTFail("Error: \(error)") - } - } - - func testModifyAssistant() async { - do { - // Simulate server response - let mockData = AssistantsAPITests.modify.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let assistantId = "asst_abc123" - let modifyRequest = ASAModifyAssistantRequest( - instructions: "You are an HR bot, and you have access to files to answer employee questions about company policies. Always response with info from either of the files.", - fileIds: ["file-abc123", "file-abc456"] - ) - - let modifiedAssistant: ASAAssistant = try await assistantsAPI.modify(by: assistantId, - modifyAssistant: modifyRequest) - - // Checks - XCTAssertEqual(modifiedAssistant.id, assistantId) - XCTAssertEqual(modifiedAssistant.objectType, "assistant") - XCTAssertEqual(modifiedAssistant.createdAt, 1699009709) - XCTAssertEqual(modifiedAssistant.name, "HR Helper") - XCTAssertEqual(modifiedAssistant.model, "gpt-4") - XCTAssertEqual(modifiedAssistant.instructions, modifyRequest.instructions) - XCTAssertEqual(modifiedAssistant.tools.count, 1) - XCTAssertEqual(modifiedAssistant.tools.first?.type, "retrieval") - XCTAssertEqual(modifiedAssistant.fileIds, modifyRequest.fileIds) - XCTAssertTrue(modifiedAssistant.metadata?.isEmpty ?? true) - } catch { - XCTFail("Error: \(error)") - } - } - - func testDeleteAssistant() async { - do { - // Simulate server response - let mockData = AssistantsAPITests.delete.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let assistantId = "asst_abc123" - let deleteResponse: ASADeleteModelResponse = try await assistantsAPI.delete(by: assistantId) - - // Checks - XCTAssertEqual(deleteResponse.id, assistantId) - XCTAssertEqual(deleteResponse.object, "assistant.deleted") - XCTAssertTrue(deleteResponse.deleted) - } catch { - XCTFail("Error: \(error)") - } + let session = URLSession(configuration: configuration) + let api = AssistantsAPI(urlSession: session) + return api + }() + + @Test + func testGetAssistants_Success() async throws { + + + await MockURLProtocol.setHandler ({ request in + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: ["Content-Type": "application/json"] + )! + return (response, Self.list.data(using: .utf8)!) + }, for: "getAssistants") + + let result = try await api.get(with: nil) + + #expect(result.data.count == 7) + #expect(result.data.first?.id == "asst_advanced_full") + #expect(result.data.first?.name == "Advanced Assistant") + #expect(result.data.first?.model == "gpt-4-turbo") + #expect(result.hasMore == false) } - func testCreateFile() async { - do { - // Simulate server response - let mockData = AssistantsAPITests.createFile.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let createRequest = ASACreateAssistantFileRequest(fileId: "file-abc123") - let file: ASAAssistantFile = try await assistantsAPI.createFile(for: "asst_abc123", with: createRequest) - - // Checks - XCTAssertEqual(file.id, "file-abc123") - XCTAssertEqual(file.objectType, "assistant.file") - XCTAssertEqual(file.createdAt, 1699055364) - XCTAssertEqual(file.assistantId, "asst_abc123") - } catch { - XCTFail("Error: \(error)") - } - } - - func testRetrieveFile() async { - do { - // Simulate server response - let mockData = AssistantsAPITests.retrieveFile.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let file: ASAAssistantFile = try await assistantsAPI.retrieveFile(for: "asst_abc123", fileId: "file-abc123") - - // Checks - XCTAssertEqual(file.id, "file-abc123") - XCTAssertEqual(file.objectType, "assistant.file") - XCTAssertEqual(file.createdAt, 1699055364) - XCTAssertEqual(file.assistantId, "asst_abc123") - } catch { - XCTFail("Error: \(error)") - } - } - - func testDeleteFile() async { - do { - // Simulate server response - let mockData = AssistantsAPITests.deleteFile.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let deleteResponse: ASADeleteModelResponse = try await assistantsAPI.deleteFile(for: "asst_abc123", fileId: "file-abc123") - - // Checks - XCTAssertEqual(deleteResponse.id, "file-abc123") - XCTAssertEqual(deleteResponse.object, "assistant.file.deleted") - XCTAssertTrue(deleteResponse.deleted) - } catch { - XCTFail("Error: \(error)") - } + @Test + func testRetrieveAssistant_Success() async throws { + let configuration = URLSessionConfiguration.ephemeral + configuration.protocolClasses = [MockURLProtocol.self] + let session = URLSession(configuration: configuration) + let assistantAPI = AssistantsAPI(urlSession: session) + + await MockURLProtocol.setHandler ({ request in + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: ["Content-Type": "application/json"] + )! + return (response, Self.retrieve.data(using: .utf8)!) + }, for: "retrieveAssistant") + + let assistant = try await assistantAPI.retrieve(by: "asst_advanced_full") + + #expect(assistant.id == "asst_advanced_full") + #expect(assistant.name == "Advanced Assistant") + #expect(assistant.model == "gpt-4-turbo") + #expect(assistant.tools.count == 3) } - func testListFiles() async { - do { - // Simulate server response - let mockData = AssistantsAPITests.listFiles.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let listParameters = ASAListAssistantsParameters() - let fileList: ASAAssistantFilesListResponse = try await assistantsAPI.listFiles(for: "asst_abc123", with: listParameters) - - // Checks - XCTAssertEqual(fileList.object, "list") - XCTAssertEqual(fileList.data.count, 2) - XCTAssertEqual(fileList.firstId, "file-abc123") - XCTAssertEqual(fileList.lastId, "file-abc456") - XCTAssertFalse(fileList.hasMore) - XCTAssertEqual(fileList.data[0].id, "file-abc123") - XCTAssertEqual(fileList.data[1].id, "file-abc456") - } catch { - XCTFail("Error: \(error)") - } + @Test + func testModifyAssistant_Success() async throws { + let configuration = URLSessionConfiguration.ephemeral + configuration.protocolClasses = [MockURLProtocol.self] + let session = URLSession(configuration: configuration) + let assistantAPI = AssistantsAPI(urlSession: session) + + await MockURLProtocol.setHandler ({ request in + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: ["Content-Type": "application/json"] + )! + return (response, Self.modify.data(using: .utf8)!) + }, for: "modifyAssistant") + + let modifyRequest = ASAModifyAssistantRequest( + model: "gpt-4", + name: "Advanced", + instructions: "You are a professional assistant with advanced functionality, tool support, and strict output controls." + ) + + let assistant = try await assistantAPI.modify( + by: "asst_advanced_full", + modifyAssistant: modifyRequest + ) + + #expect(assistant.id == "asst_advanced_full") + #expect(assistant.name == "Advanced") + #expect(assistant.model == "gpt-4") } } diff --git a/Tests/AISwiftAssistTests/APIs/MessagesAPITests.swift b/Tests/AISwiftAssistTests/APIs/MessagesAPITests.swift deleted file mode 100644 index fe44858..0000000 --- a/Tests/AISwiftAssistTests/APIs/MessagesAPITests.swift +++ /dev/null @@ -1,305 +0,0 @@ -// -// MessagesAPITests.swift -// -// -// Created by Alexey on 11/20/23. -// - -import XCTest -@testable import AISwiftAssist - -final class MessagesAPITests: XCTestCase { - - var messagesAPI: IMessagesAPI! - - override func setUp() { - super.setUp() - let configuration = URLSessionConfiguration.default - configuration.protocolClasses = [MockURLProtocol.self] - let mockURLSession = URLSession(configuration: configuration) - messagesAPI = MessagesAPI(urlSession: mockURLSession) - } - - override func tearDown() { - messagesAPI = nil - super.tearDown() - } - - func testCreateMessage() async { - do { - let mockData = Self.create.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let createMessageRequest = ASACreateMessageRequest(role: "user", content: "How does AI work? Explain it in simple terms.") - let message: ASAMessage = try await messagesAPI.create(by: "thread123", createMessage: createMessageRequest) - - XCTAssertEqual(message.id, "12345") - XCTAssertEqual(message.object, "thread.message") - XCTAssertEqual(message.createdAt, 1639530000) - XCTAssertEqual(message.threadId, "thread123") - XCTAssertEqual(message.role, "assistant") - XCTAssertEqual(message.content.count, 2) - - if let firstContent = message.content.first { - switch firstContent { - case .text(let textContent): - XCTAssertEqual(textContent.value, "This is a text message with annotations.") - XCTAssertEqual(textContent.annotations?.count, 2) - default: - XCTFail("First content is not of type text.") - } - } else { - XCTFail("First content is empty") - } - - if message.content.count > 1 { - switch message.content[1] { - case .image(let imageContent): - XCTAssertEqual(imageContent.file_id, "image789") - default: - XCTFail("Second content is not of type image.") - } - } else { - XCTFail("Second content is missing.") - } - - XCTAssertEqual(message.assistantId, "assistant123") - XCTAssertEqual(message.runId, "run123") - XCTAssertEqual(message.fileIds, ["file123", "file456", "image789"]) - XCTAssertEqual(message.metadata?["key1"], "value1") - XCTAssertEqual(message.metadata?["key2"], "value2") - } catch { - XCTFail("Error: \(error)") - } - } - - - func testRetrieveMessage() async { - do { - let mockData = Self.retrieve.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let message: ASAMessage = try await messagesAPI.retrieve(by: "thread123", messageId: "12345") - - XCTAssertEqual(message.id, "12345") - XCTAssertEqual(message.object, "thread.message") - XCTAssertEqual(message.createdAt, 1639530000) - XCTAssertEqual(message.threadId, "thread123") - XCTAssertEqual(message.role, "assistant") - XCTAssertEqual(message.content.count, 2) - - if let firstContent = message.content.first { - switch firstContent { - case .text(let textContent): - XCTAssertEqual(textContent.value, "This is a text message with annotations.") - XCTAssertEqual(textContent.annotations?.count, 2) - - if let firstAnnotation = textContent.annotations?.first { - XCTAssertEqual(firstAnnotation.type, "file_citation") - XCTAssertEqual(firstAnnotation.text, "document link") - XCTAssertEqual(firstAnnotation.startIndex, 0) - XCTAssertEqual(firstAnnotation.endIndex, 23) - XCTAssertEqual(firstAnnotation.fileCitation?.fileId, "file123") - XCTAssertEqual(firstAnnotation.fileCitation?.quote, "A quote from the file") - } else { - XCTFail("First annotation not found.") - } - default: - XCTFail("First content is not of type text.") - } - } else { - XCTFail("Content is empty") - } - - if let secondContent = message.content.last { - switch secondContent { - case .image(let imageContent): - XCTAssertEqual(imageContent.file_id, "image789") - default: - XCTFail("Second content is not of type image.") - } - } else { - XCTFail("Second content is missing.") - } - - XCTAssertEqual(message.assistantId, "assistant123") - XCTAssertEqual(message.runId, "run123") - XCTAssertEqual(message.fileIds, ["file123", "file456", "image789"]) - XCTAssertEqual(message.metadata?["key1"], "value1") - XCTAssertEqual(message.metadata?["key2"], "value2") - } catch { - XCTFail("Error: \(error)") - } - } - - - func testModifyMessage() async { - do { - let mockData = Self.modify.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let modifyMessageRequest = ASAModifyMessageRequest(metadata: ["modified": "true", - "user": "abc123"]) - let message: ASAMessage = try await messagesAPI.modify(by: "thread123", - messageId: "12345", - modifyMessage: modifyMessageRequest) - - XCTAssertEqual(message.id, "12345") - XCTAssertEqual(message.object, "thread.message") - XCTAssertEqual(message.createdAt, 1639530000) - XCTAssertEqual(message.threadId, "thread123") - XCTAssertEqual(message.role, "assistant") - - if let firstContent = message.content.first { - switch firstContent { - case .text(let textContent): - XCTAssertEqual(textContent.value, "This is a text message with annotations.") - XCTAssertEqual(textContent.annotations?.count, 2) - default: - XCTFail("First content is not of type text.") - } - } else { - XCTFail("Content is empty") - } - - if message.content.count > 1 { - switch message.content[1] { - case .image(let imageContent): - XCTAssertEqual(imageContent.file_id, "image789") - default: - XCTFail("Second content is not of type image.") - } - } else { - XCTFail("Second content is missing.") - } - - XCTAssertEqual(message.assistantId, "assistant123") - XCTAssertEqual(message.runId, "run123") - XCTAssertEqual(message.fileIds, ["file123", "file456", "image789"]) - XCTAssertEqual(message.metadata?["key1"], "value1") - XCTAssertEqual(message.metadata?["key2"], "value2") - } catch { - XCTFail("Error: \(error)") - } - } - - - func testListMessages() async { - do { - let mockData = Self.list.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let listResponse: ASAMessagesListResponse = try await messagesAPI.getMessages(by: "thread_abc123", parameters: nil) - - XCTAssertEqual(listResponse.object, "list") - XCTAssertEqual(listResponse.data.count, 3) - XCTAssertEqual(listResponse.data[0].id, "msg_SM7CyvQn3UnrAyOh96TGN5jR") - XCTAssertEqual(listResponse.data[0].threadId, "thread_11LlFPiPpEw7WhZr0AqB2WhF") - XCTAssertEqual(listResponse.data[0].role, "assistant") - - // Тестирование содержимого первого сообщения - if let firstContent = listResponse.data[0].content.first { - switch firstContent { - case .text(let textContent): - XCTAssertEqual(textContent.value, "I now have full annotations for every date mentioned in the file") - XCTAssertEqual(textContent.annotations?.count, 4) - - if let firstAnnotation = textContent.annotations?.first { - XCTAssertEqual(firstAnnotation.type, "file_citation") - XCTAssertEqual(firstAnnotation.text, "【21†source】") - XCTAssertEqual(firstAnnotation.startIndex, 136) - XCTAssertEqual(firstAnnotation.endIndex, 147) - XCTAssertEqual(firstAnnotation.fileCitation?.fileId, "") - XCTAssertEqual(firstAnnotation.fileCitation?.quote, "adfadlfkjamdf") - } else { - XCTFail("First annotation not found.") - } - - if let firstAnnotation = textContent.annotations?.last { - XCTAssertEqual(firstAnnotation.type, "file_citation") - XCTAssertEqual(firstAnnotation.text, "【33†source】") - XCTAssertEqual(firstAnnotation.startIndex, 378) - XCTAssertEqual(firstAnnotation.endIndex, 389) - XCTAssertEqual(firstAnnotation.fileCitation?.fileId, "") - XCTAssertEqual(firstAnnotation.fileCitation?.quote, "DXB") - } else { - XCTFail("First annotation not found.") - } - default: - XCTFail("First content is not of type text.") - } - } else { - XCTFail("Content is empty") - } - - XCTAssertEqual(listResponse.data[1].id, "msg_oacFAKp8WbIKYnV2Wmsyh5aE") - XCTAssertEqual(listResponse.data[1].threadId, "thread_11LlFPiPpEw7WhZr0AqB2WhF") - XCTAssertEqual(listResponse.data[1].role, "assistant") - - XCTAssertEqual(listResponse.data[2].id, "msg_V8hf7PCvWceW4DpQKpQV83Ia") - XCTAssertEqual(listResponse.data[2].threadId, "thread_11LlFPiPpEw7WhZr0AqB2WhF") - XCTAssertEqual(listResponse.data[2].role, "user") - } catch { - XCTFail("Error: \(error)") - } - } - - - func testRetrieveFile() async { - do { - let mockData = Self.retrieveFile.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let file: ASAMessageFile = try await messagesAPI.retrieveFile(by: "thread_abc123", messageId: "msg_abc123", fileId: "file-abc123") - - XCTAssertEqual(file.id, "file-abc123") - XCTAssertEqual(file.object, "thread.message.file") - XCTAssertEqual(file.createdAt, 1698107661) - XCTAssertEqual(file.messageId, "message_QLoItBbqwyAJEzlTy4y9kOMM") - } catch { - XCTFail("Error: \(error)") - } - } - - func testListFiles() async { - do { - let mockData = Self.listFiles.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let listResponse: ASAMessageFilesListResponse = try await messagesAPI.listFiles(by: "thread_abc123", messageId: "msg_abc123", parameters: nil) - - XCTAssertEqual(listResponse.object, "list") - XCTAssertEqual(listResponse.data.count, 2) - XCTAssertEqual(listResponse.data[0].id, "file-abc123") - XCTAssertEqual(listResponse.data[0].createdAt, 1698107661) - XCTAssertEqual(listResponse.data[0].messageId, "message_QLoItBbqwyAJEzlTy4y9kOMM") - } catch { - XCTFail("Error: \(error)") - } - } - -} diff --git a/Tests/AISwiftAssistTests/APIs/ModelsAPITests.swift b/Tests/AISwiftAssistTests/APIs/ModelsAPITests.swift deleted file mode 100644 index f386526..0000000 --- a/Tests/AISwiftAssistTests/APIs/ModelsAPITests.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// ModelsAPITests.swift -// -// -// Created by Alexey on 11/20/23. -// - -import XCTest -@testable import AISwiftAssist - -final class ModelsAPITests: XCTestCase { - - var modelsAPI: IModelsAPI! - - override func setUp() { - super.setUp() - let configuration = URLSessionConfiguration.default - configuration.protocolClasses = [MockURLProtocol.self] - let mockURLSession = URLSession(configuration: configuration) - modelsAPI = ModelsAPI(urlSession: mockURLSession) - } - - override func tearDown() { - modelsAPI = nil - super.tearDown() - } - - func testListModels() async { - do { - // Simulate server response - let mockData = Self.list.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let listResponse: ASAModelsListResponse = try await modelsAPI.get() - - XCTAssertEqual(listResponse.data[0].id, "model-id-0") - XCTAssertEqual(listResponse.data[0].object, "model") - XCTAssertEqual(listResponse.data[0].created, 1686935002) - XCTAssertEqual(listResponse.data[0].ownedBy, "organization-owner") - - XCTAssertEqual(listResponse.data[1].id, "model-id-1") - XCTAssertEqual(listResponse.data[1].object, "model") - XCTAssertEqual(listResponse.data[1].created, 1686935002) - XCTAssertEqual(listResponse.data[1].ownedBy, "organization-owner") - - XCTAssertEqual(listResponse.data[2].id, "model-id-2") - XCTAssertEqual(listResponse.data[2].object, "model") - XCTAssertEqual(listResponse.data[2].created, 1686935002) - XCTAssertEqual(listResponse.data[2].ownedBy, "openai") - } catch { - XCTFail("Error: \(error)") - } - } - - func testRetrieveModel() async { - do { - // Simulate server response - let mockData = Self.retrieve.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let modelId = "gpt-3.5-turbo-instruct" - let model: ASAModel = try await modelsAPI.retrieve(by: modelId) - - XCTAssertEqual(model.id, modelId) - XCTAssertEqual(model.object, "model") - XCTAssertEqual(model.created, 1686935002) - XCTAssertEqual(model.ownedBy, "openai") - } catch { - XCTFail("Error: \(error)") - } - } - - func testDeleteModel() async { - do { - // Simulate server response - let mockData = Self.delete.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let modelId = "ft:gpt-3.5-turbo:acemeco:suffix:abc123" - let deleteResponse: ASADeleteModelResponse = try await modelsAPI.delete(by: modelId) - - XCTAssertEqual(deleteResponse.id, "ft:gpt-3.5-turbo:acemeco:suffix:abc123") - XCTAssertEqual(deleteResponse.object, "model") - XCTAssertTrue(deleteResponse.deleted) - } catch { - XCTFail("Error: \(error)") - } - } - -} diff --git a/Tests/AISwiftAssistTests/APIs/RunsAPITests.swift b/Tests/AISwiftAssistTests/APIs/RunsAPITests.swift deleted file mode 100644 index 868582a..0000000 --- a/Tests/AISwiftAssistTests/APIs/RunsAPITests.swift +++ /dev/null @@ -1,358 +0,0 @@ -// -// RunsAPITests.swift -// -// -// Created by Alexey on 11/20/23. -// - -import XCTest -@testable import AISwiftAssist - -final class RunsAPITests: XCTestCase { - - var runsAPI: IRunsAPI! - - override func setUp() { - super.setUp() - let configuration = URLSessionConfiguration.default - configuration.protocolClasses = [MockURLProtocol.self] - let mockURLSession = URLSession(configuration: configuration) - runsAPI = RunsAPI(urlSession: mockURLSession) - } - - override func tearDown() { - runsAPI = nil - super.tearDown() - } - - func testCreateRun() async { - do { - let mockData = Self.create.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let createRunRequest = ASACreateRunRequest(assistantId: "asst_abc123") - let run: ASARun = try await runsAPI.create(by: "thread_abc123", - createRun: createRunRequest) - - XCTAssertEqual(run.id, "run_abc123") - XCTAssertEqual(run.object, "thread.run") - XCTAssertEqual(run.createdAt, 1699063290) - XCTAssertEqual(run.assistantId, "asst_abc123") - XCTAssertEqual(run.threadId, "thread_abc123") - XCTAssertEqual(run.status, "queued") - XCTAssertEqual(run.startedAt, 1699063290) - XCTAssertNil(run.expiresAt) - XCTAssertNil(run.cancelledAt) - XCTAssertNil(run.failedAt) - XCTAssertEqual(run.completedAt, 1699063291) - XCTAssertNil(run.lastError) - XCTAssertEqual(run.model, "gpt-4") - XCTAssertNil(run.instructions) - XCTAssertEqual(run.tools.count, 1) - XCTAssertEqual(run.tools.first?.type, "code_interpreter") - XCTAssertEqual(run.fileIds, ["file-abc123", "file-abc456"]) - XCTAssertTrue(run.metadata?.isEmpty ?? true) - } catch { - XCTFail("Error: \(error.localizedDescription)") - } - } - - func testRetrieveRun() async { - do { - let mockData = Self.retrieve.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let run: ASARun = try await runsAPI.retrieve(by: "thread_abc123", - runId: "run_abc123") - - XCTAssertEqual(run.id, "run_abc123") - XCTAssertEqual(run.object, "thread.run") - XCTAssertEqual(run.createdAt, 1699075072) - XCTAssertEqual(run.assistantId, "asst_abc123") - XCTAssertEqual(run.threadId, "thread_abc123") - XCTAssertEqual(run.status, "completed") - XCTAssertEqual(run.startedAt, 1699075072) - XCTAssertNil(run.expiresAt) - XCTAssertNil(run.cancelledAt) - XCTAssertNil(run.failedAt) - XCTAssertEqual(run.completedAt, 1699075073) - XCTAssertNil(run.lastError) - XCTAssertEqual(run.model, "gpt-3.5-turbo") - XCTAssertNil(run.instructions) - XCTAssertEqual(run.tools.count, 1) - XCTAssertEqual(run.tools.first?.type, "code_interpreter") - XCTAssertEqual(run.fileIds, ["file-abc123", "file-abc456"]) - XCTAssertTrue(run.metadata?.isEmpty ?? true) - } catch { - XCTFail("Error: \(error)") - } - } - - func testModifyRun() async { - do { - let mockData = Self.modify.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let modifyRunRequest = ASAModifyRunRequest(metadata: ["user_id": "user_abc123"]) - let run: ASARun = try await runsAPI.modify(by: "thread_abc123", - runId: "run_abc123", - modifyRun: modifyRunRequest) - - XCTAssertEqual(run.id, "run_abc123") - XCTAssertEqual(run.object, "thread.run") - XCTAssertEqual(run.createdAt, 1699075072) - XCTAssertEqual(run.assistantId, "asst_abc123") - XCTAssertEqual(run.threadId, "thread_abc123") - XCTAssertEqual(run.status, "completed") - XCTAssertEqual(run.startedAt, 1699075072) - XCTAssertNil(run.expiresAt) - XCTAssertNil(run.cancelledAt) - XCTAssertNil(run.failedAt) - XCTAssertEqual(run.completedAt, 1699075073) - XCTAssertNil(run.lastError) - XCTAssertEqual(run.model, "gpt-3.5-turbo") - XCTAssertNil(run.instructions) - XCTAssertEqual(run.tools.count, 1) - XCTAssertEqual(run.tools.first?.type, "code_interpreter") - XCTAssertEqual(run.fileIds, ["file-abc123", "file-abc456"]) - XCTAssertEqual(run.metadata?["user_id"], "user_abc123") - } catch { - XCTFail("Error: \(error)") - } - } - - func testListRuns() async { - do { - let mockData = Self.list.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let listResponse: ASARunsListResponse = try await runsAPI.listRuns(by: "thread_abc123", - parameters: nil) - - XCTAssertEqual(listResponse.object, "list") - XCTAssertEqual(listResponse.data.count, 2) - - let firstRun = listResponse.data[0] - XCTAssertEqual(firstRun.id, "run_abc123") - XCTAssertEqual(firstRun.object, "thread.run") - XCTAssertEqual(firstRun.createdAt, 1699075072) - XCTAssertEqual(firstRun.assistantId, "asst_abc123") - XCTAssertEqual(firstRun.threadId, "thread_abc123") - XCTAssertEqual(firstRun.status, "completed") - XCTAssertEqual(firstRun.startedAt, 1699075072) - XCTAssertNil(firstRun.expiresAt) - XCTAssertNil(firstRun.cancelledAt) - XCTAssertNil(firstRun.failedAt) - XCTAssertEqual(firstRun.completedAt, 1699075073) - XCTAssertNil(firstRun.lastError) - XCTAssertEqual(firstRun.model, "gpt-3.5-turbo") - XCTAssertNil(firstRun.instructions) - XCTAssertEqual(firstRun.tools.count, 1) - XCTAssertEqual(firstRun.tools[0].type, "code_interpreter") - XCTAssertEqual(firstRun.fileIds, ["file-abc123", "file-abc456"]) - XCTAssertTrue(firstRun.metadata?.isEmpty ?? true) - - XCTAssertEqual(listResponse.firstId, "run_abc123") - XCTAssertEqual(listResponse.lastId, "run_abc456") - XCTAssertFalse(listResponse.hasMore) - } catch { - XCTFail("Error: \(error)") - } - } - - func testSubmitToolOutputs() async { - do { - let mockData = RunsAPITests.submitToolOutputs.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - XCTAssertEqual(request.url?.path, "/v1/threads/thread_abc123/runs/run_abc123/submit_tool_outputs") - XCTAssertEqual(request.httpMethod, "POST") - // Проверка тела запроса может быть добавлена здесь - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let toolOutputs = [ASAToolOutput(toolCallId: "call_abc123", output: "28C")] - let run: ASARun = try await runsAPI.submitToolOutputs(by: "thread_abc123", runId: "run_abc123", toolOutputs: toolOutputs) - - XCTAssertEqual(run.id, "run_abc123") - XCTAssertEqual(run.object, "thread.run") - XCTAssertEqual(run.createdAt, 1699063291) - XCTAssertEqual(run.threadId, "thread_abc123") - XCTAssertEqual(run.assistantId, "asst_abc123") - XCTAssertEqual(run.status, "completed") - XCTAssertEqual(run.startedAt, 1699063292) - XCTAssertEqual(run.expiresAt, 1699066891) - XCTAssertNil(run.cancelledAt) - XCTAssertNil(run.failedAt) - XCTAssertEqual(run.completedAt, 1699063391) - XCTAssertNil(run.lastError) - XCTAssertEqual(run.model, "gpt-3.5-turbo") - XCTAssertEqual(run.instructions, "You are a helpful assistant.") - XCTAssertEqual(run.tools.count, 1) - XCTAssertEqual(run.tools.first?.type, "function") - XCTAssertEqual(run.fileIds, ["file-abc123"]) - XCTAssertEqual(run.metadata?["additional_info"], "test") - } catch { - XCTFail("Error: \(error.localizedDescription)") - } - } - - func testCancelRun() async { - do { - - let mockData = RunsAPITests.cancelRun.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - XCTAssertEqual(request.url?.path, "/v1/threads/thread_abc123/runs/run_abc123/cancel") - XCTAssertEqual(request.httpMethod, "POST") - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let run: ASARun = try await runsAPI.cancelRun(by: "thread_abc123", runId: "run_abc123") - - XCTAssertEqual(run.id, "run_abc123") - XCTAssertEqual(run.object, "thread.run") - XCTAssertEqual(run.status, "cancelled") - XCTAssertEqual(run.createdAt, 1699075072) - XCTAssertEqual(run.assistantId, "asst_abc123") - XCTAssertEqual(run.threadId, "thread_abc123") - XCTAssertEqual(run.status, "cancelled") - XCTAssertEqual(run.startedAt, 1699075072) - XCTAssertEqual(run.expiresAt, 1699075672) - XCTAssertEqual(run.cancelledAt, 1699075092) - XCTAssertNil(run.failedAt) - XCTAssertNil(run.completedAt) - XCTAssertNil(run.lastError) - XCTAssertEqual(run.model, "gpt-3.5-turbo") - XCTAssertEqual(run.instructions, "Provide instructions") - XCTAssertTrue(run.tools.isEmpty) - XCTAssertEqual(run.fileIds, ["file-abc123"]) - XCTAssertEqual(run.metadata?["key"], "value") - } catch { - XCTFail("Error: \(error.localizedDescription)") - } - } - - func testCreateThreadAndRun() async { - do { - let mockData = RunsAPITests.createThreadAndRun.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - XCTAssertEqual(request.url?.path, "/v1/threads/runs") - XCTAssertEqual(request.httpMethod, "POST") - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let createThreadRunRequest = ASACreateThreadRunRequest(assistantId: "", thread: ASACreateThreadRunRequest.Thread(messages: [])) - let run: ASARun = try await runsAPI.createThreadAndRun(createThreadRun: createThreadRunRequest) - - XCTAssertEqual(run.id, "run_xyz123") - XCTAssertEqual(run.object, "thread.run") - XCTAssertEqual(run.createdAt, 1699080000) - XCTAssertEqual(run.threadId, "thread_xyz123") - XCTAssertEqual(run.assistantId, "asst_xyz123") - XCTAssertEqual(run.status, "in_progress") - XCTAssertEqual(run.startedAt, 1699080001) - XCTAssertEqual(run.expiresAt, 1699080600) - XCTAssertNil(run.cancelledAt) - XCTAssertNil(run.failedAt) - XCTAssertNil(run.completedAt) - XCTAssertNil(run.lastError) - XCTAssertEqual(run.model, "gpt-3.5-turbo") - XCTAssertEqual(run.instructions, "Explain deep learning to a 5 year old.") - XCTAssertEqual(run.tools.count, 1) - XCTAssertEqual(run.fileIds, ["file-xyz123"]) - XCTAssertEqual(run.metadata?["session"], "1") - } catch { - XCTFail("Error: \(error.localizedDescription)") - } - } - - func testRetrieveRunStep() async { - do { - let mockData = RunsAPITests.retrieveRunStep.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - XCTAssertEqual(request.url?.path, "/v1/threads/thread_abc123/runs/run_abc123/steps/step_xyz123") - XCTAssertEqual(request.httpMethod, "GET") - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let runStep: ASARunStep = try await runsAPI.retrieveRunStep(by: "thread_abc123", runId: "run_abc123", stepId: "step_xyz123") - - XCTAssertEqual(runStep.id, "step_abc123") - XCTAssertEqual(runStep.object, "thread.run.step") - XCTAssertEqual(runStep.createdAt, 1699063291) - XCTAssertEqual(runStep.runId, "run_abc123") - XCTAssertEqual(runStep.assistantId, "asst_abc123") - XCTAssertEqual(runStep.threadId, "thread_abc123") - XCTAssertEqual(runStep.type, "message_creation") - XCTAssertEqual(runStep.status, "completed") - XCTAssertNil(runStep.cancelledAt) - XCTAssertEqual(runStep.completedAt, 1699063291) - XCTAssertNil(runStep.expiredAt) - XCTAssertNil(runStep.failedAt) - XCTAssertNil(runStep.lastError) - } catch { - XCTFail("Error: \(error.localizedDescription)") - } - } - - func testListRunSteps() async { - do { - let mockData = RunsAPITests.listRunSteps.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - XCTAssertEqual(request.url?.path, "/v1/threads/thread_abc123/runs/run_abc123/steps") - XCTAssertEqual(request.httpMethod, "GET") - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let listResponse: ASARunStepsListResponse = try await runsAPI.listRunSteps(by: "thread_abc123", runId: "run_abc123", parameters: nil) - - XCTAssertEqual(listResponse.object, "list") - XCTAssertEqual(listResponse.data.count, 1) - - let firstStep = listResponse.data[0] - XCTAssertEqual(firstStep.id, "step_xyz123") - XCTAssertEqual(firstStep.object, "thread.run.step") - XCTAssertEqual(firstStep.createdAt, 1699080100) - XCTAssertEqual(firstStep.runId, "run_xyz123") - XCTAssertEqual(firstStep.assistantId, "asst_xyz123") - XCTAssertEqual(firstStep.threadId, "thread_xyz123") - XCTAssertEqual(firstStep.type, "message_creation") - XCTAssertEqual(firstStep.status, "completed") - XCTAssertNil(firstStep.cancelledAt) - XCTAssertEqual(firstStep.completedAt, 1699080200) - XCTAssertNil(firstStep.expiredAt) - XCTAssertNil(firstStep.failedAt) - XCTAssertNil(firstStep.lastError) - - } catch { - XCTFail("Error: \(error.localizedDescription)") - } - } - - -} diff --git a/Tests/AISwiftAssistTests/APIs/ThreadsAPITests.swift b/Tests/AISwiftAssistTests/APIs/ThreadsAPITests.swift deleted file mode 100644 index 9c4884f..0000000 --- a/Tests/AISwiftAssistTests/APIs/ThreadsAPITests.swift +++ /dev/null @@ -1,128 +0,0 @@ -// -// ThreadsAPITests.swift -// -// -// Created by Alexey on 11/20/23. -// - -import XCTest -@testable import AISwiftAssist - -final class ThreadsAPITests: XCTestCase { - - var threadsAPI: IThreadsAPI! - - override func setUp() { - super.setUp() - let configuration = URLSessionConfiguration.default - configuration.protocolClasses = [MockURLProtocol.self] - let mockURLSession = URLSession(configuration: configuration) - threadsAPI = ThreadsAPI(urlSession: mockURLSession) - } - - override func tearDown() { - threadsAPI = nil - super.tearDown() - } - - func testCreateThread() async { - do { - // Simulate server response - let mockData = Self.create.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let message = ASACreateThreadRequest.Message(role: "user", - content: "Hello World", - fileIds: nil, - metadata: nil) - let createRequest = ASACreateThreadRequest(messages: [message]) - let thread: ASAThread = try await threadsAPI.create(by: createRequest) - - // Checks - XCTAssertEqual(thread.id, "thread_abc123") - XCTAssertEqual(thread.object, "thread") - XCTAssertEqual(thread.createdAt, 1699012949) - XCTAssertTrue(thread.metadata?.isEmpty ?? true) - } catch { - XCTFail("Error: \(error)") - } - } - - func testRetrieveThread() async { - do { - // Simulate server response - let mockData = Self.retrieve.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let threadId = "thread_abc123" - let thread: ASAThread = try await threadsAPI.retrieve(threadId: threadId) - - // Checks - XCTAssertEqual(thread.id, threadId) - XCTAssertEqual(thread.object, "thread") - XCTAssertEqual(thread.createdAt, 1699014083) - XCTAssertTrue(thread.metadata?.isEmpty ?? true) - } catch { - XCTFail("Error: \(error)") - } - } - - func testModifyThread() async { - do { - // Simulate server response - let mockData = Self.modify.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let threadId = "thread_abc123" - let modifyRequest = ASAModifyThreadRequest(metadata: ["modified": "true", - "user": "abc123"]) - - let modifiedThread: ASAThread = try await threadsAPI.modify(threadId: threadId, - with: modifyRequest) - - // Checks - XCTAssertEqual(modifiedThread.id, threadId) - XCTAssertEqual(modifiedThread.object, "thread") - XCTAssertEqual(modifiedThread.createdAt, 1699014083) - XCTAssertEqual(modifiedThread.metadata?["modified"], "true") - XCTAssertEqual(modifiedThread.metadata?["user"], "abc123") - } catch { - XCTFail("Error: \(error)") - } - } - - func testDeleteThread() async { - do { - // Simulate server response - let mockData = Self.delete.data(using: .utf8)! - - MockURLProtocol.requestHandler = { request in - let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! - return (response, mockData) - } - - let threadId = "thread_abc123" - let deleteResponse: ASADeleteModelResponse = try await threadsAPI.delete(threadId: threadId) - - // Checks - XCTAssertEqual(deleteResponse.id, threadId) - XCTAssertEqual(deleteResponse.object, "thread.deleted") - XCTAssertTrue(deleteResponse.deleted) - } catch { - XCTFail("Error: \(error)") - } - } - -} diff --git a/Tests/AISwiftAssistTests/Mosks/AssistantMocks.swift b/Tests/AISwiftAssistTests/Mosks/AssistantMocks.swift index 5a4acff..361fcee 100644 --- a/Tests/AISwiftAssistTests/Mosks/AssistantMocks.swift +++ b/Tests/AISwiftAssistTests/Mosks/AssistantMocks.swift @@ -9,122 +9,170 @@ import Foundation extension AssistantsAPITests { static let list: String = - """ + """ { "object": "list", "data": [ { - "id": "asst_abc123", + "id": "asst_advanced_full", "object": "assistant", - "created_at": 1698982736, - "name": "Coding Tutor", - "description": null, + "created_at": 1709800000, + "name": "Advanced Assistant", + "description": "Fully featured assistant with comprehensive tools and strict output control.", + "model": "gpt-4-turbo", + "instructions": "You are a professional assistant with advanced functionality, tool support, and strict output controls.", + "tools": [ + {"type": "code_interpreter"}, + {"type": "file_search"}, + {"type": "function", "function": {"name": "fetchData", "description": "Fetches data from external sources."}} + ], + "file_ids": ["file_adv_01", "file_adv_02"], + "metadata": {"role": "advanced", "version": "1.0"} + }, + { + "id": "asst_professional", + "object": "assistant", + "created_at": 1709800500, + "name": "Professional Assistant", + "description": "Professional assistant designed for complex tasks and detailed analysis.", + "model": "gpt-4-turbo", + "instructions": "You provide detailed and professional assistance for complex and analytical tasks.", + "tools": [{"type": "code_interpreter"}, {"type": "file_search"}], + "file_ids": ["file_pro_01"], + "metadata": {"role": "professional", "version": "1.0"} + }, + { + "id": "asst_standard", + "object": "assistant", + "created_at": 1709801000, + "name": "Standard Assistant", + "description": "A standard assistant suitable for everyday tasks.", + "model": "gpt-4", + "instructions": "You are a helpful assistant equipped with basic tools for everyday tasks.", + "tools": [{"type": "file_search"}], + "file_ids": ["file_std_01"], + "metadata": {"role": "standard", "version": "0.9"} + }, + { + "id": "asst_intermediate", + "object": "assistant", + "created_at": 1709801200, + "name": "Intermediate Assistant", + "description": "Intermediate assistant with moderate capabilities.", "model": "gpt-4", - "instructions": "You are a helpful assistant designed to make me better at coding!", + "instructions": "You help users with moderate complexity tasks efficiently.", "tools": [], "file_ids": [], - "metadata": {} + "metadata": {"role": "intermediate", "version": "0.8"} }, { - "id": "asst_abc456", + "id": "asst_basic", "object": "assistant", - "created_at": 1698982718, - "name": "My Assistant", + "created_at": 1709801500, + "name": "Basic Assistant", + "description": "Simple assistant for basic queries.", + "model": "gpt-3.5-turbo", + "instructions": "You assist with basic queries and simple tasks.", + "tools": [], + "file_ids": [], + "metadata": {"role": "basic", "version": "0.7"} + }, + { + "id": "asst_simple", + "object": "assistant", + "created_at": 1709801800, + "name": "Simple Assistant", "description": null, - "model": "gpt-4", - "instructions": "You are a helpful assistant designed to make me better at coding!", + "model": "gpt-3.5-turbo", + "instructions": "You are a simple and friendly assistant for straightforward tasks.", "tools": [], "file_ids": [], "metadata": {} }, { - "id": "asst_abc789", + "id": "asst_minimal", "object": "assistant", - "created_at": 1698982643, - "name": null, + "created_at": 1709802000, + "name": "Minimal Assistant", "description": null, - "model": "gpt-4", + "model": "gpt-3.5-turbo", "instructions": null, "tools": [], "file_ids": [], "metadata": {} } ], - "first_id": "asst_abc123", - "last_id": "asst_abc789", + "first_id": "asst_advanced_full", + "last_id": "asst_minimal", "has_more": false } """ + static let create: String = """ - { - "id": "asst_abc123", - "object": "assistant", - "created_at": 1698984975, - "name": "Math Tutor", - "description": null, - "model": "gpt-4", - "instructions": "You are a personal math tutor. When asked a question, write and run Python code to answer the question.", - "tools": [ - { - "type": "code_interpreter" - } - ], - "file_ids": [], - "metadata": {} - } + { + "id": "asst_advanced_full", + "object": "assistant", + "created_at": 1709800000, + "name": "Advanced Assistant", + "description": "Fully featured assistant with comprehensive tools and strict output control.", + "model": "gpt-4-turbo", + "instructions": "You are a professional assistant with advanced functionality, tool support, and strict output controls.", + "tools": [ + {"type": "code_interpreter"}, + {"type": "file_search"}, + {"type": "function", "function": {"name": "fetchData", "description": "Fetches data from external sources."}} + ], + "file_ids": ["file_adv_01", "file_adv_02"], + "metadata": {"role": "advanced", "version": "1.0"} + } """ static let retrieve: String = """ - { - "id": "asst_abc123", - "object": "assistant", - "created_at": 1699009709, - "name": "HR Helper", - "description": null, - "model": "gpt-4", - "instructions": "You are an HR bot, and you have access to files to answer employee questions about company policies.", - "tools": [ - { - "type": "retrieval" - } - ], - "file_ids": [ - "file-abc123" - ], - "metadata": {} - } + { + "id": "asst_advanced_full", + "object": "assistant", + "created_at": 1709800000, + "name": "Advanced Assistant", + "description": "Fully featured assistant with comprehensive tools and strict output control.", + "model": "gpt-4-turbo", + "instructions": "You are a professional assistant with advanced functionality, tool support, and strict output controls.", + "tools": [ + {"type": "code_interpreter"}, + {"type": "file_search"}, + {"type": "function", "function": {"name": "fetchData", "description": "Fetches data from external sources."}} + ], + "file_ids": ["file_adv_01", "file_adv_02"], + "metadata": {"role": "advanced", "version": "1.0"} + } """ static let modify: String = """ - { - "id": "asst_abc123", - "object": "assistant", - "created_at": 1699009709, - "name": "HR Helper", - "description": null, - "model": "gpt-4", - "instructions": "You are an HR bot, and you have access to files to answer employee questions about company policies. Always response with info from either of the files.", - "tools": [ - { - "type": "retrieval" - } - ], - "file_ids": [ - "file-abc123", - "file-abc456" - ], - "metadata": {} - } + { + "id": "asst_advanced_full", + "object": "assistant", + "created_at": 1709800000, + "name": "Advanced", + "description": "Fully featured assistant with comprehensive tools and strict output control.", + "model": "gpt-4", + "instructions": "You are a professional assistant with advanced functionality, tool support, and strict output controls.", + "tools": [ + {"type": "code_interpreter"}, + {"type": "file_search"}, + {"type": "function", "function": {"name": "fetchData", "description": "Fetches data from external sources."}} + ], + "file_ids": ["file_adv_01", "file_adv_02"], + "metadata": {"role": "advanced", "version": "1.0"} + } """ static let delete: String = """ { - "id": "asst_abc123", + "id": "asst_advanced_full", "object": "assistant.deleted", "deleted": true } diff --git a/Tests/AISwiftAssistTests/Mosks/MessagesMocks.swift b/Tests/AISwiftAssistTests/Mosks/MessagesMocks.swift deleted file mode 100644 index ed481ee..0000000 --- a/Tests/AISwiftAssistTests/Mosks/MessagesMocks.swift +++ /dev/null @@ -1,321 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 11/20/23. -// - -import Foundation - -extension MessagesAPITests { - - static let list: String = - """ - { - "object": "list", - "data": [ - { - "id": "msg_SM7CyvQn3UnrAyOh96TGN5jR", - "object": "thread.message", - "created_at": 1702586152, - "thread_id": "thread_11LlFPiPpEw7WhZr0AqB2WhF", - "role": "assistant", - "content": [ - { - "type": "text", - "text": { - "value": "I now have full annotations for every date mentioned in the file", - "annotations": [ - { - "type": "file_citation", - "text": "【21†source】", - "start_index": 136, - "end_index": 147, - "file_citation": { - "file_id": "", - "quote": "adfadlfkjamdf" - } - }, - { - "type": "file_citation", - "text": "【22†source】", - "start_index": 197, - "end_index": 208, - "file_citation": { - "file_id": "", - "quote": "01.12.202318" - } - }, - { - "type": "file_citation", - "text": "【29†source】", - "start_index": 287, - "end_index": 298, - "file_citation": { - "file_id": "", - "quote": "ajfnailfbnloabiufnliajnsfl" - } - }, - { - "type": "file_citation", - "text": "【33†source】", - "start_index": 378, - "end_index": 389, - "file_citation": { - "file_id": "", - "quote": "DXB" - } - } - ] - } - } - ], - "file_ids": [], - "assistant_id": "asst_wAI24kgbPjUpBqnEbsyjS8iO", - "run_id": "run_RapQjuYYvH1gpyOQB2DSAurf", - "metadata": {} - }, - { - "id": "msg_oacFAKp8WbIKYnV2Wmsyh5aE", - "object": "thread.message", - "created_at": 1702560337, - "thread_id": "thread_11LlFPiPpEw7WhZr0AqB2WhF", - "role": "assistant", - "content": [ - { - "type": "text", - "text": { - "value": "Привет! Как я могу помочь вам сегодня?", - "annotations": [] - } - } - ], - "file_ids": [], - "assistant_id": "asst_wAI24kgbPjUpBqnEbsyjS8iO", - "run_id": "run_9P57TYv8dxBPQkHK8plrNztk", - "metadata": {} - }, - { - "id": "msg_V8hf7PCvWceW4DpQKpQV83Ia", - "object": "thread.message", - "created_at": 1702560334, - "thread_id": "thread_11LlFPiPpEw7WhZr0AqB2WhF", - "role": "user", - "content": [ - { - "type": "text", - "text": { - "value": "привет", - "annotations": [] - } - } - ], - "file_ids": [], - "assistant_id": null, - "run_id": null, - "metadata": {} - } - ], - "first_id": "msg_SM7CyvQn3UnrAyOh96TGN5jR", - "last_id": "msg_V8hf7PCvWceW4DpQKpQV83Ia", - "has_more": false - } - """ - - static let modify: String = - """ - { - "id": "12345", - "object": "thread.message", - "created_at": 1639530000, - "thread_id": "thread123", - "role": "assistant", - "content": [ - { - "type": "text", - "text": { - "value": "This is a text message with annotations.", - "annotations": [ - - { - "type": "file_citation", - "text": "document link", - "file_citation": { - "file_id": "file123", - "quote": "A quote from the file" - }, - "start_index": 0, - "end_index": 23 - }, - { - "type": "file_path", - "text": "path to file", - "file_path": { - "file_id": "file456" - }, - "start_index": 24, - "end_index": 37 - } - ] - } - }, - { - "type": "image_file", - "image_file": { - "file_id": "image789" - } - } - ], - "assistant_id": "assistant123", - "run_id": "run123", - "file_ids": ["file123", "file456", "image789"], - "metadata": { - "key1": "value1", - "key2": "value2" - } - } - """ - - static let retrieve: String = - """ - { - "id": "12345", - "object": "thread.message", - "created_at": 1639530000, - "thread_id": "thread123", - "role": "assistant", - "content": [ - { - "type": "text", - "text": { - "value": "This is a text message with annotations.", - "annotations": [ - - { - "type": "file_citation", - "text": "document link", - "file_citation": { - "file_id": "file123", - "quote": "A quote from the file" - }, - "start_index": 0, - "end_index": 23 - }, - { - "type": "file_path", - "text": "path to file", - "file_path": { - "file_id": "file456" - }, - "start_index": 24, - "end_index": 37 - } - ] - } - }, - { - "type": "image_file", - "image_file": { - "file_id": "image789" - } - } - ], - "assistant_id": "assistant123", - "run_id": "run123", - "file_ids": ["file123", "file456", "image789"], - "metadata": { - "key1": "value1", - "key2": "value2" - } - } - """ - - static let create: String = - """ - { - "id": "12345", - "object": "thread.message", - "created_at": 1639530000, - "thread_id": "thread123", - "role": "assistant", - "content": [ - { - "type": "text", - "text": { - "value": "This is a text message with annotations.", - "annotations": [ - - { - "type": "file_citation", - "text": "document link", - "file_citation": { - "file_id": "file123", - "quote": "A quote from the file" - }, - "start_index": 0, - "end_index": 23 - }, - { - "type": "file_path", - "text": "path to file", - "file_path": { - "file_id": "file456" - }, - "start_index": 24, - "end_index": 37 - } - ] - } - }, - { - "type": "image_file", - "image_file": { - "file_id": "image789" - } - } - ], - "assistant_id": "assistant123", - "run_id": "run123", - "file_ids": ["file123", "file456", "image789"], - "metadata": { - "key1": "value1", - "key2": "value2" - } - } - """ - - static let retrieveFile: String = - """ - { - "id": "file-abc123", - "object": "thread.message.file", - "created_at": 1698107661, - "message_id": "message_QLoItBbqwyAJEzlTy4y9kOMM", - "file_id": "file-abc123" - } - """ - - static let listFiles: String = - """ - { - "object": "list", - "data": [ - { - "id": "file-abc123", - "object": "thread.message.file", - "created_at": 1698107661, - "message_id": "message_QLoItBbqwyAJEzlTy4y9kOMM" - }, - { - "id": "file-abc456", - "object": "thread.message.file", - "created_at": 1698107662, - "message_id": "message_QLoItBbqwyAJEzlTy4y9kONN" - } - ], - "first_id": "file-abc123", - "last_id": "file-abc456", - "has_more": false - } - """ -} diff --git a/Tests/AISwiftAssistTests/Mosks/ModelsMocks.swift b/Tests/AISwiftAssistTests/Mosks/ModelsMocks.swift deleted file mode 100644 index d842772..0000000 --- a/Tests/AISwiftAssistTests/Mosks/ModelsMocks.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 11/20/23. -// - -import Foundation - -extension ModelsAPITests { - static let list: String = - """ - { - "object": "list", - "data": [ - { - "id": "model-id-0", - "object": "model", - "created": 1686935002, - "owned_by": "organization-owner" - }, - { - "id": "model-id-1", - "object": "model", - "created": 1686935002, - "owned_by": "organization-owner" - }, - { - "id": "model-id-2", - "object": "model", - "created": 1686935002, - "owned_by": "openai" - } - ] - } - """ - - static let retrieve: String = - """ - { - "id": "gpt-3.5-turbo-instruct", - "object": "model", - "created": 1686935002, - "owned_by": "openai" - } - """ - - static let delete: String = - """ - { - "id": "ft:gpt-3.5-turbo:acemeco:suffix:abc123", - "object": "model", - "deleted": true - } - """ -} diff --git a/Tests/AISwiftAssistTests/Mosks/RunsMocks.swift b/Tests/AISwiftAssistTests/Mosks/RunsMocks.swift deleted file mode 100644 index b76360c..0000000 --- a/Tests/AISwiftAssistTests/Mosks/RunsMocks.swift +++ /dev/null @@ -1,310 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 11/20/23. -// - -import Foundation - -extension RunsAPITests { - static let create: String = - """ - { - "id": "run_abc123", - "object": "thread.run", - "created_at": 1699063290, - "assistant_id": "asst_abc123", - "thread_id": "thread_abc123", - "status": "queued", - "started_at": 1699063290, - "expires_at": null, - "cancelled_at": null, - "failed_at": null, - "completed_at": 1699063291, - "last_error": null, - "model": "gpt-4", - "instructions": null, - "tools": [ - { - "type": "code_interpreter" - } - ], - "file_ids": [ - "file-abc123", - "file-abc456" - ], - "metadata": {} - } - """ - - static let retrieve: String = - """ - { - "id": "run_abc123", - "object": "thread.run", - "created_at": 1699075072, - "assistant_id": "asst_abc123", - "thread_id": "thread_abc123", - "status": "completed", - "started_at": 1699075072, - "expires_at": null, - "cancelled_at": null, - "failed_at": null, - "completed_at": 1699075073, - "last_error": null, - "model": "gpt-3.5-turbo", - "instructions": null, - "tools": [ - { - "type": "code_interpreter" - } - ], - "file_ids": [ - "file-abc123", - "file-abc456" - ], - "metadata": {} - } - """ - - static let modify: String = - """ - { - "id": "run_abc123", - "object": "thread.run", - "created_at": 1699075072, - "assistant_id": "asst_abc123", - "thread_id": "thread_abc123", - "status": "completed", - "started_at": 1699075072, - "expires_at": null, - "cancelled_at": null, - "failed_at": null, - "completed_at": 1699075073, - "last_error": null, - "model": "gpt-3.5-turbo", - "instructions": null, - "tools": [ - { - "type": "code_interpreter" - } - ], - "file_ids": [ - "file-abc123", - "file-abc456" - ], - "metadata": { - "user_id": "user_abc123" - } - } - """ - - static let list: String = - """ -{ - "object": "list", - "data": [ - { - "id": "run_abc123", - "object": "thread.run", - "created_at": 1699075072, - "assistant_id": "asst_abc123", - "thread_id": "thread_abc123", - "status": "completed", - "started_at": 1699075072, - "expires_at": null, - "cancelled_at": null, - "failed_at": null, - "completed_at": 1699075073, - "last_error": null, - "model": "gpt-3.5-turbo", - "instructions": null, - "tools": [ - { - "type": "code_interpreter" - } - ], - "file_ids": [ - "file-abc123", - "file-abc456" - ], - "metadata": {} - }, - { - "id": "run_abc456", - "object": "thread.run", - "created_at": 1699063290, - "assistant_id": "asst_abc123", - "thread_id": "thread_abc123", - "status": "completed", - "started_at": 1699063290, - "expires_at": null, - "cancelled_at": null, - "failed_at": null, - "completed_at": 1699063291, - "last_error": null, - "model": "gpt-3.5-turbo", - "instructions": null, - "tools": [ - { - "type": "code_interpreter" - } - ], - "file_ids": [ - "file-abc123", - "file-abc456" - ], - "metadata": {} - } - ], - "first_id": "run_abc123", - "last_id": "run_abc456", - "has_more": false -} -""" - static let submitToolOutputs: String = - """ - { - "id": "run_abc123", - "object": "thread.run", - "created_at": 1699063291, - "thread_id": "thread_abc123", - "assistant_id": "asst_abc123", - "status": "completed", - "started_at": 1699063292, - "expires_at": 1699066891, - "cancelled_at": null, - "failed_at": null, - "completed_at": 1699063391, - "last_error": null, - "model": "gpt-3.5-turbo", - "instructions": "You are a helpful assistant.", - "tools": [ - { - "type": "function", - "function": { - "name": "get_weather", - "description": "Determine weather in my location", - "parameters": { - "location": "San Francisco, CA", - "unit": "c" - } - } - } - ], - "file_ids": ["file-abc123"], - "metadata": { - "additional_info": "test" - } - } - """ - static let cancelRun: String = - """ - { - "id": "run_abc123", - "object": "thread.run", - "created_at": 1699075072, - "assistant_id": "asst_abc123", - "thread_id": "thread_abc123", - "status": "cancelled", - "started_at": 1699075072, - "expires_at": 1699075672, - "cancelled_at": 1699075092, - "failed_at": null, - "completed_at": null, - "last_error": null, - "model": "gpt-3.5-turbo", - "instructions": "Provide instructions", - "tools": [], - "file_ids": ["file-abc123"], - "metadata": {"key": "value"} - } - """ - - static let createThreadAndRun: String = - """ - { - "id": "run_xyz123", - "object": "thread.run", - "created_at": 1699080000, - "thread_id": "thread_xyz123", - "assistant_id": "asst_xyz123", - "status": "in_progress", - "started_at": 1699080001, - "expires_at": 1699080600, - "cancelled_at": null, - "failed_at": null, - "completed_at": null, - "last_error": null, - "model": "gpt-3.5-turbo", - "instructions": "Explain deep learning to a 5 year old.", - "tools": [ - { - "type": "code_interpreter" - } - ], - "file_ids": ["file-xyz123"], - "metadata": {"session": "1"} - } - """ - - static let retrieveRunStep: String = - """ - { - "id": "step_abc123", - "object": "thread.run.step", - "created_at": 1699063291, - "run_id": "run_abc123", - "assistant_id": "asst_abc123", - "thread_id": "thread_abc123", - "type": "message_creation", - "status": "completed", - "cancelled_at": null, - "completed_at": 1699063291, - "expired_at": null, - "failed_at": null, - "last_error": null, - "step_details": { - "type": "message_creation", - "message_creation": { - "message_id": "msg_abc123" - } - } - } - """ - - static let listRunSteps: String = - """ - { - "object": "list", - "data": [ - { - "id": "step_xyz123", - "object": "thread.run.step", - "created_at": 1699080100, - "run_id": "run_xyz123", - "assistant_id": "asst_xyz123", - "thread_id": "thread_xyz123", - "type": "message_creation", - "status": "completed", - "cancelled_at": null, - "completed_at": 1699080200, - "expired_at": null, - "failed_at": null, - "last_error": null, - "step_details": { - "type": "message_creation", - "message_creation": { - "message_id": "msg_xyz123" - } - } - }, - ], - "first_id": "step_xyz123", - "last_id": "step_xyz456", - "has_more": false - } - """ - -} diff --git a/Tests/AISwiftAssistTests/Mosks/ThreadsMocks.swift b/Tests/AISwiftAssistTests/Mosks/ThreadsMocks.swift deleted file mode 100644 index d809ac9..0000000 --- a/Tests/AISwiftAssistTests/Mosks/ThreadsMocks.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// File.swift -// -// -// Created by Alexey on 11/20/23. -// - -import Foundation - -extension ThreadsAPITests { - static let create: String = - """ - { - "id": "thread_abc123", - "object": "thread", - "created_at": 1699012949, - "metadata": {} - } - """ - - static let retrieve: String = - """ - { - "id": "thread_abc123", - "object": "thread", - "created_at": 1699014083, - "metadata": {} - } - """ - - static let modify: String = - """ - { - "id": "thread_abc123", - "object": "thread", - "created_at": 1699014083, - "metadata": { - "modified": "true", - "user": "abc123" - } - } - """ - - static let delete: String = - """ - { - "id": "thread_abc123", - "object": "thread.deleted", - "deleted": true - } - """ -} diff --git a/Tests/AISwiftAssistTests/Resourses/MockURLProtocol.swift b/Tests/AISwiftAssistTests/Resourses/MockURLProtocol.swift index d670477..eef0464 100644 --- a/Tests/AISwiftAssistTests/Resourses/MockURLProtocol.swift +++ b/Tests/AISwiftAssistTests/Resourses/MockURLProtocol.swift @@ -7,48 +7,66 @@ import Foundation -class MockURLProtocol: URLProtocol { - // Handler to test the request and return a mock response. - static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data?))? +actor RequestHandlerStorage { + private var requestHandlers: [String: (@Sendable (URLRequest) async throws -> (HTTPURLResponse, Data))] = [:] + + func setHandler(_ handler: @Sendable @escaping (URLRequest) async throws -> (HTTPURLResponse, Data), for key: String) async { + requestHandlers[key] = handler + } + + func executeHandler(for request: URLRequest, for key: String) async throws -> (HTTPURLResponse, Data) { + guard let handler = requestHandlers[key] else { + throw MockURLProtocolError.noRequestHandler + } + return try await handler(request) + } +} + +final class MockURLProtocol: URLProtocol, @unchecked Sendable { + + private static let requestHandlerStorage = RequestHandlerStorage() + + static func setHandler(_ handler: @Sendable @escaping (URLRequest) async throws -> (HTTPURLResponse, Data), for key: String) async { + await requestHandlerStorage.setHandler ({ request in + try await handler(request) + }, for: key) + } + + func executeHandler(for request: URLRequest, key: String) async throws -> (HTTPURLResponse, Data) { + return try await Self.requestHandlerStorage.executeHandler(for: request, for: key) + } override class func canInit(with request: URLRequest) -> Bool { - // To check if this protocol can handle the given request. return true } override class func canonicalRequest(for request: URLRequest) -> URLRequest { - // Return the original request as the canonical version. return request } override func startLoading() { - guard let handler = MockURLProtocol.requestHandler else { - fatalError("Handler is unavailable.") - } - - do { - // Call handler with received request and capture the tuple of response and data. - let (response, data) = try handler(request) - - // Send received response to the client. - client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) - - if let data = data { - // Send received data to the client. + Task { + do { + guard let key = request.value(forHTTPHeaderField: "ForTest") else { + client?.urlProtocol(self, didFailWithError: MockURLProtocolError.invalidURL) + return + } + let (response, data) = try await self.executeHandler(for: request, key: key) + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: data) + client?.urlProtocolDidFinishLoading(self) + } catch { + client?.urlProtocol(self, didFailWithError: error) } - - // Notify request has been finished. - client?.urlProtocolDidFinishLoading(self) - } catch { - // Notify received error. - client?.urlProtocol(self, didFailWithError: error) } - } - override func stopLoading() { - // This is called if the request gets canceled or completed. } + + override func stopLoading() {} } +enum MockURLProtocolError: Error { + case noRequestHandler + case invalidURL +}