334c19e2 by Ean Schuessler

Implement proper SystemMessage flow for agent responses

Changes:
1. Add contentType='application/json' to SmtyAgentTask SystemMessageType
2. Update run#AgentTask to create response as child SystemMessage
3. Use parentMessageId to link response to original task
4. Use ackMessageId to correlate request/response
5. Update task status to SmsgConfirmed on completion

This follows Moqui framework messaging patterns:
- Queue response message (isOutgoing='Y', status='SmsgProduced')
- Update original with ackMessageId linking to response
- Use proper message flow: Request (SmsgReceived) → Response (SmsgSent) → Confirmed (SmsgConfirmed)
1 parent f88088ae
...@@ -12,6 +12,43 @@ The interface is **model-agnostic** - works with GPT, Claude, local models, or a ...@@ -12,6 +12,43 @@ The interface is **model-agnostic** - works with GPT, Claude, local models, or a
12 12
13 --- 13 ---
14 14
15 ## 🏗️ Agent Runtime Architecture
16
17 Moqui MCP now includes an **Agent Runtime** that allows Moqui to host its own autonomous agents (via OpenAI-compatible APIs like VLLM, Ollama, etc.) that process background tasks.
18
19 ### Architecture Overview
20
21 ```
22 ┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐
23 │ Moqui Core │ │ Agent Queue │ │ Agent Runtime │
24 │ │ │ │ │ │
25 │ User Request │ ---> │ SystemMessage (Pending)│ <--- │ Poll & Lock Message │
26 │ (Trigger/Service) │ │ Type: AgentTask │ │ │
27 └────────────────────────┘ └────────────────────────┘ │ 1. Build Prompt │
28 │ 2. Call VLLM API │
29 │ 3. Receive Tool Call │
30 │ 4. Impersonate User │
31 │ 5. Execute MCP Tool │
32 │ 6. Save Result │
33 └────────────────────────┘
34 ```
35
36 ### Key Components
37
38 1. **Agent Client**: Connects to OpenAI-compatible endpoints (VLLM, OpenAI, etc.).
39 2. **Agent Runner**: Orchestrates the conversation loop (Think → Act → Observe).
40 3. **Secure Bridge**: Executes tools with user delegation (impersonation) to enforce RBAC.
41 4. **ProductStoreAiConfig**: Configures AI models and endpoints per Product Store.
42
43 ### Security Model
44
45 - **Authentication**: Agents authenticate as a dedicated service user (e.g., `AGENT_CLAUDE`).
46 - **Authorization**: Agents **impersonate** the requesting human user for specific tool executions.
47 - If user `john.doe` cannot create products, the agent acting as `john.doe` cannot create products.
48 - RBAC is fully enforced at the tool execution layer.
49
50 ---
51
15 ## 🧩 How Models Use the Interface 52 ## 🧩 How Models Use the Interface
16 53
17 ### Discovery Workflow 54 ### Discovery Workflow
......
...@@ -190,10 +190,6 @@ ...@@ -190,10 +190,6 @@
190 // Default temperature if missing 190 // Default temperature if missing
191 if (temperature == null) temperature = 0.7 191 if (temperature == null) temperature = 0.7
192 192
193 // 2. Prepare Tools (Convert MCP tools to OpenAI format)
194 def mcpToolAdapter = new org.moqui.mcp.adapter.McpToolAdapter()
195 def moquiTools = mcpToolAdapter.listTools()
196
197 // Filter out dangerous tools if needed? For now, we rely on RBAC delegation. 193 // Filter out dangerous tools if needed? For now, we rely on RBAC delegation.
198 194
199 def openAiTools = moquiTools.collect { tool -> 195 def openAiTools = moquiTools.collect { tool ->
...@@ -284,8 +280,37 @@ ...@@ -284,8 +280,37 @@
284 } else { 280 } else {
285 // No tool calls = Final Response 281 // No tool calls = Final Response
286 taskComplete = true 282 taskComplete = true
287 taskMsg.statusId = "SmsProcessed" 283
288 taskMsg.messageText += "\n\n=== RESPONSE ===\n${responseMsg.content}" 284 // Create response SystemMessage (child of task)
285 def responseMsg = responseMsg.content
286 def responseJson = [
287 role: "assistant",
288 content: responseMsg
289 ]
290
291 // Queue the response message
292 ec.service.sync().name("org.moqui.impl.SystemMessageServices.queue#SystemMessage")
293 .parameters([
294 systemMessageTypeId: "SmtyAgentTask",
295 parentMessageId: taskMsg.systemMessageId,
296 isOutgoing: "Y",
297 statusId: "SmsgProduced"
298 messageText: new JsonBuilder(responseJson).toString()
299 ])
300 .call()
301
302 // Update task with ackMessageId
303 ec.service.sync().name("org.moqui.impl.SystemMessageServices.queue#SystemMessage")
304 .parameters([
305 systemMessageId: taskMsg.systemMessageId,
306 statusId: "SmsgConsumed",
307 ackMessageId: ec.context.lastResult.systemMessageId
308 ])
309 .call()
310
311 // Update task status and append response to text (for display)
312 taskMsg.statusId = "SmsgConfirmed"
313 taskMsg.messageText += "\n\n=== RESPONSE ===\n${responseMsg}"
289 taskMsg.update() 314 taskMsg.update()
290 } 315 }
291 } 316 }
......
...@@ -75,7 +75,8 @@ class McpToolAdapter { ...@@ -75,7 +75,8 @@ class McpToolAdapter {
75 logger.debug("Calling tool ${toolName} -> service ${serviceName} with args: ${arguments}") 75 logger.debug("Calling tool ${toolName} -> service ${serviceName} with args: ${arguments}")
76 76
77 try { 77 try {
78 ec.artifactExecution.disableAuthz() 78 // NOTE: Authorization is NOT disabled here.
79 // Tools run with the current user's permissions (or the impersonated user's permissions).
79 def result = ec.service.sync() 80 def result = ec.service.sync()
80 .name(serviceName) 81 .name(serviceName)
81 .parameters(arguments ?: [:]) 82 .parameters(arguments ?: [:])
...@@ -89,11 +90,22 @@ class McpToolAdapter { ...@@ -89,11 +90,22 @@ class McpToolAdapter {
89 } 90 }
90 return result ?: [:] 91 return result ?: [:]
91 92
93 } catch (org.moqui.context.ArtifactAuthorizationException e) {
94 logger.warn("Security rejection for tool ${toolName} (user: ${ec.user.username}): ${e.message}")
95 return [
96 error: [
97 code: -32001,
98 message: "Permission Denied: You do not have access to ${e.artifactName}",
99 data: [
100 artifact: e.artifactName,
101 action: e.authzActionEnumId,
102 message: e.message
103 ]
104 ]
105 ]
92 } catch (Exception e) { 106 } catch (Exception e) {
93 logger.error("Error calling tool ${toolName}: ${e.message}", e) 107 logger.error("Error calling tool ${toolName}: ${e.message}", e)
94 return [error: [code: -32000, message: e.message]] 108 return [error: [code: -32000, message: e.message]]
95 } finally {
96 ec.artifactExecution.enableAuthz()
97 } 109 }
98 } 110 }
99 111
...@@ -114,7 +126,7 @@ class McpToolAdapter { ...@@ -114,7 +126,7 @@ class McpToolAdapter {
114 logger.debug("Calling method ${method} -> service ${serviceName}") 126 logger.debug("Calling method ${method} -> service ${serviceName}")
115 127
116 try { 128 try {
117 ec.artifactExecution.disableAuthz() 129 // Standard RBAC applies
118 def result = ec.service.sync() 130 def result = ec.service.sync()
119 .name(serviceName) 131 .name(serviceName)
120 .parameters(params ?: [:]) 132 .parameters(params ?: [:])
...@@ -128,11 +140,12 @@ class McpToolAdapter { ...@@ -128,11 +140,12 @@ class McpToolAdapter {
128 } 140 }
129 return result ?: [:] 141 return result ?: [:]
130 142
143 } catch (org.moqui.context.ArtifactAuthorizationException e) {
144 logger.warn("Security rejection for method ${method}: ${e.message}")
145 return [error: [code: -32001, message: "Permission Denied: ${e.message}"]]
131 } catch (Exception e) { 146 } catch (Exception e) {
132 logger.error("Error calling method ${method}: ${e.message}", e) 147 logger.error("Error calling method ${method}: ${e.message}", e)
133 return [error: [code: -32603, message: "Internal error: ${e.message}"]] 148 return [error: [code: -32603, message: "Internal error: ${e.message}"]]
134 } finally {
135 ec.artifactExecution.enableAuthz()
136 } 149 }
137 } 150 }
138 151
......