Skip to content

Commit b8f7793

Browse files
Merge pull request #1881 from nextcloud/feat/chat-api
feat: chat api
2 parents 2ac15a8 + 39ac846 commit b8f7793

18 files changed

Lines changed: 804 additions & 2 deletions
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Nextcloud Android Library
3+
*
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
6+
* SPDX-License-Identifier: MIT
7+
*/
8+
9+
package com.owncloud.android.lib.resources.assistant.chat
10+
11+
import com.owncloud.android.AbstractIT
12+
import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessageRequest
13+
import com.owncloud.android.lib.resources.assistant.v2.GetTaskTypesRemoteOperationV2
14+
import com.owncloud.android.lib.resources.status.NextcloudVersion
15+
import junit.framework.TestCase.assertEquals
16+
import junit.framework.TestCase.assertTrue
17+
import org.junit.Assert.assertNotNull
18+
import org.junit.Before
19+
import org.junit.Test
20+
21+
class AssistantChatTests : AbstractIT() {
22+
private lateinit var sessionId: String
23+
24+
@Before
25+
fun before() {
26+
testOnlyOnServer(NextcloudVersion.nextcloud_30)
27+
28+
val result =
29+
CreateConversationRemoteOperation(null, System.currentTimeMillis())
30+
.execute(nextcloudClient)
31+
assertTrue(result.isSuccess)
32+
sessionId =
33+
result.resultData.session.id
34+
.toString()
35+
}
36+
37+
@Test
38+
fun testCreateAndGetMessages() {
39+
val messageRequest =
40+
ChatMessageRequest(
41+
sessionId = sessionId,
42+
role = "human",
43+
content = "Hello assistant!",
44+
timestamp = System.currentTimeMillis()
45+
)
46+
47+
val createResult = CreateMessageRemoteOperation(messageRequest).execute(nextcloudClient)
48+
assertTrue(createResult.isSuccess)
49+
50+
val createdMessage = createResult.resultData!!
51+
assertEquals("Hello assistant!", createdMessage.content)
52+
assertEquals("human", createdMessage.role)
53+
assertEquals(sessionId.toLongOrNull(), createdMessage.sessionId)
54+
55+
// Get messages for session
56+
val getResult = GetMessagesRemoteOperation(sessionId).execute(nextcloudClient)
57+
assertTrue(getResult.isSuccess)
58+
59+
val messages = getResult.resultData
60+
assertTrue(messages.isNotEmpty())
61+
assertTrue(messages.any { it.id == createdMessage.id })
62+
}
63+
64+
@Test
65+
fun testDeleteMessage() {
66+
val messageRequest =
67+
ChatMessageRequest(
68+
sessionId = sessionId,
69+
role = "human",
70+
content = "Message to delete",
71+
timestamp = System.currentTimeMillis()
72+
)
73+
val createResult = CreateMessageRemoteOperation(messageRequest).execute(nextcloudClient)
74+
assertTrue(createResult.isSuccess)
75+
76+
val messageId = createResult.resultData!!.id.toString()
77+
78+
// Delete the message
79+
val deleteResult = DeleteMessageRemoteOperation(messageId, sessionId).execute(nextcloudClient)
80+
assertTrue(deleteResult.isSuccess)
81+
82+
// Ensure the message is gone
83+
val getResult = GetMessagesRemoteOperation(sessionId).execute(nextcloudClient)
84+
assertTrue(getResult.isSuccess)
85+
assertTrue(getResult.resultData!!.none { it.id.toString() == messageId })
86+
}
87+
88+
@Test
89+
fun testGetAndDeleteConversations() {
90+
// Create a message to have a session
91+
val messageRequest =
92+
ChatMessageRequest(
93+
sessionId = sessionId,
94+
role = "human",
95+
content = "Starting conversation",
96+
timestamp = System.currentTimeMillis()
97+
)
98+
CreateMessageRemoteOperation(messageRequest).execute(nextcloudClient)
99+
100+
// Get list of conversations
101+
val getConversationsResult = GetConversationListRemoteOperation().execute(nextcloudClient)
102+
assertTrue(getConversationsResult.isSuccess)
103+
104+
val conversations = getConversationsResult.resultData
105+
assertTrue(conversations.any { it.id.toString() == sessionId })
106+
107+
// Delete conversation
108+
val deleteResult = DeleteConversationRemoteOperation(sessionId).execute(nextcloudClient)
109+
assertTrue(deleteResult.isSuccess)
110+
111+
// Ensure conversation is gone
112+
val getAfterDelete = GetConversationListRemoteOperation().execute(nextcloudClient)
113+
assertTrue(getAfterDelete.isSuccess)
114+
assertTrue(getAfterDelete.resultData!!.none { it.id.toString() == sessionId })
115+
}
116+
117+
@Test
118+
fun testGetTaskTypesAndVerifyChatAndSorting() {
119+
testOnlyOnServer(NextcloudVersion.nextcloud_34)
120+
121+
val result = GetTaskTypesRemoteOperationV2().execute(nextcloudClient)
122+
123+
assertTrue("Request must succeed", result.isSuccess)
124+
val types = result.resultData
125+
assertNotNull("Task types must not be null", types)
126+
assertTrue("Task types list must not be empty", types!!.isNotEmpty())
127+
128+
val firstElementIsChat = types.first().isChat()
129+
assertTrue(
130+
"The first task type must be a chat type (sorted by isChat descending)",
131+
firstElementIsChat
132+
)
133+
134+
val chatTypes = types.filter { it.isChat() }
135+
assertTrue("There must be at least one chat-type task", chatTypes.isNotEmpty())
136+
137+
val nonChat = types.filterNot { it.isChat() }
138+
assertTrue(
139+
"There must be at least one non-chat task with single text input/output",
140+
nonChat.isNotEmpty()
141+
)
142+
143+
val indexOfFirstNonChat = types.indexOfFirst { !it.isChat() }
144+
if (indexOfFirstNonChat > 0) {
145+
val anyChatAfterNonChat = types.drop(indexOfFirstNonChat).any { it.isChat() }
146+
assertTrue(
147+
"Chat types must appear before non-chat types in the list",
148+
!anyChatAfterNonChat
149+
)
150+
}
151+
152+
types.forEach { tt ->
153+
assertNotNull("Each task type must have an ID assigned", tt.id)
154+
}
155+
}
156+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Nextcloud Android Library
3+
*
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
6+
* SPDX-License-Identifier: MIT
7+
*/
8+
9+
package com.owncloud.android.lib.resources.assistant.chat
10+
import com.nextcloud.common.NextcloudClient
11+
import com.nextcloud.operations.GetMethod
12+
import com.owncloud.android.lib.common.operations.RemoteOperation
13+
import com.owncloud.android.lib.common.operations.RemoteOperationResult
14+
import com.owncloud.android.lib.common.utils.Log_OC
15+
import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessage
16+
import org.apache.commons.httpclient.HttpStatus
17+
18+
class CheckGenerationRemoteOperation(
19+
private val taskId: String,
20+
private val sessionId: String
21+
) : RemoteOperation<ChatMessage>() {
22+
@Suppress("TooGenericExceptionCaught")
23+
override fun run(client: NextcloudClient): RemoteOperationResult<ChatMessage> {
24+
val url =
25+
client.baseUri.toString() +
26+
"$BASE_URL/check_generation?taskId=$taskId&sessionId=$sessionId"
27+
28+
val getMethod = GetMethod(url, true)
29+
val status = getMethod.execute(client)
30+
31+
return try {
32+
if (status == HttpStatus.SC_OK) {
33+
val responseBody = getMethod.getResponseBodyAsString()
34+
val jsonResponse = gson.fromJson(responseBody, ChatMessage::class.java)
35+
36+
val result = RemoteOperationResult<ChatMessage>(true, getMethod)
37+
result.resultData = jsonResponse
38+
result
39+
} else {
40+
RemoteOperationResult(false, getMethod)
41+
}
42+
} catch (e: Exception) {
43+
Log_OC.e(TAG, "check generation failed: ", e)
44+
RemoteOperationResult(false, getMethod)
45+
} finally {
46+
getMethod.releaseConnection()
47+
}
48+
}
49+
50+
companion object {
51+
private const val TAG = "CheckGenerationRemoteOperation"
52+
private const val BASE_URL = "/ocs/v2.php/apps/assistant/chat"
53+
}
54+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Nextcloud Android Library
3+
*
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
6+
* SPDX-License-Identifier: MIT
7+
*/
8+
9+
package com.owncloud.android.lib.resources.assistant.chat
10+
11+
import com.nextcloud.common.NextcloudClient
12+
import com.nextcloud.operations.GetMethod
13+
import com.owncloud.android.lib.common.operations.RemoteOperation
14+
import com.owncloud.android.lib.common.operations.RemoteOperationResult
15+
import com.owncloud.android.lib.common.utils.Log_OC
16+
import com.owncloud.android.lib.resources.assistant.chat.model.Session
17+
import org.apache.commons.httpclient.HttpStatus
18+
19+
class CheckSessionRemoteOperation(
20+
private val sessionId: String
21+
) : RemoteOperation<Session>() {
22+
@Suppress("TooGenericExceptionCaught")
23+
override fun run(client: NextcloudClient): RemoteOperationResult<Session> {
24+
val getMethod =
25+
GetMethod(
26+
client.baseUri.toString() + "$BASE_URL/check_session?sessionId=$sessionId",
27+
true
28+
)
29+
val status = getMethod.execute(client)
30+
31+
return try {
32+
if (status == HttpStatus.SC_OK) {
33+
val responseBody = getMethod.getResponseBodyAsString()
34+
val jsonResponse = gson.fromJson(responseBody, Session::class.java)
35+
36+
val result = RemoteOperationResult<Session>(true, getMethod)
37+
result.resultData = jsonResponse
38+
result
39+
} else {
40+
RemoteOperationResult(false, getMethod)
41+
}
42+
} catch (e: Exception) {
43+
Log_OC.e(TAG, "check session failed: ", e)
44+
RemoteOperationResult(false, getMethod)
45+
} finally {
46+
getMethod.releaseConnection()
47+
}
48+
}
49+
50+
companion object {
51+
private const val TAG = "CheckSessionRemoteOperation"
52+
private const val BASE_URL = "/ocs/v2.php/apps/assistant/chat"
53+
}
54+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Nextcloud Android Library
3+
*
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
6+
* SPDX-License-Identifier: MIT
7+
*/
8+
9+
package com.owncloud.android.lib.resources.assistant.chat
10+
11+
import com.google.gson.reflect.TypeToken
12+
import com.nextcloud.common.NextcloudClient
13+
import com.nextcloud.operations.PutMethod
14+
import com.owncloud.android.lib.common.operations.RemoteOperation
15+
import com.owncloud.android.lib.common.operations.RemoteOperationResult
16+
import com.owncloud.android.lib.common.utils.Log_OC
17+
import com.owncloud.android.lib.resources.assistant.chat.model.CreateConversation
18+
import okhttp3.MediaType.Companion.toMediaTypeOrNull
19+
import okhttp3.RequestBody.Companion.toRequestBody
20+
import org.apache.commons.httpclient.HttpStatus
21+
22+
class CreateConversationRemoteOperation(
23+
private val title: String?,
24+
private val timestamp: Long
25+
) : RemoteOperation<CreateConversation>() {
26+
@Suppress("TooGenericExceptionCaught")
27+
override fun run(client: NextcloudClient): RemoteOperationResult<CreateConversation> {
28+
val bodyMap =
29+
hashMapOf(
30+
"title" to title,
31+
"timestamp" to timestamp
32+
)
33+
34+
val json = gson.toJson(bodyMap)
35+
val requestBody = json.toRequestBody("application/json".toMediaTypeOrNull())
36+
37+
val putMethod = PutMethod(client.baseUri.toString() + "$BASE_URL/new_session", true, requestBody)
38+
val status = putMethod.execute(client)
39+
40+
return try {
41+
if (status == HttpStatus.SC_OK) {
42+
val responseBody = putMethod.getResponseBodyAsString()
43+
val type = object : TypeToken<CreateConversation>() {}.type
44+
val response: CreateConversation = gson.fromJson(responseBody, type)
45+
val result: RemoteOperationResult<CreateConversation> = RemoteOperationResult(true, putMethod)
46+
result.resultData = response
47+
result
48+
} else {
49+
RemoteOperationResult(false, putMethod)
50+
}
51+
} catch (e: Exception) {
52+
Log_OC.e(TAG, "create conversation: ", e)
53+
RemoteOperationResult(false, putMethod)
54+
} finally {
55+
putMethod.releaseConnection()
56+
}
57+
}
58+
59+
companion object {
60+
private const val TAG = "CreateConversationRemoteOperation"
61+
private const val BASE_URL = "/ocs/v2.php/apps/assistant/chat"
62+
}
63+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Nextcloud Android Library
3+
*
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
6+
* SPDX-License-Identifier: MIT
7+
*/
8+
9+
package com.owncloud.android.lib.resources.assistant.chat
10+
11+
import com.google.gson.reflect.TypeToken
12+
import com.nextcloud.common.NextcloudClient
13+
import com.nextcloud.operations.PutMethod
14+
import com.owncloud.android.lib.common.operations.RemoteOperation
15+
import com.owncloud.android.lib.common.operations.RemoteOperationResult
16+
import com.owncloud.android.lib.common.utils.Log_OC
17+
import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessage
18+
import com.owncloud.android.lib.resources.assistant.chat.model.ChatMessageRequest
19+
import okhttp3.MediaType.Companion.toMediaTypeOrNull
20+
import okhttp3.RequestBody.Companion.toRequestBody
21+
import org.apache.commons.httpclient.HttpStatus
22+
23+
class CreateMessageRemoteOperation(
24+
private val messageRequest: ChatMessageRequest
25+
) : RemoteOperation<ChatMessage>() {
26+
@Suppress("TooGenericExceptionCaught")
27+
override fun run(client: NextcloudClient): RemoteOperationResult<ChatMessage> {
28+
val json = gson.toJson(messageRequest.bodyMap)
29+
val requestBody = json.toRequestBody("application/json".toMediaTypeOrNull())
30+
31+
val putMethod = PutMethod(client.baseUri.toString() + "$BASE_URL/new_message", true, requestBody)
32+
val status = putMethod.execute(client)
33+
34+
return try {
35+
if (status == HttpStatus.SC_OK) {
36+
val responseBody = putMethod.getResponseBodyAsString()
37+
val type = object : TypeToken<ChatMessage>() {}.type
38+
val response: ChatMessage = gson.fromJson(responseBody, type)
39+
val result: RemoteOperationResult<ChatMessage> = RemoteOperationResult(true, putMethod)
40+
result.resultData = response
41+
result
42+
} else {
43+
RemoteOperationResult(false, putMethod)
44+
}
45+
} catch (e: Exception) {
46+
Log_OC.e(TAG, "create message: ", e)
47+
RemoteOperationResult(false, putMethod)
48+
} finally {
49+
putMethod.releaseConnection()
50+
}
51+
}
52+
53+
companion object {
54+
private const val TAG = "CreateMessageRemoteOperation"
55+
private const val BASE_URL = "/ocs/v2.php/apps/assistant/chat"
56+
}
57+
}

0 commit comments

Comments
 (0)