Skip to content

Commit ba0a83e

Browse files
committed
feat(agent): add local mode support and logging for LLM responses #379
1 parent 551d1cd commit ba0a83e

25 files changed

+1220
-16
lines changed

core/src/main/resources/messages/AutoDevBundle_en.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,3 +267,6 @@ mcp.editor.preview.title=Preview
267267
mcp.editor.preview.tooltip=Preview panel
268268
mcp.editor.refresh.title=Refresh
269269
shire.toolchain.function.not.found=Toolchain Not Found
270+
shire.run.local.mode=Run Local Model
271+
devins.llm.notfound=LLM not found
272+
devins.llm.done=Done

core/src/main/resources/messages/AutoDevBundle_zh.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,3 +257,6 @@ mcp.editor.preview.title=预览
257257
mcp.editor.preview.tooltip=预览面板
258258
mcp.editor.refresh.title=刷新
259259
shire.toolchain.function.not.found=Toolchain Not Found
260+
shire.run.local.mode=Run Local Model
261+
devins.llm.notfound=LLM not found
262+
devins.llm.done=Done
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package cc.unitmesh.devti.language
2+
3+
import com.intellij.openapi.project.Project
4+
import com.intellij.openapi.project.guessProjectDir
5+
import com.intellij.openapi.vfs.VirtualFile
6+
7+
public const val SHIRE_CHAT_BOX_FILE = "shire-chatbox.default.shire"
8+
public const val SHIRE_TEMP_OUTPUT = ".shire-output"
9+
public const val LLM_LOGGING_JSONL = "logging.jsonl"
10+
public const val LLM_LOGGING = "logging.log"
11+
public const val SHIRE_MKT_HOST = "https://shire.run/packages.json"
12+
13+
object ShireConstants {
14+
fun outputDir(project: Project): VirtualFile? {
15+
val baseDir = project.guessProjectDir() ?: throw IllegalStateException("Project directory not found")
16+
val virtualFile = baseDir.findFileByRelativePath(SHIRE_TEMP_OUTPUT)
17+
if (virtualFile == null) {
18+
baseDir.createChildDirectory(this, SHIRE_TEMP_OUTPUT)
19+
}
20+
21+
return virtualFile
22+
}
23+
}
24+

exts/devins-lang/src/main/kotlin/cc/unitmesh/devti/language/ast/HobbitHole.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import com.intellij.openapi.application.runReadAction
1919
import com.intellij.openapi.diagnostic.logger
2020
import com.intellij.openapi.editor.Editor
2121
import com.intellij.openapi.project.Project
22-
import com.phodal.shirelang.compiler.ast.action.PatternAction
22+
import cc.unitmesh.devti.language.ast.action.PatternAction
2323
import kotlinx.coroutines.CoroutineScope
2424
import kotlinx.coroutines.launch
2525

exts/devins-lang/src/main/kotlin/cc/unitmesh/devti/language/ast/action/PatternAction.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
package com.phodal.shirelang.compiler.ast.action
1+
package cc.unitmesh.devti.language.ast.action
22

33
import cc.unitmesh.devti.language.ast.FrontMatterType
4-
import cc.unitmesh.devti.language.ast.action.DirectAction
5-
import cc.unitmesh.devti.language.ast.action.RuleBasedPatternAction
64
import com.intellij.openapi.diagnostic.logger
75
import cc.unitmesh.devti.language.ast.ShirePsiQueryStatement
8-
import cc.unitmesh.devti.language.ast.action.PatternActionFunc
96

107
/**
118
* PatternFun is a sealed class in Kotlin representing different pattern processing functions.
@@ -14,7 +11,7 @@ import cc.unitmesh.devti.language.ast.action.PatternActionFunc
1411
*
1512
* @property funcName The name of the pattern processing function.
1613
*/
17-
data class PatternAction(
14+
class PatternAction(
1815
val pattern: String,
1916
val patternFuncs: List<PatternActionFunc>,
2017
val isQueryStatement: Boolean = false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package cc.unitmesh.devti.language.compiler.streaming
2+
3+
import cc.unitmesh.devti.gui.chat.message.ChatRole
4+
import cc.unitmesh.devti.language.LLM_LOGGING
5+
import cc.unitmesh.devti.language.LLM_LOGGING_JSONL
6+
import cc.unitmesh.devti.language.ShireConstants
7+
import cc.unitmesh.devti.language.console.DevInConsoleViewBase
8+
import cc.unitmesh.devti.llms.custom.ChatMessage
9+
import com.intellij.openapi.application.ApplicationManager
10+
import com.intellij.openapi.application.WriteAction
11+
import com.intellij.openapi.application.runInEdt
12+
import com.intellij.openapi.application.runWriteAction
13+
import com.intellij.openapi.project.Project
14+
import com.intellij.openapi.vfs.VirtualFile
15+
import com.intellij.testFramework.LightVirtualFile
16+
import kotlinx.serialization.encodeToString
17+
import kotlinx.serialization.json.Json
18+
import java.io.File
19+
20+
/**
21+
* The `LoggingStreamingService` class is an implementation of the `StreamingServiceProvider` interface.
22+
* It provides functionality to log streaming data to a file within a project's directory.
23+
*
24+
* ### Properties:
25+
* - `name`: A string that represents the name of the streaming service, initialized to "logging".
26+
* - `result`: A private string that accumulates the streaming data received.
27+
*/
28+
class LoggingStreamingService : StreamingServiceProvider {
29+
private var outputDir: VirtualFile = LightVirtualFile()
30+
override var name: String = "logging"
31+
32+
private var result: String = ""
33+
private var userPrompt: String = ""
34+
35+
override fun onBeforeStreaming(project: Project, userPrompt: String, console: DevInConsoleViewBase?) {
36+
this.userPrompt = userPrompt
37+
this.outputDir = ShireConstants.outputDir(project) ?: throw IllegalStateException("Project directory not found")
38+
if (outputDir.findChild(LLM_LOGGING) == null) {
39+
ApplicationManager.getApplication().invokeAndWait {
40+
WriteAction.compute<VirtualFile, Throwable> {
41+
outputDir.createChildData(this, LLM_LOGGING)
42+
}
43+
}
44+
} else {
45+
runInEdt {
46+
val file = outputDir.findChild(LLM_LOGGING)
47+
runWriteAction {
48+
file?.setBinaryContent(ByteArray(0))
49+
}
50+
}
51+
}
52+
53+
if (outputDir.findChild(LLM_LOGGING_JSONL) == null) {
54+
ApplicationManager.getApplication().invokeAndWait {
55+
WriteAction.compute<VirtualFile, Throwable> {
56+
outputDir.createChildData(this, LLM_LOGGING_JSONL)
57+
}
58+
}
59+
}
60+
}
61+
62+
override fun onStreaming(project: Project, flow: String, args: List<Any>) {
63+
result += flow
64+
65+
val virtualFile = outputDir.findChild(LLM_LOGGING)
66+
val file = virtualFile?.path?.let { File(it) }
67+
file?.appendText(flow)
68+
}
69+
70+
override fun afterStreamingDone(project: Project) {
71+
ApplicationManager.getApplication().invokeAndWait {
72+
WriteAction.compute<VirtualFile, Throwable> {
73+
val virtualFile = outputDir.createChildData(this, LLM_LOGGING_JSONL)
74+
val file = File(virtualFile.path)
75+
val value: List<ChatMessage> = listOf(
76+
ChatMessage(ChatRole.User.name, userPrompt),
77+
ChatMessage(ChatRole.System.name, result)
78+
)
79+
80+
val result = Json.encodeToString<List<ChatMessage>>(value)
81+
82+
file.appendText(result)
83+
file.appendText("\n")
84+
virtualFile
85+
}
86+
}
87+
}
88+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package cc.unitmesh.devti.language.compiler.streaming
2+
3+
import cc.unitmesh.devti.AutoDevNotifications
4+
import cc.unitmesh.devti.language.console.DevInConsoleViewBase
5+
import cc.unitmesh.devti.language.middleware.post.LifecycleProcessorSignature
6+
import com.intellij.openapi.components.Service
7+
import com.intellij.openapi.project.Project
8+
9+
10+
/**
11+
* The OnStreamingService class is responsible for managing all the [StreamingServiceProvider] instances related to streaming services.
12+
* It offers methods for registering, clearing, and initiating streaming services within the application.
13+
*
14+
* This class is annotated with the @Service annotation at the project level, indicating its role in the service management infrastructure.
15+
*
16+
* The class maintains a mutable map to associate [LifecycleProcessorSignature] objects with corresponding [StreamingServiceProvider] instances.
17+
* It also holds an optional reference to a console view object that can be used for outputting information to the user.
18+
*/
19+
@Service(Service.Level.PROJECT)
20+
class OnStreamingService {
21+
val map = mutableMapOf<LifecycleProcessorSignature, StreamingServiceProvider>()
22+
var console: DevInConsoleViewBase? = null
23+
24+
fun registerStreamingService(sign: LifecycleProcessorSignature, console: DevInConsoleViewBase?) {
25+
this.console = console
26+
val streamingService = StreamingServiceProvider.Companion.getStreamingService(sign.funcName)
27+
if (streamingService != null) {
28+
map[sign] = streamingService
29+
streamingService.onCreated(console)
30+
}
31+
}
32+
33+
fun clearStreamingService() {
34+
map.clear()
35+
}
36+
37+
fun all(): List<StreamingServiceProvider> {
38+
return StreamingServiceProvider.Companion.all()
39+
}
40+
41+
fun onStart(project: Project, userPrompt: String) {
42+
map.forEach { (_, service) ->
43+
try {
44+
service.onBeforeStreaming(project, userPrompt, console)
45+
} catch (e: Exception) {
46+
AutoDevNotifications.error(project, "Error on start streaming service: ${e.message}")
47+
}
48+
}
49+
}
50+
51+
fun onStreaming(project: Project, chunk: String) {
52+
map.forEach { (sign, service) ->
53+
try {
54+
service.
55+
onStreaming(project, chunk, sign.args)
56+
} catch (e: Exception) {
57+
AutoDevNotifications.error(project, "Error on streaming service: ${e.message}")
58+
}
59+
}
60+
}
61+
62+
fun onDone(project: Project) {
63+
map.forEach { (_, service) ->
64+
try {
65+
service.afterStreamingDone(project)
66+
} catch (e: Exception) {
67+
AutoDevNotifications.error(project, "Error on done streaming service: ${e.message}")
68+
}
69+
}
70+
}
71+
72+
fun onStreamingError() {
73+
// todo
74+
}
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package cc.unitmesh.devti.language.compiler.streaming
2+
3+
import cc.unitmesh.devti.AutoDevNotifications
4+
import cc.unitmesh.devti.language.console.DevInConsoleViewBase
5+
import com.intellij.execution.ui.ConsoleViewContentType
6+
import com.intellij.openapi.project.Project
7+
import com.intellij.util.io.IOUtil
8+
9+
10+
/**
11+
* The ProfilingStreamingService class is a concrete implementation of the StreamingServiceProvider interface.
12+
* It provides profiling capabilities during the streaming process, outputting memory usage information to the console.
13+
*/
14+
class ProfilingStreamingService : StreamingServiceProvider {
15+
override var name: String = "profiling"
16+
private var console: DevInConsoleViewBase? = null
17+
18+
override fun onBeforeStreaming(project: Project, userPrompt: String, console: DevInConsoleViewBase?) {
19+
this.console = console
20+
console?.print("Start profiling: ${getMemory()}", ConsoleViewContentType.SYSTEM_OUTPUT)
21+
22+
}
23+
24+
override fun afterStreamingDone(project: Project) {
25+
console?.print("End profiling: ${getMemory()}", ConsoleViewContentType.SYSTEM_OUTPUT)
26+
AutoDevNotifications.warn(project, "Memory: ${getMemory()}MB")
27+
}
28+
29+
private fun getMemory(): Long {
30+
val runtime = Runtime.getRuntime()
31+
val allocatedMem = runtime.totalMemory()
32+
val usedMem = allocatedMem - runtime.freeMemory()
33+
return toMb(usedMem)
34+
}
35+
36+
private fun toMb(value: Long): Long {
37+
return value / IOUtil.MiB
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package cc.unitmesh.devti.language.compiler.streaming
2+
3+
import cc.unitmesh.devti.language.console.DevInConsoleViewBase
4+
import com.intellij.openapi.Disposable
5+
import com.intellij.openapi.extensions.ExtensionPointName
6+
import com.intellij.openapi.project.Project
7+
8+
/**
9+
* all - Returns a list of all registered StreamingServiceProvider implementations.
10+
*
11+
* @return A list of StreamingServiceProvider instances.
12+
*/
13+
interface StreamingServiceProvider : Disposable {
14+
var name: String
15+
16+
/**
17+
* When create the service, you can do some initialization here, like start timer, etc.
18+
*/
19+
fun onCreated(console: DevInConsoleViewBase?) {
20+
/// do nothing
21+
}
22+
23+
/**
24+
* For the start of the LLM streaming, you can do some initialization here, for example, you can create a file to log the data
25+
*/
26+
fun onBeforeStreaming(project: Project, userPrompt: String, console: DevInConsoleViewBase?) {
27+
/// do nothing
28+
}
29+
30+
/**
31+
* For the streaming data, you can do some processing here, for example, you can log the data to a file
32+
*/
33+
fun onStreaming(project: Project, flow: String, args: List<Any>) {
34+
/// do nothing
35+
}
36+
37+
/**
38+
* For the end of the streaming, for example, you can do some cleanup here, or show some notification
39+
*/
40+
fun afterStreamingDone(project: Project) {
41+
/// do nothing
42+
}
43+
44+
override fun dispose() {
45+
/// do nothing
46+
}
47+
48+
companion object {
49+
val EP_NAME =
50+
ExtensionPointName.create<StreamingServiceProvider>("com.phodal.shireStreamingService")
51+
52+
fun getStreamingService(name: String): StreamingServiceProvider? {
53+
return EP_NAME.extensions.firstOrNull { it.name == name }
54+
}
55+
56+
fun all(): List<StreamingServiceProvider> {
57+
return EP_NAME.extensions.toList()
58+
}
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package cc.unitmesh.devti.language.compiler.streaming
2+
3+
import cc.unitmesh.devti.AutoDevNotifications
4+
import cc.unitmesh.devti.language.console.DevInConsoleViewBase
5+
import com.intellij.execution.ui.ConsoleViewContentType
6+
import com.intellij.openapi.project.Project
7+
8+
/**
9+
* Logging start time and end time for each lifecycle
10+
*/
11+
class TimingStreamingService : StreamingServiceProvider {
12+
override var name: String = "timing"
13+
14+
private var time: Long = 0
15+
private var console: DevInConsoleViewBase? = null
16+
17+
override fun onCreated(console: DevInConsoleViewBase?) {
18+
this.console = console
19+
val currentTime = System.currentTimeMillis()
20+
time = currentTime
21+
// new line
22+
console?.print("\n", ConsoleViewContentType.SYSTEM_OUTPUT)
23+
console?.print("Start timing: $currentTime \n", ConsoleViewContentType.SYSTEM_OUTPUT)
24+
}
25+
26+
override fun afterStreamingDone(project: Project) {
27+
val currentTime = System.currentTimeMillis()
28+
29+
console?.print("\n", ConsoleViewContentType.SYSTEM_OUTPUT)
30+
console?.print("End timing: $currentTime \n", ConsoleViewContentType.SYSTEM_OUTPUT)
31+
32+
AutoDevNotifications.warn(project, "Timing: ${currentTime - time}ms")
33+
}
34+
}

0 commit comments

Comments
 (0)