diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Package.swift b/Package.swift index 32836ac..970cd47 100644 --- a/Package.swift +++ b/Package.swift @@ -22,6 +22,7 @@ let package = Package( name: "AISwiftAssist"), .testTarget( name: "AISwiftAssistTests", - dependencies: ["AISwiftAssist"]), + dependencies: ["AISwiftAssist"] + ), ] ) diff --git a/Sources/AISwiftAssist/APIs/AssistantsAPI.swift b/Sources/AISwiftAssist/APIs/AssistantsAPI.swift index 284b082..1e52fd2 100644 --- a/Sources/AISwiftAssist/APIs/AssistantsAPI.swift +++ b/Sources/AISwiftAssist/APIs/AssistantsAPI.swift @@ -13,7 +13,7 @@ public protocol IAssistantsAPI: AnyObject { /// Returns a list of assistants. /// - Parameter parameters: Parameters for the list of assistants. /// - Returns: A list of assistant objects. - func get(with parameters: ASAListAssistantsParameters?) async throws -> [ASAAssistant] + func get(with parameters: ASAListAssistantsParameters?) async throws -> ASAListAssistantsResponse /// Create an assistant with a model and instructions. /// - Parameter createAssistant: The create assistant model. @@ -58,29 +58,29 @@ public final class AssistantsAPI: HTTPClient, IAssistantsAPI { self.urlSession = urlSession } - public func get(with parameters: ASAListAssistantsParameters? = nil) async throws -> [ASAAssistant] { + public func get(with parameters: ASAListAssistantsParameters? = nil) async throws -> ASAListAssistantsResponse { let endpoint = AssistantEndpoint.getAssistants(parameters) - return try await sendRequest(endpoint: endpoint, responseModel: [ASAAssistant].self) + return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAListAssistantsResponse.self) } public func create(by createAssistant: ASACreateAssistantRequest) async throws -> ASAAssistant { let endpoint = AssistantEndpoint.createAssistant(createAssistant) - return try await sendRequest(endpoint: endpoint, responseModel: ASAAssistant.self) + return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAAssistant.self) } public func retrieve(by assistantId: String) async throws -> ASAAssistant { let endpoint = AssistantEndpoint.retrieveAssistant(assistantId) - return try await sendRequest(endpoint: endpoint, responseModel: ASAAssistant.self) + return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAAssistant.self) } public func modify(by assistantId: String, modifyAssistant: ASAModifyAssistantRequest) async throws -> ASAAssistant { let endpoint = AssistantEndpoint.modifyAssistant(assistantId, modifyAssistant) - return try await sendRequest(endpoint: endpoint, responseModel: ASAAssistant.self) + return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAAssistant.self) } public func delete(by assistantId: String) async throws -> ASADeleteModelResponse { let endpoint = AssistantEndpoint.deleteAssistant(assistantId) - return try await sendRequest(endpoint: endpoint, responseModel: ASADeleteModelResponse.self) + return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASADeleteModelResponse.self) } } diff --git a/Sources/AISwiftAssist/APIs/MessagesAPI.swift b/Sources/AISwiftAssist/APIs/MessagesAPI.swift index 3b64bde..315f101 100644 --- a/Sources/AISwiftAssist/APIs/MessagesAPI.swift +++ b/Sources/AISwiftAssist/APIs/MessagesAPI.swift @@ -62,21 +62,21 @@ public final class MessagesAPI: HTTPClient, IMessagesAPI { public func create(by threadId: String, createMessage: ASACreateMessageRequest) async throws -> ASAMessage { let endpoint = MessagesEndpoint.createMessage(threadId, createMessage) - return try await sendRequest(endpoint: endpoint, responseModel: ASAMessage.self) + return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAMessage.self) } public func retrieve(by threadId: String, messageId: String) async throws -> ASAMessage { let endpoint = MessagesEndpoint.retrieveMessage(threadId, messageId) - return try await sendRequest(endpoint: endpoint, responseModel: ASAMessage.self) + return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAMessage.self) } public func modify(by threadId: String, messageId: String, modifyMessage: ASAModifyMessageRequest) async throws -> ASAMessage { let endpoint = MessagesEndpoint.modifyMessage(threadId, messageId, modifyMessage) - return try await sendRequest(endpoint: endpoint, responseModel: ASAMessage.self) + 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(endpoint: endpoint, responseModel: ASAMessagesListResponse.self) + return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAMessagesListResponse.self) } } diff --git a/Sources/AISwiftAssist/APIs/ModelsAPI.swift b/Sources/AISwiftAssist/APIs/ModelsAPI.swift index 83fa94b..d57816d 100644 --- a/Sources/AISwiftAssist/APIs/ModelsAPI.swift +++ b/Sources/AISwiftAssist/APIs/ModelsAPI.swift @@ -47,16 +47,16 @@ public final class ModelsAPI: HTTPClient, IModelsAPI { public func get() async throws -> ASAListModelsResponse { let endpoint = ModelsEndpoint.getModels - return try await sendRequest(endpoint: endpoint, responseModel: ASAListModelsResponse.self) + return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAListModelsResponse.self) } public func retrieve(by modelId: String) async throws -> ASAModel { let endpoint = ModelsEndpoint.retrieveModel(modelId) - return try await sendRequest(endpoint: endpoint, responseModel: ASAModel.self) + 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(endpoint: endpoint, responseModel: ASADeleteModelResponse.self) + 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 f56bb5e..e2784ef 100644 --- a/Sources/AISwiftAssist/APIs/RunsAPI.swift +++ b/Sources/AISwiftAssist/APIs/RunsAPI.swift @@ -40,7 +40,7 @@ public final class RunsAPI: HTTPClient, IRunsAPI { public func create(by threadId: String, createRun: ASACreateRunRequest) async throws -> ASARun { let endpoint = RunsEndpoint.createRun(threadId, createRun) - return try await sendRequest(endpoint: endpoint, responseModel: ASARun.self) + return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASARun.self) } } diff --git a/Sources/AISwiftAssist/APIs/ThreadsAPI.swift b/Sources/AISwiftAssist/APIs/ThreadsAPI.swift index 040fb08..531f7c5 100644 --- a/Sources/AISwiftAssist/APIs/ThreadsAPI.swift +++ b/Sources/AISwiftAssist/APIs/ThreadsAPI.swift @@ -55,21 +55,21 @@ public final class ThreadsAPI: HTTPClient, IThreadsAPI { public func create(by createThreads: ASACreateThreadRequest?) async throws -> ASAThread { let endpoint = ThreadsEndpoint.createThread(createThreads) - return try await sendRequest(endpoint: endpoint, responseModel: ASAThread.self) + return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAThread.self) } public func retrieve(threadId: String) async throws -> ASAThread { let endpoint = ThreadsEndpoint.retrieveThread(threadId) - return try await sendRequest(endpoint: endpoint, responseModel: ASAThread.self) + return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAThread.self) } public func modify(threadId: String, with modifyThread: ASAModifyThreadRequest) async throws -> ASAThread { let endpoint = ThreadsEndpoint.modifyThread(threadId, modifyThread) - return try await sendRequest(endpoint: endpoint, responseModel: ASAThread.self) + return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASAThread.self) } public func delete(threadId: String) async throws -> ASADeleteModelResponse { let endpoint = ThreadsEndpoint.deleteThread(threadId) - return try await sendRequest(endpoint: endpoint, responseModel: ASADeleteModelResponse.self) + return try await sendRequest(session: urlSession, endpoint: endpoint, responseModel: ASADeleteModelResponse.self) } } diff --git a/Tests/AISwiftAssistTests/AISwiftAssistTests.swift b/Tests/AISwiftAssistTests/AISwiftAssistTests.swift deleted file mode 100644 index 513dd4e..0000000 --- a/Tests/AISwiftAssistTests/AISwiftAssistTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import XCTest -@testable import AISwiftAssist - -final class AISwiftAssistTests: XCTestCase { - func testExample() throws { - // XCTest Documentation - // https://developer.apple.com/documentation/xctest - - // Defining Test Cases and Test Methods - // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods - } -} diff --git a/Tests/AISwiftAssistTests/APIs/AssistantsAPITests.swift b/Tests/AISwiftAssistTests/APIs/AssistantsAPITests.swift new file mode 100644 index 0000000..2b7ff8b --- /dev/null +++ b/Tests/AISwiftAssistTests/APIs/AssistantsAPITests.swift @@ -0,0 +1,192 @@ +// +// AssistantsAPITests.swift +// +// +// Created by Alexey on 11/19/23. +// + +import XCTest +@testable import AISwiftAssist + +final class AssistantsAPITests: XCTestCase { + + var assistantsAPI: IAssistantsAPI! + + override func setUp() { + super.setUp() + 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: ASAListAssistantsResponse = 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)") + } + } + + +} diff --git a/Tests/AISwiftAssistTests/APIs/MessagesAPITests.swift b/Tests/AISwiftAssistTests/APIs/MessagesAPITests.swift new file mode 100644 index 0000000..238c8a6 --- /dev/null +++ b/Tests/AISwiftAssistTests/APIs/MessagesAPITests.swift @@ -0,0 +1,126 @@ +// +// 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: "thread_abc123", + createMessage: createMessageRequest) + + XCTAssertEqual(message.id, "msg_abc123") + XCTAssertEqual(message.object, "thread.message") + XCTAssertEqual(message.createdAt, 1699017614) + XCTAssertEqual(message.threadId, "thread_abc123") + XCTAssertEqual(message.role, "user") + XCTAssertEqual(message.content.first?.text?.value, "How does AI work? Explain it in simple terms.") + } 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: "thread_abc123", + messageId: "msg_abc123") + + XCTAssertEqual(message.id, "msg_abc123") + XCTAssertEqual(message.object, "thread.message") + XCTAssertEqual(message.createdAt, 1699017614) + XCTAssertEqual(message.threadId, "thread_abc123") + XCTAssertEqual(message.role, "user") + XCTAssertEqual(message.content.first?.text?.value, "How does AI work? Explain it in simple terms.") + } 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: "thread_abc123", + messageId: "msg_abc123", + modifyMessage: modifyMessageRequest) + + XCTAssertEqual(message.id, "msg_abc123") + XCTAssertEqual(message.object, "thread.message") + XCTAssertEqual(message.createdAt, 1699017614) + XCTAssertEqual(message.threadId, "thread_abc123") + XCTAssertEqual(message.role, "user") + XCTAssertEqual(message.metadata?["modified"], "true") + XCTAssertEqual(message.metadata?["user"], "abc123") + } 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, 2) + XCTAssertEqual(listResponse.data[0].id, "msg_abc123") + XCTAssertEqual(listResponse.data[0].threadId, "thread_abc123") + XCTAssertEqual(listResponse.data[0].role, "user") + XCTAssertEqual(listResponse.data[0].content.first?.text?.value, "How does AI work? Explain it in simple terms.") + } catch { + XCTFail("Error: \(error)") + } + } + + +} diff --git a/Tests/AISwiftAssistTests/APIs/ModelsAPITests.swift b/Tests/AISwiftAssistTests/APIs/ModelsAPITests.swift new file mode 100644 index 0000000..c6bfb72 --- /dev/null +++ b/Tests/AISwiftAssistTests/APIs/ModelsAPITests.swift @@ -0,0 +1,102 @@ +// +// 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: ASAListModelsResponse = 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/ThreadsAPITests.swift b/Tests/AISwiftAssistTests/APIs/ThreadsAPITests.swift new file mode 100644 index 0000000..9c4884f --- /dev/null +++ b/Tests/AISwiftAssistTests/APIs/ThreadsAPITests.swift @@ -0,0 +1,128 @@ +// +// 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 new file mode 100644 index 0000000..6ead8b2 --- /dev/null +++ b/Tests/AISwiftAssistTests/Mosks/AssistantMocks.swift @@ -0,0 +1,134 @@ +// +// File.swift +// +// +// Created by Alexey on 11/19/23. +// + +import Foundation + + +extension AssistantsAPITests { + static let list: String = + """ + { + "object": "list", + "data": [ + { + "id": "asst_abc123", + "object": "assistant", + "created_at": 1698982736, + "name": "Coding Tutor", + "description": null, + "model": "gpt-4", + "instructions": "You are a helpful assistant designed to make me better at coding!", + "tools": [], + "file_ids": [], + "metadata": {} + }, + { + "id": "asst_abc456", + "object": "assistant", + "created_at": 1698982718, + "name": "My Assistant", + "description": null, + "model": "gpt-4", + "instructions": "You are a helpful assistant designed to make me better at coding!", + "tools": [], + "file_ids": [], + "metadata": {} + }, + { + "id": "asst_abc789", + "object": "assistant", + "created_at": 1698982643, + "name": null, + "description": null, + "model": "gpt-4", + "instructions": null, + "tools": [], + "file_ids": [], + "metadata": {} + } + ], + "first_id": "asst_abc123", + "last_id": "asst_abc789", + "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": {} + } + """ + + 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": {} + } + """ + + 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": {} + } + """ + + static let delete: String = + """ + { + "id": "asst_abc123", + "object": "assistant.deleted", + "deleted": true + } + """ +} + diff --git a/Tests/AISwiftAssistTests/Mosks/MessagesMocks.swift b/Tests/AISwiftAssistTests/Mosks/MessagesMocks.swift new file mode 100644 index 0000000..6c2b210 --- /dev/null +++ b/Tests/AISwiftAssistTests/Mosks/MessagesMocks.swift @@ -0,0 +1,140 @@ +// +// File.swift +// +// +// Created by Alexey on 11/20/23. +// + +import Foundation + +extension MessagesAPITests { + + static let list: String = + """ + { + "object": "list", + "data": [ + { + "id": "msg_abc123", + "object": "thread.message", + "created_at": 1699016383, + "thread_id": "thread_abc123", + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "How does AI work? Explain it in simple terms.", + "annotations": [] + } + } + ], + "file_ids": [], + "assistant_id": null, + "run_id": null, + "metadata": {} + }, + { + "id": "msg_abc456", + "object": "thread.message", + "created_at": 1699016383, + "thread_id": "thread_abc123", + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "Hello, what is AI?", + "annotations": [] + } + } + ], + "file_ids": [ + "file-abc123" + ], + "assistant_id": null, + "run_id": null, + "metadata": {} + } + ], + "first_id": "msg_abc123", + "last_id": "msg_abc456", + "has_more": false + } + """ + + static let modify: String = + """ + { + "id": "msg_abc123", + "object": "thread.message", + "created_at": 1699017614, + "thread_id": "thread_abc123", + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "How does AI work? Explain it in simple terms.", + "annotations": [] + } + } + ], + "file_ids": [], + "assistant_id": null, + "run_id": null, + "metadata": { + "modified": "true", + "user": "abc123" + } + } + """ + + static let retrieve: String = + """ + { + "id": "msg_abc123", + "object": "thread.message", + "created_at": 1699017614, + "thread_id": "thread_abc123", + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "How does AI work? Explain it in simple terms.", + "annotations": [] + } + } + ], + "file_ids": [], + "assistant_id": null, + "run_id": null, + "metadata": {} + } + """ + + static let create: String = + """ + { + "id": "msg_abc123", + "object": "thread.message", + "created_at": 1699017614, + "thread_id": "thread_abc123", + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "How does AI work? Explain it in simple terms.", + "annotations": [] + } + } + ], + "file_ids": [], + "assistant_id": null, + "run_id": null, + "metadata": {} + } + """ +} diff --git a/Tests/AISwiftAssistTests/Mosks/ModelsMocks.swift b/Tests/AISwiftAssistTests/Mosks/ModelsMocks.swift new file mode 100644 index 0000000..d842772 --- /dev/null +++ b/Tests/AISwiftAssistTests/Mosks/ModelsMocks.swift @@ -0,0 +1,56 @@ +// +// 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/ThreadsMocks.swift b/Tests/AISwiftAssistTests/Mosks/ThreadsMocks.swift new file mode 100644 index 0000000..d809ac9 --- /dev/null +++ b/Tests/AISwiftAssistTests/Mosks/ThreadsMocks.swift @@ -0,0 +1,52 @@ +// +// 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 new file mode 100644 index 0000000..d670477 --- /dev/null +++ b/Tests/AISwiftAssistTests/Resourses/MockURLProtocol.swift @@ -0,0 +1,54 @@ +// +// File.swift +// +// +// Created by Alexey on 11/19/23. +// + +import Foundation + +class MockURLProtocol: URLProtocol { + // Handler to test the request and return a mock response. + static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data?))? + + 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. + client?.urlProtocol(self, didLoad: data) + } + + // 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. + } +} + +