diff --git a/library/src/androidTest/java/com/owncloud/android/lib/resources/assistant/chat/AssistantChatTests.kt b/library/src/androidTest/java/com/owncloud/android/lib/resources/assistant/chat/AssistantChatTests.kt new file mode 100644 index 0000000000..22e8531234 --- /dev/null +++ b/library/src/androidTest/java/com/owncloud/android/lib/resources/assistant/chat/AssistantChatTests.kt @@ -0,0 +1,156 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.resources.assistant.chat + +import com.owncloud.android.AbstractIT +import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessageRequest +import com.owncloud.android.lib.resources.assistant.v2.GetTaskTypesRemoteOperationV2 +import com.owncloud.android.lib.resources.status.NextcloudVersion +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Test + +class AssistantChatTests : AbstractIT() { + private lateinit var sessionId: String + + @Before + fun before() { + testOnlyOnServer(NextcloudVersion.nextcloud_30) + + val result = + CreateConversationRemoteOperation(null, System.currentTimeMillis()) + .execute(nextcloudClient) + assertTrue(result.isSuccess) + sessionId = + result.resultData.session.id + .toString() + } + + @Test + fun testCreateAndGetMessages() { + val messageRequest = + ChatMessageRequest( + sessionId = sessionId, + role = "human", + content = "Hello assistant!", + timestamp = System.currentTimeMillis() + ) + + val createResult = CreateMessageRemoteOperation(messageRequest).execute(nextcloudClient) + assertTrue(createResult.isSuccess) + + val createdMessage = createResult.resultData!! + assertEquals("Hello assistant!", createdMessage.content) + assertEquals("human", createdMessage.role) + assertEquals(sessionId.toLongOrNull(), createdMessage.sessionId) + + // Get messages for session + val getResult = GetMessagesRemoteOperation(sessionId).execute(nextcloudClient) + assertTrue(getResult.isSuccess) + + val messages = getResult.resultData + assertTrue(messages.isNotEmpty()) + assertTrue(messages.any { it.id == createdMessage.id }) + } + + @Test + fun testDeleteMessage() { + val messageRequest = + ChatMessageRequest( + sessionId = sessionId, + role = "human", + content = "Message to delete", + timestamp = System.currentTimeMillis() + ) + val createResult = CreateMessageRemoteOperation(messageRequest).execute(nextcloudClient) + assertTrue(createResult.isSuccess) + + val messageId = createResult.resultData!!.id.toString() + + // Delete the message + val deleteResult = DeleteMessageRemoteOperation(messageId, sessionId).execute(nextcloudClient) + assertTrue(deleteResult.isSuccess) + + // Ensure the message is gone + val getResult = GetMessagesRemoteOperation(sessionId).execute(nextcloudClient) + assertTrue(getResult.isSuccess) + assertTrue(getResult.resultData!!.none { it.id.toString() == messageId }) + } + + @Test + fun testGetAndDeleteConversations() { + // Create a message to have a session + val messageRequest = + ChatMessageRequest( + sessionId = sessionId, + role = "human", + content = "Starting conversation", + timestamp = System.currentTimeMillis() + ) + CreateMessageRemoteOperation(messageRequest).execute(nextcloudClient) + + // Get list of conversations + val getConversationsResult = GetConversationListRemoteOperation().execute(nextcloudClient) + assertTrue(getConversationsResult.isSuccess) + + val conversations = getConversationsResult.resultData + assertTrue(conversations.any { it.id.toString() == sessionId }) + + // Delete conversation + val deleteResult = DeleteConversationRemoteOperation(sessionId).execute(nextcloudClient) + assertTrue(deleteResult.isSuccess) + + // Ensure conversation is gone + val getAfterDelete = GetConversationListRemoteOperation().execute(nextcloudClient) + assertTrue(getAfterDelete.isSuccess) + assertTrue(getAfterDelete.resultData!!.none { it.id.toString() == sessionId }) + } + + @Test + fun testGetTaskTypesAndVerifyChatAndSorting() { + testOnlyOnServer(NextcloudVersion.nextcloud_34) + + val result = GetTaskTypesRemoteOperationV2().execute(nextcloudClient) + + assertTrue("Request must succeed", result.isSuccess) + val types = result.resultData + assertNotNull("Task types must not be null", types) + assertTrue("Task types list must not be empty", types!!.isNotEmpty()) + + val firstElementIsChat = types.first().isChat() + assertTrue( + "The first task type must be a chat type (sorted by isChat descending)", + firstElementIsChat + ) + + val chatTypes = types.filter { it.isChat() } + assertTrue("There must be at least one chat-type task", chatTypes.isNotEmpty()) + + val nonChat = types.filterNot { it.isChat() } + assertTrue( + "There must be at least one non-chat task with single text input/output", + nonChat.isNotEmpty() + ) + + val indexOfFirstNonChat = types.indexOfFirst { !it.isChat() } + if (indexOfFirstNonChat > 0) { + val anyChatAfterNonChat = types.drop(indexOfFirstNonChat).any { it.isChat() } + assertTrue( + "Chat types must appear before non-chat types in the list", + !anyChatAfterNonChat + ) + } + + types.forEach { tt -> + assertNotNull("Each task type must have an ID assigned", tt.id) + } + } +} diff --git a/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/CheckGenerationRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/CheckGenerationRemoteOperation.kt new file mode 100644 index 0000000000..138daf9b9e --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/CheckGenerationRemoteOperation.kt @@ -0,0 +1,54 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.resources.assistant.chat +import com.nextcloud.common.NextcloudClient +import com.nextcloud.operations.GetMethod +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessage +import org.apache.commons.httpclient.HttpStatus + +class CheckGenerationRemoteOperation( + private val taskId: String, + private val sessionId: String +) : RemoteOperation() { + @Suppress("TooGenericExceptionCaught") + override fun run(client: NextcloudClient): RemoteOperationResult { + val url = + client.baseUri.toString() + + "$BASE_URL/check_generation?taskId=$taskId&sessionId=$sessionId" + + val getMethod = GetMethod(url, true) + val status = getMethod.execute(client) + + return try { + if (status == HttpStatus.SC_OK) { + val responseBody = getMethod.getResponseBodyAsString() + val jsonResponse = gson.fromJson(responseBody, ChatMessage::class.java) + + val result = RemoteOperationResult(true, getMethod) + result.resultData = jsonResponse + result + } else { + RemoteOperationResult(false, getMethod) + } + } catch (e: Exception) { + Log_OC.e(TAG, "check generation failed: ", e) + RemoteOperationResult(false, getMethod) + } finally { + getMethod.releaseConnection() + } + } + + companion object { + private const val TAG = "CheckGenerationRemoteOperation" + private const val BASE_URL = "/ocs/v2.php/apps/assistant/chat" + } +} diff --git a/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/CheckSessionRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/CheckSessionRemoteOperation.kt new file mode 100644 index 0000000000..98cedf3b7c --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/CheckSessionRemoteOperation.kt @@ -0,0 +1,54 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.resources.assistant.chat + +import com.nextcloud.common.NextcloudClient +import com.nextcloud.operations.GetMethod +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.assistant.chat.model.Session +import org.apache.commons.httpclient.HttpStatus + +class CheckSessionRemoteOperation( + private val sessionId: String +) : RemoteOperation() { + @Suppress("TooGenericExceptionCaught") + override fun run(client: NextcloudClient): RemoteOperationResult { + val getMethod = + GetMethod( + client.baseUri.toString() + "$BASE_URL/check_session?sessionId=$sessionId", + true + ) + val status = getMethod.execute(client) + + return try { + if (status == HttpStatus.SC_OK) { + val responseBody = getMethod.getResponseBodyAsString() + val jsonResponse = gson.fromJson(responseBody, Session::class.java) + + val result = RemoteOperationResult(true, getMethod) + result.resultData = jsonResponse + result + } else { + RemoteOperationResult(false, getMethod) + } + } catch (e: Exception) { + Log_OC.e(TAG, "check session failed: ", e) + RemoteOperationResult(false, getMethod) + } finally { + getMethod.releaseConnection() + } + } + + companion object { + private const val TAG = "CheckSessionRemoteOperation" + private const val BASE_URL = "/ocs/v2.php/apps/assistant/chat" + } +} diff --git a/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/CreateConversationRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/CreateConversationRemoteOperation.kt new file mode 100644 index 0000000000..51818f16cd --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/CreateConversationRemoteOperation.kt @@ -0,0 +1,63 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.resources.assistant.chat + +import com.google.gson.reflect.TypeToken +import com.nextcloud.common.NextcloudClient +import com.nextcloud.operations.PutMethod +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.assistant.chat.model.CreateConversation +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody +import org.apache.commons.httpclient.HttpStatus + +class CreateConversationRemoteOperation( + private val title: String?, + private val timestamp: Long +) : RemoteOperation() { + @Suppress("TooGenericExceptionCaught") + override fun run(client: NextcloudClient): RemoteOperationResult { + val bodyMap = + hashMapOf( + "title" to title, + "timestamp" to timestamp + ) + + val json = gson.toJson(bodyMap) + val requestBody = json.toRequestBody("application/json".toMediaTypeOrNull()) + + val putMethod = PutMethod(client.baseUri.toString() + "$BASE_URL/new_session", true, requestBody) + val status = putMethod.execute(client) + + return try { + if (status == HttpStatus.SC_OK) { + val responseBody = putMethod.getResponseBodyAsString() + val type = object : TypeToken() {}.type + val response: CreateConversation = gson.fromJson(responseBody, type) + val result: RemoteOperationResult = RemoteOperationResult(true, putMethod) + result.resultData = response + result + } else { + RemoteOperationResult(false, putMethod) + } + } catch (e: Exception) { + Log_OC.e(TAG, "create conversation: ", e) + RemoteOperationResult(false, putMethod) + } finally { + putMethod.releaseConnection() + } + } + + companion object { + private const val TAG = "CreateConversationRemoteOperation" + private const val BASE_URL = "/ocs/v2.php/apps/assistant/chat" + } +} diff --git a/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/CreateMessageRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/CreateMessageRemoteOperation.kt new file mode 100644 index 0000000000..1bb98be677 --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/CreateMessageRemoteOperation.kt @@ -0,0 +1,57 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.resources.assistant.chat + +import com.google.gson.reflect.TypeToken +import com.nextcloud.common.NextcloudClient +import com.nextcloud.operations.PutMethod +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessage +import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessageRequest +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody +import org.apache.commons.httpclient.HttpStatus + +class CreateMessageRemoteOperation( + private val messageRequest: ChatMessageRequest +) : RemoteOperation() { + @Suppress("TooGenericExceptionCaught") + override fun run(client: NextcloudClient): RemoteOperationResult { + val json = gson.toJson(messageRequest.bodyMap) + val requestBody = json.toRequestBody("application/json".toMediaTypeOrNull()) + + val putMethod = PutMethod(client.baseUri.toString() + "$BASE_URL/new_message", true, requestBody) + val status = putMethod.execute(client) + + return try { + if (status == HttpStatus.SC_OK) { + val responseBody = putMethod.getResponseBodyAsString() + val type = object : TypeToken() {}.type + val response: ChatMessage = gson.fromJson(responseBody, type) + val result: RemoteOperationResult = RemoteOperationResult(true, putMethod) + result.resultData = response + result + } else { + RemoteOperationResult(false, putMethod) + } + } catch (e: Exception) { + Log_OC.e(TAG, "create message: ", e) + RemoteOperationResult(false, putMethod) + } finally { + putMethod.releaseConnection() + } + } + + companion object { + private const val TAG = "CreateMessageRemoteOperation" + private const val BASE_URL = "/ocs/v2.php/apps/assistant/chat" + } +} diff --git a/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/DeleteConversationRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/DeleteConversationRemoteOperation.kt new file mode 100644 index 0000000000..51cba71477 --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/DeleteConversationRemoteOperation.kt @@ -0,0 +1,48 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.resources.assistant.chat + +import com.nextcloud.common.NextcloudClient +import com.nextcloud.operations.DeleteMethod +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import org.apache.commons.httpclient.HttpStatus + +class DeleteConversationRemoteOperation( + private val sessionId: String +) : RemoteOperation() { + @Suppress("TooGenericExceptionCaught") + override fun run(client: NextcloudClient): RemoteOperationResult { + val deleteMethod = + DeleteMethod( + client.baseUri.toString() + "$BASE_URL/delete_session?sessionId=$sessionId", + true + ) + val status = deleteMethod.execute(client) + + return try { + if (status == HttpStatus.SC_OK) { + RemoteOperationResult(true, deleteMethod) + } else { + RemoteOperationResult(false, deleteMethod) + } + } catch (e: Exception) { + Log_OC.e(TAG, "delete session: ", e) + RemoteOperationResult(false, deleteMethod) + } finally { + deleteMethod.releaseConnection() + } + } + + companion object { + private const val TAG = "DeleteConversationRemoteOperation" + private const val BASE_URL = "/ocs/v2.php/apps/assistant/chat" + } +} diff --git a/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/DeleteMessageRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/DeleteMessageRemoteOperation.kt new file mode 100644 index 0000000000..7dadfb8822 --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/DeleteMessageRemoteOperation.kt @@ -0,0 +1,51 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.resources.assistant.chat + +import com.nextcloud.common.NextcloudClient +import com.nextcloud.operations.DeleteMethod +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import org.apache.commons.httpclient.HttpStatus + +class DeleteMessageRemoteOperation( + private val messageId: String, + private val sessionId: String +) : RemoteOperation() { + @Suppress("TooGenericExceptionCaught") + override fun run(client: NextcloudClient): RemoteOperationResult { + val deleteMethod = + DeleteMethod( + client.baseUri.toString() + + "$BASE_URL/delete_message?messageId=$messageId&sessionId=$sessionId", + true + ) + + val status = deleteMethod.execute(client) + + return try { + if (status == HttpStatus.SC_OK) { + RemoteOperationResult(true, deleteMethod) + } else { + RemoteOperationResult(false, deleteMethod) + } + } catch (e: Exception) { + Log_OC.e(TAG, "delete session: ", e) + RemoteOperationResult(false, deleteMethod) + } finally { + deleteMethod.releaseConnection() + } + } + + companion object { + private const val TAG = "DeleteMessageRemoteOperation" + private const val BASE_URL = "/ocs/v2.php/apps/assistant/chat" + } +} diff --git a/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/GenerateSessionRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/GenerateSessionRemoteOperation.kt new file mode 100644 index 0000000000..a06062f329 --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/GenerateSessionRemoteOperation.kt @@ -0,0 +1,51 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.resources.assistant.chat + +import com.nextcloud.common.NextcloudClient +import com.nextcloud.operations.GetMethod +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.assistant.chat.model.SessionTask +import org.apache.commons.httpclient.HttpStatus + +class GenerateSessionRemoteOperation( + private val sessionId: String +) : RemoteOperation() { + @Suppress("TooGenericExceptionCaught") + override fun run(client: NextcloudClient): RemoteOperationResult { + val url = client.baseUri.toString() + "$BASE_URL/generate?sessionId=$sessionId" + val getMethod = GetMethod(url, true) + val status = getMethod.execute(client) + + return try { + if (status == HttpStatus.SC_OK) { + val responseBody = getMethod.getResponseBodyAsString() + val jsonResponse = gson.fromJson(responseBody, SessionTask::class.java) + + val result = RemoteOperationResult(true, getMethod) + result.resultData = jsonResponse + result + } else { + RemoteOperationResult(false, getMethod) + } + } catch (e: Exception) { + Log_OC.e(TAG, "generate request failed: ", e) + RemoteOperationResult(false, getMethod) + } finally { + getMethod.releaseConnection() + } + } + + companion object { + private const val TAG = "GenerateRemoteOperation" + private const val BASE_URL = "/ocs/v2.php/apps/assistant/chat" + } +} diff --git a/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/GetConversationListRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/GetConversationListRemoteOperation.kt new file mode 100644 index 0000000000..a2f9db9d4a --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/GetConversationListRemoteOperation.kt @@ -0,0 +1,50 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.resources.assistant.chat + +import com.google.gson.reflect.TypeToken +import com.nextcloud.common.NextcloudClient +import com.nextcloud.operations.GetMethod +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.assistant.chat.model.Conversation +import org.apache.commons.httpclient.HttpStatus + +class GetConversationListRemoteOperation : RemoteOperation>() { + @Suppress("TooGenericExceptionCaught") + override fun run(client: NextcloudClient): RemoteOperationResult> { + val getMethod = GetMethod(client.baseUri.toString() + "$BASE_URL/sessions", true) + val status = getMethod.execute(client) + + return try { + if (status == HttpStatus.SC_OK) { + val responseBody = getMethod.getResponseBodyAsString() + val type = object : TypeToken>() {}.type + val conversationList: List = gson.fromJson(responseBody, type) + + val result = RemoteOperationResult>(true, getMethod) + result.resultData = conversationList + result + } else { + RemoteOperationResult(false, getMethod) + } + } catch (e: Exception) { + Log_OC.e(TAG, "get conversation list: ", e) + RemoteOperationResult(false, getMethod) + } finally { + getMethod.releaseConnection() + } + } + + companion object { + private const val TAG = "GetConversationListRemoteOperation" + private const val BASE_URL = "/ocs/v2.php/apps/assistant/chat" + } +} diff --git a/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/GetMessagesRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/GetMessagesRemoteOperation.kt new file mode 100644 index 0000000000..ff26c18d36 --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/GetMessagesRemoteOperation.kt @@ -0,0 +1,55 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.resources.assistant.chat + +import com.google.gson.reflect.TypeToken +import com.nextcloud.common.NextcloudClient +import com.nextcloud.operations.GetMethod +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessage +import org.apache.commons.httpclient.HttpStatus + +class GetMessagesRemoteOperation( + private val sessionId: String +) : RemoteOperation>() { + @Suppress("TooGenericExceptionCaught") + override fun run(client: NextcloudClient): RemoteOperationResult> { + val getMethod = + GetMethod( + client.baseUri.toString() + "$BASE_URL/messages?sessionId=$sessionId", + true + ) + val status = getMethod.execute(client) + + return try { + if (status == HttpStatus.SC_OK) { + val responseBody = getMethod.getResponseBodyAsString() + val type = object : TypeToken>() {}.type + val response: List = gson.fromJson(responseBody, type) + val result: RemoteOperationResult> = RemoteOperationResult(true, getMethod) + result.resultData = response + result + } else { + RemoteOperationResult(false, getMethod) + } + } catch (e: Exception) { + Log_OC.e(TAG, "get message list: ", e) + RemoteOperationResult(false, getMethod) + } finally { + getMethod.releaseConnection() + } + } + + companion object { + private const val TAG = "GetMessagesRemoteOperation" + private const val BASE_URL = "/ocs/v2.php/apps/assistant/chat" + } +} diff --git a/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/model/ChatMessage.kt b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/model/ChatMessage.kt new file mode 100644 index 0000000000..618eb95a68 --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/model/ChatMessage.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.resources.assistant.chat.model + +import com.google.gson.annotations.SerializedName + +data class ChatMessage( + val id: Long, + @SerializedName("session_id") + val sessionId: Long, + val role: String, + val content: String, + val timestamp: Long, + @SerializedName("ocp_task_id") + val ocpTaskId: Any?, + val sources: String, + val attachments: List +) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/model/ChatMessageRequest.kt b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/model/ChatMessageRequest.kt new file mode 100644 index 0000000000..a6f3792e8f --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/model/ChatMessageRequest.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.resources.assistant.chat.model + +data class ChatMessageRequest( + val sessionId: String, + val role: String, + val content: String, + val timestamp: Long, + val attachments: List? = null, + val firstHumanMessage: Boolean = false +) { + val bodyMap = + hashMapOf( + "sessionId" to sessionId, + "role" to role, + "content" to content, + "timestamp" to timestamp, + "firstHumanMessage" to firstHumanMessage, + "attachments" to attachments + ) +} diff --git a/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/model/Conversation.kt b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/model/Conversation.kt new file mode 100644 index 0000000000..c5d7d66a81 --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/model/Conversation.kt @@ -0,0 +1,53 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.resources.assistant.chat.model + +import android.os.Build +import androidx.annotation.RequiresApi +import com.google.gson.annotations.SerializedName +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.Locale +import kotlin.time.ExperimentalTime +import kotlin.time.Instant +import kotlin.time.toJavaInstant + +data class Conversation( + val id: Long, + @SerializedName("user_id") + val userId: String, + val title: String?, + val timestamp: Long, + @SerializedName("agency_conversation_token") + val agencyConversationToken: String, + @SerializedName("agency_pending_actions") + val agencyPendingActions: Any? +) { + companion object { + private const val TITLE_PRESENTATION_TIME_PATTERN = "MMMM dd, yyyy HH:mm" + } + + @OptIn(ExperimentalTime::class) + @RequiresApi(Build.VERSION_CODES.O) + fun titleRepresentation(): String { + return if (title != null) { + title + } else { + val instant = Instant.fromEpochSeconds(timestamp) + val deviceZone = ZoneId.systemDefault() + + val formatter = + DateTimeFormatter + .ofPattern(TITLE_PRESENTATION_TIME_PATTERN, Locale.getDefault()) + .withZone(deviceZone) + + return formatter.format(instant.toJavaInstant()) + } + } +} diff --git a/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/model/CreateConversation.kt b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/model/CreateConversation.kt new file mode 100644 index 0000000000..a05d6094f1 --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/model/CreateConversation.kt @@ -0,0 +1,13 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.resources.assistant.chat.model + +data class CreateConversation( + val session: Conversation +) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/model/Session.kt b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/model/Session.kt new file mode 100644 index 0000000000..e5b1048618 --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/assistant/chat/model/Session.kt @@ -0,0 +1,21 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.resources.assistant.chat.model + +data class Session( + val messageTaskId: Int?, + val titleTaskId: Int?, + val sessionTitle: String, + val sessionAgencyPendingActions: Any?, + val taskId: Long +) + +data class SessionTask( + val taskId: Long +) diff --git a/library/src/main/java/com/owncloud/android/lib/resources/assistant/v2/GetTaskTypesRemoteOperationV2.kt b/library/src/main/java/com/owncloud/android/lib/resources/assistant/v2/GetTaskTypesRemoteOperationV2.kt index f7399ff195..a783f41c98 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/assistant/v2/GetTaskTypesRemoteOperationV2.kt +++ b/library/src/main/java/com/owncloud/android/lib/resources/assistant/v2/GetTaskTypesRemoteOperationV2.kt @@ -70,7 +70,9 @@ class GetTaskTypesRemoteOperationV2 : OCSRemoteOperation>() { ?.data ?.types ?.map { (key, value) -> value.copy(id = value.id ?: key) } - ?.filter { taskType -> isSingleTextInputOutput(taskType) } + ?.filter { taskType -> + isSingleTextInputOutput(taskType) || taskType.isChat() + }?.sortedByDescending { it.isChat() } result = RemoteOperationResult(true, getMethod) result.resultData = taskTypeList diff --git a/library/src/main/java/com/owncloud/android/lib/resources/assistant/v2/model/TaskTypes.kt b/library/src/main/java/com/owncloud/android/lib/resources/assistant/v2/model/TaskTypes.kt index 376260fc03..6ce58e21e8 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/assistant/v2/model/TaskTypes.kt +++ b/library/src/main/java/com/owncloud/android/lib/resources/assistant/v2/model/TaskTypes.kt @@ -18,7 +18,23 @@ data class TaskTypeData( val description: String?, val inputShape: Map, val outputShape: Map -) +) { + private val chatTaskName = "Chat" + + fun isChat(): Boolean = (name == chatTaskName) + + companion object { + private const val CONVERSATION_LIST_ID = "ConversationList" + val conversationList = + TaskTypeData( + CONVERSATION_LIST_ID, + "", + "", + mapOf(), + mapOf() + ) + } +} data class Shape( val name: String, diff --git a/library/src/main/java/com/owncloud/android/lib/resources/status/NextcloudVersion.kt b/library/src/main/java/com/owncloud/android/lib/resources/status/NextcloudVersion.kt index 6daf025c97..fa5c22dafa 100644 --- a/library/src/main/java/com/owncloud/android/lib/resources/status/NextcloudVersion.kt +++ b/library/src/main/java/com/owncloud/android/lib/resources/status/NextcloudVersion.kt @@ -44,6 +44,12 @@ class NextcloudVersion : OwnCloudVersion { @JvmField val nextcloud_32 = NextcloudVersion(0x20000000) // 32.0 + + @JvmField + val nextcloud_33 = NextcloudVersion(0x21000000) // 33.0 + + @JvmField + val nextcloud_34 = NextcloudVersion(0x22000000) // 34.0 } constructor(string: String) : super(string)