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

Kl/aibot api call #974

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9c67118
Add necessary dependencies
kateliu20 Sep 23, 2024
c988694
Adjust message class to include role and content
kateliu20 Sep 23, 2024
468beac
Merge branch 'main' of github.com:slackhq/slack-gradle-plugin into kl…
kateliu20 Sep 23, 2024
7c90c8e
Merge branch 'main' of github.com:slackhq/slack-gradle-plugin into kl…
kateliu20 Sep 24, 2024
afe1758
Add ChatBotActionService class
kateliu20 Sep 24, 2024
81d43a5
Remove filler api call function
kateliu20 Sep 24, 2024
63ab1b4
Trying to call the script
kateliu20 Sep 25, 2024
6c79342
Passing in script path
kateliu20 Sep 25, 2024
bfd9768
Trying to pass in scriptPath to presenter
kateliu20 Sep 25, 2024
38d2177
Allow json input for multiple words
kateliu20 Sep 25, 2024
9e72258
Fix ai bot script fetch, run spotless
kateliu20 Sep 26, 2024
153fe17
Remove comments
kateliu20 Sep 26, 2024
9fdd3de
Merge main into branch
kateliu20 Sep 26, 2024
254baa1
Rename to foundry'
kateliu20 Sep 26, 2024
1780c24
Reference foundry
kateliu20 Sep 30, 2024
fb9a998
More foundry edits
kateliu20 Sep 30, 2024
de7ff6f
Merge branch 'main' of github.com:slackhq/slack-gradle-plugin into kl…
kateliu20 Oct 3, 2024
d4231de
Merge branch 'main' of github.com:slackhq/slack-gradle-plugin into kl…
kateliu20 Oct 4, 2024
b3c4109
Merge branch 'main' of github.com:slackhq/slack-gradle-plugin into kl…
kateliu20 Oct 8, 2024
86fa315
Merge branch 'main' of github.com:slackhq/slack-gradle-plugin into kl…
kateliu20 Oct 8, 2024
ba24aaf
Call curl within skate
kateliu20 Oct 9, 2024
ef8cd6a
Run spotless
kateliu20 Oct 9, 2024
a788b0e
Merge branch 'main' into kl/aibot_api_call
kateliu20 Oct 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ 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.5"
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit"}
retrofit-converters-wire = { module = "com.squareup.retrofit2:converter-wire", version.ref = "retrofit" }
rxjava = "io.reactivex.rxjava3:rxjava:3.1.9"
sarif4k = "io.github.detekt.sarif4k:sarif4k:0.6.0"
Expand Down
8 changes: 8 additions & 0 deletions platforms/intellij/compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ kotlin {
implementation(libs.jewel.bridge)
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)
implementation(libs.moshi)

implementation(projects.tools.foundryCommon)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* 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 foundry.intellij.compose.aibot.Message
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import java.nio.file.Path
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.VisibleForTesting

class ChatBotActionService(private val scriptPath: Path, private val apiLink: String) {
suspend fun executeCommand(question: String): String {
val jsonInput = createJsonInput(question)
val authInfo = getAuthInfo(scriptPath)
println("authInfo $authInfo")
val (userAgent, cookies) = parseAuthJson(authInfo)

val scriptContent = createScriptContent(userAgent, cookies, jsonInput)

val tempScript = createTempScript(scriptContent)

val response = runScript(tempScript)

println("User Agent: $userAgent")
println("Cookies: $cookies")

println("authInfo $authInfo")
println("scriptContent $scriptContent")

println("Response from API: $response")

val parsedOutput = parseOutput(response)

return parsedOutput
}

@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 getAuthInfo(scriptPath: Path): String {
val processBuilder = ProcessBuilder("/bin/bash", scriptPath.toString())
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")
println("Script output: $line")
}
}

val completed = process.waitFor(600, TimeUnit.SECONDS)
if (!completed) {
process.destroyForcibly()
throw RuntimeException("Process timed out after 600 seconds")
}

val jsonStartIndex = output.indexOf("{")
return if (jsonStartIndex != -1) {
output.substring(jsonStartIndex).trim()
} else {
throw IllegalArgumentException("No valid JSON found in output")
}
}

private fun parseAuthJson(authJsonString: String): Pair<String, String> {
val gson = Gson()
val authJson = gson.fromJson(authJsonString, JsonObject::class.java)

println("authJson $authJson")
println("Parsed AuthJson: $authJson")

val userAgent = authJson.get("user-agent")?.asString ?: "unknown"
val machineCookie = authJson.get("machine-cookie")?.asString ?: "unknown"
val slauthSession = authJson.get("slauth-session")?.asString ?: "unknown"
val tsa2 = authJson.get("tsa2")?.asString ?: "unknown"

val cookies = "machine-cookie=$machineCookie; slauth-session=$slauthSession; tsa2=$tsa2"

println("User Agent: $userAgent")
println("cookies $cookies")
return Pair(userAgent, cookies)
}

private fun createScriptContent(userAgent: String, cookies: String, jsonInput: String): String {
return """
#!/bin/bash
curl -s -X POST $apiLink \
-H "Content-Type: application/json" \
-H "User-Agent: $userAgent" \
-H "Cookie: $cookies" \
-d '$jsonInput'
"""
.trimIndent()
}

private suspend fun createTempScript(scriptContent: String): File {
return withContext(Dispatchers.IO) {
val tempScript = File.createTempFile("run_command", ".sh")
tempScript.writeText(scriptContent)
tempScript.setExecutable(true)
tempScript
}
}

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)?.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 @@ -22,21 +22,24 @@ import com.slack.circuit.foundation.Circuit
import com.slack.circuit.foundation.CircuitContent
import foundry.intellij.compose.projectgen.FoundryDesktopTheme
import java.awt.Dimension
import java.nio.file.Path
import javax.swing.JComponent

object ChatPanel {
fun createPanel(): JComponent {
fun createPanel(scriptPath: Path, apiLink: String): JComponent {
return ComposePanel().apply {
preferredSize = Dimension(400, 600)
setContent { FoundryDesktopTheme { ChatApp() } }
setContent { FoundryDesktopTheme { ChatApp(scriptPath, apiLink) } }
}
}

@Composable
private fun ChatApp() {
private fun ChatApp(scriptPath: Path, apiLink: String) {
println("ChatApp Script Path $scriptPath")
println("ChatApp Script Path absolutely ${scriptPath.toAbsolutePath()}")
val circuit = remember {
Circuit.Builder()
.addPresenter<ChatScreen, ChatScreen.State>(ChatPresenter())
.addPresenter<ChatScreen, ChatScreen.State>(ChatPresenter(scriptPath, apiLink))
.addUi<ChatScreen, ChatScreen.State> { state, modifier -> ChatWindowUi(state, modifier) }
.build()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,33 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import com.slack.circuit.runtime.presenter.Presenter
import java.nio.file.Path
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import slack.tooling.aibot.ChatBotActionService

class ChatPresenter(private val scriptPath: Path, apiLink: String) : Presenter<ChatScreen.State> {
val user = "user"
val bot = "bot"
private val chatBotActionService = ChatBotActionService(scriptPath, apiLink)

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

println("print script path $scriptPath")

return ChatScreen.State(messages = messages, isLoading = isLoading) { 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 {
val response = chatBotActionService.executeCommand(event.message)
messages = messages + Message(role = bot, response)
}
isLoading = true
val response = Message(callApi(event.message), isMe = false)
messages = messages + response
Expand All @@ -41,10 +56,4 @@ class ChatPresenter : Presenter<ChatScreen.State> {
}
}
}

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 @@ -62,9 +62,10 @@ fun ChatWindowUi(state: ChatScreen.State, modifier: Modifier = Modifier) {
Column(modifier = modifier.fillMaxSize().background(JewelTheme.globalColors.panelBackground)) {
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 @@ -159,18 +160,17 @@ private fun ConversationField(

@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 foundry.intellij.compose.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
Expand Up @@ -122,6 +122,18 @@ class SkatePluginSettings : SimplePersistentStateComponent<SkatePluginSettings.S
state.codeOwnerFilePath = value
}

var devxpAPIcall: String?
get() = state.devxpAPIcall
set(value) {
state.devxpAPIcall = value
}

var devxpAPIlink: String?
get() = state.devxpAPIlink
set(value) {
state.devxpAPIlink = value
}

class State : BaseState() {
var whatsNewFilePath by string()
var isWhatsNewEnabled by property(true)
Expand All @@ -137,5 +149,7 @@ class SkatePluginSettings : SimplePersistentStateComponent<SkatePluginSettings.S
var tracingEndpoint by string()
var codeOwnerFilePath by string()
var isCodeOwnerEnabled by property(true)
var devxpAPIcall by string()
var devxpAPIlink by string()
}
}
Loading
Loading