Skip to content

Commit 799f5d4

Browse files
authored
Merge pull request #1 from phodal/feat/idea-agent-toolwindow
feat(mpp-idea): add Agent ToolWindow with tab-based navigation
2 parents 0632b7f + 0c97915 commit 799f5d4

File tree

21 files changed

+1489
-356
lines changed

21 files changed

+1489
-356
lines changed

mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/subagent/DomainDictAgent.kt

Lines changed: 179 additions & 88 deletions
Large diffs are not rendered by default.

mpp-core/src/commonMain/kotlin/cc/unitmesh/llm/KoogLLMService.kt

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class KoogLLMService(
4646
private var hasFailedCompressionAttempt = false
4747

4848
fun streamPrompt(
49-
userPrompt: String,
49+
userPrompt: String,
5050
fileSystem: ProjectFileSystem = EmptyFileSystem(),
5151
historyMessages: List<Message> = emptyList(),
5252
compileDevIns: Boolean = true,
@@ -58,37 +58,61 @@ class KoogLLMService(
5858
} else {
5959
userPrompt
6060
}
61-
61+
62+
val promptLength = finalPrompt.length
63+
logger.info { "🚀 [LLM] Starting stream request - prompt length: $promptLength chars, model: ${config.modelName}" }
64+
val startTime = Clock.System.now().toEpochMilliseconds()
65+
6266
val prompt = buildPrompt(finalPrompt, historyMessages)
63-
executor.executeStreaming(prompt, model)
64-
.cancellable()
65-
.collect { frame ->
66-
when (frame) {
67-
is StreamFrame.Append -> emit(frame.text)
68-
is StreamFrame.End -> {
69-
logger.debug { "StreamFrame.End -> finishReason=${frame.finishReason}, metaInfo=${frame.metaInfo}" }
70-
frame.metaInfo?.let { metaInfo ->
71-
lastTokenInfo = TokenInfo(
72-
totalTokens = metaInfo.totalTokensCount ?: 0,
73-
inputTokens = metaInfo.inputTokensCount ?: 0,
74-
outputTokens = metaInfo.outputTokensCount ?: 0,
75-
timestamp = Clock.System.now().toEpochMilliseconds()
76-
)
77-
78-
onTokenUpdate?.invoke(lastTokenInfo)
79-
if (compressionConfig.autoCompressionEnabled) {
80-
val maxTokens = getMaxTokens()
81-
if (lastTokenInfo.needsCompression(maxTokens, compressionConfig.contextPercentageThreshold)) {
82-
onCompressionNeeded?.invoke(lastTokenInfo.inputTokens, maxTokens)
67+
var chunkCount = 0
68+
var totalChars = 0
69+
70+
try {
71+
executor.executeStreaming(prompt, model)
72+
.cancellable()
73+
.collect { frame ->
74+
when (frame) {
75+
is StreamFrame.Append -> {
76+
chunkCount++
77+
totalChars += frame.text.length
78+
if (chunkCount == 1) {
79+
val ttfb = Clock.System.now().toEpochMilliseconds() - startTime
80+
logger.info { "📥 [LLM] First chunk received - TTFB: ${ttfb}ms" }
81+
}
82+
emit(frame.text)
83+
}
84+
is StreamFrame.End -> {
85+
val elapsed = Clock.System.now().toEpochMilliseconds() - startTime
86+
logger.info { "✅ [LLM] Stream completed - chunks: $chunkCount, chars: $totalChars, time: ${elapsed}ms" }
87+
logger.debug { "StreamFrame.End -> finishReason=${frame.finishReason}, metaInfo=${frame.metaInfo}" }
88+
frame.metaInfo?.let { metaInfo ->
89+
lastTokenInfo = TokenInfo(
90+
totalTokens = metaInfo.totalTokensCount ?: 0,
91+
inputTokens = metaInfo.inputTokensCount ?: 0,
92+
outputTokens = metaInfo.outputTokensCount ?: 0,
93+
timestamp = Clock.System.now().toEpochMilliseconds()
94+
)
95+
logger.info { "📊 [LLM] Token usage - input: ${lastTokenInfo.inputTokens}, output: ${lastTokenInfo.outputTokens}, total: ${lastTokenInfo.totalTokens}" }
96+
97+
onTokenUpdate?.invoke(lastTokenInfo)
98+
if (compressionConfig.autoCompressionEnabled) {
99+
val maxTokens = getMaxTokens()
100+
if (lastTokenInfo.needsCompression(maxTokens, compressionConfig.contextPercentageThreshold)) {
101+
onCompressionNeeded?.invoke(lastTokenInfo.inputTokens, maxTokens)
102+
}
83103
}
84104
}
105+
106+
messagesSinceLastCompression++
85107
}
86-
87-
messagesSinceLastCompression++
108+
is StreamFrame.ToolCall -> { /* Tool calls (可以后续扩展) */ }
88109
}
89-
is StreamFrame.ToolCall -> { /* Tool calls (可以后续扩展) */ }
90110
}
91-
}
111+
} catch (e: Exception) {
112+
val elapsed = Clock.System.now().toEpochMilliseconds() - startTime
113+
logger.error { "❌ [LLM] Stream error after ${elapsed}ms - chunks: $chunkCount, error: ${e.message}" }
114+
throw e
115+
}
92116
}
93117

94118
suspend fun sendPrompt(prompt: String): String {

mpp-core/src/commonTest/kotlin/cc/unitmesh/agent/subagent/DomainDictAgentTest.kt

Lines changed: 3 additions & 242 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ package cc.unitmesh.agent.subagent
22

33
import kotlin.test.Test
44
import kotlin.test.assertEquals
5-
import kotlin.test.assertNotNull
65
import kotlin.test.assertTrue
76

87
/**
9-
* Test for DomainDictAgent data classes and Deep Research flow
8+
* Test for DomainDictAgent data classes
109
*/
1110
class DomainDictAgentTest {
1211

@@ -32,7 +31,7 @@ class DomainDictAgentTest {
3231
)
3332

3433
assertEquals("Simple query", context.userQuery)
35-
assertEquals(7, context.maxIterations) // New default value for Deep Research
34+
assertEquals(1, context.maxIterations) // Default value is 1
3635
assertEquals(null, context.focusArea)
3736
assertEquals(null, context.currentDict)
3837
}
@@ -78,251 +77,13 @@ class DomainDictAgentTest {
7877
assertTrue(csvRow.contains("Gateway | API | GatewayService"))
7978
}
8079

81-
// ============= Deep Research Step 1: Problem Definition Tests =============
82-
83-
@Test
84-
fun testProblemDefinitionCreation() {
85-
val problemDef = ProblemDefinition(
86-
goal = "Optimize domain dictionary for authentication module",
87-
scope = "Authentication and authorization related code",
88-
depth = "Comprehensive analysis",
89-
deliverableFormat = "CSV",
90-
constraints = listOf("Focus on security terms", "Exclude test files")
91-
)
92-
93-
assertEquals("Optimize domain dictionary for authentication module", problemDef.goal)
94-
assertEquals(2, problemDef.constraints.size)
95-
}
96-
97-
// ============= Deep Research Step 2: Research Dimensions Tests =============
98-
99-
@Test
100-
fun testResearchDimensionCreation() {
101-
val dimension = ResearchDimension(
102-
name = "Core Domain",
103-
description = "Main business logic entities",
104-
priority = 5,
105-
queries = listOf("*Service*.kt", "*Repository*.kt")
106-
)
107-
108-
assertEquals("Core Domain", dimension.name)
109-
assertEquals(5, dimension.priority)
110-
assertEquals(2, dimension.queries.size)
111-
}
112-
113-
@Test
114-
fun testResearchDimensionPriority() {
115-
val dimensions = listOf(
116-
ResearchDimension("Core", "Core domain", 5),
117-
ResearchDimension("Infra", "Infrastructure", 3),
118-
ResearchDimension("API", "API layer", 4)
119-
)
120-
121-
val sorted = dimensions.sortedByDescending { it.priority }
122-
assertEquals("Core", sorted[0].name)
123-
assertEquals("API", sorted[1].name)
124-
assertEquals("Infra", sorted[2].name)
125-
}
126-
127-
// ============= Deep Research Step 3: Information Plan Tests =============
128-
129-
@Test
130-
fun testInformationPlanCreation() {
131-
val plan = InformationPlan(
132-
searchPaths = listOf("src/main", "src/commonMain"),
133-
filePatterns = listOf("*Agent*.kt", "*Service*.kt"),
134-
knowledgeSources = listOf("source code", "README"),
135-
analysisStrategies = listOf("class analysis", "function analysis")
136-
)
137-
138-
assertEquals(2, plan.searchPaths.size)
139-
assertEquals(2, plan.filePatterns.size)
140-
assertEquals(2, plan.analysisStrategies.size)
141-
}
142-
143-
// ============= Deep Research Step 4: Dimension Result Tests =============
144-
145-
@Test
146-
fun testDimensionResearchResult() {
147-
val result = DimensionResearchResult(
148-
dimension = "Core Domain",
149-
collected = listOf("class:UserService", "fun:authenticate"),
150-
organized = mapOf(
151-
"classes" to listOf("UserService", "AuthService"),
152-
"functions" to listOf("authenticate", "authorize")
153-
),
154-
validated = true,
155-
conflicts = emptyList(),
156-
conclusion = "Found 2 classes and 2 functions",
157-
newEntries = listOf(
158-
DomainEntry("Auth", "AuthService", "Authentication service")
159-
)
160-
)
161-
162-
assertEquals("Core Domain", result.dimension)
163-
assertTrue(result.validated)
164-
assertEquals(1, result.newEntries.size)
165-
assertEquals(2, result.collected.size)
166-
}
167-
168-
// ============= Deep Research Step 5: Second-Order Insights Tests =============
169-
170-
@Test
171-
fun testSecondOrderInsights() {
172-
val insights = SecondOrderInsights(
173-
principles = listOf("Domain terms reflect business concepts"),
174-
patterns = listOf("*Service suffix for business logic"),
175-
frameworks = listOf("Clean Architecture"),
176-
unifiedModel = "Domain vocabulary mirrors the ubiquitous language"
177-
)
178-
179-
assertEquals(1, insights.principles.size)
180-
assertEquals(1, insights.patterns.size)
181-
assertTrue(insights.unifiedModel.contains("ubiquitous"))
182-
}
183-
184-
// ============= Deep Research Step 6: Research Narrative Tests =============
185-
186-
@Test
187-
fun testResearchNarrative() {
188-
val narrative = ResearchNarrative(
189-
summary = "Comprehensive analysis of domain vocabulary",
190-
keyFindings = listOf("Found 50 domain terms", "Identified 3 modules"),
191-
implications = listOf("Need to add authentication terms"),
192-
recommendations = listOf("Review generated entries", "Add more payment terms")
193-
)
194-
195-
assertEquals(2, narrative.keyFindings.size)
196-
assertEquals(2, narrative.recommendations.size)
197-
}
198-
199-
// ============= Deep Research Step 7: Final Deliverables Tests =============
200-
201-
@Test
202-
fun testFinalDeliverables() {
203-
val deliverables = FinalDeliverables(
204-
updatedDictionary = "Chinese,Code Translation,Description\nAuth,AuthService,Auth module",
205-
changeLog = listOf("Added: Auth -> AuthService"),
206-
qualityMetrics = mapOf("completeness" to 0.85f, "relevance" to 0.9f),
207-
nextSteps = listOf("Review dictionary", "Test enhancement")
208-
)
209-
210-
assertTrue(deliverables.updatedDictionary.contains("Chinese,Code Translation"))
211-
assertEquals(1, deliverables.changeLog.size)
212-
assertEquals(0.85f, deliverables.qualityMetrics["completeness"])
213-
}
214-
215-
// ============= Deep Research State Tests =============
216-
217-
@Test
218-
fun testDeepResearchStateInitial() {
219-
val state = DeepResearchState()
220-
221-
assertEquals(0, state.step)
222-
assertEquals("", state.stepName)
223-
assertEquals(false, state.isComplete)
224-
assertTrue(state.dimensions.isEmpty())
225-
}
226-
227-
@Test
228-
fun testDeepResearchStateProgression() {
229-
var state = DeepResearchState()
230-
231-
// Step 1
232-
state = state.copy(
233-
step = 1,
234-
stepName = "Clarify",
235-
problemDefinition = ProblemDefinition("goal", "scope", "depth", "format")
236-
)
237-
assertEquals(1, state.step)
238-
assertNotNull(state.problemDefinition)
239-
240-
// Step 2
241-
state = state.copy(
242-
step = 2,
243-
stepName = "Decompose",
244-
dimensions = listOf(
245-
ResearchDimension("Dim1", "desc", 5),
246-
ResearchDimension("Dim2", "desc", 3)
247-
)
248-
)
249-
assertEquals(2, state.step)
250-
assertEquals(2, state.dimensions.size)
251-
252-
// Complete
253-
state = state.copy(
254-
step = 7,
255-
stepName = "Actionization",
256-
isComplete = true
257-
)
258-
assertTrue(state.isComplete)
259-
}
260-
261-
// ============= Legacy Compatibility Tests =============
262-
263-
@Test
264-
fun testDictAssessmentCreation() {
265-
val assessment = DictAssessment(
266-
satisfiesRequirement = false,
267-
completenessScore = 0.6f,
268-
relevanceScore = 0.8f,
269-
gaps = listOf("Missing authentication terms", "Missing payment terms"),
270-
reasoning = "Current dictionary lacks auth-related vocabulary"
271-
)
272-
273-
assertEquals(false, assessment.satisfiesRequirement)
274-
assertEquals(0.6f, assessment.completenessScore)
275-
assertEquals(0.8f, assessment.relevanceScore)
276-
assertEquals(2, assessment.gaps.size)
277-
}
278-
279-
@Test
280-
fun testSuggestionTypes() {
281-
val types = SuggestionType.entries
282-
assertTrue(types.contains(SuggestionType.ADD_TERMS))
283-
assertTrue(types.contains(SuggestionType.REFINE_TRANSLATION))
284-
assertTrue(types.contains(SuggestionType.ADD_DESCRIPTION))
285-
assertTrue(types.contains(SuggestionType.QUERY_MORE_FILES))
286-
assertTrue(types.contains(SuggestionType.CLUSTER_ANALYSIS))
287-
assertTrue(types.contains(SuggestionType.COMPLETE))
288-
}
289-
290-
@Test
291-
fun testDomainDictReviewResult() {
292-
val reviewResult = DomainDictReviewResult(
293-
iteration = 1,
294-
assessment = DictAssessment(
295-
satisfiesRequirement = false,
296-
completenessScore = 0.5f,
297-
relevanceScore = 0.7f,
298-
reasoning = "Needs more terms"
299-
),
300-
suggestions = listOf(
301-
DictSuggestion(
302-
type = SuggestionType.ADD_TERMS,
303-
description = "Add authentication terms"
304-
)
305-
),
306-
queriesNeeded = listOf("$.code.class(*Auth*)"),
307-
newEntries = listOf(
308-
DomainEntry("Auth", "Auth", "Authentication module")
309-
)
310-
)
311-
312-
assertEquals(1, reviewResult.iteration)
313-
assertEquals(false, reviewResult.assessment.satisfiesRequirement)
314-
assertEquals(1, reviewResult.suggestions.size)
315-
assertEquals(1, reviewResult.queriesNeeded.size)
316-
assertEquals(1, reviewResult.newEntries.size)
317-
}
318-
31980
// ============= Schema Tests =============
32081

32182
@Test
32283
fun testDomainDictAgentSchemaExampleUsage() {
32384
val example = DomainDictAgentSchema.getExampleUsage("domain-dict-agent")
32485
assertTrue(example.contains("/domain-dict-agent"))
325-
assertTrue(example.contains("userQuery"))
86+
assertTrue(example.contains("focusArea"))
32687
}
32788

32889
// ============= CSV Generation Tests =============

0 commit comments

Comments
 (0)