Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calling DevXP AI API to Chatbot #969

Closed
wants to merge 14 commits into from
3 changes: 1 addition & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -221,7 +220,7 @@ subprojects {
compilerOptions {
val kotlinVersion =
if (isForIntelliJPlugin) {
KOTLIN_1_8
KOTLIN_1_9
} else {
KOTLIN_1_9
}
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", versio
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" }
Expand Down
6 changes: 6 additions & 0 deletions skate-plugin/project-gen/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Message>,
val source: String = "curl",
val max_tokens: Int = 512,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,31 @@ 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<ChatScreen.State> {
val user = "user"
val bot = "bot"
private val chatBotActionService = ChatBotActionService()

@Composable
override fun present(): ChatScreen.State {
var messages by remember { mutableStateOf(emptyList<Message>()) }

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}\"")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
@@ -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<Message>,
val source: String = "curl",
val max_tokens: Int = 512,
)
}
Loading