diff --git a/Sources/NextcloudKit/Models/Assistant/v2/Chat.swift b/Sources/NextcloudKit/Models/Assistant/v2/Chat.swift new file mode 100644 index 00000000..94633c10 --- /dev/null +++ b/Sources/NextcloudKit/Models/Assistant/v2/Chat.swift @@ -0,0 +1,137 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +// MARK: - ChatMessage + +public struct AssistantChatMessage: Codable, Identifiable, Equatable { + public let id: Int + public let sessionId: Int + public let role: String + public let content: String + public let timestamp: Int + + public var isFromHuman: Bool { + role == "human" + } + + public init(id: Int, sessionId: Int, role: String, content: String, timestamp: Int) { + self.id = id + self.sessionId = sessionId + self.role = role + self.content = content + self.timestamp = timestamp + } + + enum CodingKeys: String, CodingKey { + case id + case sessionId = "session_id" + case role + case content + case timestamp + } +} + +// MARK: - ChatMessageRequest + +public struct AssistantChatMessageRequest: Encodable { + public let sessionId: Int + public let role: String + public let content: String + public let timestamp: Int + public let firstHumanMessage: Bool + + public init(sessionId: Int, role: String, content: String, timestamp: Int, firstHumanMessage: Bool) { + self.sessionId = sessionId + self.role = role + self.content = content + self.timestamp = timestamp + self.firstHumanMessage = firstHumanMessage + } + + var bodyMap: [String: Any] { + return [ + "sessionId": sessionId, + "role": role, + "content": content, + "timestamp": timestamp + ] + } + + enum CodingKeys: String, CodingKey { + case sessionId + case role + case content + case timestamp + case firstHumanMessage + } +} + +// MARK: - Session + +public struct AssistantConversation: Codable, Equatable, Hashable { + public let id: Int + public let userId: String? + private let title: String? + public let timestamp: Int + + enum CodingKeys: String, CodingKey { + case id + case userId = "user_id" + case title + case timestamp + } + + public var validTitle: String { + return title ?? createTitle() + + func createTitle() -> String { + let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) + let formatter = DateFormatter() + formatter.locale = .current + formatter.timeZone = .current + formatter.dateFormat = "MMM d yyyy, HH:mm" + return formatter.string(from: date) + } + } +} + +// MARK: - CreateConversation + +public struct AssistantCreatedConversation: Codable, Equatable { + public let conversation: AssistantConversation + + enum CodingKeys: String, CodingKey { + case conversation = "session" + } +} + +// MARK: - Session + +public struct AssistantSession: Codable, Equatable { + public let messageTaskId: Int? + public let titleTaskId: Int? + public let sessionTitle: String? + public let sessionAgencyPendingActions: String? + public let taskId: Int? + + enum CodingKeys: String, CodingKey { + case messageTaskId + case titleTaskId + case sessionTitle + case sessionAgencyPendingActions + case taskId + } +} + +// MARK: - SessionTask + +public struct AssistantSessionTask: Codable, Equatable { + public let taskId: Int + + enum CodingKeys: String, CodingKey { + case taskId + } +} diff --git a/Sources/NextcloudKit/Models/Assistant/v2/TaskList.swift b/Sources/NextcloudKit/Models/Assistant/v2/TaskList.swift index b17978a8..a988026b 100644 --- a/Sources/NextcloudKit/Models/Assistant/v2/TaskList.swift +++ b/Sources/NextcloudKit/Models/Assistant/v2/TaskList.swift @@ -2,30 +2,41 @@ // SPDX-FileCopyrightText: 2025 Milen Pivchev // SPDX-License-Identifier: GPL-3.0-or-later -import SwiftyJSON +import Foundation + +// MARK: - OCS Response Wrappers + +public struct OCSTaskListResponse: Codable { + public let ocs: OCSTaskListOCS + + public struct OCSTaskListOCS: Codable { + public let data: OCSTaskListData + } + + public struct OCSTaskListData: Codable { + public let tasks: [AssistantTask] + } +} + +public struct OCSTaskResponse: Codable { + public let ocs: OCSTaskOCS + + public struct OCSTaskOCS: Codable { + public let data: OCSTaskData + } + + public struct OCSTaskData: Codable { + public let task: AssistantTask + } +} + +// MARK: - Task Models public struct TaskList: Codable { public var tasks: [AssistantTask] - static func deserialize(from data: JSON) -> TaskList? { - let tasks = data.arrayValue.map { taskJson in - AssistantTask( - id: taskJson["id"].int64Value, - type: taskJson["type"].string, - status: taskJson["status"].string, - userId: taskJson["userId"].string, - appId: taskJson["appId"].string, - input: TaskInput(input: taskJson["input"]["input"].string), - output: TaskOutput(output: taskJson["output"]["output"].string), - completionExpectedAt: taskJson["completionExpectedAt"].int, - progress: taskJson["progress"].int, - lastUpdated: taskJson["lastUpdated"].int, - scheduledAt: taskJson["scheduledAt"].int, - endedAt: taskJson["endedAt"].int - ) - } - - return TaskList(tasks: tasks) + public init(tasks: [AssistantTask]) { + self.tasks = tasks } } @@ -57,25 +68,6 @@ public struct AssistantTask: Codable { self.scheduledAt = scheduledAt self.endedAt = endedAt } - - static func deserialize(from data: JSON) -> AssistantTask? { - let task = AssistantTask( - id: data["id"].int64Value, - type: data["type"].string, - status: data["status"].string, - userId: data["userId"].string, - appId: data["appId"].string, - input: TaskInput(input: data["input"]["input"].string), - output: TaskOutput(output: data["output"]["output"].string), - completionExpectedAt: data["completionExpectedAt"].int, - progress: data["progress"].int, - lastUpdated: data["lastUpdated"].int, - scheduledAt: data["scheduledAt"].int, - endedAt: data["endedAt"].int - ) - - return task - } } public struct TaskInput: Codable { @@ -93,5 +85,3 @@ public struct TaskOutput: Codable { self.output = output } } - - diff --git a/Sources/NextcloudKit/Models/Assistant/v2/TaskTypes.swift b/Sources/NextcloudKit/Models/Assistant/v2/TaskTypes.swift index 70b502a7..b9201f55 100644 --- a/Sources/NextcloudKit/Models/Assistant/v2/TaskTypes.swift +++ b/Sources/NextcloudKit/Models/Assistant/v2/TaskTypes.swift @@ -2,44 +2,34 @@ // SPDX-FileCopyrightText: 2025 Milen Pivchev // SPDX-License-Identifier: GPL-3.0-or-later -import SwiftyJSON +import Foundation + +// MARK: - OCS Response Wrapper + +public struct OCSTaskTypesResponse: Codable { + public let ocs: OCSTaskTypesOCS + + public struct OCSTaskTypesOCS: Codable { + public let data: OCSTaskTypesData + } + + public struct OCSTaskTypesData: Codable { + public let types: [String: TaskTypeData] + } +} + +// MARK: - Task Type Models public struct TaskTypes: Codable { public let types: [TaskTypeData] - static func deserialize(from data: JSON) -> TaskTypes? { - var taskTypes: [TaskTypeData] = [] - - for (key, subJson) in data { - let taskTypeData = TaskTypeData( - id: key, - name: subJson["name"].string, - description: subJson["description"].string, - inputShape: subJson["inputShape"].dictionary != nil ? TaskInputShape( - input: subJson["inputShape"]["input"].dictionary != nil ? Shape( - name: subJson["inputShape"]["input"]["name"].stringValue, - description: subJson["inputShape"]["input"]["description"].stringValue, - type: subJson["inputShape"]["input"]["type"].stringValue - ) : nil - ) : nil, - outputShape: subJson["outputShape"].dictionary != nil ? TaskOutputShape( - output: subJson["outputShape"]["output"].dictionary != nil ? Shape( - name: subJson["outputShape"]["output"]["name"].stringValue, - description: subJson["outputShape"]["output"]["description"].stringValue, - type: subJson["outputShape"]["output"]["type"].stringValue - ) : nil - ) : nil - ) - - taskTypes.append(taskTypeData) - } - - return TaskTypes(types: taskTypes) + public init(types: [TaskTypeData]) { + self.types = types } } public struct TaskTypeData: Codable { - public let id: String? + public var id: String? public let name: String? public let description: String? public let inputShape: TaskInputShape? @@ -81,9 +71,3 @@ public struct Shape: Codable { self.type = type } } - - - - - - diff --git a/Sources/NextcloudKit/NKInterceptor.swift b/Sources/NextcloudKit/NKInterceptor.swift index af3d3289..5deedca8 100644 --- a/Sources/NextcloudKit/NKInterceptor.swift +++ b/Sources/NextcloudKit/NKInterceptor.swift @@ -12,7 +12,7 @@ final class NKInterceptor: RequestInterceptor, Sendable { self.nkCommonInstance = nkCommonInstance } - func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { + func adapt(_ urlRequest: URLRequest, for session: AssistantSession, completion: @escaping (Result) -> Void) { // Log request URL in verbose mode if NKLogFileManager.shared.logLevel == .verbose, let url = urlRequest.url?.absoluteString { diff --git a/Sources/NextcloudKit/NextcloudKit+AssistantV2.swift b/Sources/NextcloudKit/NextcloudKit+AssistantV2.swift index 20dbead4..da1d3162 100644 --- a/Sources/NextcloudKit/NextcloudKit+AssistantV2.swift +++ b/Sources/NextcloudKit/NextcloudKit+AssistantV2.swift @@ -4,67 +4,21 @@ import Foundation import Alamofire -import SwiftyJSON public extension NextcloudKit { /// Retrieves the list of supported task types for a specific account and task category. /// Typically used to discover available AI or text processing capabilities. /// - /// Parameters: - /// - account: The Nextcloud account making the request. - /// - supportedTaskType: Type of tasks to retrieve, default is "Text". - /// - options: Optional HTTP request configuration. - /// - taskHandler: Optional closure to access the URLSessionTask. - /// - completion: Completion handler returning the account, list of supported types, raw response, and NKError. - func textProcessingGetTypesV2(account: String, - supportedTaskType: String = "Text", - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ types: [TaskTypeData]?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { - let endpoint = "ocs/v2.php/taskprocessing/tasktypes" - guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, nil, .urlError) } - } - - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in - task.taskDescription = options.taskDescription - taskHandler(task) - }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in - switch response.result { - case .failure(let error): - let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, response, error) } - case .success(let jsonData): - let json = JSON(jsonData) - let data = json["ocs"]["data"]["types"] - let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError - if 200..<300 ~= statusCode { - let dict = TaskTypes.deserialize(from: data) - let result = dict?.types.map({$0}) - let filteredResult = result? - .filter({ $0.inputShape?.input?.type == supportedTaskType && $0.outputShape?.output?.type == supportedTaskType }) - .sorted(by: {$0.id! < $1.id!}) - options.queue.async { completion(account, filteredResult, response, .success) } - } else { - options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } - } - } - } - } - - /// Asynchronously retrieves the supported task types for the given account and category. /// - Parameters: - /// - account: Account performing the request. - /// - supportedTaskType: The task category to filter by (default: "Text"). - /// - options: Optional configuration. - /// - taskHandler: Callback for the underlying URLSessionTask. + /// - account: The Nextcloud account making the request. + /// - supportedTaskType: Type of tasks to retrieve, default is "Text". + /// - options: Optional HTTP request configuration. + /// - taskHandler: Optional closure to access the URLSessionTask. /// - Returns: A tuple with named values for account, supported types, response, and error. - func textProcessingGetTypesV2Async(account: String, - supportedTaskType: String = "Text", - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + func textProcessingGetTypesV2(account: String, + supportedTaskType: String = "Text", + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } ) async -> ( account: String, types: [TaskTypeData]?, @@ -72,16 +26,45 @@ public extension NextcloudKit { error: NKError ) { await withCheckedContinuation { continuation in - textProcessingGetTypesV2(account: account, - supportedTaskType: supportedTaskType, - options: options, - taskHandler: taskHandler) { account, types, responseData, error in - continuation.resume(returning: ( - account: account, - types: types, - responseData: responseData, - error: error - )) + let endpoint = "ocs/v2.php/taskprocessing/tasktypes" + guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { + continuation.resume(returning: (account: account, types: nil, responseData: nil, error: .urlError)) + } + } + + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { + continuation.resume(returning: (account: account, types: nil, responseData: response, error: error)) + } + case .success(let data): + let decoder = JSONDecoder() + if let result = try? decoder.decode(OCSTaskTypesResponse.self, from: data) { + var types = result.ocs.data.types.map { (key, value) -> TaskTypeData in + var taskType = value + taskType.id = key + return taskType + } + types = types + .filter { $0.inputShape?.input?.type == supportedTaskType && $0.outputShape?.output?.type == supportedTaskType } + .sorted { ($0.id ?? "") < ($1.id ?? "") } + options.queue.async { + continuation.resume(returning: (account: account, types: types, responseData: response, error: .success)) + } + } else { + options.queue.async { + continuation.resume(returning: (account: account, types: nil, responseData: response, error: .success)) + } + } + } } } } @@ -89,231 +72,508 @@ public extension NextcloudKit { /// Schedules a new text processing task for a specific account and task type. /// Useful for initiating assistant-based text analysis, generation, or transformation. /// - /// Parameters: - /// - input: The input text to be processed. - /// - taskType: The specific task type to execute (e.g., summarization, sentiment analysis). - /// - account: The Nextcloud account initiating the task. - /// - options: Optional HTTP request configuration. - /// - taskHandler: Optional closure to access the underlying URLSessionTask. - /// - completion: Completion handler returning the account, scheduled task, raw response, and NKError. + /// - Parameters: + /// - input: The input text to be processed. + /// - taskType: The specific task type to execute (e.g., summarization, sentiment analysis). + /// - account: The Nextcloud account initiating the task. + /// - options: Optional HTTP request configuration. + /// - taskHandler: Optional closure to access the underlying URLSessionTask. + /// - Returns: A tuple with named values for account, scheduled task, response, and error. func textProcessingScheduleV2(input: String, - taskType: TaskTypeData, - account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ task: AssistantTask?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { - let endpoint = "/ocs/v2.php/taskprocessing/schedule" - guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, nil, .urlError) } - } + taskType: TaskTypeData, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + ) async -> ( + account: String, + task: AssistantTask?, + responseData: AFDataResponse?, + error: NKError + ) { + await withCheckedContinuation { continuation in + let endpoint = "/ocs/v2.php/taskprocessing/schedule" + guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { + continuation.resume(returning: (account: account, task: nil, responseData: nil, error: .urlError)) + } + } - let inputField: [String: String] = ["input": input] - let parameters: [String: Any] = ["input": inputField, "type": taskType.id ?? "", "appId": "assistant", "customId": ""] + let inputField: [String: String] = ["input": input] + let parameters: [String: Any] = ["input": inputField, "type": taskType.id ?? "", "appId": "assistant", "customId": ""] - nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in - task.taskDescription = options.taskDescription - taskHandler(task) - }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in - switch response.result { - case .failure(let error): - let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, response, error) } - case .success(let jsonData): - let json = JSON(jsonData) - let data = json["ocs"]["data"]["task"] - let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError - if 200..<300 ~= statusCode { - let result = AssistantTask.deserialize(from: data) - options.queue.async { completion(account, result, response, .success) } - } else { - options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + nkSession.sessionData.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { + continuation.resume(returning: (account: account, task: nil, responseData: response, error: error)) + } + case .success(let data): + let decoder = JSONDecoder() + let result = try? decoder.decode(OCSTaskResponse.self, from: data) + options.queue.async { + continuation.resume(returning: (account: account, task: result?.ocs.data.task, responseData: response, error: .success)) + } } } } } - /// Asynchronously schedules a new text processing task using the specified task type. + /// Retrieves all scheduled text processing tasks of a specific type for the given account. + /// Useful for listing and tracking tasks like summarization, transcription, or classification. + /// /// - Parameters: - /// - input: Input text to be processed. - /// - taskType: Type of task to be executed. - /// - account: The account performing the scheduling. - /// - options: Optional configuration. - /// - taskHandler: Callback to access the associated URLSessionTask. - /// - Returns: A tuple with named values for account, scheduled task, response, and error. - func textProcessingScheduleV2Async(input: String, - taskType: TaskTypeData, - account: String, - options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + /// - taskType: Identifier of the task type to filter tasks (e.g., "Text"). + /// - account: The Nextcloud account performing the request. + /// - options: Optional HTTP request configuration. + /// - taskHandler: Optional closure to access the underlying URLSessionTask. + /// - Returns: A tuple with named values for account, task list, response, and error. + func textProcessingGetTasksV2(taskType: String, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } ) async -> ( account: String, - task: AssistantTask?, + tasks: TaskList?, responseData: AFDataResponse?, error: NKError ) { await withCheckedContinuation { continuation in - textProcessingScheduleV2(input: input, - taskType: taskType, - account: account, - options: options, - taskHandler: taskHandler) { account, task, responseData, error in - continuation.resume(returning: ( - account: account, - task: task, - responseData: responseData, - error: error - )) + let endpoint = "/ocs/v2.php/taskprocessing/tasks?taskType=\(taskType)" + guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { + continuation.resume(returning: (account: account, tasks: nil, responseData: nil, error: .urlError)) + } + } + + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { + continuation.resume(returning: (account: account, tasks: nil, responseData: response, error: error)) + } + case .success(let data): + let decoder = JSONDecoder() + let result = try? decoder.decode(OCSTaskListResponse.self, from: data) + options.queue.async { + continuation.resume(returning: (account: account, tasks: result.map { TaskList(tasks: $0.ocs.data.tasks) }, responseData: response, error: .success)) + } + } } } } - /// Retrieves all scheduled text processing tasks of a specific type for the given account. - /// Useful for listing and tracking tasks like summarization, transcription, or classification. + /// Deletes a scheduled text processing task with a specific identifier. + /// Useful for canceling tasks that are no longer needed or invalid. /// - /// Parameters: - /// - taskType: Identifier of the task type to filter tasks (e.g., "Text"). - /// - account: The Nextcloud account performing the request. - /// - options: Optional HTTP request configuration. - /// - taskHandler: Optional closure to access the underlying URLSessionTask. - /// - completion: Completion handler returning the account, list of tasks, raw response, and NKError. - func textProcessingGetTasksV2(taskType: String, + /// - Parameters: + /// - taskId: The unique identifier of the task to delete. + /// - account: The Nextcloud account executing the deletion. + /// - options: Optional HTTP request configuration. + /// - taskHandler: Optional closure to access the underlying URLSessionTask. + /// - Returns: A tuple with named values for account, response, and error. + func textProcessingDeleteTaskV2(taskId: Int64, account: String, options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ tasks: TaskList?, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { - let endpoint = "/ocs/v2.php/taskprocessing/tasks?taskType=\(taskType)" - guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, nil, .urlError) } - } + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + ) async -> ( + account: String, + responseData: AFDataResponse?, + error: NKError + ) { + await withCheckedContinuation { continuation in + let endpoint = "/ocs/v2.php/taskprocessing/task/\(taskId)" + guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { + continuation.resume(returning: (account: account, responseData: nil, error: .urlError)) + } + } - nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in - task.taskDescription = options.taskDescription - taskHandler(task) - }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in - switch response.result { - case .failure(let error): - let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, nil, response, error) } - case .success(let jsonData): - let json = JSON(jsonData) - let data = json["ocs"]["data"]["tasks"] - let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError - if 200..<300 ~= statusCode { - let result = TaskList.deserialize(from: data) - options.queue.async { completion(account, result, response, .success) } - } else { - options.queue.async { completion(account, nil, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { + continuation.resume(returning: (account: account, responseData: response, error: error)) + } + case .success: + options.queue.async { + continuation.resume(returning: (account: account, responseData: response, error: .success)) + } } } } } - /// Asynchronously retrieves a list of scheduled text processing tasks for a specific type. + /// Retrieves all chat sessions. Each session has messages. + /// /// - Parameters: - /// - taskType: Type of the tasks to query. - /// - account: The account performing the query. - /// - options: Optional configuration. - /// - taskHandler: Callback to access the associated URLSessionTask. - /// - Returns: A tuple with named values for account, task list, response, and error. - func textProcessingGetTasksV2Async(taskType: String, - account: String, + /// - account: The Nextcloud account performing the request. + /// - options: Optional HTTP request configuration. + /// - taskHandler: Optional closure to access the underlying URLSessionTask. + /// - Returns: A tuple with named values for account, sessions, response, and error. + func getAssistantChatConversations(account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } ) async -> ( account: String, - tasks: TaskList?, + sessions: [AssistantConversation]?, responseData: AFDataResponse?, error: NKError ) { await withCheckedContinuation { continuation in - textProcessingGetTasksV2(taskType: taskType, - account: account, - options: options, - taskHandler: taskHandler) { account, tasks, responseData, error in - continuation.resume(returning: ( - account: account, - tasks: tasks, - responseData: responseData, - error: error - )) + let endpoint = "/ocs/v2.php/apps/assistant/chat/sessions" + guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { + continuation.resume(returning: (account: account, sessions: nil, responseData: nil, error: .urlError)) + } + } + + nkSession.sessionData.request(url, method: .get, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { + continuation.resume(returning: (account: account, sessions: nil, responseData: response, error: error)) + } + case .success(let data): + let decoder = JSONDecoder() + let result = try? decoder.decode([AssistantConversation].self, from: data) + options.queue.async { + continuation.resume(returning: (account: account, sessions: result, responseData: response, error: .success)) + } + } } } } - /// Deletes a scheduled text processing task with a specific identifier. - /// Useful for canceling tasks that are no longer needed or invalid. + /// Retrieves all messages for a given chat session. /// - /// Parameters: - /// - taskId: The unique identifier of the task to delete. - /// - account: The Nextcloud account executing the deletion. - /// - options: Optional HTTP request configuration. - /// - taskHandler: Optional closure to access the underlying URLSessionTask. - /// - completion: Completion handler returning the account, raw response, and NKError. - func textProcessingDeleteTaskV2(taskId: Int64, + /// - Parameters: + /// - sessionId: The chat session from which to fetch all messages. + /// - account: The Nextcloud account performing the request. + /// - options: Optional HTTP request configuration. + /// - taskHandler: Optional closure to access the underlying URLSessionTask. + /// - Returns: A tuple with named values for account, chat messages, response, and error. + func getAssistantChatMessages(sessionId: Int, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + ) async -> ( + account: String, + chatMessages: [AssistantChatMessage]?, + responseData: AFDataResponse?, + error: NKError + ) { + await withCheckedContinuation { continuation in + let endpoint = "/ocs/v2.php/apps/assistant/chat/messages" + guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { + continuation.resume(returning: (account: account, chatMessages: nil, responseData: nil, error: .urlError)) + } + } + + nkSession.sessionData.request(url, method: .get, parameters: ["sessionId": sessionId], encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { + continuation.resume(returning: (account: account, chatMessages: nil, responseData: response, error: error)) + } + case .success(let data): + let decoder = JSONDecoder() + let result = try? decoder.decode([AssistantChatMessage].self, from: data) + options.queue.async { + continuation.resume(returning: (account: account, chatMessages: result, responseData: response, error: .success)) + } + } + } + } + } + + /// Creates a new message in a chat session. + /// + /// - Parameters: + /// - messageRequest: The message request containing sessionId, role, content, and timestamp. + /// - account: The Nextcloud account performing the request. + /// - options: Optional HTTP request configuration. + /// - taskHandler: Optional closure to access the underlying URLSessionTask. + /// - Returns: A tuple with named values for account, created message, response, and error. + func createAssistantChatMessage(messageRequest: AssistantChatMessageRequest, account: String, options: NKRequestOptions = NKRequestOptions(), - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ responseData: AFDataResponse?, _ error: NKError) -> Void) { - let endpoint = "/ocs/v2.php/taskprocessing/task/\(taskId)" - guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), - let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint), - let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { - return options.queue.async { completion(account, nil, .urlError) } - } + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + ) async -> ( + account: String, + chatMessage: AssistantChatMessage?, + responseData: AFDataResponse?, + error: NKError + ) { + await withCheckedContinuation { continuation in + let endpoint = "/ocs/v2.php/apps/assistant/chat/new_message" + guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { + continuation.resume(returning: (account: account, chatMessage: nil, responseData: nil, error: .urlError)) + } + } - nkSession.sessionData.request(url, method: .delete, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in - task.taskDescription = options.taskDescription - taskHandler(task) - }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in - switch response.result { - case .failure(let error): - let error = NKError(error: error, afResponse: response, responseData: response.data) - options.queue.async { completion(account, response, error) } - case .success(let jsonData): - let json = JSON(jsonData) - let statusCode = json["ocs"]["meta"]["statuscode"].int ?? NKError.internalError - if 200..<300 ~= statusCode { - options.queue.async { completion(account, response, .success) } - } else { - options.queue.async { completion(account, response, NKError(rootJson: json, fallbackStatusCode: response.response?.statusCode)) } + nkSession.sessionData.request(url, method: .put, parameters: messageRequest.bodyMap, encoding: JSONEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { + continuation.resume(returning: (account: account, chatMessage: nil, responseData: response, error: error)) + } + case .success(let data): + let decoder = JSONDecoder() + let result = try? decoder.decode(AssistantChatMessage.self, from: data) + options.queue.async { + continuation.resume(returning: (account: account, chatMessage: result, responseData: response, error: .success)) + } } } } } - /// Asynchronously deletes a text processing task by ID for the specified account. + /// Creates a new chat conversation/session. + /// /// - Parameters: - /// - taskId: ID of the task to be deleted. - /// - account: The account performing the operation. - /// - options: Optional configuration. - /// - taskHandler: Callback to access the associated URLSessionTask. - /// - Returns: A tuple with named values for account, response, and error. - func textProcessingDeleteTaskV2Async(taskId: Int64, + /// - title: Optional title for the conversation. + /// - timestamp: The timestamp for the conversation creation. + /// - account: The Nextcloud account performing the request. + /// - options: Optional HTTP request configuration. + /// - taskHandler: Optional closure to access the underlying URLSessionTask. + /// - Returns: A tuple with named values for account, created conversation, response, and error. + func createAssistantChatConversation(title: String?, + timestamp: Int, account: String, options: NKRequestOptions = NKRequestOptions(), taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } ) async -> ( account: String, + conversation: AssistantCreatedConversation?, responseData: AFDataResponse?, error: NKError ) { await withCheckedContinuation { continuation in - textProcessingDeleteTaskV2(taskId: taskId, - account: account, - options: options, - taskHandler: taskHandler) { account, responseData, error in - continuation.resume(returning: ( - account: account, - responseData: responseData, - error: error - )) + let endpoint = "/ocs/v2.php/apps/assistant/chat/new_session" + guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { + continuation.resume(returning: (account: account, conversation: nil, responseData: nil, error: .urlError)) + } + } + + var parameters: [String: Any] = ["timestamp": timestamp] + if let title = title { + parameters["title"] = title + } + + nkSession.sessionData.request(url, method: .put, parameters: parameters, encoding: JSONEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { + continuation.resume(returning: (account: account, conversation: nil, responseData: response, error: error)) + } + case .success(let data): + let decoder = JSONDecoder() + let result = try? decoder.decode(AssistantCreatedConversation.self, from: data) + options.queue.async { + continuation.resume(returning: (account: account, conversation: result, responseData: response, error: .success)) + } + } } } } -} + /// Checks the generation status of a chat message task. + /// + /// - Parameters: + /// - taskId: The ID of the generation task to check. + /// - sessionId: The chat session ID. + /// - account: The Nextcloud account performing the request. + /// - options: Optional HTTP request configuration. + /// - taskHandler: Optional closure to access the underlying URLSessionTask. + /// - Returns: A tuple with named values for account, chat message (if ready), response, and error. + func checkAssistantChatGeneration(taskId: Int, + sessionId: Int, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + ) async -> ( + account: String, + chatMessage: AssistantChatMessage?, + responseData: AFDataResponse?, + error: NKError + ) { + await withCheckedContinuation { continuation in + let endpoint = "/ocs/v2.php/apps/assistant/chat/check_generation" + guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { + continuation.resume(returning: (account: account, chatMessage: nil, responseData: nil, error: .urlError)) + } + } + + let parameters: [String: Any] = ["taskId": taskId, "sessionId": sessionId] + nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { + continuation.resume(returning: (account: account, chatMessage: nil, responseData: response, error: error)) + } + case .success(let data): + let decoder = JSONDecoder() + let result = try? decoder.decode(AssistantChatMessage.self, from: data) + options.queue.async { + continuation.resume(returning: (account: account, chatMessage: result, responseData: response, error: .success)) + } + } + } + } + } + + /// Triggers generation for a chat session. + /// + /// - Parameters: + /// - sessionId: The chat session ID to generate for. + /// - account: The Nextcloud account performing the request. + /// - options: Optional HTTP request configuration. + /// - taskHandler: Optional closure to access the underlying URLSessionTask. + /// - Returns: A tuple with named values for account, session task, response, and error. + func generateAssistantChatSession(sessionId: Int, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + ) async -> ( + account: String, + sessionTask: AssistantSessionTask?, + responseData: AFDataResponse?, + error: NKError + ) { + await withCheckedContinuation { continuation in + let endpoint = "/ocs/v2.php/apps/assistant/chat/generate" + guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { + continuation.resume(returning: (account: account, sessionTask: nil, responseData: nil, error: .urlError)) + } + } + + let parameters: [String: Any] = ["sessionId": sessionId] + + nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { + continuation.resume(returning: (account: account, sessionTask: nil, responseData: response, error: error)) + } + case .success(let data): + let decoder = JSONDecoder() + let result = try? decoder.decode(AssistantSessionTask.self, from: data) + options.queue.async { + continuation.resume(returning: (account: account, sessionTask: result, responseData: response, error: .success)) + } + } + } + } + } + + /// Checks if a chat session exists and retrieves its details. + /// + /// - Parameters: + /// - sessionId: The ID of the chat session to check. + /// - account: The Nextcloud account performing the request. + /// - options: Optional HTTP request configuration. + /// - taskHandler: Optional closure to access the underlying URLSessionTask. + /// - Returns: A tuple with named values for account, session (if found), response, and error. + func checkAssistantChatSession(sessionId: Int, + account: String, + options: NKRequestOptions = NKRequestOptions(), + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + ) async -> ( + account: String, + session: AssistantSession?, + responseData: AFDataResponse?, + error: NKError + ) { + await withCheckedContinuation { continuation in + let endpoint = "/ocs/v2.php/apps/assistant/chat/check_session" + guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account), + let url = nkCommonInstance.createStandardUrl(serverUrl: nkSession.urlBase, endpoint: endpoint), + let headers = nkCommonInstance.getStandardHeaders(account: account, options: options) else { + return options.queue.async { + continuation.resume(returning: (account: account, session: nil, responseData: nil, error: .urlError)) + } + } + + let parameters: [String: Any] = ["sessionId": sessionId] + + nkSession.sessionData.request(url, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance)).validate(statusCode: 200..<300).onURLSessionTaskCreation { task in + task.taskDescription = options.taskDescription + taskHandler(task) + }.responseData(queue: self.nkCommonInstance.backgroundQueue) { response in + switch response.result { + case .failure(let error): + let error = NKError(error: error, afResponse: response, responseData: response.data) + options.queue.async { + continuation.resume(returning: (account: account, session: nil, responseData: response, error: error)) + } + case .success(let data): + let decoder = JSONDecoder() + let result = try? decoder.decode(AssistantSession.self, from: data) + options.queue.async { + continuation.resume(returning: (account: account, session: result, responseData: response, error: .success)) + } + } + } + } + } +}