39daa815 by Ean Schuessler

Implement Reliable Agent Architecture using CommunicationEvent thread logic

- Update AgentServices.xml: Single-turn state machine (process one turn, re-queue if tool called)
- Add Agent.secas.xml: Trigger Agent Turn on new CommunicationEvent to Agent Party
- Update AgentEntities.xml: Add rootCommEventId to SystemMessage for thread tracking
- Update AgentData.xml: Define Agent Party, Role, and default VLLM config
1 parent 85eedd26
...@@ -2,20 +2,18 @@ ...@@ -2,20 +2,18 @@
2 <entity-facade-xml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 2 <entity-facade-xml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3 xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/entity-facade-3.xsd" type="seed"> 3 xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/entity-facade-3.xsd" type="seed">
4 4
5 <!-- ========================================================= --> 5 <moqui.security.UserGroup userGroupId="AgentUsers" description="AI Agent Users"/>
6 <!-- Agent User Account (for authentication) -->
7 <!-- ========================================================= -->
8 6
9 <moqui.security.UserAccount 7 <!-- Agent Party -->
10 userId="AGENT_CLAUDE" 8 <mantle.party.Party partyId="AGENT_CLAUDE_PARTY" partyTypeEnumId="PtyPerson"/>
11 username="agent-claude" 9 <mantle.party.Person partyId="AGENT_CLAUDE_PARTY" firstName="Claude" lastName="Agent"/>
12 currentPassword="16ac58bbfa332c1c55bd98b53e60720bfa90d394" 10 <mantle.party.PartyRole partyId="AGENT_CLAUDE_PARTY" roleTypeId="Agent"/>
13 passwordHashType="SHA"/>
14 11
15 <moqui.security.UserGroup userGroupId="AgentUsers" description="AI Agent Users"/> 12 <moqui.security.UserAccount userId="AGENT_CLAUDE" username="agent-claude" partyId="AGENT_CLAUDE_PARTY"
13 currentPassword="16ac58bbfa332c1c55bd98b53e60720bfa90d394" passwordHashType="SHA"/>
16 <moqui.security.UserGroupMember userGroupId="AgentUsers" userId="AGENT_CLAUDE" fromDate="2026-02-04 00:00:00.000"/> 14 <moqui.security.UserGroupMember userGroupId="AgentUsers" userId="AGENT_CLAUDE" fromDate="2026-02-04 00:00:00.000"/>
17 15
18 <!-- Agent users have permission to execute the delegation service --> 16 <!-- Agent users have permission to execute delegation service -->
19 <moqui.security.ArtifactGroup artifactGroupId="AgentDelegationServices" description="Agent Tool Delegation Services"/> 17 <moqui.security.ArtifactGroup artifactGroupId="AgentDelegationServices" description="Agent Tool Delegation Services"/>
20 <moqui.security.ArtifactGroupMember artifactGroupId="AgentDelegationServices" artifactName="AgentServices.call#McpToolWithDelegation" artifactTypeEnumId="AT_SERVICE"/> 18 <moqui.security.ArtifactGroupMember artifactGroupId="AgentDelegationServices" artifactName="AgentServices.call#McpToolWithDelegation" artifactTypeEnumId="AT_SERVICE"/>
21 <moqui.security.ArtifactAuthz userGroupId="AgentUsers" artifactGroupId="AgentDelegationServices" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/> 19 <moqui.security.ArtifactAuthz userGroupId="AgentUsers" artifactGroupId="AgentDelegationServices" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/>
...@@ -27,7 +25,13 @@ ...@@ -27,7 +25,13 @@
27 <!-- Agent Task Message Type --> 25 <!-- Agent Task Message Type -->
28 <moqui.service.message.SystemMessageType systemMessageTypeId="SmtyAgentTask" description="Agent Task" 26 <moqui.service.message.SystemMessageType systemMessageTypeId="SmtyAgentTask" description="Agent Task"
29 contentType="application/json" 27 contentType="application/json"
30 consumeServiceName="AgentServices.poll#AgentQueue" 28 consumeServiceName="AgentServices.poll#AgentQueue"/>
31 receiveServiceName=""/> 29
30 <!-- Default AI Config (Brainfood VLLM) -->
31 <moqui.mcp.agent.ProductStoreAiConfig
32 productStoreId="POPC_DEFAULT" aiConfigId="DEFAULT"
33 serviceTypeEnumId="AistVllm" description="Brainfood VLLM"
34 endpointUrl="http://crunchy.private.brainfood.com:11434/v1" apiKey="brainfood"
35 modelName="bf-ai" temperature="0.7" maxTokens="4096"/>
32 36
33 </entity-facade-xml> 37 </entity-facade-xml>
......
...@@ -19,6 +19,9 @@ ...@@ -19,6 +19,9 @@
19 <field name="aiConfigId" type="id"> 19 <field name="aiConfigId" type="id">
20 <description>Specific AI configuration used for this task.</description> 20 <description>Specific AI configuration used for this task.</description>
21 </field> 21 </field>
22 <field name="rootCommEventId" type="id">
23 <description>The root CommunicationEvent ID for the conversation thread.</description>
24 </field>
22 25
23 <relationship type="one" title="RequestedBy" related="mantle.party.Party"> 26 <relationship type="one" title="RequestedBy" related="mantle.party.Party">
24 <key-map field-name="requestedByPartyId" related="partyId"/> 27 <key-map field-name="requestedByPartyId" related="partyId"/>
......
1 <?xml version="1.0" encoding="UTF-8"?>
2 <secas xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-eca-3.xsd">
3 <seca id="AgentTriggerOnCommunication" service="create#mantle.party.communication.CommunicationEvent" when="post-service">
4 <condition>
5 <expression>toPartyId == 'AGENT_CLAUDE_PARTY'</expression>
6 </condition>
7 <actions>
8 <!-- Ensure rootCommEventId is set (thread tracking) -->
9 <script><![CDATA[
10 def rootId = rootCommEventId ?: communicationEventId
11 if (!rootCommEventId) {
12 ec.service.sync().name("update#mantle.party.communication.CommunicationEvent")
13 .parameters([communicationEventId: communicationEventId, rootCommEventId: rootId])
14 .call()
15 }
16
17 // Trigger Agent Turn
18 ec.service.sync().name("create#moqui.service.message.SystemMessage").parameters([
19 systemMessageTypeId: 'SmtyAgentTask',
20 statusId: 'SmsgReceived',
21 requestedByPartyId: fromPartyId,
22 effectiveUserId: ec.user.userId, // Use the actual human user ID for RBAC
23 productStoreId: 'POPC_DEFAULT',
24 aiConfigId: 'DEFAULT',
25 rootCommEventId: rootId,
26 isOutgoing: 'N'
27 ]).call()
28 ]]></script>
29 </actions>
30 </seca>
31 </secas>
...@@ -32,16 +32,13 @@ ...@@ -32,16 +32,13 @@
32 32
33 try { 33 try {
34 // 2. Switch identity to target user 34 // 2. Switch identity to target user
35 // 'false' arg means don't trigger history/visit updates for this switch
36 boolean loggedIn = ec.user.internalLoginUser(runAsUserId, false) 35 boolean loggedIn = ec.user.internalLoginUser(runAsUserId, false)
37 if (!loggedIn) throw new Exception("Could not switch to user ${runAsUserId}") 36 if (!loggedIn) throw new Exception("Could not switch to user ${runAsUserId}")
38 37
39 ec.logger.info("Agent ${agentUsername} executing ${toolName} AS ${ec.user.username} (${runAsUserId})") 38 ec.logger.info("Agent ${agentUsername} executing ${toolName} AS ${ec.user.username} (${runAsUserId})")
40 39
41 // 3. Execute Tool (Standard RBAC applies to this user) 40 // 3. Execute Tool
42 McpToolAdapter adapter = new McpToolAdapter() 41 McpToolAdapter adapter = new McpToolAdapter()
43
44 // The adapter MUST NOT disableAuthz internally for this to be secure
45 result = adapter.callTool(ec, toolName, arguments) 42 result = adapter.callTool(ec, toolName, arguments)
46 43
47 } finally { 44 } finally {
...@@ -79,7 +76,6 @@ ...@@ -79,7 +76,6 @@
79 import groovy.json.JsonBuilder 76 import groovy.json.JsonBuilder
80 import groovy.json.JsonSlurper 77 import groovy.json.JsonSlurper
81 78
82 // Construct payload
83 def payloadMap = [ 79 def payloadMap = [
84 model: model, 80 model: model,
85 messages: messages, 81 messages: messages,
...@@ -87,38 +83,27 @@ ...@@ -87,38 +83,27 @@
87 stream: false 83 stream: false
88 ] 84 ]
89 85
90 if (maxTokens) payloadMap.maxTokens = maxTokens 86 if (maxTokens) payloadMap.max_tokens = maxTokens
91 if (tools) payloadMap.tools = tools 87 if (tools) payloadMap.tools = tools
92 88
93 String jsonPayload = new JsonBuilder(payloadMap).toString() 89 String jsonPayload = new JsonBuilder(payloadMap).toString()
94 90
95 // Setup connection
96 URL url = new URL(endpointUrl + "/chat/completions") 91 URL url = new URL(endpointUrl + "/chat/completions")
97 HttpURLConnection conn = (HttpURLConnection) url.openConnection() 92 HttpURLConnection conn = (HttpURLConnection) url.openConnection()
98 conn.setRequestMethod("POST") 93 conn.setRequestMethod("POST")
99 conn.setRequestProperty("Content-Type", "application/json") 94 conn.setRequestProperty("Content-Type", "application/json")
100 if (apiKey) conn.setRequestProperty("Authorization", "Bearer " + apiKey) 95 if (apiKey) conn.setRequestProperty("Authorization", "Bearer " + apiKey)
101 conn.setDoOutput(true) 96 conn.setDoOutput(true)
102 conn.setConnectTimeout(10000) // 10s connect 97 conn.setConnectTimeout(10000)
103 conn.setReadTimeout(60000) // 60s read (LLMs are slow) 98 conn.setReadTimeout(60000)
104 99
105 try { 100 try {
106 conn.outputStream.write(jsonPayload.getBytes("UTF-8")) 101 conn.outputStream.write(jsonPayload.getBytes("UTF-8"))
107
108 httpStatus = conn.responseCode 102 httpStatus = conn.responseCode
109
110 InputStream is = (httpStatus >= 200 && httpStatus < 300) ? conn.inputStream : conn.errorStream 103 InputStream is = (httpStatus >= 200 && httpStatus < 300) ? conn.inputStream : conn.errorStream
111 String responseText = is?.text 104 String responseText = is?.text
112 105 if (responseText) response = new JsonSlurper().parseText(responseText)
113 if (responseText) { 106 if (httpStatus >= 300) error = "HTTP ${httpStatus}: ${responseText}"
114 response = new JsonSlurper().parseText(responseText)
115 }
116
117 if (httpStatus >= 300) {
118 error = "HTTP ${httpStatus}: ${responseText}"
119 ec.logger.error("OpenAI Client Error: ${error}")
120 }
121
122 } catch (Exception e) { 107 } catch (Exception e) {
123 error = e.message 108 error = e.message
124 httpStatus = 500 109 httpStatus = 500
...@@ -129,13 +114,13 @@ ...@@ -129,13 +114,13 @@
129 </service> 114 </service>
130 115
131 <!-- ========================================================= --> 116 <!-- ========================================================= -->
132 <!-- Agent Runner (The Loop) --> 117 <!-- Agent Runner (Single Turn State Machine) -->
133 <!-- ========================================================= --> 118 <!-- ========================================================= -->
134 119
135 <service verb="run" noun="AgentTask" authenticate="false"> 120 <service verb="run" noun="AgentTaskTurn" authenticate="false">
136 <description> 121 <description>
137 Processes a single Agent Task SystemMessage. 122 Processes ONE turn of an Agent Task.
138 Handles the loop of: Prompt -> LLM -> Tool Call -> Tool Execution -> Prompt. 123 Loads thread history, calls LLM, executes ONE set of tools, saves state, and re-queues if needed.
139 </description> 124 </description>
140 <in-parameters> 125 <in-parameters>
141 <parameter name="systemMessageId" required="true"/> 126 <parameter name="systemMessageId" required="true"/>
...@@ -144,147 +129,136 @@ ...@@ -144,147 +129,136 @@
144 <script><![CDATA[ 129 <script><![CDATA[
145 import groovy.json.JsonOutput 130 import groovy.json.JsonOutput
146 import groovy.json.JsonSlurper 131 import groovy.json.JsonSlurper
147 132 import org.moqui.mcp.adapter.McpToolAdapter
148 // 1. Load SystemMessage and Config 133
134 // 1. Load SystemMessage Task
149 def taskMsg = ec.entity.find("moqui.service.message.SystemMessage") 135 def taskMsg = ec.entity.find("moqui.service.message.SystemMessage")
150 .condition("systemMessageId", systemMessageId) 136 .condition("systemMessageId", systemMessageId).one()
151 .one() 137 if (!taskMsg || taskMsg.statusId != "SmsgReceived") return
152 138
153 if (!taskMsg) return
154
155 // Get AI Config 139 // Get AI Config
156 def aiConfig = ec.entity.find("moqui.mcp.agent.ProductStoreAiConfig") 140 def aiConfig = ec.entity.find("moqui.mcp.agent.ProductStoreAiConfig")
157 .condition("productStoreId", taskMsg.productStoreId) 141 .condition("productStoreId", taskMsg.productStoreId)
158 .condition("aiConfigId", taskMsg.aiConfigId) 142 .condition("aiConfigId", taskMsg.aiConfigId).one()
159 .one()
160 143
161 // Fallback to ProductStoreSetting if no specific AI Config found 144 if (!aiConfig?.endpointUrl || !aiConfig?.modelName) {
162 def endpointUrl, apiKey, modelName, temperature 145 taskMsg.statusId = "SmsgError"; taskMsg.update()
163
164 if (aiConfig) {
165 endpointUrl = aiConfig.endpointUrl
166 apiKey = aiConfig.apiKey
167 modelName = aiConfig.modelName
168 temperature = aiConfig.temperature
169 } else if (taskMsg.productStoreId) {
170 // Try ProductStoreSettings
171 def settings = ec.entity.find("mantle.product.store.ProductStoreSetting")
172 .condition("productStoreId", taskMsg.productStoreId)
173 .condition("settingTypeEnumId", ["AiEndpointUrl", "AiApiKey", "AiModelName", "AiTemperature"])
174 .list()
175
176 endpointUrl = settings.find { it.settingTypeEnumId == "AiEndpointUrl" }?.settingValue
177 apiKey = settings.find { it.settingTypeEnumId == "AiApiKey" }?.settingValue
178 modelName = settings.find { it.settingTypeEnumId == "AiModelName" }?.settingValue
179 temperature = settings.find { it.settingTypeEnumId == "AiTemperature" }?.settingValue?.toBigDecimal()
180 }
181
182 if (!endpointUrl || !modelName) {
183 ec.logger.error("No AI Configuration (Entity or Settings) found for task ${systemMessageId}")
184 taskMsg.statusId = "SmsError"
185 taskMsg.messageText = "Missing AI Configuration (Endpoint or Model)"
186 taskMsg.update()
187 return 146 return
188 } 147 }
148
149 // 2. Reconstruct Conversation History from CommunicationEvents
150 def messages = []
151 messages.add([role: "system", content: "You are a helpful Moqui ERP assistant. You act as user ${taskMsg.effectiveUserId}."])
189 152
190 // Default temperature if missing 153 if (taskMsg.rootCommEventId) {
191 if (temperature == null) temperature = 0.7 154 def threadEvents = ec.entity.find("mantle.party.communication.CommunicationEvent")
192 155 .condition("rootCommEventId", taskMsg.rootCommEventId)
193 // Filter out dangerous tools if needed? For now, we rely on RBAC delegation. 156 .orderBy("entryDate").list()
194
195 def openAiTools = moquiTools.collect { tool ->
196 [
197 type: "function",
198 function: [
199 name: tool.name,
200 description: tool.description,
201 // Helper to build schema (simplified for now, ideally strictly typed)
202 parameters: [
203 type: "object",
204 properties: [
205 path: [type: "string", description: "Screen path or resource URI"],
206 action: [type: "string", description: "Action to perform (create, update, etc)"],
207 parameters: [type: "object", description: "Key-value pairs for the action"]
208 ]
209 ]
210 ]
211 ]
212 }
213
214 // 3. Build Conversation History
215 // TODO: Load history if this is a continuation. For now, simple start.
216 def messages = [
217 [role: "system", content: "You are a helpful Moqui ERP assistant. You act as user ${taskMsg.requestedByUserId}."],
218 [role: "user", content: taskMsg.messageText]
219 ]
220
221 // 4. The Loop (Max 5 turns for safety)
222 int maxTurns = 5
223 int currentTurn = 0
224 boolean taskComplete = false
225
226 while (currentTurn < maxTurns && !taskComplete) {
227 currentTurn++
228
229 // Call LLM
230 def llmResult = ec.service.sync().name("AgentServices.call#OpenAiChatCompletion").parameters([
231 endpointUrl: aiConfig.endpointUrl,
232 apiKey: aiConfig.apiKey, // Decrypt if needed
233 model: aiConfig.modelName,
234 messages: messages,
235 tools: openAiTools,
236 temperature: aiConfig.temperature
237 ]).call()
238
239 if (llmResult.error) {
240 taskMsg.statusId = "SmsError"
241 taskMsg.messageText += "\nError: ${llmResult.error}"
242 taskMsg.update()
243 return
244 }
245
246 def responseMsg = llmResult.response.choices[0].message
247 messages.add(responseMsg) // Add assistant response to history
248 157
249 // Check for Tool Calls 158 threadEvents.each { ev ->
250 if (responseMsg.tool_calls) { 159 // Distinguish roles based on fromPartyId
251 ec.logger.info("Agent requesting ${responseMsg.tool_calls.size()} tools") 160 String role = (ev.fromPartyId == "AGENT_CLAUDE_PARTY") ? "assistant" : "user"
252 161
253 responseMsg.tool_calls.each { toolCall -> 162 // Check if it's a tool result (stored in contentType application/json)
254 def functionName = toolCall.function.name 163 if (ev.contentType == "application/json") {
255 def functionArgs = new JsonSlurper().parseText(toolCall.function.arguments) 164 def json = new JsonSlurper().parseText(ev.body)
256 def toolCallId = toolCall.id 165 if (json.tool_call_id) {
257 166 messages.add([role: "tool", tool_call_id: json.tool_call_id, content: json.content])
258 // EXECUTE TOOL via Secure Bridge 167 } else if (json.tool_calls) {
259 def executionResult = [:] 168 messages.add([role: "assistant", tool_calls: json.tool_calls])
260 try {
261 def runResult = ec.service.sync().name("AgentServices.call#McpToolWithDelegation").parameters([
262 toolName: functionName,
263 arguments: functionArgs,
264 runAsUserId: taskMsg.effectiveUserId // DELEGATION!
265 ]).call()
266
267 executionResult = runResult.result
268 } catch (Exception e) {
269 executionResult = [error: e.message]
270 } 169 }
271 170 } else {
272 // Add result to history 171 messages.add([role: role, content: ev.body])
273 messages.add([
274 role: "tool",
275 tool_call_id: toolCallId,
276 content: JsonOutput.toJson(executionResult)
277 ])
278 } 172 }
279 } else {
280 // No tool calls = Final Response
281 taskComplete = true
282 } 173 }
174 } else {
175 // Initial task message
176 messages.add([role: "user", content: taskMsg.messageText])
177 }
178
179 // 3. Prepare Tools
180 def mcpToolAdapter = new org.moqui.mcp.adapter.McpToolAdapter()
181 def moquiTools = mcpToolAdapter.listTools()
182 def openAiTools = moquiTools.collect { tool ->
183 [type: "function", function: [
184 name: tool.name, description: tool.description,
185 parameters: [type: "object", properties: [
186 path: [type: "string"], action: [type: "string"], parameters: [type: "object"]
187 ]]
188 ]]
189 }
190
191 // 4. Call LLM
192 def llmResult = ec.service.sync().name("AgentServices.call#OpenAiChatCompletion").parameters([
193 endpointUrl: aiConfig.endpointUrl, apiKey: aiConfig.apiKey,
194 model: aiConfig.modelName, messages: messages, tools: openAiTools,
195 temperature: aiConfig.temperature
196 ]).call()
197
198 if (llmResult.error) {
199 taskMsg.statusId = "SmsgError"; taskMsg.update()
200 return
201 }
202
203 def responseMsg = llmResult.response.choices[0].message
204
205 // 5. Handle Response
206 if (responseMsg.tool_calls) {
207 // SAVE Assistant "Thought" (Tool Calls)
208 def assistantComm = ec.service.sync().name("create#mantle.party.communication.CommunicationEvent").parameters([
209 fromPartyId: "AGENT_CLAUDE_PARTY", toPartyId: taskMsg.requestedByPartyId,
210 rootCommEventId: taskMsg.rootCommEventId, parentCommEventId: taskMsg.rootCommEventId,
211 communicationEventTypeId: "Message", contentType: "application/json",
212 body: JsonOutput.toJson([tool_calls: responseMsg.tool_calls]),
213 entryDate: ec.user.nowTimestamp, statusId: "CeReceived"
214 ]).call()
215
216 // EXECUTE Tools and Save Results
217 responseMsg.tool_calls.each { toolCall ->
218 def result = [:]
219 try {
220 def runResult = ec.service.sync().name("AgentServices.call#McpToolWithDelegation").parameters([
221 toolName: toolCall.function.name, arguments: new JsonSlurper().parseText(toolCall.function.arguments),
222 runAsUserId: taskMsg.effectiveUserId
223 ]).call()
224 result = runResult.result
225 } catch (Exception e) { result = [error: e.message] }
226
227 // Save Tool Result as CommEvent
228 ec.service.sync().name("create#mantle.party.communication.CommunicationEvent").parameters([
229 fromPartyId: taskMsg.requestedByPartyId, toPartyId: "AGENT_CLAUDE_PARTY",
230 rootCommEventId: taskMsg.rootCommEventId, parentCommEventId: assistantComm.communicationEventId,
231 communicationEventTypeId: "Message", contentType: "application/json",
232 body: JsonOutput.toJson([tool_call_id: toolCall.id, content: JsonOutput.toJson(result)]),
233 entryDate: ec.user.nowTimestamp, statusId: "CeReceived"
234 ]).call()
235 }
236
237 // 6. RE-QUEUE: Create next turn message
238 ec.service.sync().name("create#moqui.service.message.SystemMessage").parameters([
239 systemMessageTypeId: "SmtyAgentTask", statusId: "SmsgReceived",
240 productStoreId: taskMsg.productStoreId, aiConfigId: taskMsg.aiConfigId,
241 requestedByPartyId: taskMsg.requestedByPartyId, effectiveUserId: taskMsg.effectiveUserId,
242 rootCommEventId: taskMsg.rootCommEventId, isOutgoing: "N"
243 ]).call()
244
245 taskMsg.statusId = "SmsgConsumed"; taskMsg.update()
246
247 } else {
248 // FINAL Response
249 ec.service.sync().name("create#mantle.party.communication.CommunicationEvent").parameters([
250 fromPartyId: "AGENT_CLAUDE_PARTY", toPartyId: taskMsg.requestedByPartyId,
251 rootCommEventId: taskMsg.rootCommEventId, parentCommEventId: taskMsg.rootCommEventId,
252 communicationEventTypeId: "Message", contentType: "text/plain",
253 body: responseMsg.content, entryDate: ec.user.nowTimestamp, statusId: "CeReceived"
254 ]).call()
255
256 taskMsg.statusId = "SmsgConfirmed"; taskMsg.update()
283 } 257 }
284 ]]></script> 258 ]]></script>
285 </actions> 259 </actions>
286 </service> 260 </service>
287 261
288 <!-- ========================================================= --> 262 <!-- ========================================================= -->
289 <!-- Task Scheduler (Polls Queue) --> 263 <!-- Task Scheduler (Polls Queue) -->
290 <!-- ========================================================= --> 264 <!-- ========================================================= -->
...@@ -293,27 +267,18 @@ ...@@ -293,27 +267,18 @@
293 <description>Scheduled service to pick up pending tasks and process them.</description> 267 <description>Scheduled service to pick up pending tasks and process them.</description>
294 <actions> 268 <actions>
295 <script><![CDATA[ 269 <script><![CDATA[
296 import org.moqui.entity.EntityCondition
297
298 // Find pending tasks 270 // Find pending tasks
299 def pendingTasks = ec.entity.find("moqui.service.message.SystemMessage") 271 def pendingTasks = ec.entity.find("moqui.service.message.SystemMessage")
300 .condition("statusId", "SmsReceived") // Or generic 'Pending' 272 .condition("statusId", "SmsgReceived")
301 .condition("systemMessageTypeId", "SmtyAgentTask") 273 .condition("systemMessageTypeId", "SmtyAgentTask")
302 .limit(5) // Batch size 274 .limit(5)
303 .disableAuthz() // System service needs to see all tasks 275 .disableAuthz()
304 .list() 276 .list()
305 277
306 pendingTasks.each { task -> 278 pendingTasks.each { task ->
307 // Mark as In Progress 279 // Run Agent Task Turn
308 task.statusId = "SmsConsumed" 280 ec.service.sync().name("AgentServices.run#AgentTaskTurn")
309 task.update() 281 .parameters([systemMessageId: task.systemMessageId])
310
311 // Run Agent Task service (noStatusUpdate=false to prevent auto status change)
312 ec.service.sync().name("AgentServices.run#AgentTask")
313 .parameters([
314 systemMessageId: task.systemMessageId,
315 noStatusUpdate: false
316 ])
317 .call() 282 .call()
318 } 283 }
319 ]]></script> 284 ]]></script>
......