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)
Showing
3 changed files
with
89 additions
and
14 deletions
| ... | @@ -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 | ... | ... |
| ... | @@ -150,7 +150,7 @@ | ... | @@ -150,7 +150,7 @@ |
| 150 | .condition("systemMessageId", systemMessageId) | 150 | .condition("systemMessageId", systemMessageId) |
| 151 | .one() | 151 | .one() |
| 152 | 152 | ||
| 153 | if (!taskMsg) return | 153 | if (!taskMsg) return |
| 154 | 154 | ||
| 155 | // Get AI Config | 155 | // Get AI Config |
| 156 | def aiConfig = ec.entity.find("moqui.mcp.agent.ProductStoreAiConfig") | 156 | def aiConfig = ec.entity.find("moqui.mcp.agent.ProductStoreAiConfig") |
| ... | @@ -189,10 +189,6 @@ | ... | @@ -189,10 +189,6 @@ |
| 189 | 189 | ||
| 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 | |||
| 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 | 192 | ||
| 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 | ||
| ... | @@ -281,11 +277,40 @@ | ... | @@ -281,11 +277,40 @@ |
| 281 | ]) | 277 | ]) |
| 282 | } | 278 | } |
| 283 | // Loop continues to let LLM see results | 279 | // Loop continues to let LLM see results |
| 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 | ... | ... |
-
Please register or sign in to post a comment