Skip to content

Commit b141a9a

Browse files
committed
feat(renderer): introduce BaseRenderer class and enhance CodingAgentRenderer interface with new methods #453
1 parent 8f2b468 commit b141a9a

File tree

7 files changed

+315
-22
lines changed

7 files changed

+315
-22
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package cc.unitmesh.agent.render
2+
3+
/**
4+
* Base abstract renderer providing common functionality
5+
* All specific renderer implementations should extend this class
6+
*/
7+
abstract class BaseRenderer : CodingAgentRenderer {
8+
protected val reasoningBuffer = StringBuilder()
9+
protected var isInDevinBlock = false
10+
protected var lastIterationReasoning = ""
11+
protected var consecutiveRepeats = 0
12+
13+
/**
14+
* Common devin block filtering logic
15+
*/
16+
protected fun filterDevinBlocks(content: String): String {
17+
var filtered = content
18+
19+
// Remove complete devin blocks
20+
filtered = filtered.replace(Regex("<devin[^>]*>[\\s\\S]*?</devin>"), "")
21+
22+
// Handle incomplete devin blocks at the end
23+
val openDevinIndex = filtered.lastIndexOf("<devin")
24+
if (openDevinIndex != -1) {
25+
val closeDevinIndex = filtered.indexOf("</devin>", openDevinIndex)
26+
if (closeDevinIndex == -1) {
27+
// Incomplete devin block, remove it
28+
filtered = filtered.substring(0, openDevinIndex)
29+
}
30+
}
31+
32+
// Remove partial devin tags
33+
filtered = filtered.replace(Regex("<de(?:v(?:i(?:n)?)?)?$|<$"), "")
34+
35+
return filtered
36+
}
37+
38+
/**
39+
* Check for incomplete devin blocks
40+
*/
41+
protected fun hasIncompleteDevinBlock(content: String): Boolean {
42+
val lastOpenDevin = content.lastIndexOf("<devin")
43+
val lastCloseDevin = content.lastIndexOf("</devin>")
44+
45+
// Check for partial opening tags
46+
val partialDevinPattern = Regex("<de(?:v(?:i(?:n)?)?)?$|<$")
47+
val hasPartialTag = partialDevinPattern.containsMatchIn(content)
48+
49+
return lastOpenDevin > lastCloseDevin || hasPartialTag
50+
}
51+
52+
/**
53+
* Calculate similarity between two strings for repeat detection
54+
*/
55+
protected fun calculateSimilarity(str1: String, str2: String): Double {
56+
if (str1.isEmpty() || str2.isEmpty()) return 0.0
57+
58+
val words1 = str1.lowercase().split(Regex("\\s+"))
59+
val words2 = str2.lowercase().split(Regex("\\s+"))
60+
61+
val commonWords = words1.intersect(words2.toSet())
62+
val totalWords = maxOf(words1.size, words2.size)
63+
64+
return if (totalWords > 0) commonWords.size.toDouble() / totalWords else 0.0
65+
}
66+
67+
/**
68+
* Clean up excessive newlines in content
69+
*/
70+
protected fun cleanNewlines(content: String): String {
71+
return content.replace(Regex("\n{3,}"), "\n\n")
72+
}
73+
74+
/**
75+
* Default implementation for LLM response start
76+
*/
77+
override fun renderLLMResponseStart() {
78+
reasoningBuffer.clear()
79+
isInDevinBlock = false
80+
}
81+
82+
/**
83+
* Default implementation for LLM response end with similarity checking
84+
*/
85+
override fun renderLLMResponseEnd() {
86+
val currentReasoning = reasoningBuffer.toString().trim()
87+
val similarity = calculateSimilarity(currentReasoning, lastIterationReasoning)
88+
89+
if (similarity > 0.8 && lastIterationReasoning.isNotEmpty()) {
90+
consecutiveRepeats++
91+
if (consecutiveRepeats >= 2) {
92+
renderRepeatAnalysisWarning()
93+
}
94+
} else {
95+
consecutiveRepeats = 0
96+
}
97+
98+
lastIterationReasoning = currentReasoning
99+
}
100+
101+
/**
102+
* Render warning for repetitive analysis - can be overridden by subclasses
103+
*/
104+
protected open fun renderRepeatAnalysisWarning() {
105+
renderError("Agent appears to be repeating similar analysis...")
106+
}
107+
}
Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,33 @@
11
package cc.unitmesh.agent.render
22

33
/**
4-
* Output renderer interface for CodingAgent
5-
* Allows customization of output formatting (e.g., CLI vs TUI)
4+
* Core renderer interface for CodingAgent
5+
* Defines the contract for all renderer implementations
66
*/
77
interface CodingAgentRenderer {
8+
// Lifecycle methods
89
fun renderIterationHeader(current: Int, max: Int)
910
fun renderLLMResponseStart()
1011
fun renderLLMResponseChunk(chunk: String)
1112
fun renderLLMResponseEnd()
13+
14+
// Tool execution methods
1215
fun renderToolCall(toolName: String, paramsStr: String)
1316
fun renderToolResult(toolName: String, success: Boolean, output: String?, fullOutput: String?)
17+
18+
// Status and completion methods
1419
fun renderTaskComplete()
1520
fun renderFinalResult(success: Boolean, message: String, iterations: Int)
1621
fun renderError(message: String)
1722
fun renderRepeatWarning(toolName: String, count: Int)
23+
}
24+
25+
/**
26+
* Renderer type enumeration for different UI implementations
27+
*/
28+
enum class RendererType {
29+
CONSOLE, // Default console output
30+
CLI, // Enhanced CLI with colors and formatting
31+
TUI, // Terminal UI with interactive elements
32+
WEB // Web-based UI
1833
}

mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/render/DefaultCodingAgentRenderer.kt

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,38 @@
11
package cc.unitmesh.agent.render
22

33
/**
4-
* Default console renderer
4+
* Default console renderer - simple text output
5+
* Suitable for basic console applications and testing
56
*/
6-
class DefaultCodingAgentRenderer : CodingAgentRenderer {
7-
private val reasoningBuffer = StringBuilder()
8-
private var isInDevinBlock = false
7+
class DefaultCodingAgentRenderer : BaseRenderer() {
98

109
override fun renderIterationHeader(current: Int, max: Int) {
1110
println("\n[$current/$max] Analyzing and executing...")
1211
}
1312

1413
override fun renderLLMResponseStart() {
15-
reasoningBuffer.clear()
16-
isInDevinBlock = false
14+
super.renderLLMResponseStart()
1715
print("💭 ")
1816
}
1917

2018
override fun renderLLMResponseChunk(chunk: String) {
21-
// Parse chunk to detect devin blocks
2219
reasoningBuffer.append(chunk)
23-
val text = reasoningBuffer.toString()
2420

25-
// Check if we're entering or leaving a devin block
26-
if (text.contains("<devin>")) {
27-
isInDevinBlock = true
28-
}
29-
if (text.contains("</devin>")) {
30-
isInDevinBlock = false
21+
// Wait for more content if we detect an incomplete devin block
22+
if (hasIncompleteDevinBlock(reasoningBuffer.toString())) {
23+
return
3124
}
3225

33-
// Only print if not in devin block
34-
if (!isInDevinBlock && !chunk.contains("<devin>") && !chunk.contains("</devin>")) {
35-
print(chunk)
36-
}
26+
// Filter devin blocks and output clean content
27+
val processedContent = filterDevinBlocks(reasoningBuffer.toString())
28+
val cleanContent = cleanNewlines(processedContent)
29+
30+
// Simple output for default renderer
31+
print(cleanContent)
3732
}
3833

3934
override fun renderLLMResponseEnd() {
35+
super.renderLLMResponseEnd()
4036
println("\n")
4137
}
4238

mpp-core/src/jsMain/kotlin/cc/unitmesh/agent/RendererExports.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,53 @@
11
package cc.unitmesh.agent
22

33
import cc.unitmesh.agent.render.CodingAgentRenderer
4+
import cc.unitmesh.agent.render.RendererType
45
import kotlin.js.JsExport
56

67
/**
78
* JS-friendly renderer interface
89
* Allows TypeScript to provide custom rendering implementations
10+
* This interface mirrors the Kotlin CodingAgentRenderer interface
911
*/
1012
@JsExport
1113
interface JsCodingAgentRenderer {
14+
// Lifecycle methods
1215
fun renderIterationHeader(current: Int, max: Int)
1316
fun renderLLMResponseStart()
1417
fun renderLLMResponseChunk(chunk: String)
1518
fun renderLLMResponseEnd()
19+
20+
// Tool execution methods
1621
fun renderToolCall(toolName: String, paramsStr: String)
1722
fun renderToolResult(toolName: String, success: Boolean, output: String?, fullOutput: String?)
23+
24+
// Status and completion methods
1825
fun renderTaskComplete()
1926
fun renderFinalResult(success: Boolean, message: String, iterations: Int)
2027
fun renderError(message: String)
2128
fun renderRepeatWarning(toolName: String, count: Int)
2229
}
2330

31+
/**
32+
* Renderer factory for creating different types of renderers
33+
*/
34+
@JsExport
35+
object RendererFactory {
36+
/**
37+
* Create a renderer adapter from JS implementation
38+
*/
39+
fun createRenderer(jsRenderer: JsCodingAgentRenderer): CodingAgentRenderer {
40+
return JsRendererAdapter(jsRenderer)
41+
}
42+
43+
/**
44+
* Get renderer type information for JS consumers
45+
*/
46+
fun getRendererTypes(): Array<String> {
47+
return RendererType.values().map { it.name }.toTypedArray()
48+
}
49+
}
50+
2451
/**
2552
* Adapter to convert JS renderer to Kotlin renderer
2653
*/

0 commit comments

Comments
 (0)