From c3d43cf8fea0c7118143fa9e5ba73365bf2cd312 Mon Sep 17 00:00:00 2001 From: kateliu20 Date: Mon, 16 Sep 2024 11:45:24 -0400 Subject: [PATCH 01/15] Add necessary dependencies --- gradle/libs.versions.toml | 1 + skate-plugin/project-gen/build.gradle.kts | 6 + .../tooling/aibot/ChatBotActionService.kt | 122 ++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6f7738a21..bbf8a1804 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -120,6 +120,7 @@ okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", versio oshi = "com.github.oshi:oshi-core:6.6.3" retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-converters-wire = { module = "com.squareup.retrofit2:converter-wire", version.ref = "retrofit" } +retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit"} rxjava = "io.reactivex.rxjava3:rxjava:3.1.9" slackLints-checks = { module = "com.slack.lint:slack-lint-checks", version.ref = "slack-lint" } slackLints-annotations = { module = "com.slack.lint:slack-lint-annotations", version.ref = "slack-lint" } diff --git a/skate-plugin/project-gen/build.gradle.kts b/skate-plugin/project-gen/build.gradle.kts index fb6aa6de6..5cc8f15d8 100644 --- a/skate-plugin/project-gen/build.gradle.kts +++ b/skate-plugin/project-gen/build.gradle.kts @@ -45,6 +45,12 @@ kotlin { implementation(libs.jewel.bridge232) implementation(libs.kotlin.poet) implementation(libs.markdown) + + implementation(libs.kaml) + implementation(libs.okhttp) + implementation(libs.okhttp.loggingInterceptor) + implementation(libs.retrofit) + implementation(libs.retrofit.converter.gson) } } } diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt new file mode 100644 index 000000000..6833b37dd --- /dev/null +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2024 Slack Technologies, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package slack.tooling.aibot + +import com.google.gson.Gson +import com.google.gson.JsonObject +import java.io.File +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.* + +class ChatBotActionService { + // private val retrofit: Retrofit + // + // init { + // val loggingInterceptor = + // HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } + // + // val client = OkHttpClient.Builder().addInterceptor(loggingInterceptor).build() + // + // retrofit = + // Retrofit.Builder() + // .baseUrl("https://jsonplaceholder.typicode.com/") + // .client(client) + // .addConverterFactory(GsonConverterFactory.create()) + // .build() + // } + + // interface JsonPlaceholderApi { + // @GET("posts") + // suspend fun getPosts(): List + // } + + // private val api: JsonPlaceholderApi = retrofit.create(JsonPlaceholderApi::class.java) + + // suspend fun executeCommand(userInput: String): String { + // println("Received input: '$userInput'. Fetching a random post.") + // return withContext(Dispatchers.IO) { + // try { + // val posts = api.getPosts() + // val randomPost = posts[Random.nextInt(posts.size)] + // """I received your message: "$userInput" + // Here's a random post for you: + // Title: ${randomPost.title} + // Body: ${randomPost.body} + // """.trimIndent() + // } catch (e: Exception) { + // println("Error fetching post: ${e.message}") + // e.printStackTrace() + // "I received your message, but I'm unable to fetch a post at this time. Error: + // ${e.message}" + // } + // } + // } + // + // data class Post( + // val id: Int, + // val title: String, + // val body: String, + // val userId: Int + // ) + suspend fun executeCommand(question: String): String { + val jsonContent = + """ + { + "messages": [{"role": "user", "content": "$question"}], + "source": "curl", + "max_tokens": 2048 + } + """ + .trimIndent() + + val scriptContent = + """ + #!/bin/bash + export PATH="/usr/local/bin:/usr/bin:${'$'}PATH" + export SSH_OPTIONS="-T" + + /usr/local/bin/slack-uberproxy-curl -X POST https://devxp-ai-api.tinyspeck.com/v1/chat/ -H "Content-Type: application/json" -d '$jsonContent' + """ + .trimIndent() + + return withContext(Dispatchers.IO) { + val tempScript = File.createTempFile("run_command", ".sh") + tempScript.writeText(scriptContent) + tempScript.setExecutable(true) + + val process = + ProcessBuilder("/bin/bash", tempScript.absolutePath).redirectErrorStream(true).start() + + val output = process.inputStream.bufferedReader().use { it.readText() } + process.waitFor() + + tempScript.delete() + + val regex = """\{.*\}""".toRegex(RegexOption.DOT_MATCHES_ALL) + val result = regex.find(output.toString())?.value ?: "{}" + val gson = Gson() + val jsonObject = gson.fromJson(result, JsonObject::class.java) + val contentArray = jsonObject.getAsJsonArray("content") + val contentObject = contentArray.get(0).asJsonObject + val actualContent = contentObject.get("content").asString + + actualContent + + // output + } + } +} From 1fabb66adefce77f98b559f70b7a58b8d7957299 Mon Sep 17 00:00:00 2001 From: kateliu20 Date: Mon, 16 Sep 2024 11:46:51 -0400 Subject: [PATCH 02/15] Add ChatBotActionService --- .../tooling/aibot/ChatBotActionService.kt | 56 +------------------ 1 file changed, 2 insertions(+), 54 deletions(-) diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt index 6833b37dd..cb90c94e1 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt @@ -20,60 +20,10 @@ import com.google.gson.JsonObject import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import kotlinx.serialization.* class ChatBotActionService { - // private val retrofit: Retrofit - // - // init { - // val loggingInterceptor = - // HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } - // - // val client = OkHttpClient.Builder().addInterceptor(loggingInterceptor).build() - // - // retrofit = - // Retrofit.Builder() - // .baseUrl("https://jsonplaceholder.typicode.com/") - // .client(client) - // .addConverterFactory(GsonConverterFactory.create()) - // .build() - // } - - // interface JsonPlaceholderApi { - // @GET("posts") - // suspend fun getPosts(): List - // } - - // private val api: JsonPlaceholderApi = retrofit.create(JsonPlaceholderApi::class.java) - - // suspend fun executeCommand(userInput: String): String { - // println("Received input: '$userInput'. Fetching a random post.") - // return withContext(Dispatchers.IO) { - // try { - // val posts = api.getPosts() - // val randomPost = posts[Random.nextInt(posts.size)] - // """I received your message: "$userInput" - // Here's a random post for you: - // Title: ${randomPost.title} - // Body: ${randomPost.body} - // """.trimIndent() - // } catch (e: Exception) { - // println("Error fetching post: ${e.message}") - // e.printStackTrace() - // "I received your message, but I'm unable to fetch a post at this time. Error: - // ${e.message}" - // } - // } - // } - // - // data class Post( - // val id: Int, - // val title: String, - // val body: String, - // val userId: Int - // ) suspend fun executeCommand(question: String): String { - val jsonContent = + val content = """ { "messages": [{"role": "user", "content": "$question"}], @@ -89,7 +39,7 @@ class ChatBotActionService { export PATH="/usr/local/bin:/usr/bin:${'$'}PATH" export SSH_OPTIONS="-T" - /usr/local/bin/slack-uberproxy-curl -X POST https://devxp-ai-api.tinyspeck.com/v1/chat/ -H "Content-Type: application/json" -d '$jsonContent' + /usr/local/bin/slack-uberproxy-curl -X POST https://devxp-ai-api.tinyspeck.com/v1/chat/ -H "Content-Type: application/json" -d '$content' """ .trimIndent() @@ -115,8 +65,6 @@ class ChatBotActionService { val actualContent = contentObject.get("content").asString actualContent - - // output } } } From 1b575a2c3b3d4452b71ea64f2df89e422e33bc7b Mon Sep 17 00:00:00 2001 From: kateliu20 Date: Mon, 16 Sep 2024 11:47:26 -0400 Subject: [PATCH 03/15] Adjust ChatPresenter for the service --- .../slack/tooling/aibot/ChatPresenter.kt | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt index 83eaf348b..dac25544d 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt @@ -21,8 +21,13 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.slack.circuit.runtime.presenter.Presenter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class ChatPresenter : Presenter { + private val chatBotActionService = ChatBotActionService() + @Composable override fun present(): ChatScreen.State { var messages by remember { mutableStateOf(emptyList()) } @@ -32,16 +37,16 @@ class ChatPresenter : Presenter { is ChatScreen.Event.SendMessage -> { val newMessage = Message(event.message, isMe = true) messages = messages + newMessage - val response = Message(callApi(event.message), isMe = false) - messages = messages + response + + CoroutineScope(Dispatchers.Main).launch { + println("${newMessage}") + println("ChatPresenter: Fetching a quote") + val response = chatBotActionService.executeCommand(event.message) + println("ChatPresenter: Received response: $newMessage") + messages = messages + Message(response, isMe = false) + } } } } } - - private fun callApi(message: String): String { - // function set up to call the DevXP API in the future. - // right now, just sends back the user input message - return ("I am a bot. You said \"${message}\"") - } } From 9f62911640677222b35cb6650181df445233781c Mon Sep 17 00:00:00 2001 From: kateliu20 Date: Mon, 16 Sep 2024 13:18:25 -0400 Subject: [PATCH 04/15] Adjust so it can take in special character input --- .../tooling/aibot/ChatBotActionService.kt | 79 ++++++++++++------- 1 file changed, 52 insertions(+), 27 deletions(-) diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt index cb90c94e1..3206b6aeb 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt @@ -16,22 +16,36 @@ package slack.tooling.aibot import com.google.gson.Gson +import com.google.gson.JsonArray import com.google.gson.JsonObject +import java.io.BufferedReader import java.io.File +import java.io.InputStreamReader +import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class ChatBotActionService { suspend fun executeCommand(question: String): String { - val content = - """ - { - "messages": [{"role": "user", "content": "$question"}], - "source": "curl", - "max_tokens": 2048 - } - """ - .trimIndent() + val gsonInput = Gson() + val jsonObjectInput = + JsonObject().apply { + add( + "messages", + JsonArray().apply { + add( + JsonObject().apply { + addProperty("role", "user") + addProperty("content", question) + } + ) + }, + ) + addProperty("source", "curl") + addProperty("max_tokens", 2048) + } + + val content = gsonInput.toJson(jsonObjectInput) val scriptContent = """ @@ -43,28 +57,39 @@ class ChatBotActionService { """ .trimIndent() - return withContext(Dispatchers.IO) { - val tempScript = File.createTempFile("run_command", ".sh") - tempScript.writeText(scriptContent) - tempScript.setExecutable(true) - - val process = - ProcessBuilder("/bin/bash", tempScript.absolutePath).redirectErrorStream(true).start() + val tempScript = withContext(Dispatchers.IO) { File.createTempFile("run_command", ".sh") } + tempScript.writeText(scriptContent) + tempScript.setExecutable(true) - val output = process.inputStream.bufferedReader().use { it.readText() } - process.waitFor() + val processBuilder = ProcessBuilder("/bin/bash", tempScript.absolutePath) + processBuilder.redirectErrorStream(true) - tempScript.delete() + val process = processBuilder.start() + val output = StringBuilder() - val regex = """\{.*\}""".toRegex(RegexOption.DOT_MATCHES_ALL) - val result = regex.find(output.toString())?.value ?: "{}" - val gson = Gson() - val jsonObject = gson.fromJson(result, JsonObject::class.java) - val contentArray = jsonObject.getAsJsonArray("content") - val contentObject = contentArray.get(0).asJsonObject - val actualContent = contentObject.get("content").asString + BufferedReader(InputStreamReader(process.inputStream)).use { reader -> + var line: String? + while (reader.readLine().also { line = it } != null) { + output.append(line).append("\n") + } + } - actualContent + val completed = process.waitFor(600, TimeUnit.SECONDS) + if (!completed) { + process.destroyForcibly() + throw RuntimeException("Process timed out after 600 seconds") } + + tempScript.delete() + + val regex = """\{.*\}""".toRegex(RegexOption.DOT_MATCHES_ALL) + val result = regex.find(output.toString())?.value ?: "{}" + val gson = Gson() + val jsonObject = gson.fromJson(result, JsonObject::class.java) + val contentArray = jsonObject.getAsJsonArray("content") + val contentObject = contentArray.get(0).asJsonObject + val actualContent = contentObject.get("content").asString + + return actualContent } } From 2eadbf78c44c9e3f9dcdd1bdd97c90ec8d378a09 Mon Sep 17 00:00:00 2001 From: kateliu20 Date: Mon, 16 Sep 2024 15:39:14 -0400 Subject: [PATCH 05/15] Separate executeCommand in multiple functions for testing --- .../tooling/aibot/ChatBotActionService.kt | 94 ++++++++++++++++++- .../slack/tooling/aibot/ChatPresenter.kt | 2 +- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt index 3206b6aeb..c9a1e57c8 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt @@ -27,6 +27,15 @@ import kotlinx.coroutines.withContext class ChatBotActionService { suspend fun executeCommand(question: String): String { + val jsonInput = createJsonInput(question) + val scriptContent = createScriptContent(jsonInput) + val tempScript = createTempScript(scriptContent) + val output = runScript(tempScript) + tempScript.delete() + return parseOutput(output) + } + + private fun createJsonInput(question: String): String { val gsonInput = Gson() val jsonObjectInput = JsonObject().apply { @@ -46,20 +55,30 @@ class ChatBotActionService { } val content = gsonInput.toJson(jsonObjectInput) + return content + } + private fun createScriptContent(jsonInput: String): String { val scriptContent = """ #!/bin/bash export PATH="/usr/local/bin:/usr/bin:${'$'}PATH" export SSH_OPTIONS="-T" - /usr/local/bin/slack-uberproxy-curl -X POST https://devxp-ai-api.tinyspeck.com/v1/chat/ -H "Content-Type: application/json" -d '$content' + /usr/local/bin/slack-uberproxy-curl -X POST https://devxp-ai-api.tinyspeck.com/v1/chat/ -H "Content-Type: application/json" -d '$jsonInput' """ .trimIndent() + return scriptContent + } + suspend fun createTempScript(scriptContent: String): File { val tempScript = withContext(Dispatchers.IO) { File.createTempFile("run_command", ".sh") } tempScript.writeText(scriptContent) tempScript.setExecutable(true) + return tempScript + } + + private fun runScript(tempScript: File): String { val processBuilder = ProcessBuilder("/bin/bash", tempScript.absolutePath) processBuilder.redirectErrorStream(true) @@ -81,7 +100,10 @@ class ChatBotActionService { } tempScript.delete() + return output.toString() + } + private fun parseOutput(output: String): String { val regex = """\{.*\}""".toRegex(RegexOption.DOT_MATCHES_ALL) val result = regex.find(output.toString())?.value ?: "{}" val gson = Gson() @@ -92,4 +114,74 @@ class ChatBotActionService { return actualContent } + + // original suspend function command before splitting up: + // suspend fun executeCommand(question: String): String { + // val gsonInput = Gson() + // val jsonObjectInput = + // JsonObject().apply { + // add( + // "messages", + // JsonArray().apply { + // add( + // JsonObject().apply { + // addProperty("role", "user") + // addProperty("content", question) + // } + // ) + // }, + // ) + // addProperty("source", "curl") + // addProperty("max_tokens", 2048) + // } + // + // val content = gsonInput.toJson(jsonObjectInput) + // + // val scriptContent = + // """ + // #!/bin/bash + // export PATH="/usr/local/bin:/usr/bin:${'$'}PATH" + // export SSH_OPTIONS="-T" + // + // /usr/local/bin/slack-uberproxy-curl -X POST https://devxp-ai-api.tinyspeck.com/v1/chat/ + // -H "Content-Type: application/json" -d '$content' + // """ + // .trimIndent() + // + // val tempScript = withContext(Dispatchers.IO) { File.createTempFile("run_command", ".sh") + // } + // tempScript.writeText(scriptContent) + // tempScript.setExecutable(true) + // + // val processBuilder = ProcessBuilder("/bin/bash", tempScript.absolutePath) + // processBuilder.redirectErrorStream(true) + // + // val process = processBuilder.start() + // val output = StringBuilder() + // + // BufferedReader(InputStreamReader(process.inputStream)).use { reader -> + // var line: String? + // while (reader.readLine().also { line = it } != null) { + // output.append(line).append("\n") + // } + // } + // + // val completed = process.waitFor(600, TimeUnit.SECONDS) + // if (!completed) { + // process.destroyForcibly() + // throw RuntimeException("Process timed out after 600 seconds") + // } + // + // tempScript.delete() + // + // val regex = """\{.*\}""".toRegex(RegexOption.DOT_MATCHES_ALL) + // val result = regex.find(output.toString())?.value ?: "{}" + // val gson = Gson() + // val jsonObject = gson.fromJson(result, JsonObject::class.java) + // val contentArray = jsonObject.getAsJsonArray("content") + // val contentObject = contentArray.get(0).asJsonObject + // val actualContent = contentObject.get("content").asString + // + // return actualContent + // } } diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt index dac25544d..effc973f9 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt @@ -38,7 +38,7 @@ class ChatPresenter : Presenter { val newMessage = Message(event.message, isMe = true) messages = messages + newMessage - CoroutineScope(Dispatchers.Main).launch { + CoroutineScope(Dispatchers.IO).launch { println("${newMessage}") println("ChatPresenter: Fetching a quote") val response = chatBotActionService.executeCommand(event.message) From 10dcd2f5fc18a52e51899e1b4d7319ddc83a2a7a Mon Sep 17 00:00:00 2001 From: kateliu20 Date: Mon, 16 Sep 2024 15:51:57 -0400 Subject: [PATCH 06/15] Make visible for testing --- .../kotlin/slack/tooling/aibot/ChatBotActionService.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt index c9a1e57c8..d9f2b4f96 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt @@ -24,6 +24,7 @@ import java.io.InputStreamReader import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.jetbrains.annotations.VisibleForTesting class ChatBotActionService { suspend fun executeCommand(question: String): String { @@ -35,6 +36,7 @@ class ChatBotActionService { return parseOutput(output) } + @VisibleForTesting private fun createJsonInput(question: String): String { val gsonInput = Gson() val jsonObjectInput = @@ -58,6 +60,7 @@ class ChatBotActionService { return content } + @VisibleForTesting private fun createScriptContent(jsonInput: String): String { val scriptContent = """ @@ -78,6 +81,7 @@ class ChatBotActionService { return tempScript } + @VisibleForTesting private fun runScript(tempScript: File): String { val processBuilder = ProcessBuilder("/bin/bash", tempScript.absolutePath) @@ -103,6 +107,7 @@ class ChatBotActionService { return output.toString() } + @VisibleForTesting private fun parseOutput(output: String): String { val regex = """\{.*\}""".toRegex(RegexOption.DOT_MATCHES_ALL) val result = regex.find(output.toString())?.value ?: "{}" From 450317e777a5fa4426741a13aea5518655c2653d Mon Sep 17 00:00:00 2001 From: kateliu20 Date: Tue, 17 Sep 2024 11:57:57 -0400 Subject: [PATCH 07/15] Initial test for json input, failing --- .../tooling/aibot/ChatBotActionService.kt | 29 +++----- .../tooling/aibot/ChatBotActionServiceTest.kt | 70 +++++++++++++++++++ 2 files changed, 80 insertions(+), 19 deletions(-) create mode 100644 skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt index d9f2b4f96..371c5c016 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt @@ -16,7 +16,6 @@ package slack.tooling.aibot import com.google.gson.Gson -import com.google.gson.JsonArray import com.google.gson.JsonObject import java.io.BufferedReader import java.io.File @@ -39,25 +38,11 @@ class ChatBotActionService { @VisibleForTesting private fun createJsonInput(question: String): String { val gsonInput = Gson() - val jsonObjectInput = - JsonObject().apply { - add( - "messages", - JsonArray().apply { - add( - JsonObject().apply { - addProperty("role", "user") - addProperty("content", question) - } - ) - }, - ) - addProperty("source", "curl") - addProperty("max_tokens", 2048) - } + val content = + Content(messages = listOf(Message(question, isMe = true)), source = "curl", max_tokens = 2048) - val content = gsonInput.toJson(jsonObjectInput) - return content + val jsonContent = gsonInput.toJson(content) + return jsonContent } @VisibleForTesting @@ -120,6 +105,12 @@ class ChatBotActionService { return actualContent } + data class Content( + val messages: List, + val source: String = "curl", + val max_tokens: Int = 512, + ) + // original suspend function command before splitting up: // suspend fun executeCommand(question: String): String { // val gsonInput = Gson() diff --git a/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt b/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt new file mode 100644 index 000000000..874fb20de --- /dev/null +++ b/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 Slack Technologies, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package slack.tooling.aibot + +import com.google.common.truth.Truth.assertThat +import com.google.gson.Gson +import org.junit.Test + +class ChatBotActionServiceTest { + @Test + fun `say 1+1 equals 2`() { + val result = 1 + 1 + val actual = 2 + assertThat(actual).isEqualTo(result) + } + + @Test + fun `test creating the Json input`() { + val question = "What's the capital of Canada" + + val result = createJsonInput(question) + + val expectedJson = + """ + { + "messages": [ + { + "role": "user", + "content": "What's the capital of Canada" + } + ], + "source": "curl", + "max_tokens": 2048 + """ + .trimIndent() + + println("expected is ${expectedJson}") + println("actual is ${result}") + + assertThat(result).isEqualTo(expectedJson) + } + + private fun createJsonInput(question: String): String { + val gsonInput = Gson() + val content = + Content(messages = listOf(Message(question, isMe = true)), source = "curl", max_tokens = 2048) + + val jsonContent = gsonInput.toJson(content) + return jsonContent + } + + data class Content( + val messages: List, + val source: String = "curl", + val max_tokens: Int = 512, + ) +} From 9cbda30bfa8d44b07e2d4ad6fb0d477e39f199ab Mon Sep 17 00:00:00 2001 From: kateliu20 Date: Tue, 17 Sep 2024 13:56:30 -0400 Subject: [PATCH 08/15] Adjust so there's a role in the Message data class --- .../tooling/aibot/ChatBotActionService.kt | 108 ++++++------------ .../slack/tooling/aibot/ChatPresenter.kt | 6 +- .../slack/tooling/aibot/ChatWindowUi.kt | 14 ++- .../kotlin/slack/tooling/aibot/Message.kt | 2 +- .../tooling/aibot/ChatBotActionServiceTest.kt | 3 +- 5 files changed, 49 insertions(+), 84 deletions(-) diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt index 371c5c016..7378a9aa0 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt @@ -16,6 +16,7 @@ package slack.tooling.aibot import com.google.gson.Gson +import com.google.gson.JsonArray import com.google.gson.JsonObject import java.io.BufferedReader import java.io.File @@ -38,11 +39,37 @@ class ChatBotActionService { @VisibleForTesting private fun createJsonInput(question: String): String { val gsonInput = Gson() - val content = - Content(messages = listOf(Message(question, isMe = true)), source = "curl", max_tokens = 2048) + val jsonObjectInput = + Content( + messages = listOf(Message(role = "user", question)), + source = "curl", + max_tokens = 2048, + ) + + val jsonObjectInput2 = + JsonObject().apply { + add( + "messages", + JsonArray().apply { + add( + JsonObject().apply { + addProperty("role", "user") + addProperty("content", question) + } + ) + }, + ) + addProperty("source", "curl") + addProperty("max_tokens", 2048) + } + + val content = gsonInput.toJson(jsonObjectInput) + val content2 = gsonInput.toJson(jsonObjectInput2) + + println("jsonContent ${content}") + println("jsonContent2 ${content2}") - val jsonContent = gsonInput.toJson(content) - return jsonContent + return content } @VisibleForTesting @@ -94,6 +121,7 @@ class ChatBotActionService { @VisibleForTesting private fun parseOutput(output: String): String { + println("output: ${output}") val regex = """\{.*\}""".toRegex(RegexOption.DOT_MATCHES_ALL) val result = regex.find(output.toString())?.value ?: "{}" val gson = Gson() @@ -102,6 +130,8 @@ class ChatBotActionService { val contentObject = contentArray.get(0).asJsonObject val actualContent = contentObject.get("content").asString + println("actual content ${actualContent}") + return actualContent } @@ -110,74 +140,4 @@ class ChatBotActionService { val source: String = "curl", val max_tokens: Int = 512, ) - - // original suspend function command before splitting up: - // suspend fun executeCommand(question: String): String { - // val gsonInput = Gson() - // val jsonObjectInput = - // JsonObject().apply { - // add( - // "messages", - // JsonArray().apply { - // add( - // JsonObject().apply { - // addProperty("role", "user") - // addProperty("content", question) - // } - // ) - // }, - // ) - // addProperty("source", "curl") - // addProperty("max_tokens", 2048) - // } - // - // val content = gsonInput.toJson(jsonObjectInput) - // - // val scriptContent = - // """ - // #!/bin/bash - // export PATH="/usr/local/bin:/usr/bin:${'$'}PATH" - // export SSH_OPTIONS="-T" - // - // /usr/local/bin/slack-uberproxy-curl -X POST https://devxp-ai-api.tinyspeck.com/v1/chat/ - // -H "Content-Type: application/json" -d '$content' - // """ - // .trimIndent() - // - // val tempScript = withContext(Dispatchers.IO) { File.createTempFile("run_command", ".sh") - // } - // tempScript.writeText(scriptContent) - // tempScript.setExecutable(true) - // - // val processBuilder = ProcessBuilder("/bin/bash", tempScript.absolutePath) - // processBuilder.redirectErrorStream(true) - // - // val process = processBuilder.start() - // val output = StringBuilder() - // - // BufferedReader(InputStreamReader(process.inputStream)).use { reader -> - // var line: String? - // while (reader.readLine().also { line = it } != null) { - // output.append(line).append("\n") - // } - // } - // - // val completed = process.waitFor(600, TimeUnit.SECONDS) - // if (!completed) { - // process.destroyForcibly() - // throw RuntimeException("Process timed out after 600 seconds") - // } - // - // tempScript.delete() - // - // val regex = """\{.*\}""".toRegex(RegexOption.DOT_MATCHES_ALL) - // val result = regex.find(output.toString())?.value ?: "{}" - // val gson = Gson() - // val jsonObject = gson.fromJson(result, JsonObject::class.java) - // val contentArray = jsonObject.getAsJsonArray("content") - // val contentObject = contentArray.get(0).asJsonObject - // val actualContent = contentObject.get("content").asString - // - // return actualContent - // } } diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt index effc973f9..f1ed2c74f 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt @@ -26,6 +26,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class ChatPresenter : Presenter { + val user = "user" + val bot = "bot" private val chatBotActionService = ChatBotActionService() @Composable @@ -35,7 +37,7 @@ class ChatPresenter : Presenter { return ChatScreen.State(messages = messages) { event -> when (event) { is ChatScreen.Event.SendMessage -> { - val newMessage = Message(event.message, isMe = true) + val newMessage = Message(role = user, event.message) messages = messages + newMessage CoroutineScope(Dispatchers.IO).launch { @@ -43,7 +45,7 @@ class ChatPresenter : Presenter { println("ChatPresenter: Fetching a quote") val response = chatBotActionService.executeCommand(event.message) println("ChatPresenter: Received response: $newMessage") - messages = messages + Message(response, isMe = false) + messages = messages + Message(role = user, response) } } } diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatWindowUi.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatWindowUi.kt index e72aec314..3448d6b8d 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatWindowUi.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatWindowUi.kt @@ -63,9 +63,10 @@ fun ChatWindowUi(state: ChatScreen.State, modifier: Modifier = Modifier) { Column(modifier = modifier.fillMaxSize().background(JewelTheme.globalColors.paneBackground)) { LazyColumn(modifier = Modifier.weight(1f), reverseLayout = true) { items(state.messages.reversed()) { message -> + val isMe = message.role == "user" Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = if (message.isMe) Arrangement.End else Arrangement.Start, + horizontalArrangement = if (isMe) Arrangement.End else Arrangement.Start, ) { ChatBubble(message) } @@ -148,18 +149,19 @@ private fun ConversationField(modifier: Modifier = Modifier, onSendMessage: (Str @Composable private fun ChatBubble(message: Message, modifier: Modifier = Modifier) { + val user = "user" + val isMe = message.role == user + Box( Modifier.wrapContentWidth() .padding(8.dp) .shadow(elevation = 0.5.dp, shape = RoundedCornerShape(25.dp), clip = true) - .background( - color = if (message.isMe) ChatColors.promptBackground else ChatColors.responseBackground - ) + .background(color = if (isMe) ChatColors.promptBackground else ChatColors.responseBackground) .padding(8.dp) ) { Text( - text = message.text, - color = if (message.isMe) ChatColors.userTextColor else ChatColors.responseTextColor, + text = message.content, + color = if (isMe) ChatColors.userTextColor else ChatColors.responseTextColor, modifier = modifier.padding(8.dp), fontFamily = FontFamily.SansSerif, ) diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/Message.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/Message.kt index 2b2da9499..3c184dfba 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/Message.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/Message.kt @@ -17,4 +17,4 @@ package slack.tooling.aibot import androidx.compose.runtime.Immutable -@Immutable data class Message(val text: String, val isMe: Boolean) +@Immutable data class Message(var role: String, val content: String) diff --git a/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt b/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt index 874fb20de..a54630fdf 100644 --- a/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt +++ b/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt @@ -54,9 +54,10 @@ class ChatBotActionServiceTest { } private fun createJsonInput(question: String): String { + val user = "user" val gsonInput = Gson() val content = - Content(messages = listOf(Message(question, isMe = true)), source = "curl", max_tokens = 2048) + Content(messages = listOf(Message(role = user, question)), source = "curl", max_tokens = 2048) val jsonContent = gsonInput.toJson(content) return jsonContent From 04d514c1b0e9a4234bc96d6c5a56e37d38d49857 Mon Sep 17 00:00:00 2001 From: OSS-Bot <93565511+slack-oss-bot@users.noreply.github.com> Date: Wed, 18 Sep 2024 01:06:00 -0700 Subject: [PATCH 09/15] Update dependency com.github.oshi:oshi-core to v6.6.4 (#965) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [com.github.oshi:oshi-core](https://redirect.github.com/oshi/oshi) | dependencies | patch | `6.6.3` -> `6.6.4` | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Release Notes
oshi/oshi (com.github.oshi:oshi-core) ### [`v6.6.4`](https://redirect.github.com/oshi/oshi/blob/HEAD/CHANGELOG.md#660-2024-04-13--661-2024-05-26--662-2024-07-21--663-2024-08-20--664-2024-09-15) ##### New Features - [#​2603](https://redirect.github.com/oshi/oshi/pull/2603), [#​2625](https://redirect.github.com/oshi/oshi/pull/2625): Add part number to Physical Memory - [@​BartekDziurowicz](https://redirect.github.com/BartekDziurowicz), [@​dbwiddis](https://redirect.github.com/dbwiddis). - [#​2644](https://redirect.github.com/oshi/oshi/pull/2644): Add serial number to Physical Memory - [@​Tegrason](https://redirect.github.com/Tegrason). ##### Bug fixes / Improvements - [#​2605](https://redirect.github.com/oshi/oshi/pull/2605): Reduce CpuStat.getSystemCpuLoadticks memory allocation pressure - [@​chrisribble](https://redirect.github.com/chrisribble). - [#​2612](https://redirect.github.com/oshi/oshi/pull/2612): Use 1k buffer in FileUtils.readLines to reduce heap allocation pressure - [@​chrisribble](https://redirect.github.com/chrisribble). - [#​2621](https://redirect.github.com/oshi/oshi/pull/2621): Cache thread counters when updating OS Process with suspended state - [@​dbwiddis](https://redirect.github.com/dbwiddis). - [#​2626](https://redirect.github.com/oshi/oshi/pull/2626): Make sys and dev paths on Linux configurable - [@​dbwiddis](https://redirect.github.com/dbwiddis). - [#​2627](https://redirect.github.com/oshi/oshi/pull/2627): Add more SMBIOSMemoryType values - [@​dbwiddis](https://redirect.github.com/dbwiddis). - [#​2645](https://redirect.github.com/oshi/oshi/pull/2645): fix getOwningProcessId sometimes return -1 on 64x linux - [@​yourancc](https://redirect.github.com/yourancc). - [#​2660](https://redirect.github.com/oshi/oshi/pull/2660): Add macOS 15 (Sequoia) to version properties - [@​dbwiddis](https://redirect.github.com/dbwiddis). - [#​2662](https://redirect.github.com/oshi/oshi/pull/2662): Only warn on duplicate properties files if they differ - [@​dbwiddis](https://redirect.github.com/dbwiddis). - [#​2692](https://redirect.github.com/oshi/oshi/pull/2692): Do not log errors for reading process arguments on Linux - [@​wolfs](https://redirect.github.com/wolfs). - [#​2704](https://redirect.github.com/oshi/oshi/pull/2704): Properly parse CPU vendor when lscpu not available - [@​dbwiddis](https://redirect.github.com/dbwiddis). - [#​2705](https://redirect.github.com/oshi/oshi/pull/2705): Restore optional legacy method of calculating Windows System CPU - [@​dbwiddis](https://redirect.github.com/dbwiddis). - [#​2711](https://redirect.github.com/oshi/oshi/pull/2711): Do not log error on macOS for hw.nperflevels - [@​Puppy4C](https://redirect.github.com/Puppy4C). - [#​2722](https://redirect.github.com/oshi/oshi/pull/2722): Fix speed value for LinuxNetworkIF - [@​Puppy4C](https://redirect.github.com/Puppy4C). - [#​2724](https://redirect.github.com/oshi/oshi/pull/2724): Clarify IO bytes documentation on OSProcess - [@​dbwiddis](https://redirect.github.com/dbwiddis). - [#​2725](https://redirect.github.com/oshi/oshi/pull/2725): Reduce redundant logging on perf counter failures - [@​dbwiddis](https://redirect.github.com/dbwiddis). - [#​2726](https://redirect.github.com/oshi/oshi/pull/2726): JNA 5.15.0 - [@​dbwiddis](https://redirect.github.com/dbwiddis).
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://redirect.github.com/renovatebot/renovate). --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6f7738a21..fbd205d03 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -117,7 +117,7 @@ okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp okhttp-loggingInterceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } okio = { module = "com.squareup.okio:okio", version.ref = "okio" } okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okio" } -oshi = "com.github.oshi:oshi-core:6.6.3" +oshi = "com.github.oshi:oshi-core:6.6.4" retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-converters-wire = { module = "com.squareup.retrofit2:converter-wire", version.ref = "retrofit" } rxjava = "io.reactivex.rxjava3:rxjava:3.1.9" From 9f9d447c953915450543265f15fa78b0d4e3978a Mon Sep 17 00:00:00 2001 From: kateliu20 Date: Wed, 18 Sep 2024 10:16:37 -0400 Subject: [PATCH 10/15] Add passing test for creating json input --- .../slack/tooling/aibot/ChatPresenter.kt | 2 +- .../slack/tooling/aibot/ChatWindowUi.kt | 4 +-- .../tooling/aibot/ChatBotActionServiceTest.kt | 26 ++++++++----------- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt index f1ed2c74f..f26215961 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt @@ -45,7 +45,7 @@ class ChatPresenter : Presenter { println("ChatPresenter: Fetching a quote") val response = chatBotActionService.executeCommand(event.message) println("ChatPresenter: Received response: $newMessage") - messages = messages + Message(role = user, response) + messages = messages + Message(role = bot, response) } } } diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatWindowUi.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatWindowUi.kt index 3448d6b8d..70d5955e3 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatWindowUi.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatWindowUi.kt @@ -149,9 +149,7 @@ private fun ConversationField(modifier: Modifier = Modifier, onSendMessage: (Str @Composable private fun ChatBubble(message: Message, modifier: Modifier = Modifier) { - val user = "user" - val isMe = message.role == user - + val isMe = message.role == "user" Box( Modifier.wrapContentWidth() .padding(8.dp) diff --git a/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt b/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt index a54630fdf..06a725cac 100644 --- a/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt +++ b/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt @@ -20,16 +20,9 @@ import com.google.gson.Gson import org.junit.Test class ChatBotActionServiceTest { - @Test - fun `say 1+1 equals 2`() { - val result = 1 + 1 - val actual = 2 - assertThat(actual).isEqualTo(result) - } - @Test fun `test creating the Json input`() { - val question = "What's the capital of Canada" + val question = "Why is the sky blue?" val result = createJsonInput(question) @@ -39,27 +32,30 @@ class ChatBotActionServiceTest { "messages": [ { "role": "user", - "content": "What's the capital of Canada" + "content": "Why is the sky blue?" } ], "source": "curl", - "max_tokens": 2048 + "max_tokens": 512 + } """ .trimIndent() - println("expected is ${expectedJson}") - println("actual is ${result}") + val trimmedExpected = expectedJson.replace(Regex("\\s"), "") + val trimmedResult = result.replace(Regex("\\s"), "") + println("expected is ${trimmedExpected}") + println("actual is ${trimmedResult}") - assertThat(result).isEqualTo(expectedJson) + assertThat(trimmedResult).isEqualTo(trimmedExpected) } private fun createJsonInput(question: String): String { val user = "user" val gsonInput = Gson() val content = - Content(messages = listOf(Message(role = user, question)), source = "curl", max_tokens = 2048) + Content(messages = listOf(Message(role = user, question)), source = "curl", max_tokens = 512) - val jsonContent = gsonInput.toJson(content) + val jsonContent = gsonInput.toJson(content).toString() return jsonContent } From 6284a1751dd61a7429e97d0a4546b8e1a6926437 Mon Sep 17 00:00:00 2001 From: kateliu20 Date: Wed, 18 Sep 2024 10:38:50 -0400 Subject: [PATCH 11/15] Clean up, add more JsonInput tests --- .../tooling/aibot/ChatBotActionService.kt | 29 +++------------- .../slack/tooling/aibot/ChatPresenter.kt | 3 -- .../tooling/aibot/ChatBotActionServiceTest.kt | 33 +++++++++++++++++-- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt index 7378a9aa0..4ae5bd0b6 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt @@ -16,7 +16,6 @@ package slack.tooling.aibot import com.google.gson.Gson -import com.google.gson.JsonArray import com.google.gson.JsonObject import java.io.BufferedReader import java.io.File @@ -46,28 +45,9 @@ class ChatBotActionService { max_tokens = 2048, ) - val jsonObjectInput2 = - JsonObject().apply { - add( - "messages", - JsonArray().apply { - add( - JsonObject().apply { - addProperty("role", "user") - addProperty("content", question) - } - ) - }, - ) - addProperty("source", "curl") - addProperty("max_tokens", 2048) - } - val content = gsonInput.toJson(jsonObjectInput) - val content2 = gsonInput.toJson(jsonObjectInput2) - println("jsonContent ${content}") - println("jsonContent2 ${content2}") + println("jsonContent $content") return content } @@ -86,7 +66,8 @@ class ChatBotActionService { return scriptContent } - suspend fun createTempScript(scriptContent: String): File { + @VisibleForTesting + private suspend fun createTempScript(scriptContent: String): File { val tempScript = withContext(Dispatchers.IO) { File.createTempFile("run_command", ".sh") } tempScript.writeText(scriptContent) tempScript.setExecutable(true) @@ -121,7 +102,7 @@ class ChatBotActionService { @VisibleForTesting private fun parseOutput(output: String): String { - println("output: ${output}") + println("output: $output") val regex = """\{.*\}""".toRegex(RegexOption.DOT_MATCHES_ALL) val result = regex.find(output.toString())?.value ?: "{}" val gson = Gson() @@ -130,7 +111,7 @@ class ChatBotActionService { val contentObject = contentArray.get(0).asJsonObject val actualContent = contentObject.get("content").asString - println("actual content ${actualContent}") + println("actual content $actualContent") return actualContent } diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt index f26215961..8cf9c2439 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt @@ -41,10 +41,7 @@ class ChatPresenter : Presenter { messages = messages + newMessage CoroutineScope(Dispatchers.IO).launch { - println("${newMessage}") - println("ChatPresenter: Fetching a quote") val response = chatBotActionService.executeCommand(event.message) - println("ChatPresenter: Received response: $newMessage") messages = messages + Message(role = bot, response) } } diff --git a/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt b/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt index 06a725cac..8c59e0efa 100644 --- a/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt +++ b/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt @@ -17,11 +17,13 @@ package slack.tooling.aibot import com.google.common.truth.Truth.assertThat import com.google.gson.Gson +import com.google.gson.JsonObject +import junit.framework.TestCase.assertEquals import org.junit.Test class ChatBotActionServiceTest { @Test - fun `test creating the Json input`() { + fun `createJsonInput with simple input`() { val question = "Why is the sky blue?" val result = createJsonInput(question) @@ -43,12 +45,37 @@ class ChatBotActionServiceTest { val trimmedExpected = expectedJson.replace(Regex("\\s"), "") val trimmedResult = result.replace(Regex("\\s"), "") - println("expected is ${trimmedExpected}") - println("actual is ${trimmedResult}") + println("expected is $trimmedExpected") + println("actual is $trimmedResult") assertThat(trimmedResult).isEqualTo(trimmedExpected) } + @Test + fun `createJsonInput with long strings`() { + val question = "A".repeat(10000) + val result = createJsonInput(question) + println("result $result") + val jsonObject = Gson().fromJson(result, JsonObject::class.java) + println(jsonObject) + assertEquals( + question, + jsonObject.get("messages").asJsonArray[0].asJsonObject.get("content").asString, + ) + } + + @Test + fun `createJsonInput with special characters`() { + val question = "What about \n, \t, and \"quotes\"? and \'apostrophes" + val result = createJsonInput(question) + println("result $result") + val jsonObject = Gson().fromJson(result, JsonObject::class.java) + assertEquals( + question, + jsonObject.get("messages").asJsonArray[0].asJsonObject.get("content").asString, + ) + } + private fun createJsonInput(question: String): String { val user = "user" val gsonInput = Gson() From 1c7497eddab223e52546e06ff72e7496e70ec604 Mon Sep 17 00:00:00 2001 From: kateliu20 Date: Wed, 18 Sep 2024 10:55:05 -0400 Subject: [PATCH 12/15] Change chat colors --- .../src/jvmMain/kotlin/slack/tooling/aibot/ChatColors.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatColors.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatColors.kt index e7e085bfe..6238a1092 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatColors.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatColors.kt @@ -24,12 +24,12 @@ object ChatColors { val promptBackground = Color(0xFF45494A) // Color(0xFF2d2f30) responseBackground - val responseBackground: Color - @Composable @ReadOnlyComposable get() = JewelTheme.globalColors.infoContent + val responseBackground = Color(0xFF2d2f30) +// @Composable @ReadOnlyComposable get() = JewelTheme.globalColors.infoContent // Color(0xFFEAEEF7) userTextColor - val userTextColor: Color - @Composable @ReadOnlyComposable get() = JewelTheme.globalColors.infoContent + val userTextColor = Color(0xFFEAEEF7) +// @Composable @ReadOnlyComposable get() = JewelTheme.globalColors.infoContent val responseTextColor = Color(0xFFE0EEF7) } From 55cc59790dd96aaa581494f1dcb940984b381a76 Mon Sep 17 00:00:00 2001 From: kateliu20 Date: Wed, 18 Sep 2024 10:57:55 -0400 Subject: [PATCH 13/15] Run spotless --- .../src/jvmMain/kotlin/slack/tooling/aibot/ChatColors.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatColors.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatColors.kt index 6238a1092..b2d9207d9 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatColors.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatColors.kt @@ -15,21 +15,18 @@ */ package slack.tooling.aibot -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.graphics.Color -import org.jetbrains.jewel.foundation.theme.JewelTheme object ChatColors { val promptBackground = Color(0xFF45494A) // Color(0xFF2d2f30) responseBackground val responseBackground = Color(0xFF2d2f30) -// @Composable @ReadOnlyComposable get() = JewelTheme.globalColors.infoContent + // @Composable @ReadOnlyComposable get() = JewelTheme.globalColors.infoContent // Color(0xFFEAEEF7) userTextColor val userTextColor = Color(0xFFEAEEF7) -// @Composable @ReadOnlyComposable get() = JewelTheme.globalColors.infoContent + // @Composable @ReadOnlyComposable get() = JewelTheme.globalColors.infoContent val responseTextColor = Color(0xFFE0EEF7) } From 33c8f291bab8ee4cf17e0034fa9924ed2b95584f Mon Sep 17 00:00:00 2001 From: kateliu20 Date: Wed, 18 Sep 2024 20:30:11 -0400 Subject: [PATCH 14/15] Remove script content --- .../kotlin/slack/tooling/aibot/ChatBotActionService.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt index 4ae5bd0b6..89170dfc8 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt @@ -56,12 +56,8 @@ class ChatBotActionService { private fun createScriptContent(jsonInput: String): String { val scriptContent = """ - #!/bin/bash - export PATH="/usr/local/bin:/usr/bin:${'$'}PATH" - export SSH_OPTIONS="-T" - - /usr/local/bin/slack-uberproxy-curl -X POST https://devxp-ai-api.tinyspeck.com/v1/chat/ -H "Content-Type: application/json" -d '$jsonInput' - """ + temp + """ .trimIndent() return scriptContent } From 755db0ef14837e988ea86373f8b48e0b430979cc Mon Sep 17 00:00:00 2001 From: kateliu20 Date: Wed, 18 Sep 2024 20:41:53 -0400 Subject: [PATCH 15/15] Call API --- CHANGELOG.md | 3 + build.gradle.kts | 3 +- gradle/libs.versions.toml | 9 +- skate-plugin/project-gen/build.gradle.kts | 6 + .../tooling/aibot/ChatBotActionService.kt | 120 ++++++++++++++++++ .../kotlin/slack/tooling/aibot/ChatColors.kt | 11 +- .../slack/tooling/aibot/ChatPresenter.kt | 22 ++-- .../slack/tooling/aibot/ChatWindowUi.kt | 12 +- .../kotlin/slack/tooling/aibot/Message.kt | 2 +- .../tooling/aibot/ChatBotActionServiceTest.kt | 94 ++++++++++++++ .../kotlin/com/slack/skippy/CliktSgpLogger.kt | 7 +- .../skippy/ComputeAffectedProjectsCli.kt | 28 ++-- .../src/main/kotlin/slack/gradle/GradleExt.kt | 4 + .../kotlin/slack/gradle/SlackProperties.kt | 10 +- .../main/kotlin/slack/gradle/kgp/KgpTasks.kt | 11 +- 15 files changed, 286 insertions(+), 56 deletions(-) create mode 100644 skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt create mode 100644 skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 213a15c5f..fc34e857a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changelog **Unreleased** -------------- +- Change `slack.allowWarnings` property to `sgp.kotlin.allowWarnings`. +- Disallow warnings in Kotlin test compilations by default, add `sgp.kotlin.allowWarningsInTests` property to opt-out. + 0.19.3 ------ diff --git a/build.gradle.kts b/build.gradle.kts index 70f01fb8c..372b4c99d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,7 +32,6 @@ import org.jetbrains.intellij.tasks.BuildPluginTask import org.jetbrains.intellij.tasks.PatchPluginXmlTask import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension -import org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_8 import org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -221,7 +220,7 @@ subprojects { compilerOptions { val kotlinVersion = if (isForIntelliJPlugin) { - KOTLIN_1_8 + KOTLIN_1_9 } else { KOTLIN_1_9 } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6f7738a21..5b49b69ec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] agp = "8.6.0" -agpAlpha = "8.6.0" +agpAlpha = "8.6.1" anvil = "2.5.0-beta11" bugsnagGradle = "8.1.0" circuit = "0.23.1" @@ -13,7 +13,7 @@ errorproneGradle = "3.0.1" jdk = "22" jvmTarget = "17" jewel = "0.15.2.2" -jna = "5.14.0" +jna = "5.15.0" kaml = "0.61.0" kotlin = "2.0.20" ksp = "2.0.20-1.0.25" @@ -64,7 +64,7 @@ agpAlpha = { module = "com.android.tools.build:gradle", version.ref = "agpAlpha" autoService-annotations = "com.google.auto.service:auto-service-annotations:1.1.1" autoService-ksp = "dev.zacsweers.autoservice:auto-service-ksp:1.2.0" bugsnag = "com.bugsnag:bugsnag:3.7.2" -clikt = "com.github.ajalt.clikt:clikt:4.4.0" +clikt = "com.github.ajalt.clikt:clikt:5.0.0" circuit-foundation = { module = "com.slack.circuit:circuit-foundation", version.ref = "circuit" } commonsText = "org.apache.commons:commons-text:1.12.0" composeLints = "com.slack.lint.compose:compose-lint-checks:1.3.1" @@ -117,9 +117,10 @@ okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp okhttp-loggingInterceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } okio = { module = "com.squareup.okio:okio", version.ref = "okio" } okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okio" } -oshi = "com.github.oshi:oshi-core:6.6.3" +oshi = "com.github.oshi:oshi-core:6.6.4" retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-converters-wire = { module = "com.squareup.retrofit2:converter-wire", version.ref = "retrofit" } +retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit"} rxjava = "io.reactivex.rxjava3:rxjava:3.1.9" slackLints-checks = { module = "com.slack.lint:slack-lint-checks", version.ref = "slack-lint" } slackLints-annotations = { module = "com.slack.lint:slack-lint-annotations", version.ref = "slack-lint" } diff --git a/skate-plugin/project-gen/build.gradle.kts b/skate-plugin/project-gen/build.gradle.kts index fb6aa6de6..5cc8f15d8 100644 --- a/skate-plugin/project-gen/build.gradle.kts +++ b/skate-plugin/project-gen/build.gradle.kts @@ -45,6 +45,12 @@ kotlin { implementation(libs.jewel.bridge232) implementation(libs.kotlin.poet) implementation(libs.markdown) + + implementation(libs.kaml) + implementation(libs.okhttp) + implementation(libs.okhttp.loggingInterceptor) + implementation(libs.retrofit) + implementation(libs.retrofit.converter.gson) } } } diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt new file mode 100644 index 000000000..89170dfc8 --- /dev/null +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2024 Slack Technologies, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package slack.tooling.aibot + +import com.google.gson.Gson +import com.google.gson.JsonObject +import java.io.BufferedReader +import java.io.File +import java.io.InputStreamReader +import java.util.concurrent.TimeUnit +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.annotations.VisibleForTesting + +class ChatBotActionService { + suspend fun executeCommand(question: String): String { + val jsonInput = createJsonInput(question) + val scriptContent = createScriptContent(jsonInput) + val tempScript = createTempScript(scriptContent) + val output = runScript(tempScript) + tempScript.delete() + return parseOutput(output) + } + + @VisibleForTesting + private fun createJsonInput(question: String): String { + val gsonInput = Gson() + val jsonObjectInput = + Content( + messages = listOf(Message(role = "user", question)), + source = "curl", + max_tokens = 2048, + ) + + val content = gsonInput.toJson(jsonObjectInput) + + println("jsonContent $content") + + return content + } + + @VisibleForTesting + private fun createScriptContent(jsonInput: String): String { + val scriptContent = + """ + temp + """ + .trimIndent() + return scriptContent + } + + @VisibleForTesting + private suspend fun createTempScript(scriptContent: String): File { + val tempScript = withContext(Dispatchers.IO) { File.createTempFile("run_command", ".sh") } + tempScript.writeText(scriptContent) + tempScript.setExecutable(true) + return tempScript + } + + @VisibleForTesting + private fun runScript(tempScript: File): String { + + val processBuilder = ProcessBuilder("/bin/bash", tempScript.absolutePath) + processBuilder.redirectErrorStream(true) + + val process = processBuilder.start() + val output = StringBuilder() + + BufferedReader(InputStreamReader(process.inputStream)).use { reader -> + var line: String? + while (reader.readLine().also { line = it } != null) { + output.append(line).append("\n") + } + } + + val completed = process.waitFor(600, TimeUnit.SECONDS) + if (!completed) { + process.destroyForcibly() + throw RuntimeException("Process timed out after 600 seconds") + } + + tempScript.delete() + return output.toString() + } + + @VisibleForTesting + private fun parseOutput(output: String): String { + println("output: $output") + val regex = """\{.*\}""".toRegex(RegexOption.DOT_MATCHES_ALL) + val result = regex.find(output.toString())?.value ?: "{}" + val gson = Gson() + val jsonObject = gson.fromJson(result, JsonObject::class.java) + val contentArray = jsonObject.getAsJsonArray("content") + val contentObject = contentArray.get(0).asJsonObject + val actualContent = contentObject.get("content").asString + + println("actual content $actualContent") + + return actualContent + } + + data class Content( + val messages: List, + val source: String = "curl", + val max_tokens: Int = 512, + ) +} diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatColors.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatColors.kt index e7e085bfe..b2d9207d9 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatColors.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatColors.kt @@ -15,21 +15,18 @@ */ package slack.tooling.aibot -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.graphics.Color -import org.jetbrains.jewel.foundation.theme.JewelTheme object ChatColors { val promptBackground = Color(0xFF45494A) // Color(0xFF2d2f30) responseBackground - val responseBackground: Color - @Composable @ReadOnlyComposable get() = JewelTheme.globalColors.infoContent + val responseBackground = Color(0xFF2d2f30) + // @Composable @ReadOnlyComposable get() = JewelTheme.globalColors.infoContent // Color(0xFFEAEEF7) userTextColor - val userTextColor: Color - @Composable @ReadOnlyComposable get() = JewelTheme.globalColors.infoContent + val userTextColor = Color(0xFFEAEEF7) + // @Composable @ReadOnlyComposable get() = JewelTheme.globalColors.infoContent val responseTextColor = Color(0xFFE0EEF7) } diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt index 83eaf348b..8cf9c2439 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt @@ -21,8 +21,15 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.slack.circuit.runtime.presenter.Presenter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class ChatPresenter : Presenter { + val user = "user" + val bot = "bot" + private val chatBotActionService = ChatBotActionService() + @Composable override fun present(): ChatScreen.State { var messages by remember { mutableStateOf(emptyList()) } @@ -30,18 +37,15 @@ class ChatPresenter : Presenter { return ChatScreen.State(messages = messages) { event -> when (event) { is ChatScreen.Event.SendMessage -> { - val newMessage = Message(event.message, isMe = true) + val newMessage = Message(role = user, event.message) messages = messages + newMessage - val response = Message(callApi(event.message), isMe = false) - messages = messages + response + + CoroutineScope(Dispatchers.IO).launch { + val response = chatBotActionService.executeCommand(event.message) + messages = messages + Message(role = bot, response) + } } } } } - - private fun callApi(message: String): String { - // function set up to call the DevXP API in the future. - // right now, just sends back the user input message - return ("I am a bot. You said \"${message}\"") - } } diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatWindowUi.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatWindowUi.kt index e72aec314..70d5955e3 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatWindowUi.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatWindowUi.kt @@ -63,9 +63,10 @@ fun ChatWindowUi(state: ChatScreen.State, modifier: Modifier = Modifier) { Column(modifier = modifier.fillMaxSize().background(JewelTheme.globalColors.paneBackground)) { LazyColumn(modifier = Modifier.weight(1f), reverseLayout = true) { items(state.messages.reversed()) { message -> + val isMe = message.role == "user" Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = if (message.isMe) Arrangement.End else Arrangement.Start, + horizontalArrangement = if (isMe) Arrangement.End else Arrangement.Start, ) { ChatBubble(message) } @@ -148,18 +149,17 @@ private fun ConversationField(modifier: Modifier = Modifier, onSendMessage: (Str @Composable private fun ChatBubble(message: Message, modifier: Modifier = Modifier) { + val isMe = message.role == "user" Box( Modifier.wrapContentWidth() .padding(8.dp) .shadow(elevation = 0.5.dp, shape = RoundedCornerShape(25.dp), clip = true) - .background( - color = if (message.isMe) ChatColors.promptBackground else ChatColors.responseBackground - ) + .background(color = if (isMe) ChatColors.promptBackground else ChatColors.responseBackground) .padding(8.dp) ) { Text( - text = message.text, - color = if (message.isMe) ChatColors.userTextColor else ChatColors.responseTextColor, + text = message.content, + color = if (isMe) ChatColors.userTextColor else ChatColors.responseTextColor, modifier = modifier.padding(8.dp), fontFamily = FontFamily.SansSerif, ) diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/Message.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/Message.kt index 2b2da9499..3c184dfba 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/Message.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/Message.kt @@ -17,4 +17,4 @@ package slack.tooling.aibot import androidx.compose.runtime.Immutable -@Immutable data class Message(val text: String, val isMe: Boolean) +@Immutable data class Message(var role: String, val content: String) diff --git a/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt b/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt new file mode 100644 index 000000000..8c59e0efa --- /dev/null +++ b/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2024 Slack Technologies, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package slack.tooling.aibot + +import com.google.common.truth.Truth.assertThat +import com.google.gson.Gson +import com.google.gson.JsonObject +import junit.framework.TestCase.assertEquals +import org.junit.Test + +class ChatBotActionServiceTest { + @Test + fun `createJsonInput with simple input`() { + val question = "Why is the sky blue?" + + val result = createJsonInput(question) + + val expectedJson = + """ + { + "messages": [ + { + "role": "user", + "content": "Why is the sky blue?" + } + ], + "source": "curl", + "max_tokens": 512 + } + """ + .trimIndent() + + val trimmedExpected = expectedJson.replace(Regex("\\s"), "") + val trimmedResult = result.replace(Regex("\\s"), "") + println("expected is $trimmedExpected") + println("actual is $trimmedResult") + + assertThat(trimmedResult).isEqualTo(trimmedExpected) + } + + @Test + fun `createJsonInput with long strings`() { + val question = "A".repeat(10000) + val result = createJsonInput(question) + println("result $result") + val jsonObject = Gson().fromJson(result, JsonObject::class.java) + println(jsonObject) + assertEquals( + question, + jsonObject.get("messages").asJsonArray[0].asJsonObject.get("content").asString, + ) + } + + @Test + fun `createJsonInput with special characters`() { + val question = "What about \n, \t, and \"quotes\"? and \'apostrophes" + val result = createJsonInput(question) + println("result $result") + val jsonObject = Gson().fromJson(result, JsonObject::class.java) + assertEquals( + question, + jsonObject.get("messages").asJsonArray[0].asJsonObject.get("content").asString, + ) + } + + private fun createJsonInput(question: String): String { + val user = "user" + val gsonInput = Gson() + val content = + Content(messages = listOf(Message(role = user, question)), source = "curl", max_tokens = 512) + + val jsonContent = gsonInput.toJson(content).toString() + return jsonContent + } + + data class Content( + val messages: List, + val source: String = "curl", + val max_tokens: Int = 512, + ) +} diff --git a/skippy/src/main/kotlin/com/slack/skippy/CliktSgpLogger.kt b/skippy/src/main/kotlin/com/slack/skippy/CliktSgpLogger.kt index 7ccd1bfe7..5b0e73d81 100644 --- a/skippy/src/main/kotlin/com/slack/skippy/CliktSgpLogger.kt +++ b/skippy/src/main/kotlin/com/slack/skippy/CliktSgpLogger.kt @@ -15,12 +15,13 @@ */ package com.slack.skippy -import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.BaseCliktCommand import com.slack.sgp.common.SgpLogger -internal fun SgpLogger.Companion.clikt(command: CliktCommand): SgpLogger = CliktSgpLogger(command) +internal fun SgpLogger.Companion.clikt(command: BaseCliktCommand<*>): SgpLogger = + CliktSgpLogger(command) -private class CliktSgpLogger(private val command: CliktCommand) : SgpLogger { +private class CliktSgpLogger(private val command: BaseCliktCommand<*>) : SgpLogger { override fun debug(message: String) { command.echo(message) } diff --git a/skippy/src/main/kotlin/com/slack/skippy/ComputeAffectedProjectsCli.kt b/skippy/src/main/kotlin/com/slack/skippy/ComputeAffectedProjectsCli.kt index c4f2aa220..614056c89 100644 --- a/skippy/src/main/kotlin/com/slack/skippy/ComputeAffectedProjectsCli.kt +++ b/skippy/src/main/kotlin/com/slack/skippy/ComputeAffectedProjectsCli.kt @@ -15,7 +15,8 @@ */ package com.slack.skippy -import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.command.SuspendingCliktCommand +import com.github.ajalt.clikt.core.Context import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.required @@ -33,7 +34,6 @@ import kotlin.io.path.readText import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.newFixedThreadPoolContext -import kotlinx.coroutines.runBlocking import okio.FileSystem import okio.Path.Companion.toOkioPath @@ -41,10 +41,10 @@ import okio.Path.Companion.toOkioPath * @see AffectedProjectsComputer for most of the salient docs! The inputs in this CLI more or less * match 1:1 to the properties of that class. */ -public class ComputeAffectedProjectsCli : - CliktCommand( - help = "Computes affected projects and writes output files to an output directory." - ) { +public class ComputeAffectedProjectsCli : SuspendingCliktCommand() { + + override fun help(context: Context): String = + "Computes affected projects and writes output files to an output directory." private val debug: Boolean by option("--debug", help = "Enable debug logging.").flag(default = false) @@ -102,7 +102,7 @@ public class ComputeAffectedProjectsCli : private val logger = SgpLogger.clikt(this) @OptIn(DelicateCoroutinesApi::class) - override fun run() { + override suspend fun run() { val moshi = Moshi.Builder().build() val dependencyGraph = ObjectInputStream(serializedDependencyGraph.inputStream()).use { @@ -136,15 +136,11 @@ public class ComputeAffectedProjectsCli : .run(context) } - runBlocking { - if (parallelism == 1) { - body(Dispatchers.Unconfined) - } else { - logger.lifecycle("Running $parallelism configs in parallel") - newFixedThreadPoolContext(3, "computeAffectedProjects").use { dispatcher -> - body(dispatcher) - } - } + if (parallelism == 1) { + body(Dispatchers.Unconfined) + } else { + logger.lifecycle("Running $parallelism configs in parallel") + newFixedThreadPoolContext(3, "computeAffectedProjects").use { dispatcher -> body(dispatcher) } } } } diff --git a/slack-plugin/src/main/kotlin/slack/gradle/GradleExt.kt b/slack-plugin/src/main/kotlin/slack/gradle/GradleExt.kt index 025a825b5..12903ee07 100644 --- a/slack-plugin/src/main/kotlin/slack/gradle/GradleExt.kt +++ b/slack-plugin/src/main/kotlin/slack/gradle/GradleExt.kt @@ -35,6 +35,7 @@ import org.gradle.api.plugins.PluginManager import org.gradle.api.provider.ListProperty import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider import org.gradle.api.provider.SetProperty import org.gradle.api.reflect.TypeOf import org.gradle.api.tasks.TaskContainer @@ -282,3 +283,6 @@ internal inline fun TaskContainer.registerOrConfigure( in names -> named(taskName) as TaskProvider else -> register(taskName, T::class.java) }.apply { configure { configureAction() } } + +/** Returns a provider that is the inverse of this. */ +internal fun Provider.not(): Provider = map { !it } diff --git a/slack-plugin/src/main/kotlin/slack/gradle/SlackProperties.kt b/slack-plugin/src/main/kotlin/slack/gradle/SlackProperties.kt index 9f52744aa..59db8ef88 100644 --- a/slack-plugin/src/main/kotlin/slack/gradle/SlackProperties.kt +++ b/slack-plugin/src/main/kotlin/slack/gradle/SlackProperties.kt @@ -199,9 +199,13 @@ internal constructor( public val robolectricIVersion: Int get() = intProperty("slack.robolectricIVersion") - /** Opt out for -Werror, should only be used for prototype projects. */ - public val allowWarnings: Boolean - get() = booleanProperty("slack.allowWarnings") + /** Opt out for -Werror. */ + public val allowWarnings: Provider + get() = resolver.booleanProvider("sgp.kotlin.allowWarnings", defaultValue = false) + + /** Opt out for -Werror in tests. */ + public val allowWarningsInTests: Provider + get() = resolver.booleanProvider("sgp.kotlin.allowWarningsInTests", defaultValue = false) /** * Anvil generator projects that should always be included when Anvil is enabled. diff --git a/slack-plugin/src/main/kotlin/slack/gradle/kgp/KgpTasks.kt b/slack-plugin/src/main/kotlin/slack/gradle/kgp/KgpTasks.kt index 139e4140a..b37ba98ee 100644 --- a/slack-plugin/src/main/kotlin/slack/gradle/kgp/KgpTasks.kt +++ b/slack-plugin/src/main/kotlin/slack/gradle/kgp/KgpTasks.kt @@ -37,8 +37,10 @@ import slack.gradle.SlackTools import slack.gradle.asProvider import slack.gradle.configure import slack.gradle.lint.DetektTasks +import slack.gradle.not import slack.gradle.onFirst import slack.gradle.util.configureKotlinCompilationTask +import slack.gradle.util.setDisallowChanges /** Common configuration for Kotlin projects. */ internal object KgpTasks { @@ -179,11 +181,10 @@ internal object KgpTasks { slackProperties.kotlinLanguageVersionOverride.map(KotlinVersion::fromVersion) ) } - if ( - !slackProperties.allowWarnings && - !this@configureKotlinCompilationTask.name.contains("test", ignoreCase = true) - ) { - allWarningsAsErrors.set(true) + if (this@configureKotlinCompilationTask.name.contains("test", ignoreCase = true)) { + allWarningsAsErrors.setDisallowChanges(slackProperties.allowWarningsInTests.not()) + } else { + allWarningsAsErrors.setDisallowChanges(slackProperties.allowWarnings.not()) } freeCompilerArgs.addAll(slackProperties.kotlinFreeArgs)