From cce014af24a268c235e6d8f99cd73c822a0e0d17 Mon Sep 17 00:00:00 2001 From: Aleksei Zelentsov Date: Tue, 5 Dec 2023 14:35:56 +0300 Subject: [PATCH] add new assistant methods *createFile *retrieveFile *deleteFile *listFiles --- .../AISwiftAssist/APIs/AssistantsAPI.swift | 47 ++++++++++ .../Endpoints/AssistantEndpoint.swift | 19 +++- .../Models/Main/ASAAssistantFile.swift | 27 ++++++ .../ASACreateAssistantFileRequest.swift | 23 +++++ .../Request/ASAListFilesParameters.swift | 34 +++++++ .../ASAAssistantFilesListResponse.swift | 30 ++++++ .../APIs/AssistantsAPITests.swift | 92 +++++++++++++++++++ .../Mosks/AssistantMocks.swift | 66 +++++++++++-- 8 files changed, 328 insertions(+), 10 deletions(-) create mode 100644 Sources/AISwiftAssist/Models/Main/ASAAssistantFile.swift create mode 100644 Sources/AISwiftAssist/Models/Request/ASACreateAssistantFileRequest.swift create mode 100644 Sources/AISwiftAssist/Models/Request/ASAListFilesParameters.swift create mode 100644 Sources/AISwiftAssist/Models/Response/ASAAssistantFilesListResponse.swift diff --git a/Sources/AISwiftAssist/APIs/AssistantsAPI.swift b/Sources/AISwiftAssist/APIs/AssistantsAPI.swift index be8bda1..44a9920 100644 --- a/Sources/AISwiftAssist/APIs/AssistantsAPI.swift +++ b/Sources/AISwiftAssist/APIs/AssistantsAPI.swift @@ -36,6 +36,34 @@ 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: ASAListFilesParameters?) async throws -> ASAAssistantFilesListResponse } public final class AssistantsAPI: HTTPClient, IAssistantsAPI { @@ -82,6 +110,25 @@ 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: ASAListFilesParameters? = 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/Endpoints/AssistantEndpoint.swift b/Sources/AISwiftAssist/Endpoints/AssistantEndpoint.swift index e096af4..e07ecf9 100644 --- a/Sources/AISwiftAssist/Endpoints/AssistantEndpoint.swift +++ b/Sources/AISwiftAssist/Endpoints/AssistantEndpoint.swift @@ -13,6 +13,10 @@ 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, ASAListFilesParameters?) } extension AssistantEndpoint: CustomEndpoint { @@ -26,8 +30,9 @@ extension AssistantEndpoint: CustomEndpoint { public var queryItems: [URLQueryItem]? { var items: [URLQueryItem]? switch self { - case .createAssistant, .deleteAssistant, .retrieveAssistant, .modifyAssistant: items = nil + case .createAssistant, .deleteAssistant, .retrieveAssistant, .modifyAssistant, .createFile, .retrieveFile, .deleteFile: items = nil case .getAssistants(let params): items = Utils.createURLQueryItems(from: params) + case .listFiles(_, let params): items = Utils.createURLQueryItems(from: params) } return items } @@ -38,6 +43,10 @@ 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" } } @@ -48,6 +57,10 @@ 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 } } @@ -61,8 +74,8 @@ extension AssistantEndpoint: CustomEndpoint { switch self { case .createAssistant(let createAssistant): return .init(object: createAssistant) case .modifyAssistant(_, let request): return .init(object: request) - case .deleteAssistant, .retrieveAssistant, .getAssistants: return nil - + case .createFile(_, let request): return .init(object: request) + case .deleteAssistant, .retrieveAssistant, .getAssistants, .retrieveFile, .deleteFile, .listFiles: return nil } } } diff --git a/Sources/AISwiftAssist/Models/Main/ASAAssistantFile.swift b/Sources/AISwiftAssist/Models/Main/ASAAssistantFile.swift new file mode 100644 index 0000000..822f8b3 --- /dev/null +++ b/Sources/AISwiftAssist/Models/Main/ASAAssistantFile.swift @@ -0,0 +1,27 @@ +// +// 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/Request/ASACreateAssistantFileRequest.swift b/Sources/AISwiftAssist/Models/Request/ASACreateAssistantFileRequest.swift new file mode 100644 index 0000000..e285b87 --- /dev/null +++ b/Sources/AISwiftAssist/Models/Request/ASACreateAssistantFileRequest.swift @@ -0,0 +1,23 @@ +// +// 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/ASAListFilesParameters.swift b/Sources/AISwiftAssist/Models/Request/ASAListFilesParameters.swift new file mode 100644 index 0000000..5984878 --- /dev/null +++ b/Sources/AISwiftAssist/Models/Request/ASAListFilesParameters.swift @@ -0,0 +1,34 @@ +// +// File.swift +// +// +// Created by Alexey on 12/5/23. +// + +import Foundation + +/// Parameters for listing assistant files. +public struct ASAListFilesParameters: Encodable { + /// A limit on the number of objects to be returned. + /// Can range between 1 and 100. Defaults to 20. + public let limit: Int? + + /// Sort order by the created_at timestamp of the objects. + /// 'asc' for ascending order and 'desc' for descending order. + public let order: String? + + /// A cursor for use in pagination. 'after' is an object ID that defines + /// your place in the list, to fetch the next page of the list. + public let after: String? + + /// 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) { + self.limit = limit + self.order = order + self.after = after + self.before = before + } +} diff --git a/Sources/AISwiftAssist/Models/Response/ASAAssistantFilesListResponse.swift b/Sources/AISwiftAssist/Models/Response/ASAAssistantFilesListResponse.swift new file mode 100644 index 0000000..0cbc0a1 --- /dev/null +++ b/Sources/AISwiftAssist/Models/Response/ASAAssistantFilesListResponse.swift @@ -0,0 +1,30 @@ +// +// 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/Tests/AISwiftAssistTests/APIs/AssistantsAPITests.swift b/Tests/AISwiftAssistTests/APIs/AssistantsAPITests.swift index abbdbdb..be1f99e 100644 --- a/Tests/AISwiftAssistTests/APIs/AssistantsAPITests.swift +++ b/Tests/AISwiftAssistTests/APIs/AssistantsAPITests.swift @@ -188,5 +188,97 @@ final class AssistantsAPITests: XCTestCase { } } + 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)") + } + } + + 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 = ASAListFilesParameters() + 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)") + } + } + } diff --git a/Tests/AISwiftAssistTests/Mosks/AssistantMocks.swift b/Tests/AISwiftAssistTests/Mosks/AssistantMocks.swift index 6ead8b2..5a4acff 100644 --- a/Tests/AISwiftAssistTests/Mosks/AssistantMocks.swift +++ b/Tests/AISwiftAssistTests/Mosks/AssistantMocks.swift @@ -7,7 +7,6 @@ import Foundation - extension AssistantsAPITests { static let list: String = """ @@ -56,7 +55,7 @@ extension AssistantsAPITests { "has_more": false } """ - + static let create: String = """ { @@ -76,9 +75,9 @@ extension AssistantsAPITests { "metadata": {} } """ - + static let retrieve: String = - """ + """ { "id": "asst_abc123", "object": "assistant", @@ -98,9 +97,9 @@ extension AssistantsAPITests { "metadata": {} } """ - + static let modify: String = - """ + """ { "id": "asst_abc123", "object": "assistant", @@ -121,7 +120,7 @@ extension AssistantsAPITests { "metadata": {} } """ - + static let delete: String = """ { @@ -130,5 +129,58 @@ extension AssistantsAPITests { "deleted": true } """ + + static let createFile: String = + """ + { + "id": "file-abc123", + "object": "assistant.file", + "created_at": 1699055364, + "assistant_id": "asst_abc123" + } + """ + + static let retrieveFile: String = + """ + { + "id": "file-abc123", + "object": "assistant.file", + "created_at": 1699055364, + "assistant_id": "asst_abc123" + } + """ + + static let deleteFile: String = + """ + { + "id": "file-abc123", + "object": "assistant.file.deleted", + "deleted": true + } + """ + + static let listFiles: String = + """ + { + "object": "list", + "data": [ + { + "id": "file-abc123", + "object": "assistant.file", + "created_at": 1699060412, + "assistant_id": "asst_abc123" + }, + { + "id": "file-abc456", + "object": "assistant.file", + "created_at": 1699060412, + "assistant_id": "asst_abc123" + } + ], + "first_id": "file-abc123", + "last_id": "file-abc456", + "has_more": false + } + """ }