Skip to content
137 changes: 137 additions & 0 deletions Sources/NextcloudKit/Models/Assistant/v2/Chat.swift
Original file line number Diff line number Diff line change
@@ -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
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChatMessageRequest includes firstHumanMessage, but bodyMap (used as the request payload) omits it. Either include firstHumanMessage in bodyMap or remove the property to avoid a mismatch between the model and what the API actually sends.

Suggested change
"timestamp": timestamp
"timestamp": timestamp,
"firstHumanMessage": firstHumanMessage

Copilot uses AI. Check for mistakes.
]
}

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
}
}
72 changes: 31 additions & 41 deletions Sources/NextcloudKit/Models/Assistant/v2/TaskList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Comment on lines +9 to +31
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OCSTaskListResponse / OCSTaskResponse wrappers don’t include the OCS meta block. If the server returns an OCS error with HTTP 200, the client can’t detect it via these models. Add meta (statuscode/message) and validate it in the calling code to avoid reporting false successes.

Copilot uses AI. Check for mistakes.

// 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
}
}

Expand Down Expand Up @@ -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 {
Expand All @@ -93,5 +85,3 @@ public struct TaskOutput: Codable {
self.output = output
}
}


56 changes: 20 additions & 36 deletions Sources/NextcloudKit/Models/Assistant/v2/TaskTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Comment on lines +12 to +15
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new OCSTaskTypesResponse omits the standard OCS meta block (statuscode/message). Since OCS endpoints can return HTTP 200 with meta.statuscode indicating an error, callers can’t validate the OCS-level status with this model. Consider adding a meta field to the wrapper (and checking it in the request code) to preserve previous behavior.

Suggested change
public struct OCSTaskTypesOCS: Codable {
public let data: OCSTaskTypesData
}
public struct OCSTaskTypesOCS: Codable {
public let meta: OCSMeta?
public let data: OCSTaskTypesData
}
public struct OCSMeta: Codable {
public let status: String?
public let statuscode: Int?
public let message: String?
}

Copilot uses AI. Check for mistakes.
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?
Expand Down Expand Up @@ -81,9 +71,3 @@ public struct Shape: Codable {
self.type = type
}
}






2 changes: 1 addition & 1 deletion Sources/NextcloudKit/NKInterceptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ final class NKInterceptor: RequestInterceptor, Sendable {
self.nkCommonInstance = nkCommonInstance
}

func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
func adapt(_ urlRequest: URLRequest, for session: AssistantSession, completion: @escaping (Result<URLRequest, Error>) -> Void) {
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RequestInterceptor.adapt must take an Alamofire.Session parameter. Changing the signature to AssistantSession breaks protocol conformance and will not compile. Use Session (or qualify as Alamofire.Session) and avoid name collisions with the new AssistantSession model type.

Suggested change
func adapt(_ urlRequest: URLRequest, for session: AssistantSession, completion: @escaping (Result<URLRequest, Error>) -> Void) {
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {

Copilot uses AI. Check for mistakes.
// Log request URL in verbose mode
if NKLogFileManager.shared.logLevel == .verbose,
let url = urlRequest.url?.absoluteString {
Expand Down
Loading
Loading