Skip to content

Commit 2896733

Browse files
committed
feat(nanodsl-agent): integrate NanoDSL as SubAgent
- Create NanoDSLAgent in mpp-core/commonMain implementing SubAgent - Add NanoDSLAgentSchema with description, componentType, includeState, includeHttp params - Add JS exports (JsNanoDSLAgent, JsNanoDSLContext) for cross-platform support - Register NanoDSLAgent in CodingAgent init block - Add NanoDSLAgent to ToolType registry - Add test script for CLI verification The NanoDSL Agent generates UI code from natural language descriptions, supporting state management and HTTP request actions. Tested: CLI successfully invokes nanodsl-agent and generates greeting card UI
1 parent 81760f8 commit 2896733

File tree

5 files changed

+358
-6
lines changed

5 files changed

+358
-6
lines changed

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import cc.unitmesh.agent.render.CodingAgentRenderer
2020
import cc.unitmesh.agent.render.DefaultCodingAgentRenderer
2121
import cc.unitmesh.agent.subagent.AnalysisAgent
2222
import cc.unitmesh.agent.subagent.ErrorRecoveryAgent
23+
import cc.unitmesh.agent.subagent.NanoDSLAgent
2324
import cc.unitmesh.agent.tool.*
2425
import cc.unitmesh.agent.tool.filesystem.DefaultToolFileSystem
2526
import cc.unitmesh.agent.tool.filesystem.ToolFileSystem
@@ -95,6 +96,7 @@ class CodingAgent(
9596

9697
private val errorRecoveryAgent = ErrorRecoveryAgent(projectPath, llmService)
9798
private val analysisAgent = AnalysisAgent(llmService, contentThreshold = 15000)
99+
private val nanoDSLAgent = NanoDSLAgent(llmService)
98100
private val mcpToolsInitializer = McpToolsInitializer()
99101

100102
// 执行器
@@ -116,10 +118,15 @@ class CodingAgent(
116118
registerTool(errorRecoveryAgent)
117119
toolRegistry.registerTool(errorRecoveryAgent)
118120
subAgentManager.registerSubAgent(errorRecoveryAgent)
119-
121+
120122
registerTool(analysisAgent)
121123
toolRegistry.registerTool(analysisAgent)
122124
subAgentManager.registerSubAgent(analysisAgent)
125+
126+
registerTool(nanoDSLAgent)
127+
toolRegistry.registerTool(nanoDSLAgent)
128+
subAgentManager.registerSubAgent(nanoDSLAgent)
129+
123130
CoroutineScope(SupervisorJob() + Dispatchers.Default).launch {
124131
initializeWorkspace(projectPath)
125132
}
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
package cc.unitmesh.agent.subagent
2+
3+
import cc.unitmesh.agent.core.SubAgent
4+
import cc.unitmesh.agent.logging.getLogger
5+
import cc.unitmesh.agent.model.AgentDefinition
6+
import cc.unitmesh.agent.model.PromptConfig
7+
import cc.unitmesh.agent.model.RunConfig
8+
import cc.unitmesh.agent.tool.ToolResult
9+
import cc.unitmesh.agent.tool.schema.DeclarativeToolSchema
10+
import cc.unitmesh.agent.tool.schema.SchemaPropertyBuilder.boolean
11+
import cc.unitmesh.agent.tool.schema.SchemaPropertyBuilder.string
12+
import cc.unitmesh.devins.parser.CodeFence
13+
import cc.unitmesh.llm.KoogLLMService
14+
import cc.unitmesh.llm.ModelConfig
15+
import kotlinx.coroutines.flow.toList
16+
import kotlinx.serialization.Serializable
17+
18+
/**
19+
* NanoDSL Agent - Generates NanoDSL UI code from natural language descriptions.
20+
*
21+
* NanoDSL is a Python-style indentation-based DSL for AI-generated UI,
22+
* prioritizing token efficiency and human readability.
23+
*
24+
* Features:
25+
* - Converts natural language UI descriptions to NanoDSL code
26+
* - Supports component generation (Card, VStack, HStack, Button, etc.)
27+
* - Supports state management and event handling
28+
* - Supports HTTP requests with Fetch action
29+
*
30+
* Cross-platform support:
31+
* - JVM: Full support with prompt templates
32+
* - JS/WASM: Available via JsNanoDSLAgent wrapper
33+
*/
34+
class NanoDSLAgent(
35+
private val llmService: KoogLLMService,
36+
private val promptTemplate: String = DEFAULT_PROMPT
37+
) : SubAgent<NanoDSLContext, ToolResult.AgentResult>(
38+
definition = createDefinition()
39+
) {
40+
private val logger = getLogger("NanoDSLAgent")
41+
42+
override val priority: Int = 50 // Higher priority for UI generation tasks
43+
44+
override fun validateInput(input: Map<String, Any>): NanoDSLContext {
45+
val description = input["description"] as? String
46+
?: throw IllegalArgumentException("Missing required parameter: description")
47+
val componentType = input["componentType"] as? String
48+
val includeState = input["includeState"] as? Boolean ?: true
49+
val includeHttp = input["includeHttp"] as? Boolean ?: false
50+
51+
return NanoDSLContext(
52+
description = description,
53+
componentType = componentType,
54+
includeState = includeState,
55+
includeHttp = includeHttp
56+
)
57+
}
58+
59+
override suspend fun execute(
60+
input: NanoDSLContext,
61+
onProgress: (String) -> Unit
62+
): ToolResult.AgentResult {
63+
onProgress("🎨 NanoDSL Agent: Generating UI from description")
64+
onProgress("Description: ${input.description.take(80)}...")
65+
66+
return try {
67+
val prompt = buildPrompt(input)
68+
onProgress("Calling LLM for code generation...")
69+
70+
val responseBuilder = StringBuilder()
71+
llmService.streamPrompt(
72+
userPrompt = prompt,
73+
compileDevIns = false
74+
).toList().forEach { chunk ->
75+
responseBuilder.append(chunk)
76+
}
77+
78+
val llmResponse = responseBuilder.toString()
79+
80+
// Extract NanoDSL code from markdown code fence
81+
val codeFence = CodeFence.parse(llmResponse)
82+
val generatedCode = if (codeFence.text.isNotBlank()) {
83+
codeFence.text.trim()
84+
} else {
85+
llmResponse.trim()
86+
}
87+
88+
onProgress("✅ Generated ${generatedCode.lines().size} lines of NanoDSL code")
89+
90+
ToolResult.AgentResult(
91+
success = true,
92+
content = generatedCode,
93+
metadata = mapOf(
94+
"description" to input.description,
95+
"componentType" to (input.componentType ?: "auto"),
96+
"linesOfCode" to generatedCode.lines().size.toString(),
97+
"includesState" to input.includeState.toString(),
98+
"includesHttp" to input.includeHttp.toString()
99+
)
100+
)
101+
} catch (e: Exception) {
102+
logger.error(e) { "NanoDSL generation failed" }
103+
onProgress("❌ Generation failed: ${e.message}")
104+
105+
ToolResult.AgentResult(
106+
success = false,
107+
content = "Failed to generate NanoDSL: ${e.message}",
108+
metadata = mapOf("error" to (e.message ?: "Unknown error"))
109+
)
110+
}
111+
}
112+
113+
private fun buildPrompt(input: NanoDSLContext): String {
114+
val featureHints = buildList {
115+
if (input.includeState) add("Include state management if needed")
116+
if (input.includeHttp) add("Include HTTP request actions (Fetch) if applicable")
117+
input.componentType?.let { add("Focus on creating a $it component") }
118+
}.joinToString("\n- ")
119+
120+
return """
121+
$promptTemplate
122+
123+
${if (featureHints.isNotEmpty()) "## Additional Requirements:\n- $featureHints\n" else ""}
124+
## User Request:
125+
${input.description}
126+
""".trim()
127+
}
128+
129+
override fun formatOutput(output: ToolResult.AgentResult): String {
130+
return if (output.success) {
131+
"Generated NanoDSL:\n```nanodsl\n${output.content}\n```"
132+
} else {
133+
"Error: ${output.content}"
134+
}
135+
}
136+
137+
override fun getParameterClass(): String = "NanoDSLContext"
138+
139+
override fun shouldTrigger(context: Map<String, Any>): Boolean {
140+
val content = context["content"] as? String ?: return false
141+
val keywords = listOf("ui", "interface", "form", "card", "button", "component", "layout")
142+
return keywords.any { content.lowercase().contains(it) }
143+
}
144+
145+
override suspend fun handleQuestion(
146+
question: String,
147+
context: Map<String, Any>
148+
): ToolResult.AgentResult {
149+
// Treat questions as UI generation requests
150+
return execute(
151+
NanoDSLContext(description = question),
152+
onProgress = {}
153+
)
154+
}
155+
156+
override fun getStateSummary(): Map<String, Any> = mapOf(
157+
"name" to name,
158+
"description" to description,
159+
"priority" to priority,
160+
"supportedFeatures" to listOf("components", "state", "actions", "http-requests")
161+
)
162+
163+
companion object {
164+
private fun createDefinition() = AgentDefinition(
165+
name = "nanodsl-agent",
166+
displayName = "NanoDSL Agent",
167+
description = "Generates NanoDSL UI code from natural language descriptions",
168+
promptConfig = PromptConfig(
169+
systemPrompt = "You are a NanoDSL expert. Generate token-efficient UI code."
170+
),
171+
modelConfig = ModelConfig.default(),
172+
runConfig = RunConfig(maxTurns = 3, maxTimeMinutes = 2)
173+
)
174+
175+
const val DEFAULT_PROMPT = """You are a NanoDSL expert. Generate UI code using NanoDSL syntax.
176+
177+
## NanoDSL Syntax
178+
179+
NanoDSL uses Python-style indentation (4 spaces) to represent hierarchy.
180+
181+
### Components
182+
- `component Name:` - Define a component
183+
- `VStack(spacing="sm"):` - Vertical stack layout
184+
- `HStack(align="center", justify="between"):` - Horizontal stack layout
185+
- `Card:` - Container with padding/shadow
186+
- `Text("content", style="h1|h2|h3|body|caption")` - Text display
187+
- `Button("label", intent="primary|secondary")` - Clickable button
188+
- `Image(src=path, aspect=16/9, radius="md")` - Image display
189+
- `Input(value=binding, placeholder="...")` - Text input
190+
- `Badge("text", color="green|red|blue")` - Status badge
191+
192+
### Properties
193+
- `padding: "sm|md|lg"` - Padding size
194+
- `shadow: "sm|md|lg"` - Shadow depth
195+
- `spacing: "sm|md|lg"` - Gap between items
196+
197+
### State (for interactive components)
198+
```
199+
state:
200+
count: int = 0
201+
name: str = ""
202+
```
203+
204+
### Bindings
205+
- `<<` - One-way binding (subscribe)
206+
- `:=` - Two-way binding
207+
208+
### Actions
209+
- `on_click: state.var += 1` - State mutation
210+
- `Navigate(to="/path")` - Navigation
211+
- `ShowToast("message")` - Show notification
212+
- `Fetch(url="/api/...", method="POST", body={...})` - HTTP request
213+
214+
### HTTP Requests
215+
```
216+
Button("Submit"):
217+
on_click:
218+
Fetch(
219+
url="/api/login",
220+
method="POST",
221+
body={"email": state.email, "password": state.password},
222+
on_success: Navigate(to="/dashboard"),
223+
on_error: ShowToast("Failed")
224+
)
225+
```
226+
227+
## Output Rules
228+
1. Output ONLY NanoDSL code, no explanations
229+
2. Use 4 spaces for indentation
230+
3. Keep it minimal - no redundant components
231+
4. Wrap output in ```nanodsl code fence"""
232+
}
233+
}
234+
235+
/**
236+
* NanoDSL generation context
237+
*/
238+
@Serializable
239+
data class NanoDSLContext(
240+
val description: String,
241+
val componentType: String? = null,
242+
val includeState: Boolean = true,
243+
val includeHttp: Boolean = false
244+
)
245+
246+
/**
247+
* Schema for NanoDSL Agent tool
248+
*/
249+
object NanoDSLAgentSchema : DeclarativeToolSchema(
250+
description = "Generate NanoDSL UI code from natural language description",
251+
properties = mapOf(
252+
"description" to string(
253+
description = "Natural language description of the UI to generate",
254+
required = true
255+
),
256+
"componentType" to string(
257+
description = "Optional: Specific component type to focus on (card, form, list, etc.)",
258+
required = false
259+
),
260+
"includeState" to boolean(
261+
description = "Whether to include state management (default: true)",
262+
required = false
263+
),
264+
"includeHttp" to boolean(
265+
description = "Whether to include HTTP request actions (default: false)",
266+
required = false
267+
)
268+
)
269+
) {
270+
override fun getExampleUsage(toolName: String): String {
271+
return """/$toolName description="Create a contact form with name, email, message fields and a submit button that sends to /api/contact" includeHttp=true"""
272+
}
273+
}
274+

mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/ToolType.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import cc.unitmesh.agent.subagent.CodebaseInvestigatorSchema
44
import cc.unitmesh.agent.subagent.ContentHandlerSchema
55
import cc.unitmesh.agent.subagent.DomainDictAgentSchema
66
import cc.unitmesh.agent.subagent.ErrorRecoverySchema
7+
import cc.unitmesh.agent.subagent.NanoDSLAgentSchema
78
import cc.unitmesh.agent.tool.impl.*
89
import cc.unitmesh.agent.tool.impl.AskSubAgentSchema
910
import cc.unitmesh.agent.tool.schema.ToolCategory
@@ -145,6 +146,15 @@ sealed class ToolType(
145146
schema = DomainDictAgentSchema
146147
)
147148

149+
data object NanoDSLAgent : ToolType(
150+
name = "nanodsl-agent",
151+
displayName = "NanoDSL Agent",
152+
tuiEmoji = "🎨",
153+
composeIcon = "palette",
154+
category = ToolCategory.SubAgent,
155+
schema = NanoDSLAgentSchema
156+
)
157+
148158
data object AskAgent : ToolType(
149159
name = "ask-agent",
150160
displayName = "Ask Agent",
@@ -171,7 +181,7 @@ sealed class ToolType(
171181
listOf(
172182
ReadFile, WriteFile, EditFile, Grep, Glob,
173183
Shell,
174-
ErrorAgent, AnalysisAgent, CodeAgent, DomainDictAgent,
184+
ErrorAgent, AnalysisAgent, CodeAgent, DomainDictAgent, NanoDSLAgent,
175185
AskAgent,
176186
WebFetch
177187
)

0 commit comments

Comments
 (0)