Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class CodingAgent(
private val toolOrchestrator = ToolOrchestrator(toolRegistry, policyEngine, renderer, mcpConfigService = mcpToolConfigService)

private val errorRecoveryAgent = ErrorRecoveryAgent(projectPath, llmService)
private val analysisAgent = AnalysisAgent(llmService, contentThreshold = 5000)
private val analysisAgent = AnalysisAgent(llmService, contentThreshold = 15000)
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The contentThreshold for AnalysisAgent has been increased from 5000 to 15000 characters without documentation. This is a significant change (3x increase) that affects when content analysis is triggered. Consider:

  1. Adding a comment explaining the rationale for this threshold value
  2. Documenting whether this was tested with real-world content sizes
  3. Considering making this threshold configurable rather than hardcoded

The change may be related to "Enhanced content analysis capacity" mentioned in the PR description, but the reasoning should be clear in the code.

Copilot uses AI. Check for mistakes.
private val mcpToolsInitializer = McpToolsInitializer()

// 执行器
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package cc.unitmesh.devins.compiler.service

import cc.unitmesh.devins.compiler.DevInsCompiler
import cc.unitmesh.devins.compiler.context.CompilerContext
import cc.unitmesh.devins.compiler.result.DevInsCompiledResult
import cc.unitmesh.devins.compiler.variable.VariableScope
import cc.unitmesh.devins.compiler.variable.VariableType
import cc.unitmesh.devins.filesystem.ProjectFileSystem

/**
* 默认的 DevIns 编译器服务实现
*
* 使用 mpp-core 的 DevInsCompiler,基于自定义 AST 解析器。
* 适用于 CLI、Desktop、WASM 等跨平台环境。
*
* 特点:
* - 跨平台支持(JS, WASM, Desktop JVM, Android, iOS)
* - 基于自定义 DevInsParser 解析
* - 命令输出为占位符格式(如 {{FILE_CONTENT:path}})
* - 不支持 IDE 特定功能(Symbol 解析、重构等)
*/
class DefaultDevInsCompilerService : DevInsCompilerService {

override suspend fun compile(source: String, fileSystem: ProjectFileSystem): DevInsCompiledResult {
val context = CompilerContext().apply {
this.fileSystem = fileSystem
}
val compiler = DevInsCompiler(context)
return compiler.compileFromSource(source)
}

override suspend fun compile(
source: String,
fileSystem: ProjectFileSystem,
variables: Map<String, Any>
): DevInsCompiledResult {
val context = CompilerContext().apply {
this.fileSystem = fileSystem
}

// 添加自定义变量
variables.forEach { (name, value) ->
context.variableTable.addVariable(
name = name,
varType = inferVariableType(value),
value = value,
scope = VariableScope.USER_DEFINED
)
}

val compiler = DevInsCompiler(context)
return compiler.compileFromSource(source)
}

override fun supportsIdeFeatures(): Boolean = false

override fun getName(): String = "DefaultDevInsCompilerService (mpp-core)"

private fun inferVariableType(value: Any): VariableType {
return when (value) {
is String -> VariableType.STRING
is Int, is Long, is Double, is Float -> VariableType.NUMBER
is Boolean -> VariableType.BOOLEAN
is List<*> -> VariableType.ARRAY
is Map<*, *> -> VariableType.OBJECT
else -> VariableType.UNKNOWN
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package cc.unitmesh.devins.compiler.service

import cc.unitmesh.devins.compiler.result.DevInsCompiledResult
import cc.unitmesh.devins.filesystem.ProjectFileSystem
import kotlin.concurrent.Volatile

/**
* DevIns 编译器服务接口
*
* 提供可切换的编译器核心,支持:
* - mpp-core 默认实现(跨平台,基于自定义 AST)
* - IDEA 专用实现(基于 PSI,支持 IDE 功能如 Symbol 解析、重构等)
*
* 使用方式:
* ```kotlin
* // 在 mpp-idea 中使用 IDEA 编译器
* val compilerService = IdeaDevInsCompilerService(project)
* val llmService = KoogLLMService(config, compilerService = compilerService)
*
* // 在 CLI/Desktop 中使用默认编译器
* val llmService = KoogLLMService(config)
* // compilerService 默认为 DefaultDevInsCompilerService
* ```
*/
interface DevInsCompilerService {

/**
* 编译 DevIns 源代码
*
* @param source DevIns 源代码字符串
* @param fileSystem 项目文件系统,用于解析文件路径
* @return 编译结果
*/
suspend fun compile(source: String, fileSystem: ProjectFileSystem): DevInsCompiledResult

/**
* 编译 DevIns 源代码,带有自定义变量
*
* @param source DevIns 源代码字符串
* @param fileSystem 项目文件系统
* @param variables 自定义变量映射
* @return 编译结果
*/
suspend fun compile(
source: String,
fileSystem: ProjectFileSystem,
variables: Map<String, Any>
): DevInsCompiledResult

/**
* 检查编译器是否支持 IDE 功能
*
* IDE 功能包括:
* - Symbol 解析 (/symbol 命令)
* - 代码重构 (/refactor 命令)
* - 数据库操作 (/database 命令)
* - 代码结构分析 (/structure 命令)
* - 符号使用查找 (/usage 命令)
*
* @return true 如果支持 IDE 功能
*/
fun supportsIdeFeatures(): Boolean = false

/**
* 获取编译器名称,用于日志和调试
*/
fun getName(): String

companion object {
/**
* 全局编译器服务实例
* 可以在应用启动时设置为 IDEA 专用实现
*/
@Volatile
private var instance: DevInsCompilerService? = null
Comment on lines +74 to +75
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @Volatile annotation is used on a JVM/Kotlin Multiplatform shared variable, but in Kotlin Multiplatform, @Volatile is only available on JVM targets. For proper thread-safety across all platforms (including JS, WASM, Native), consider using AtomicReference from kotlinx.atomicfu or document that this singleton pattern is only thread-safe on JVM platforms.

Copilot uses AI. Check for mistakes.

/**
* 获取当前编译器服务实例
* 如果未设置,返回默认实现
*/
fun getInstance(): DevInsCompilerService {
return instance ?: DefaultDevInsCompilerService()
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getInstance() creates a new DefaultDevInsCompilerService instance every time it's called when instance is null. This could lead to multiple instances being created in concurrent scenarios. Consider using double-checked locking or lazy initialization to ensure a singleton pattern, or document that creating multiple default instances is acceptable.

Suggested change
return instance ?: DefaultDevInsCompilerService()
if (instance == null) {
synchronized(this) {
if (instance == null) {
instance = DefaultDevInsCompilerService()
}
}
}
return instance!!

Copilot uses AI. Check for mistakes.
}

/**
* 设置全局编译器服务实例
* 应在应用启动时调用
*/
fun setInstance(service: DevInsCompilerService) {
instance = service
}

/**
* 重置为默认实现
*/
fun reset() {
instance = null
}
}
}

36 changes: 22 additions & 14 deletions mpp-core/src/commonMain/kotlin/cc/unitmesh/llm/KoogLLMService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import ai.koog.prompt.llm.LLModel
import ai.koog.prompt.params.LLMParams
import ai.koog.prompt.streaming.StreamFrame
import cc.unitmesh.agent.logging.getLogger
import cc.unitmesh.devins.compiler.DevInsCompilerFacade
import cc.unitmesh.devins.compiler.context.CompilerContext
import cc.unitmesh.devins.compiler.service.DevInsCompilerService
import cc.unitmesh.devins.filesystem.EmptyFileSystem
import cc.unitmesh.devins.filesystem.ProjectFileSystem
import cc.unitmesh.devins.llm.Message
Expand All @@ -17,29 +16,40 @@ import cc.unitmesh.llm.compression.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onCompletion
import kotlinx.serialization.json.Json
import kotlinx.datetime.Clock

/**
* LLM 服务
*
* @param config 模型配置
* @param compressionConfig 压缩配置
* @param compilerService 可选的编译器服务,用于编译 DevIns 命令
* 如果不提供,将使用 DevInsCompilerService.getInstance()
*/
class KoogLLMService(
private val config: ModelConfig,
private val compressionConfig: CompressionConfig = CompressionConfig()
private val compressionConfig: CompressionConfig = CompressionConfig(),
private val compilerService: DevInsCompilerService? = null
) {
private val logger = getLogger("KoogLLMService")

private val executor: SingleLLMPromptExecutor by lazy {
ExecutorFactory.create(config)
}

private val model: LLModel by lazy {
ModelRegistry.createModel(config.provider, config.modelName)
?: ModelRegistry.createGenericModel(config.provider, config.modelName)
}

private val compressionService: ChatCompressionService by lazy {
ChatCompressionService(executor, model, compressionConfig)
}


// 获取实际使用的编译器服务
private val actualCompilerService: DevInsCompilerService
get() = compilerService ?: DevInsCompilerService.getInstance()

// Token 追踪
private var lastTokenInfo: TokenInfo = TokenInfo()
private var messagesSinceLastCompression = 0
Expand Down Expand Up @@ -125,16 +135,14 @@ class KoogLLMService(
}

private suspend fun compilePrompt(userPrompt: String, fileSystem: ProjectFileSystem): String {
val context = CompilerContext().apply {
this.fileSystem = fileSystem
}

val compiledResult = DevInsCompilerFacade.compile(userPrompt, context)
val compiledResult = actualCompilerService.compile(userPrompt, fileSystem)

if (compiledResult.hasError) {
logger.warn { "⚠️ [KoogLLMService] 编译错误: ${compiledResult.errorMessage}" }
logger.warn { "⚠️ [KoogLLMService] 编译错误 (${actualCompilerService.getName()}): ${compiledResult.errorMessage}" }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using the IDEA compiler, errorMessage is always null (converter sets it null), so this log prints "null" even when hasError=true. Consider logging hasError or mapping an error description to avoid "null" in logs.

🤖 Was this useful? React with 👍 or 👎

}

logger.debug { "📝 [KoogLLMService] 使用编译器: ${actualCompilerService.getName()}, IDE功能: ${actualCompilerService.supportsIdeFeatures()}" }

return compiledResult.output
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package cc.unitmesh.devins.compiler.service

import cc.unitmesh.devins.filesystem.EmptyFileSystem
import kotlinx.coroutines.test.runTest
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

class DevInsCompilerServiceTest {

@AfterTest
fun tearDown() {
DevInsCompilerService.reset()
}

@Test
fun `getInstance returns DefaultDevInsCompilerService when not set`() {
val service = DevInsCompilerService.getInstance()
assertNotNull(service)
assertEquals("DefaultDevInsCompilerService (mpp-core)", service.getName())
assertFalse(service.supportsIdeFeatures())
}

@Test
fun `setInstance allows custom implementation`() {
val customService = object : DevInsCompilerService {
override suspend fun compile(source: String, fileSystem: cc.unitmesh.devins.filesystem.ProjectFileSystem) =
cc.unitmesh.devins.compiler.result.DevInsCompiledResult(output = "custom: $source")

override suspend fun compile(
source: String,
fileSystem: cc.unitmesh.devins.filesystem.ProjectFileSystem,
variables: Map<String, Any>
) = compile(source, fileSystem)

override fun supportsIdeFeatures() = true
override fun getName() = "CustomCompilerService"
}

DevInsCompilerService.setInstance(customService)

val service = DevInsCompilerService.getInstance()
assertEquals("CustomCompilerService", service.getName())
assertTrue(service.supportsIdeFeatures())
}

@Test
fun `reset restores default implementation`() {
val customService = object : DevInsCompilerService {
override suspend fun compile(source: String, fileSystem: cc.unitmesh.devins.filesystem.ProjectFileSystem) =
cc.unitmesh.devins.compiler.result.DevInsCompiledResult(output = "custom")

override suspend fun compile(
source: String,
fileSystem: cc.unitmesh.devins.filesystem.ProjectFileSystem,
variables: Map<String, Any>
) = compile(source, fileSystem)

override fun supportsIdeFeatures() = true
override fun getName() = "CustomCompilerService"
}

DevInsCompilerService.setInstance(customService)
assertEquals("CustomCompilerService", DevInsCompilerService.getInstance().getName())

DevInsCompilerService.reset()
assertEquals("DefaultDevInsCompilerService (mpp-core)", DevInsCompilerService.getInstance().getName())
}
}

class DefaultDevInsCompilerServiceTest {

@Test
fun `compile returns output for simple text`() = runTest {
val service = DefaultDevInsCompilerService()
val result = service.compile("Hello World", EmptyFileSystem())

assertEquals("Hello World", result.output)
assertFalse(result.hasError)
}

@Test
fun `compile handles DevIns commands`() = runTest {
val service = DefaultDevInsCompilerService()
// /file command should produce placeholder in mpp-core implementation
val result = service.compile("/file:test.kt", EmptyFileSystem())

// The mpp-core compiler outputs placeholders for commands
assertNotNull(result.output)
}

@Test
fun `supportsIdeFeatures returns false`() {
val service = DefaultDevInsCompilerService()
assertFalse(service.supportsIdeFeatures())
}

@Test
fun `getName returns correct name`() {
val service = DefaultDevInsCompilerService()
assertEquals("DefaultDevInsCompilerService (mpp-core)", service.getName())
}

@Test
fun `compile with variables works`() = runTest {
val service = DefaultDevInsCompilerService()
val variables = mapOf(
"name" to "test",
"count" to 42,
"enabled" to true
)

val result = service.compile("Hello \$name", EmptyFileSystem(), variables)
assertNotNull(result.output)
}
}

Loading
Loading