From ce40e1f5dd33f55bc20b6a900599bd54c33b2d97 Mon Sep 17 00:00:00 2001 From: Luo Tim Date: Sat, 13 Jul 2024 02:47:46 +0800 Subject: [PATCH 01/13] Fix dead lock in EDT --- .../kotlin/ai/devchat/plugin/IDEServer.kt | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/ai/devchat/plugin/IDEServer.kt b/src/main/kotlin/ai/devchat/plugin/IDEServer.kt index 42e9420..e6596d0 100644 --- a/src/main/kotlin/ai/devchat/plugin/IDEServer.kt +++ b/src/main/kotlin/ai/devchat/plugin/IDEServer.kt @@ -48,7 +48,8 @@ import kotlinx.serialization.Serializable import java.awt.Point import java.io.File import java.net.ServerSocket -import java.util.concurrent.FutureTask +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CountDownLatch import kotlin.reflect.full.memberFunctions @@ -58,7 +59,7 @@ const val START_PORT: Int = 31800 @Serializable data class ReqLocation(val abspath: String, val line: Int, val character: Int) @Serializable -data class DiffApplyRequest(val filepath: String?, val content: String?, val autoedit: Boolean?) +data class DiffApplyRequest(val filepath: String?, val content: String?, val autoedit: Boolean? = false) @Serializable data class Position(val line: Int, val character: Int) @Serializable @@ -366,13 +367,27 @@ fun getAvailablePort(startPort: Int): Int { fun runInEdtAndGet(block: () -> T): T { val app = ApplicationManager.getApplication() - return if (app.isDispatchThread) { block() } else { - val future = FutureTask(block) - app.invokeAndWait { future.run() } - future.get() + if (app.isDispatchThread) { + return block() } + val future = CompletableFuture() + val latch = CountDownLatch(1) + app.invokeLater { + try { + val result = block() + future.complete(result) + } catch (e: Exception) { + future.completeExceptionally(e) + } finally { + latch.countDown() + } + } + latch.await() + return future.get() } + + fun Project.getPsiFile(filePath: String): PsiFile = runInEdtAndGet { ReadAction.compute { val virtualFile = LocalFileSystem.getInstance().findFileByIoFile(File(filePath)) From 249d3f4de748e4fe02a493401bcd03e099e631f6 Mon Sep 17 00:00:00 2001 From: Luo Tim Date: Sat, 13 Jul 2024 02:48:54 +0800 Subject: [PATCH 02/13] Improve createTempFile & add workspace --- src/main/kotlin/ai/devchat/common/PathUtils.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/ai/devchat/common/PathUtils.kt b/src/main/kotlin/ai/devchat/common/PathUtils.kt index f5df365..0744ce9 100644 --- a/src/main/kotlin/ai/devchat/common/PathUtils.kt +++ b/src/main/kotlin/ai/devchat/common/PathUtils.kt @@ -1,5 +1,6 @@ package ai.devchat.common +import ai.devchat.plugin.currentProject import java.io.File import java.io.IOException import java.nio.file.* @@ -7,6 +8,7 @@ import java.nio.file.attribute.BasicFileAttributes object PathUtils { + val workspace: String? = currentProject?.basePath val workPath: String = Paths.get(System.getProperty("user.home"), ".chat").toString() val workflowPath: String = Paths.get(workPath, "scripts").toString() val sitePackagePath: String = Paths.get(workPath, "site-packages").toString() @@ -73,9 +75,14 @@ object PathUtils { return targetPath.toString() } - fun createTempFile(prefix: String, content: String): String { - val tempFile = File.createTempFile(prefix, "") - tempFile.writeText(content) - return tempFile.absolutePath + fun createTempFile(content: String, prefix: String = "devchat-tmp-", suffix: String = ""): String? { + return try { + val tempFile = File.createTempFile(prefix, suffix) + tempFile.writeText(content) + tempFile.absolutePath + } catch (e: IOException) { + Log.error("Failed to create a temporary file: $e") + return null + } } } From c33ca106cacc932bf717ddfcb2798bc766c7dbdf Mon Sep 17 00:00:00 2001 From: Luo Tim Date: Sat, 13 Jul 2024 02:50:45 +0800 Subject: [PATCH 03/13] Update usage of createTempFile --- src/main/kotlin/ai/devchat/plugin/DiffViewerDialog.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/ai/devchat/plugin/DiffViewerDialog.kt b/src/main/kotlin/ai/devchat/plugin/DiffViewerDialog.kt index f5e5e7d..0e8f1ad 100644 --- a/src/main/kotlin/ai/devchat/plugin/DiffViewerDialog.kt +++ b/src/main/kotlin/ai/devchat/plugin/DiffViewerDialog.kt @@ -75,11 +75,11 @@ class DiffViewerDialog( } private fun editText(): String { - val srcTempFile = PathUtils.createTempFile("code_editor_src_", editor.document.text) - val newTempFile = PathUtils.createTempFile("code_editor_new_", newText) - val resultTempFile = PathUtils.createTempFile("code_editor_res_", "") + val srcTempFile = PathUtils.createTempFile(editor.document.text, "code_editor_src_") + val newTempFile = PathUtils.createTempFile(newText, "code_editor_new_") + val resultTempFile = PathUtils.createTempFile("", "code_editor_res_") val codeEditorPath = Paths.get(PathUtils.toolsPath, PathUtils.codeEditorBinary).toString() - val result = CommandLine.exec(codeEditorPath, srcTempFile, newTempFile, resultTempFile) + val result = CommandLine.exec(codeEditorPath, srcTempFile!!, newTempFile!!, resultTempFile!!) require(result.exitCode == 0) { throw Exception("Code editor failed with exit code ${result.exitCode}") } From 14f508dbadf2c4e5e31226388b9950bb7c7440bd Mon Sep 17 00:00:00 2001 From: Luo Tim Date: Sat, 13 Jul 2024 02:55:57 +0800 Subject: [PATCH 04/13] Define data schemas & implement local service client --- .../kotlin/ai/devchat/core/DevChatClient.kt | 347 ++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 src/main/kotlin/ai/devchat/core/DevChatClient.kt diff --git a/src/main/kotlin/ai/devchat/core/DevChatClient.kt b/src/main/kotlin/ai/devchat/core/DevChatClient.kt new file mode 100644 index 0000000..bc04bc6 --- /dev/null +++ b/src/main/kotlin/ai/devchat/core/DevChatClient.kt @@ -0,0 +1,347 @@ +package ai.devchat.core + +import ai.devchat.common.Log +import ai.devchat.common.PathUtils +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flowOn +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.serializer +import okhttp3.* +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Paths +import java.time.Instant +import kotlin.system.measureTimeMillis + +private const val DEFAULT_LOG_MAX_COUNT = 10000 + +@Serializable +data class ChatRequest( + val content: String, + @SerialName("model_name") val modelName: String, + @SerialName("api_key") val apiKey: String, + @SerialName("api_base") val apiBase: String, + val parent: String?, + val context: List?, + val workspace: String? = PathUtils.workspace, + @Transient val contextContents: List? = null, + @Transient val response: ChatResponse? = null, +) +@Serializable +data class ChatResponse( + var chunkId: Int = 0, + @SerialName("prompt_hash") var promptHash: String? = null, + var user: String? = null, + var date: String? = null, + var content: String? = "", + @SerialName("finish_reason") var finishReason: String? = "", + @SerialName("is_error") var isError: Boolean = false, + var extra: JsonElement? = null +) { + fun reset() { + chunkId = 0 + promptHash = null + user = null + date = null + content = "" + finishReason = "" + isError = false + extra = null + } + + fun appendChunk(chunk: Any) : ChatResponse { + return when (chunk) { + is String -> appendChunk(chunk) + is ChatResponse -> appendChunk(chunk) + else -> this + } + } + + private fun appendChunk(chunk: String) : ChatResponse { + chunkId += 1; + when { + chunk.startsWith("User: ") -> user = user ?: chunk.substring("User: ".length) + chunk.startsWith("Date: ") -> date = date ?: chunk.substring("Date: ".length) + // 71 is the length of the prompt hash + chunk.startsWith("prompt ") && chunk.length == 71 -> { + promptHash = chunk.substring("prompt ".length) + content = content?.let { "$it\n" } ?: "\n" + } + chunk.isNotEmpty() -> content = content?.let { "$it\n$chunk" } ?: chunk + } + return this + } + + private fun appendChunk(chunk: ChatResponse) : ChatResponse { + chunkId += 1; + if (user == null) user = chunk.user + if (date == null) date = chunk.date + finishReason = chunk.finishReason + if (finishReason == "should_run_workflow") { + extra = chunk.extra + Log.debug("should run workflow via cli.") + return this + } + isError = chunk.isError + content += chunk.content + return this + } +} +@Serializable +data class LogEntry( + val model: String, + val parent: String?, + val messages: MutableList, + val timestamp: Long, + @SerialName("request_tokens") val requestTokens: Int, + @SerialName("response_tokens") val responseTokens: Int +) { + constructor( + model: String, + parent: String?, + request: String, + contexts: List?, + response: String?, + ) : this( + model, parent, mutableListOf(), Instant.now().epochSecond, 1, 1 + ) { + this.messages.add(Message("user", request)) + this.messages.add(Message("assistant", response)) + this.messages.addAll(contexts?.map { + Message("system", "$it") + }.orEmpty()) + } +} + +@Serializable +data class LogInsertRes( + val hash: String? = null, + val error: String? = null +) +@Serializable +data class LogDeleteRes( + val success: Boolean? = null, + val error: String? = null +) +@Serializable +data class Message( + val role: String, + val content: String?, +) + +@Serializable +data class ShortLog( + val hash: String, + val parent: String?, + val user: String, + val date: Long, + val request: String, + val responses: List, + val context: List?, + @SerialName("request_tokens") val requestTokens: Int, + @SerialName("response_tokens") val responseTokens: Int +) + +@Serializable +data class Workflow( + val name: String, + val namespace: String, + val active: Boolean, + @SerialName("command_conf") val commandConf: Map +) + +@Serializable +data class WorkflowConfig( + val recommend: Recommend, +) { + @Serializable + data class Recommend(val workflows: List) +} + +fun timeThis(block: suspend () -> Unit) { + runBlocking { + val time = measureTimeMillis { + block() + } + Log.debug("Execution time: ${time / 1000.0} seconds") + } +} + +class DevChatClient(port: Int = 22222) { + private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) + private val baseURL = "http://localhost:$port" + private var job: Job? = null + companion object { + const val LOG_RAW_DATA_SIZE_LIMIT = 4 * 1024 + } + private val client = OkHttpClient() + private val json = Json { ignoreUnknownKeys = true } + + private inline fun get( + path: String, + queryParams: Map = emptyMap(), + ): T? { + Log.info("GET request to $baseURL$path: $queryParams") + val urlBuilder = "$baseURL$path".toHttpUrlOrNull()?.newBuilder() ?: return null + queryParams.forEach { (k, v) -> urlBuilder.addQueryParameter(k, v.toString()) } + val url = urlBuilder.build() + val request = Request.Builder().url(url).get().build() + return try { + client.newCall(request).execute().use {response -> + if (!response.isSuccessful) throw IOException( + "Unsuccessful response: ${response.code} ${response.message}" + ) + response.body?.string()?.let { + json.decodeFromString(it) + } ?: throw IOException("Empty response body") + } + } catch (e: Exception) { + Log.warn(e.toString()) + null + } + } + + private inline fun post(path: String, body: T? = null): R? { + Log.debug("POST request to $baseURL$path: $body") + val url = "$baseURL$path".toHttpUrlOrNull() ?: return null + val requestBody = json.encodeToString(serializer(), body).toRequestBody("application/json".toMediaType()) + val request = Request.Builder().url(url).post(requestBody).build() + return try { + val response: Response = client.newCall(request).execute() + if (response.isSuccessful) { + response.body?.let {json.decodeFromString(it.string())} + } else null + } catch (e: Exception) { + Log.warn(e.toString()) + null + } + } + + private inline fun streamPost(path: String, body: T? = null): Flow = callbackFlow { + Log.debug("POST request to $baseURL$path: $body") + val url = "$baseURL$path".toHttpUrlOrNull() ?: return@callbackFlow + val requestJson = json.encodeToString(serializer(), body) + val requestBody = requestJson.toRequestBody("application/json".toMediaType()) + val request = Request.Builder().url(url).post(requestBody).build() + val call = client.newCall(request) + + val response = call.execute() + if (!response.isSuccessful) { + throw IOException("Unexpected code $response") + } + response.body?.byteStream()?.use {inputStream -> + val buffer = ByteArray(8192) // 8KB buffer + var bytesRead: Int + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + val chunk = buffer.copyOf(bytesRead).toString(Charsets.UTF_8) + send(json.decodeFromString(chunk)) + } + } + close() + awaitClose { call.cancel() } + }.flowOn(Dispatchers.IO) + + fun message( + message: ChatRequest, + onData: (ChatResponse) -> Unit, + onError: (String) -> Unit, + onFinish: (Int) -> Unit, + ) { + cancelMessage() + job = scope.launch { + streamPost("/message/msg", message) + .catch { e -> + onError(e.toString()) + Log.warn("Error on sending message: $e") + onFinish(1) + cancelMessage() + } + .collect { chunk -> + if (chunk.finishReason == "should_run_workflow") { + onFinish(-1) + cancelMessage() + } + onData(chunk) + } + onFinish(0) + } + } + + fun getWorkflowList(): List? { + return get("/workflows/list") + } + fun getWorkflowConfig(): WorkflowConfig? { + return get("/workflows/config") + } + fun updateWorkflows() { + val response: Map? = post("/workflows/update") + Log.debug("Update workflows response: $response") + } + + fun insertLog(logEntry: LogEntry): LogInsertRes? { + val body = mutableMapOf("workspace" to PathUtils.workspace) + val jsonData = json.encodeToString(serializer(), logEntry) + if (jsonData.length <= LOG_RAW_DATA_SIZE_LIMIT) { + body["jsondata"] = jsonData + } else { + body["filepath"] = PathUtils.createTempFile(jsonData, "devchat_log_insert_", ".json") + } + val response: LogInsertRes? = post("/logs/insert", body) + if (body.containsKey("filepath")) { + try { + Files.delete(Paths.get(body["filepath"]!!)) + } catch (e: Exception) { + Log.error("Failed to delete temp file ${body["filepath"]}: $e") + } + } + return response + } + fun deleteLog(logHash: String): LogDeleteRes? { + return post("/logs/delete", mapOf( + "workspace" to PathUtils.workspace, + "hash" to logHash + )) + } + fun getTopicLogs(topicRootHash: String, offset: Int = 0, limit: Int = DEFAULT_LOG_MAX_COUNT): List { + return get>("/topics/$topicRootHash/logs", mapOf( + "limit" to limit, + "offset" to offset, + "workspace" to PathUtils.workspace, + )) ?: emptyList() + } + fun getTopics(limit: Int, offset: Int): List { + val queryParams = mapOf( + "limit" to limit, + "offset" to offset, + "workspace" to PathUtils.workspace, + ) + val topics: List>? = get("/topics", queryParams) + return topics?.reversed() ?: emptyList() + } + + fun deleteTopic(topicRootHash: String) { + val response: Map? = post("/topics/delete", mapOf( + "topic_hash" to topicRootHash, + "workspace" to PathUtils.workspace, + )) + Log.debug("deleteTopic response data: $response") + } + + private fun cancelMessage() { + job?.cancel() + job = null + } +} + +val DC_CLIENT: DevChatClient = DevChatClient() \ No newline at end of file From 3219cf69f872bd49cfc24918a0c73a9da2dfa6ff Mon Sep 17 00:00:00 2001 From: Luo Tim Date: Sat, 13 Jul 2024 02:56:48 +0800 Subject: [PATCH 05/13] Remove DevChatResponse --- .../kotlin/ai/devchat/core/DevChatResponse.kt | 31 ------------------- 1 file changed, 31 deletions(-) delete mode 100644 src/main/kotlin/ai/devchat/core/DevChatResponse.kt diff --git a/src/main/kotlin/ai/devchat/core/DevChatResponse.kt b/src/main/kotlin/ai/devchat/core/DevChatResponse.kt deleted file mode 100644 index e4030c4..0000000 --- a/src/main/kotlin/ai/devchat/core/DevChatResponse.kt +++ /dev/null @@ -1,31 +0,0 @@ -package ai.devchat.core - -class DevChatResponse { - var user: String? = null - var date: String? = null - var message: String? = null - var promptHash: String? = null - - fun update(line: String) : DevChatResponse { - when { - line.startsWith("User: ") -> user = user ?: line.substring("User: ".length) - line.startsWith("Date: ") -> date = date ?: line.substring("Date: ".length) - // 71 is the length of the prompt hash - line.startsWith("prompt ") && line.length == 71 -> { - promptHash = line.substring("prompt ".length) - message = message?.let { "$it\n" } ?: "\n" - } - line.isNotEmpty() -> message = message?.let { "$it\n$line" } ?: line - } - return this - } - - override fun toString(): String { - val sb = StringBuilder() - sb.append("User: ").append(user).append("\n") - sb.append("Date: ").append(date).append("\n\n") - sb.append(message).append("\n") - sb.append("prompt ").append(promptHash).append("\n") - return sb.toString() - } -} From f63040dee1c199b4a8d26cb368ab95e5d297be3c Mon Sep 17 00:00:00 2001 From: Luo Tim Date: Sat, 13 Jul 2024 02:59:40 +0800 Subject: [PATCH 06/13] Refactor ActiveConversation and apply new getTopicLogs --- .../handlers/LoadConversationRequestHandler.kt | 12 +++--------- .../ai/devchat/storage/ActiveConversation.kt | 16 ++++++++-------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/ai/devchat/core/handlers/LoadConversationRequestHandler.kt b/src/main/kotlin/ai/devchat/core/handlers/LoadConversationRequestHandler.kt index 7e95b32..3e84bb5 100644 --- a/src/main/kotlin/ai/devchat/core/handlers/LoadConversationRequestHandler.kt +++ b/src/main/kotlin/ai/devchat/core/handlers/LoadConversationRequestHandler.kt @@ -1,6 +1,7 @@ package ai.devchat.core.handlers import ai.devchat.core.BaseActionHandler +import ai.devchat.core.DC_CLIENT import ai.devchat.core.DevChatActions import ai.devchat.storage.ActiveConversation import com.alibaba.fastjson.JSONObject @@ -19,15 +20,8 @@ class LoadConversationRequestHandler(requestAction: String, metadata: JSONObject topicHash.isNullOrEmpty() -> ActiveConversation.reset() topicHash == ActiveConversation.topic -> res["reset"] = false else -> { - val arr = wrapper.logTopic(topicHash, null) - // remove request_tokens and response_tokens in the conversations object - val messages = List(arr.size){i -> - val msg = arr.getJSONObject(i) - msg.remove("request_tokens") - msg.remove("response_tokens") - msg - } - ActiveConversation.reset(topicHash, messages) + val logs = DC_CLIENT.getTopicLogs(topicHash) + ActiveConversation.reset(topicHash, logs) } } send(payload=res) diff --git a/src/main/kotlin/ai/devchat/storage/ActiveConversation.kt b/src/main/kotlin/ai/devchat/storage/ActiveConversation.kt index a055b1a..b85a29b 100644 --- a/src/main/kotlin/ai/devchat/storage/ActiveConversation.kt +++ b/src/main/kotlin/ai/devchat/storage/ActiveConversation.kt @@ -1,27 +1,27 @@ package ai.devchat.storage -import com.alibaba.fastjson.JSONObject +import ai.devchat.core.ShortLog object ActiveConversation { - private var messages: MutableList? = null + private var messages: MutableList? = null var topic: String? = null - fun reset(topic: String? = null, messages: List? = null) { + fun reset(topic: String? = null, messages: List? = null) { this.topic = topic this.messages = messages?.toMutableList() } - fun addMessage(message: JSONObject) { + fun addMessage(message: ShortLog) { messages?.add(message) } - fun findMessage(hash: String): JSONObject? { - return messages?.find{it.getString("hash") == hash} + fun findMessage(hash: String): ShortLog? { + return messages?.find{it.hash == hash} } fun deleteMessage(hash: String) { - val idx = messages?.indexOfFirst {it.getString("hash") == hash} ?: -1 + val idx = messages?.indexOfFirst {it.hash == hash} ?: -1 if (idx >= 0) { messages?.slice(0..? { + fun getMessages(page: Int = 1, pageSize: Int = 20): List? { if (this.messages == null) { return null } From d46c13e937514dbab649d3bb658f87909c7f73b2 Mon Sep 17 00:00:00 2001 From: Luo Tim Date: Sat, 13 Jul 2024 03:01:31 +0800 Subject: [PATCH 07/13] Refactor SendMessageRequestHandler to apply new local service APIs --- .../handlers/SendMessageRequestHandler.kt | 242 +++++++++--------- 1 file changed, 114 insertions(+), 128 deletions(-) diff --git a/src/main/kotlin/ai/devchat/core/handlers/SendMessageRequestHandler.kt b/src/main/kotlin/ai/devchat/core/handlers/SendMessageRequestHandler.kt index ce6b9c9..4f17f36 100644 --- a/src/main/kotlin/ai/devchat/core/handlers/SendMessageRequestHandler.kt +++ b/src/main/kotlin/ai/devchat/core/handlers/SendMessageRequestHandler.kt @@ -1,16 +1,13 @@ package ai.devchat.core.handlers -import ai.devchat.core.DevChatResponse import ai.devchat.common.Log -import ai.devchat.core.BaseActionHandler -import ai.devchat.core.DevChatActions +import ai.devchat.common.PathUtils +import ai.devchat.core.* import ai.devchat.storage.ActiveConversation import ai.devchat.storage.CONFIG import com.alibaba.fastjson.JSONObject -import java.io.File -import java.io.IOException -import java.lang.Exception -import java.time.Instant +import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer class SendMessageRequestHandler(requestAction: String, metadata: JSONObject?, payload: JSONObject?) : BaseActionHandler( requestAction, @@ -18,93 +15,49 @@ class SendMessageRequestHandler(requestAction: String, metadata: JSONObject?, pa payload ) { override val actionName: String = DevChatActions.SEND_MESSAGE_RESPONSE + private val defaultModel = CONFIG["default_model"] as String private var currentChunkId = 0 + private val json = Json { ignoreUnknownKeys=true } override fun action() { if (requestAction == DevChatActions.REGENERATION_REQUEST) { - prevArgs?.let { + lastRequestArgs?.let { metadata = it.first payload = it.second } } else { - prevArgs = Pair(metadata, payload) + lastRequestArgs = Pair(metadata, payload) } - val flags: MutableList> = mutableListOf() - - val contexts = payload!!.getJSONArray("contexts") - val contextJSONs = mutableListOf() - contexts?.takeIf { it.isNotEmpty() }?.forEachIndexed { i, _ -> - val context = contexts.getJSONObject(i) - val contextType = context.getString("type") - - val contextPath = when (contextType) { - "code" -> { - val filename = context.getString("path").substringAfterLast(".", "") - val str = listOf( - "languageId", "path", "startLine", "content" - ).fold(JSONObject()) { acc, key -> acc[key] = context[key]; acc }.toJSONString() - contextJSONs.add(str) - createTempFile(str, filename) - } - "command" -> { - val str = listOf( - "command", "content" - ).fold(JSONObject()) { acc, key -> acc[key] = context[key]; acc }.toJSONString() - contextJSONs.add(str) - createTempFile(str, "custom.txt") - } - else -> null - } - contextPath?.let { - flags.add("context" to it) - Log.info("Context file path: $it") - } - } - val parent = metadata!!.getString("parent") - parent?.takeIf { it.isNotEmpty() }?.let { - flags.add("parent" to it) - } - val model = payload!!.getString("model") - model?.takeIf { it.isNotEmpty() }?.let { - flags.add("model" to it) - } + val parent = metadata!!.getString("parent")?.takeUnless { it.isEmpty() } + val model = payload!!.getString("model")?.takeIf { it.isNotEmpty() } ?: defaultModel val message = payload!!.getString("message") + val (contextTempFilePaths, contextContents) = processContexts( + json.decodeFromString( + payload!!.getJSONArray("contexts").toString() + ) + ).unzip() + + val chatRequest = ChatRequest( + content=message, + modelName = model, + apiKey = CONFIG["providers.devchat.api_key"] as String, + apiBase = CONFIG["providers.devchat.api_base"] as String, + parent=parent, + context = contextTempFilePaths, + response = ChatResponse(), + contextContents = contextContents, + ) - val response = DevChatResponse() - wrapper.route( - flags, - message, - callback = {line -> - response.update(line) - promptCallback(response) - }, - onError = { - send( - metadata=mapOf( - "currentChunkId" to 0, - "isFinalChunk" to true, - "finishReason" to "error", - "error" to it - ) - ) - }, - onFinish = { _ -> - val record = insertLog(contextJSONs, model, message, response.message, parent) - response.update("prompt ${record["hash"]}") - promptCallback(response) - - val currentTopic = ActiveConversation.topic ?: response.promptHash!! - val newMessage = wrapper.logTopic(currentTopic, 1).getJSONObject(0) - - if (currentTopic == ActiveConversation.topic) { - ActiveConversation.addMessage(newMessage) - } else { - ActiveConversation.reset(currentTopic, listOf(newMessage)) - } - } + DC_CLIENT.message( + chatRequest, + dataHandler(chatRequest), + ::errorHandler, + finishHandler(chatRequest) ) + + } override fun except(exception: Exception) { @@ -118,12 +71,76 @@ class SendMessageRequestHandler(requestAction: String, metadata: JSONObject?, pa ) } - private fun promptCallback(response: DevChatResponse) { - response.message?.let { + private fun runWorkflow(chatRequest: ChatRequest) { + chatRequest.response!!.reset() + val flags: List> = buildList { + add("model" to chatRequest.modelName) + chatRequest.parent?.let { add("parent" to it) } + addAll(chatRequest.context?.map {"context" to it}.orEmpty()) + } + + wrapper.route( + flags, + chatRequest.content, + callback = dataHandler(chatRequest), + onError = ::errorHandler, + onFinish = finishHandler(chatRequest) + ) + } + + + private fun dataHandler(chatRequest: ChatRequest): (Any) -> Unit { + return { data: Any -> + chatRequest.response!!.appendChunk(data) + promptCallback(chatRequest.response) + } + } + private fun finishHandler(chatRequest: ChatRequest): (Int) -> Unit { + val response = chatRequest.response!! + return { exitCode: Int -> + when(exitCode) { + 0 -> { + val entry = DC_CLIENT.insertLog( + LogEntry( + chatRequest.modelName, + chatRequest.parent, + chatRequest.content, + chatRequest.contextContents, + response.content + ) + ) + response.promptHash = entry!!.hash + promptCallback(response) + + val currentTopic = ActiveConversation.topic ?: response.promptHash!! + val logs = DC_CLIENT.getTopicLogs(currentTopic, 0, 1) + + if (currentTopic == ActiveConversation.topic) { + ActiveConversation.addMessage(logs.first()) + } else { + ActiveConversation.reset(currentTopic, logs) + } + } + -1 -> runWorkflow(chatRequest) + } + } + } + + private fun errorHandler(e: String) { + send(metadata=mapOf( + "currentChunkId" to 0, + "isFinalChunk" to true, + "finishReason" to "error", + "error" to e + )) + } + + private fun promptCallback(response: ChatResponse) { + response.content?.let { currentChunkId += 1 send( payload = mapOf( - "message" to response.message, + "message" to response.content, "user" to response.user, "date" to response.date, "promptHash" to response.promptHash @@ -138,54 +155,23 @@ class SendMessageRequestHandler(requestAction: String, metadata: JSONObject?, pa } } - private fun createTempFile(content: String, filename: String): String? { - return try { - val tempFile = File.createTempFile("devchat-tmp-", "-$filename") - tempFile.writeText(content) - tempFile.absolutePath - } catch (e: IOException) { - Log.error("Failed to create a temporary file." + e.message) - return null - } - } - - private fun insertLog( - contexts: List?, - model: String?, - request: String, - response: String?, - parent: String? - ): JSONObject { - val defaultModel = CONFIG["default_model"] - val item = mutableMapOf( - "model" to if (model.isNullOrEmpty()) defaultModel else model, - "messages" to listOf( - mutableMapOf( - "role" to "user", - "content" to request - ), - mutableMapOf( - "role" to "assistant", - "content" to response - ), - *contexts?.map { mapOf( - "role" to "system", - "content" to "$it" - ) }.orEmpty().toTypedArray() - ), - "timestamp" to Instant.now().epochSecond, - "request_tokens" to 1, - "response_tokens" to 1, - ) - parent?.let {item.put("parent", parent)} - wrapper.logInsert(JSONObject(item).toJSONString()) - val lastRecord = wrapper.logLast() - Log.info("Log item inserted: ${lastRecord!!["hash"]}") - return lastRecord + private fun processContexts(contexts: List>?): List> { + val prefix = "devchat-context-" + return contexts?.mapNotNull {context -> + when (context["type"] as? String) { + "code", "command" -> { + val data = json.encodeToString(serializer(), context) + val tempFilePath = PathUtils.createTempFile(data, prefix) + Log.info("Context file path: $tempFilePath") + tempFilePath!! to data + } + else -> null + } + }.orEmpty() } companion object { - var prevArgs: Pair? = null + var lastRequestArgs: Pair? = null } } From 438d8506052af3171302d01e469334371ed1b016 Mon Sep 17 00:00:00 2001 From: Luo Tim Date: Sat, 13 Jul 2024 03:19:05 +0800 Subject: [PATCH 08/13] Clean up unused cli wrapper funcs --- .../kotlin/ai/devchat/core/DevChatClient.kt | 2 +- .../kotlin/ai/devchat/core/DevChatWrapper.kt | 42 +------------------ .../DeleteLastConversationRequestHandler.kt | 3 +- 3 files changed, 5 insertions(+), 42 deletions(-) diff --git a/src/main/kotlin/ai/devchat/core/DevChatClient.kt b/src/main/kotlin/ai/devchat/core/DevChatClient.kt index bc04bc6..3b9cfda 100644 --- a/src/main/kotlin/ai/devchat/core/DevChatClient.kt +++ b/src/main/kotlin/ai/devchat/core/DevChatClient.kt @@ -320,7 +320,7 @@ class DevChatClient(port: Int = 22222) { "workspace" to PathUtils.workspace, )) ?: emptyList() } - fun getTopics(limit: Int, offset: Int): List { + fun getTopics(offset: Int = 0, limit: Int = DEFAULT_LOG_MAX_COUNT): List { val queryParams = mapOf( "limit" to limit, "offset" to offset, diff --git a/src/main/kotlin/ai/devchat/core/DevChatWrapper.kt b/src/main/kotlin/ai/devchat/core/DevChatWrapper.kt index 75e5fef..3d383eb 100644 --- a/src/main/kotlin/ai/devchat/core/DevChatWrapper.kt +++ b/src/main/kotlin/ai/devchat/core/DevChatWrapper.kt @@ -1,13 +1,13 @@ package ai.devchat.core -import ai.devchat.common.* +import ai.devchat.common.Log import ai.devchat.common.Notifier +import ai.devchat.common.PathUtils import ai.devchat.plugin.currentProject import ai.devchat.plugin.ideServerPort import ai.devchat.storage.CONFIG import com.alibaba.fastjson.JSON import com.alibaba.fastjson.JSONArray -import com.alibaba.fastjson.JSONObject import com.intellij.util.containers.addIfNotNull import kotlinx.coroutines.* import kotlinx.coroutines.channels.SendChannel @@ -241,7 +241,6 @@ class DevChatWrapper( } val run get() = Command(baseCommand).subcommand("run")::exec - val log get() = Command(baseCommand).subcommand("log")::exec val topic get() = Command(baseCommand).subcommand("topic")::exec val routeCmd get() = Command(baseCommand).subcommand("route")::execAsync class Workflow(private val parent: Command) { @@ -292,43 +291,6 @@ class DevChatWrapper( JSONArray() } - val logTopic: (String, Int?) -> JSONArray get() = {topic: String, maxCount: Int? -> - val num: Int = maxCount ?: DEFAULT_LOG_MAX_COUNT - try { - JSON.parseArray(log(mutableListOf( - "topic" to topic, - "max-count" to num.toString() - ))) - } catch (e: Exception) { - Log.warn("Error log topic: $e") - JSONArray() - } - } - val logInsert: (String) -> Unit get() = { item: String -> - try { - var str = item - if (OSInfo.isWindows) { - val escaped = item.replace("\\", "\\\\").replace("\"", "\\\"") - str = "\"$escaped\"" - } - log(listOf("insert" to str)) - } catch (e: Exception) { - Log.warn("Error insert log: $e") - } - } - - val logLast: () -> JSONObject? get() = { - try { - log(mutableListOf( - "max-count" to "1" - )).let { - JSON.parseArray(it).getJSONObject(0) - } - } catch (e: Exception) { - Log.warn("Error log topic: $e") - null - } - } companion object { var activeChannel: SendChannel? = null } diff --git a/src/main/kotlin/ai/devchat/core/handlers/DeleteLastConversationRequestHandler.kt b/src/main/kotlin/ai/devchat/core/handlers/DeleteLastConversationRequestHandler.kt index 327c577..b8756fe 100644 --- a/src/main/kotlin/ai/devchat/core/handlers/DeleteLastConversationRequestHandler.kt +++ b/src/main/kotlin/ai/devchat/core/handlers/DeleteLastConversationRequestHandler.kt @@ -1,6 +1,7 @@ package ai.devchat.core.handlers import ai.devchat.core.BaseActionHandler +import ai.devchat.core.DC_CLIENT import ai.devchat.core.DevChatActions import com.alibaba.fastjson.JSONObject @@ -12,7 +13,7 @@ class DeleteLastConversationRequestHandler(requestAction: String, metadata: JSON override val actionName: String = DevChatActions.DELETE_LAST_CONVERSATION_RESPONSE override fun action() { val promptHash = payload!!.getString("promptHash") - wrapper.log(mutableListOf("delete" to promptHash)) + DC_CLIENT.deleteLog(promptHash) send(payload = mapOf("promptHash" to promptHash)) } } From dda654eda76333ecd587555c80be19c250bb610b Mon Sep 17 00:00:00 2001 From: Luo Tim Date: Sat, 13 Jul 2024 04:17:27 +0800 Subject: [PATCH 09/13] Refactor commandlist and clean up workflow-related cli commands --- .../kotlin/ai/devchat/core/DevChatClient.kt | 7 +++++- .../kotlin/ai/devchat/core/DevChatWrapper.kt | 22 ------------------- .../handlers/ListCommandsRequestHandler.kt | 19 +++++++++++----- .../devchat/installer/DevChatSetupThread.kt | 4 ++-- 4 files changed, 22 insertions(+), 30 deletions(-) diff --git a/src/main/kotlin/ai/devchat/core/DevChatClient.kt b/src/main/kotlin/ai/devchat/core/DevChatClient.kt index 3b9cfda..2ed3467 100644 --- a/src/main/kotlin/ai/devchat/core/DevChatClient.kt +++ b/src/main/kotlin/ai/devchat/core/DevChatClient.kt @@ -153,12 +153,17 @@ data class ShortLog( @SerialName("response_tokens") val responseTokens: Int ) +@Serializable +data class CommandConf( + val description: String, + val help: String? = null, +) @Serializable data class Workflow( val name: String, val namespace: String, val active: Boolean, - @SerialName("command_conf") val commandConf: Map + @SerialName("command_conf") val commandConf: CommandConf, ) @Serializable diff --git a/src/main/kotlin/ai/devchat/core/DevChatWrapper.kt b/src/main/kotlin/ai/devchat/core/DevChatWrapper.kt index 3d383eb..b78371b 100644 --- a/src/main/kotlin/ai/devchat/core/DevChatWrapper.kt +++ b/src/main/kotlin/ai/devchat/core/DevChatWrapper.kt @@ -240,16 +240,8 @@ class DevChatWrapper( return env } - val run get() = Command(baseCommand).subcommand("run")::exec val topic get() = Command(baseCommand).subcommand("topic")::exec val routeCmd get() = Command(baseCommand).subcommand("route")::execAsync - class Workflow(private val parent: Command) { - private val cmd = Command(parent).subcommand("workflow") - val update = Command(cmd).subcommand("update")::exec - val list = Command(cmd).subcommand("list")::exec - val config = Command(cmd).subcommand("config")::exec - } - val workflow get() = Workflow(baseCommand) fun route( flags: List>, @@ -276,20 +268,6 @@ class DevChatWrapper( Log.warn("Error list topics: $e") JSONArray() } - val commandList: JSONArray get() = try { - JSON.parseArray(workflow.list(mutableListOf("json" to null))) - } catch (e: Exception) { - Log.warn("Error list commands: $e") - JSONArray() - } - - val recommendedCommands: JSONArray get() = try { - val conf = JSON.parseObject(workflow.config(mutableListOf("json" to null))) - conf.getJSONObject("recommend").getJSONArray("workflows") - } catch (e: Exception) { - Log.warn("Error list commands: $e") - JSONArray() - } companion object { var activeChannel: SendChannel? = null diff --git a/src/main/kotlin/ai/devchat/core/handlers/ListCommandsRequestHandler.kt b/src/main/kotlin/ai/devchat/core/handlers/ListCommandsRequestHandler.kt index bd5c020..369a604 100644 --- a/src/main/kotlin/ai/devchat/core/handlers/ListCommandsRequestHandler.kt +++ b/src/main/kotlin/ai/devchat/core/handlers/ListCommandsRequestHandler.kt @@ -1,6 +1,7 @@ package ai.devchat.core.handlers import ai.devchat.core.BaseActionHandler +import ai.devchat.core.DC_CLIENT import ai.devchat.core.DevChatActions import com.alibaba.fastjson.JSONObject @@ -12,11 +13,19 @@ class ListCommandsRequestHandler(requestAction: String, metadata: JSONObject?, p ) { override val actionName: String = DevChatActions.LIST_COMMANDS_RESPONSE override fun action() { - val recommendedWorkflows = wrapper.recommendedCommands - val indexedCommands = wrapper.commandList.map { - val commandName = (it as JSONObject).getString("name") - it["recommend"] = recommendedWorkflows.indexOf(commandName) - it + val recommendedWorkflows = DC_CLIENT.getWorkflowConfig()?.recommend?.workflows.orEmpty() + val indexedCommands = DC_CLIENT.getWorkflowList()?.map { + val commandName = it.name + mapOf( + "name" to it.name, + "namespace" to it.namespace, + "active" to it.active, + "command_conf" to mapOf( + "description" to it.commandConf.description, + "help" to it.commandConf.help + ), + "recommend" to recommendedWorkflows.indexOf(commandName) + ) } send(payload = mapOf("commands" to indexedCommands)) } diff --git a/src/main/kotlin/ai/devchat/installer/DevChatSetupThread.kt b/src/main/kotlin/ai/devchat/installer/DevChatSetupThread.kt index 34de51c..3737532 100644 --- a/src/main/kotlin/ai/devchat/installer/DevChatSetupThread.kt +++ b/src/main/kotlin/ai/devchat/installer/DevChatSetupThread.kt @@ -4,7 +4,7 @@ import ai.devchat.common.Log import ai.devchat.common.Notifier import ai.devchat.common.OSInfo import ai.devchat.common.PathUtils -import ai.devchat.core.DevChatWrapper +import ai.devchat.core.DC_CLIENT import ai.devchat.plugin.browser import ai.devchat.storage.CONFIG import ai.devchat.storage.DevChatState @@ -69,7 +69,7 @@ class DevChatSetupThread : Thread() { } try { - DevChatWrapper().workflow.update(listOf()) + DC_CLIENT.updateWorkflows() } catch (e: Exception) { Log.warn("Failed to update workflows: $e") } From 567db946b73f0df61ff1e132c3a0fb94967042b9 Mon Sep 17 00:00:00 2001 From: Luo Tim Date: Sat, 13 Jul 2024 04:20:26 +0800 Subject: [PATCH 10/13] Update tools --- tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools b/tools index 0557f55..3e8c27a 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit 0557f55a8d5dadabcf225a2b7a24db9db120f895 +Subproject commit 3e8c27abd6ae42ef691d14991238b83ded28488a From a7cbb87939c6fbc5e61e60f94a6e1b59613dff1d Mon Sep 17 00:00:00 2001 From: Luo Tim Date: Tue, 16 Jul 2024 03:20:45 +0800 Subject: [PATCH 11/13] Refactor topic list and topic deletion --- .../kotlin/ai/devchat/core/DevChatClient.kt | 25 ++++++++++++-- .../kotlin/ai/devchat/core/DevChatWrapper.kt | 13 -------- .../handlers/DeleteTopicRequestHandler.kt | 7 ++-- .../core/handlers/ListTopicsRequestHandler.kt | 33 ++++++++++--------- .../kotlin/ai/devchat/storage/DevChatState.kt | 1 - 5 files changed, 41 insertions(+), 38 deletions(-) diff --git a/src/main/kotlin/ai/devchat/core/DevChatClient.kt b/src/main/kotlin/ai/devchat/core/DevChatClient.kt index 2ed3467..3db77bd 100644 --- a/src/main/kotlin/ai/devchat/core/DevChatClient.kt +++ b/src/main/kotlin/ai/devchat/core/DevChatClient.kt @@ -13,6 +13,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject import kotlinx.serialization.serializer import okhttp3.* import okhttp3.HttpUrl.Companion.toHttpUrlOrNull @@ -26,6 +27,13 @@ import kotlin.system.measureTimeMillis private const val DEFAULT_LOG_MAX_COUNT = 10000 + +inline fun T.asMap(): Map where T : @Serializable Any { + val json = Json { encodeDefaults = true } + val jsonString = json.encodeToString(serializer(),this) + return Json.decodeFromString(jsonString).toMap() +} + @Serializable data class ChatRequest( val content: String, @@ -153,6 +161,18 @@ data class ShortLog( @SerialName("response_tokens") val responseTokens: Int ) +@Serializable +data class Topic( + @SerialName("latest_time") val latestTime: Long, + val hidden: Boolean, + @SerialName("root_prompt_hash") val rootPromptHash: String, + @SerialName("root_prompt_user") val rootPromptUser: String, + @SerialName("root_prompt_date") val rootPromptDate: Long, + @SerialName("root_prompt_request") val rootPromptRequest: String, + @SerialName("root_prompt_response") val rootPromptResponse: String, + val title: String? +) + @Serializable data class CommandConf( val description: String, @@ -325,14 +345,13 @@ class DevChatClient(port: Int = 22222) { "workspace" to PathUtils.workspace, )) ?: emptyList() } - fun getTopics(offset: Int = 0, limit: Int = DEFAULT_LOG_MAX_COUNT): List { + fun getTopics(offset: Int = 0, limit: Int = DEFAULT_LOG_MAX_COUNT): List { val queryParams = mapOf( "limit" to limit, "offset" to offset, "workspace" to PathUtils.workspace, ) - val topics: List>? = get("/topics", queryParams) - return topics?.reversed() ?: emptyList() + return get?>("/topics", queryParams).orEmpty() } fun deleteTopic(topicRootHash: String) { diff --git a/src/main/kotlin/ai/devchat/core/DevChatWrapper.kt b/src/main/kotlin/ai/devchat/core/DevChatWrapper.kt index b78371b..b11d805 100644 --- a/src/main/kotlin/ai/devchat/core/DevChatWrapper.kt +++ b/src/main/kotlin/ai/devchat/core/DevChatWrapper.kt @@ -6,8 +6,6 @@ import ai.devchat.common.PathUtils import ai.devchat.plugin.currentProject import ai.devchat.plugin.ideServerPort import ai.devchat.storage.CONFIG -import com.alibaba.fastjson.JSON -import com.alibaba.fastjson.JSONArray import com.intellij.util.containers.addIfNotNull import kotlinx.coroutines.* import kotlinx.coroutines.channels.SendChannel @@ -16,8 +14,6 @@ import kotlinx.coroutines.selects.whileSelect import java.io.File import java.io.IOException -private const val DEFAULT_LOG_MAX_COUNT = 10000 - class CommandExecutionException(message:String): Exception(message) private suspend fun Process.await( @@ -240,7 +236,6 @@ class DevChatWrapper( return env } - val topic get() = Command(baseCommand).subcommand("topic")::exec val routeCmd get() = Command(baseCommand).subcommand("route")::execAsync fun route( @@ -261,14 +256,6 @@ class DevChatWrapper( activeChannel = routeCmd(flags + additionalFlags, callback, onError, onFinish) } - val topicList: JSONArray get() = try { - val r = topic(mutableListOf("list" to null)) - JSON.parseArray(r) - } catch (e: Exception) { - Log.warn("Error list topics: $e") - JSONArray() - } - companion object { var activeChannel: SendChannel? = null } diff --git a/src/main/kotlin/ai/devchat/core/handlers/DeleteTopicRequestHandler.kt b/src/main/kotlin/ai/devchat/core/handlers/DeleteTopicRequestHandler.kt index eb87b36..d13ba3d 100644 --- a/src/main/kotlin/ai/devchat/core/handlers/DeleteTopicRequestHandler.kt +++ b/src/main/kotlin/ai/devchat/core/handlers/DeleteTopicRequestHandler.kt @@ -1,8 +1,8 @@ package ai.devchat.core.handlers import ai.devchat.core.BaseActionHandler +import ai.devchat.core.DC_CLIENT import ai.devchat.core.DevChatActions -import ai.devchat.storage.DevChatState import com.alibaba.fastjson.JSONObject class DeleteTopicRequestHandler(requestAction: String, metadata: JSONObject?, payload: JSONObject?) : BaseActionHandler( @@ -13,10 +13,7 @@ class DeleteTopicRequestHandler(requestAction: String, metadata: JSONObject?, pa override val actionName: String = DevChatActions.DELETE_TOPIC_RESPONSE override fun action() { val topicHash = payload!!.getString("topicHash") - val state = DevChatState.instance - if (!state.deletedTopicHashes.contains(topicHash)) { - state.deletedTopicHashes += topicHash - } + DC_CLIENT.deleteTopic(topicHash) send(payload = mapOf("topicHash" to topicHash)) } } diff --git a/src/main/kotlin/ai/devchat/core/handlers/ListTopicsRequestHandler.kt b/src/main/kotlin/ai/devchat/core/handlers/ListTopicsRequestHandler.kt index 81d5752..d3396fd 100644 --- a/src/main/kotlin/ai/devchat/core/handlers/ListTopicsRequestHandler.kt +++ b/src/main/kotlin/ai/devchat/core/handlers/ListTopicsRequestHandler.kt @@ -1,9 +1,8 @@ package ai.devchat.core.handlers import ai.devchat.core.BaseActionHandler +import ai.devchat.core.DC_CLIENT import ai.devchat.core.DevChatActions -import ai.devchat.storage.DevChatState -import com.alibaba.fastjson.JSONArray import com.alibaba.fastjson.JSONObject @@ -14,20 +13,22 @@ class ListTopicsRequestHandler(requestAction: String, metadata: JSONObject?, pay ) { override val actionName: String = DevChatActions.LIST_TOPICS_RESPONSE override fun action() { - val topics = wrapper.topicList - val deletedTopicHashes = DevChatState.instance.deletedTopicHashes - // Filter out deleted topics - val filteredTopics = JSONArray() - topics.forEachIndexed {i, _ -> - val topic = topics.getJSONObject(i) - val rootPrompt = topic.getJSONObject("root_prompt") - if (rootPrompt.getString("hash") !in deletedTopicHashes) { - val req = rootPrompt.getString("request") - val res = rootPrompt.getJSONArray("responses").getString(0) - rootPrompt["title"] = "$req-$res" - filteredTopics.add(topic) - } + val topics = DC_CLIENT.getTopics().map { + val request = it.rootPromptRequest + val response = it.rootPromptResponse + mapOf( + "root_prompt" to mapOf( + "hash" to it.rootPromptHash, + "date" to it.rootPromptDate, + "user" to it.rootPromptUser, + "request" to request, + "response" to response, + "title" to "$request-$response", + ), + "latest_time" to it.latestTime, + "hidden" to it.hidden, + ) } - send(payload= mapOf("topics" to filteredTopics)) + send(payload= mapOf("topics" to topics)) } } \ No newline at end of file diff --git a/src/main/kotlin/ai/devchat/storage/DevChatState.kt b/src/main/kotlin/ai/devchat/storage/DevChatState.kt index c558621..6490680 100644 --- a/src/main/kotlin/ai/devchat/storage/DevChatState.kt +++ b/src/main/kotlin/ai/devchat/storage/DevChatState.kt @@ -23,7 +23,6 @@ enum class CompletionTriggerMode { @Service @State(name = "ai.devchat.DevChatState", storages = [Storage("DevChatState.xml")]) class DevChatState : PersistentStateComponent { - var deletedTopicHashes: List = ArrayList() var lastToolWindowState: String = ToolWindowState.SHOWN.name var lastVersion: String? = null From 10f401cc29ca6fadb9cca13b11ebd2cf2815c3ac Mon Sep 17 00:00:00 2001 From: Luo Tim Date: Wed, 17 Jul 2024 18:03:24 +0800 Subject: [PATCH 12/13] Local service management --- .../kotlin/ai/devchat/common/PathUtils.kt | 1 + .../kotlin/ai/devchat/core/DevChatClient.kt | 5 +- .../kotlin/ai/devchat/core/DevChatWrapper.kt | 14 +--- .../ai/devchat/plugin/DevChatToolWindow.kt | 13 +++- .../kotlin/ai/devchat/plugin/IDEServer.kt | 45 +++++------ .../kotlin/ai/devchat/plugin/LocalService.kt | 76 +++++++++++++++++++ 6 files changed, 108 insertions(+), 46 deletions(-) create mode 100644 src/main/kotlin/ai/devchat/plugin/LocalService.kt diff --git a/src/main/kotlin/ai/devchat/common/PathUtils.kt b/src/main/kotlin/ai/devchat/common/PathUtils.kt index 0744ce9..b244f1f 100644 --- a/src/main/kotlin/ai/devchat/common/PathUtils.kt +++ b/src/main/kotlin/ai/devchat/common/PathUtils.kt @@ -16,6 +16,7 @@ object PathUtils { val mambaWorkPath = Paths.get(workPath, "mamba").toString() val mambaBinPath = Paths.get(mambaWorkPath, "micromamba").toString() val toolsPath: String = Paths.get(workPath, "tools").toString() + val localServicePath: String = Paths.get(sitePackagePath, "devchat", "_service", "main.py").toString() val codeEditorBinary: String = "${when { OSInfo.OS_ARCH.contains("aarch") || OSInfo.OS_ARCH.contains("arm") -> "aarch64" else -> "x86_64" diff --git a/src/main/kotlin/ai/devchat/core/DevChatClient.kt b/src/main/kotlin/ai/devchat/core/DevChatClient.kt index 3db77bd..531b975 100644 --- a/src/main/kotlin/ai/devchat/core/DevChatClient.kt +++ b/src/main/kotlin/ai/devchat/core/DevChatClient.kt @@ -2,6 +2,7 @@ package ai.devchat.core import ai.devchat.common.Log import ai.devchat.common.PathUtils +import ai.devchat.plugin.localServicePort import kotlinx.coroutines.* import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -203,9 +204,9 @@ fun timeThis(block: suspend () -> Unit) { } } -class DevChatClient(port: Int = 22222) { +class DevChatClient() { private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) - private val baseURL = "http://localhost:$port" + private val baseURL get() = "http://localhost:$localServicePort" private var job: Job? = null companion object { const val LOG_RAW_DATA_SIZE_LIMIT = 4 * 1024 diff --git a/src/main/kotlin/ai/devchat/core/DevChatWrapper.kt b/src/main/kotlin/ai/devchat/core/DevChatWrapper.kt index b11d805..e388a38 100644 --- a/src/main/kotlin/ai/devchat/core/DevChatWrapper.kt +++ b/src/main/kotlin/ai/devchat/core/DevChatWrapper.kt @@ -6,6 +6,7 @@ import ai.devchat.common.PathUtils import ai.devchat.plugin.currentProject import ai.devchat.plugin.ideServerPort import ai.devchat.storage.CONFIG +import com.intellij.execution.process.OSProcessUtil.killProcessTree import com.intellij.util.containers.addIfNotNull import kotlinx.coroutines.* import kotlinx.coroutines.channels.SendChannel @@ -182,19 +183,6 @@ class Command(val cmd: MutableList = mutableListOf()) { } } -private fun killProcessTree(process: Process) { - val pid = process.pid() // Get the PID of the process - ProcessHandle.of(pid).ifPresent { handle -> - handle.descendants().forEach { descendant -> - descendant.destroy() // Attempt graceful shutdown - descendant.destroyForcibly() // Force shutdown if necessary - } - handle.destroy() - handle.destroyForcibly() - } -} - - class DevChatWrapper( ) { private val apiKey get() = CONFIG["providers.devchat.api_key"] as? String diff --git a/src/main/kotlin/ai/devchat/plugin/DevChatToolWindow.kt b/src/main/kotlin/ai/devchat/plugin/DevChatToolWindow.kt index 5b333c5..469d9bf 100644 --- a/src/main/kotlin/ai/devchat/plugin/DevChatToolWindow.kt +++ b/src/main/kotlin/ai/devchat/plugin/DevChatToolWindow.kt @@ -1,10 +1,11 @@ package ai.devchat.plugin -import ai.devchat.core.DevChatWrapper import ai.devchat.common.Log +import ai.devchat.core.DevChatWrapper import ai.devchat.installer.DevChatSetupThread import com.intellij.openapi.Disposable -import com.intellij.openapi.project.* +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowFactory @@ -17,6 +18,9 @@ import javax.swing.JPanel import javax.swing.SwingConstants class DevChatToolWindow : ToolWindowFactory, DumbAware, Disposable { + private var ideService: IDEServer? = null + private var localService: LocalService? = null + override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { currentProject = project val panel = JPanel(BorderLayout()) @@ -32,11 +36,14 @@ class DevChatToolWindow : ToolWindowFactory, DumbAware, Disposable { Disposer.register(content, this) toolWindow.contentManager.addContent(content) DevChatSetupThread().start() - IDEServer(project).start() + ideService = IDEServer(project).start() + localService = LocalService().start() } override fun dispose() { DevChatWrapper.activeChannel?.close() + ideService?.stop() + localService?.stop() } companion object { diff --git a/src/main/kotlin/ai/devchat/plugin/IDEServer.kt b/src/main/kotlin/ai/devchat/plugin/IDEServer.kt index e6596d0..45134b5 100644 --- a/src/main/kotlin/ai/devchat/plugin/IDEServer.kt +++ b/src/main/kotlin/ai/devchat/plugin/IDEServer.kt @@ -21,8 +21,6 @@ import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.progress.EmptyProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.project.Project -import com.intellij.openapi.project.ProjectManager -import com.intellij.openapi.project.ProjectManagerListener import com.intellij.openapi.util.Computable import com.intellij.openapi.util.TextRange import com.intellij.openapi.vfs.LocalFileSystem @@ -53,9 +51,6 @@ import java.util.concurrent.CountDownLatch import kotlin.reflect.full.memberFunctions -const val START_PORT: Int = 31800 - - @Serializable data class ReqLocation(val abspath: String, val line: Int, val character: Int) @Serializable @@ -81,9 +76,12 @@ data class Result( class IDEServer(private var project: Project) { private var server: ApplicationEngine? = null + private var isShutdownHookRegistered: Boolean = false - fun start() { - ideServerPort = getAvailablePort(START_PORT) + fun start(): IDEServer { + ServerSocket(0).use { + ideServerPort = it.localPort + } server = embeddedServer(Netty, port= ideServerPort!!) { install(CORS) { anyHost() @@ -270,19 +268,21 @@ class IDEServer(private var project: Project) { } } - // Register listener to stop the server when project closed - ProjectManager.getInstance().addProjectManagerListener( - project, object: ProjectManagerListener { - override fun projectClosed(project: Project) { - super.projectClosed(project) - Notifier.info("Stopping IDE server...") - server?.stop(1_000, 2_000) - } - } - ) + // Register shutdown hook + if (!isShutdownHookRegistered) { + Runtime.getRuntime().addShutdownHook(Thread { stop() }) + isShutdownHookRegistered = true + } server?.start(wait = false) Notifier.info("IDE server started at $ideServerPort.") + return this + } + + fun stop() { + Log.info("Stopping IDE server...") + Notifier.info("Stopping IDE server...") + server?.stop(1_000, 2_000) } } @@ -354,17 +354,6 @@ fun Editor.diffWith(newText: String, autoEdit: Boolean) { } } -fun getAvailablePort(startPort: Int): Int { - var port = startPort - while (true) { - try { - ServerSocket(port).use { return port } - } catch (ex: Exception) { - port++ - } - } -} - fun runInEdtAndGet(block: () -> T): T { val app = ApplicationManager.getApplication() if (app.isDispatchThread) { diff --git a/src/main/kotlin/ai/devchat/plugin/LocalService.kt b/src/main/kotlin/ai/devchat/plugin/LocalService.kt new file mode 100644 index 0000000..e3a9308 --- /dev/null +++ b/src/main/kotlin/ai/devchat/plugin/LocalService.kt @@ -0,0 +1,76 @@ +package ai.devchat.plugin + +import ai.devchat.common.Log +import ai.devchat.common.Notifier +import ai.devchat.common.PathUtils +import ai.devchat.storage.CONFIG +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.process.OSProcessHandler +import com.intellij.execution.process.OSProcessUtil.killProcessTree +import com.intellij.execution.process.ProcessAdapter +import com.intellij.execution.process.ProcessEvent +import com.intellij.openapi.util.Key +import com.intellij.util.io.BaseOutputReader +import java.net.ServerSocket + + +class LocalService { + private var processHandler: OSProcessHandler? = null + private var isShutdownHookRegistered = false + + fun start(): LocalService { + ServerSocket(0).use { + localServicePort = it.localPort + } + val commandLine = GeneralCommandLine() + .withExePath(CONFIG["python_for_chat"] as String) + .withParameters(PathUtils.localServicePath) + .withWorkDirectory(PathUtils.workspace) + .withEnvironment("PYTHONPATH", PathUtils.pythonPath) + .withEnvironment("DC_SVC_PORT", localServicePort.toString()) + .withEnvironment("DC_SVC_WORKSPACE", PathUtils.workspace ?: "") + processHandler = object: OSProcessHandler(commandLine) { + override fun readerOptions(): BaseOutputReader.Options { + return BaseOutputReader.Options.forMostlySilentProcess() + } + } + + processHandler?.addProcessListener(object : ProcessAdapter() { + override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) { + Log.info("[LocalService] ${event.text}") + } + + override fun processTerminated(event: ProcessEvent) { + Log.info("Local service terminated with exit code: ${event.exitCode}") + } + }) + + // Register shutdown hook + if (!isShutdownHookRegistered) { + Runtime.getRuntime().addShutdownHook(Thread { stop() }) + isShutdownHookRegistered = true + } + + processHandler?.startNotify() + Log.info("Local service started on port: $localServicePort") + Notifier.info("Local service started at $localServicePort.") + return this + } + + fun stop() { + processHandler?.let { handler -> + Log.info("Stopping local service...") + killProcessTree(handler.process) + if (handler.waitFor(5000)) { + Log.info("Local service stopped successfully") + } else { + Log.warn("Failed to stop local service, retrying...") + killProcessTree(handler.process) + handler.waitFor(3000) + } + processHandler = null + } ?: Log.info("Local service is not running") + } +} + +var localServicePort: Int? = null From fca1418139795cf1b2f18ed31bec7161f4a4ade3 Mon Sep 17 00:00:00 2001 From: Luo Tim Date: Wed, 17 Jul 2024 18:03:50 +0800 Subject: [PATCH 13/13] Update tools --- tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools b/tools index 3e8c27a..e097b07 160000 --- a/tools +++ b/tools @@ -1 +1 @@ -Subproject commit 3e8c27abd6ae42ef691d14991238b83ded28488a +Subproject commit e097b079ed67e3dcd2e7684a4637a9b95d48b15b