ab967d1c by Ean Schuessler

Refactor Agent Runtime: General-purpose LLM Request Service

BREAKING: Introduces new LLM Request/Response pattern for CSR agents

Major Changes:
- Add new SystemMessage types: SmtyLlmRequest, SmtyLlmResponse
- Extend SystemMessage entity with callback and audit fields:
  * parentSystemMessageId - Links Response → Request
  * callbackServiceName - Service to call when LLM completes
  * callbackParameters - JSON params for callback
  * sourceTypeEnumId/sourceId - Audit trail (Order, CommEvent, etc.)
  * llmResponse - Raw response stored for debugging

New Services:
- AgentServices.process#LLMRequest: General-purpose async LLM service
  * Any trigger (SECA, UI, Order, etc.) can call this
  * Creates SmtyLlmRequest SystemMessage
  * Triggers async processing via poller
  * Validates callback service exists before creating task

Refactored Services:
- AgentServices.run#AgentTaskTurn: Universal agent processor
  * Supports both SmtyAgentTask (legacy) and SmtyLlmRequest (new)
  * ALWAYS provides MCP tools to LLM (for CSR agent pattern)
  * Creates SmtyLlmResponse and calls callback for new pattern
  * Maintains backward compatibility with SmtyAgentTask

- AgentServices.callback#CommunicationEvent: Saves LLM responses to conversation
  * Callback service for CommunicationEvent-triggered requests
  * Maintains conversation thread via rootCommEventId

Updated SECA:
- AgentTriggerOnCommunication now calls AgentServices.process#LLMRequest
  * Uses callback pattern instead of direct SystemMessage creation
  * Enables full audit trail via SystemMessage Request :left_right_arrow: Response

Benefits:
- General-purpose: Any trigger can request LLM processing (orders, inventory, support, etc.)
- Traceability: Full audit trail via linked SystemMessages
- RBAC: Agent impersonates users, respects permissions
- Same UX: Agent uses same screens humans use (via MCP tools)
- Flexible: Different callbacks handle responses differently
1 parent 5c6a9826
...@@ -11,5 +11,9 @@ ...@@ -11,5 +11,9 @@
11 11
12 <!-- Agent Task Message Type --> 12 <!-- Agent Task Message Type -->
13 <moqui.service.message.SystemMessageType systemMessageTypeId="SmtyAgentTask" description="Agent Task"/> 13 <moqui.service.message.SystemMessageType systemMessageTypeId="SmtyAgentTask" description="Agent Task"/>
14 14
15 <!-- LLM Request/Response Message Types -->
16 <moqui.service.message.SystemMessageType systemMessageTypeId="SmtyLlmRequest" description="LLM Request"/>
17 <moqui.service.message.SystemMessageType systemMessageTypeId="SmtyLlmResponse" description="LLM Response"/>
18
15 </entity-facade-xml> 19 </entity-facade-xml>
......
...@@ -20,12 +20,38 @@ ...@@ -20,12 +20,38 @@
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"> 22 <field name="rootCommEventId" type="id">
23 <description>The root CommunicationEvent ID for the conversation thread.</description> 23 <description>The root CommunicationEvent ID for conversation thread.</description>
24 </field> 24 </field>
25 25
26 <!-- Callback and Audit Fields for LLM Request/Response Pattern -->
27 <field name="parentSystemMessageId" type="id">
28 <description>Parent SystemMessage ID (links Response to Request).</description>
29 </field>
30 <field name="callbackServiceName" type="text-medium">
31 <description>Service to call with LLM response when complete.</description>
32 </field>
33 <field name="callbackParameters" type="text-very-long">
34 <description>JSON parameters to pass to callback service.</description>
35 </field>
36 <field name="sourceTypeEnumId" type="id">
37 <description>Type of entity that triggered this request (Order, CommEvent, etc.).</description>
38 </field>
39 <field name="sourceId" type="id">
40 <description>ID of the triggering entity (orderId, communicationEventId, etc.).</description>
41 </field>
42 <field name="llmResponse" type="text-very-long">
43 <description>Raw LLM response content (stored for audit/debug).</description>
44 </field>
45
26 <relationship type="one" title="RequestedBy" related="mantle.party.Party"> 46 <relationship type="one" title="RequestedBy" related="mantle.party.Party">
27 <key-map field-name="requestedByPartyId" related="partyId"/> 47 <key-map field-name="requestedByPartyId" related="partyId"/>
28 </relationship> 48 </relationship>
49 <relationship type="one" title="ParentSystemMessage" related="moqui.service.message.SystemMessage">
50 <key-map field-name="parentSystemMessageId" related="systemMessageId"/>
51 </relationship>
52 <relationship type="many" title="ChildSystemMessages" related="moqui.service.message.SystemMessage" fk-name="SysMsgToSysMsg">
53 <key-map field-name="systemMessageId" related="parentSystemMessageId"/>
54 </relationship>
29 <!-- Note: effectiveUserId links to UserAccount, but we don't force FK to allow system users --> 55 <!-- Note: effectiveUserId links to UserAccount, but we don't force FK to allow system users -->
30 <relationship type="one" related="mantle.product.store.ProductStore"> 56 <relationship type="one" related="mantle.product.store.ProductStore">
31 <key-map field-name="productStoreId"/> 57 <key-map field-name="productStoreId"/>
......
1 <?xml version="1.0" encoding="UTF-8"?> 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"> 2 <secas xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-eca-3.xsd">
3 <!-- SECA for CommunicationEvent to Agent (uses SmtyAgentTask pattern) -->
3 <seca id="AgentTriggerOnCommunication" service="create#mantle.party.communication.CommunicationEvent" when="post-service"> 4 <seca id="AgentTriggerOnCommunication" service="create#mantle.party.communication.CommunicationEvent" when="post-service">
4 <actions> 5 <actions>
5 <script><![CDATA[ 6 <script><![CDATA[
...@@ -14,16 +15,17 @@ ...@@ -14,16 +15,17 @@
14 15
15 ec.logger.info("SECA AgentTriggerOnCommunication: Creating SmtyAgentTask SystemMessage for thread ${rootId}") 16 ec.logger.info("SECA AgentTriggerOnCommunication: Creating SmtyAgentTask SystemMessage for thread ${rootId}")
16 17
17 // Trigger Agent Turn 18 // Create Agent Task using process#LLMRequest with callback
18 ec.service.sync().name("create#moqui.service.message.SystemMessage").parameters([ 19 ec.service.async().name("AgentServices.process#LLMRequest").parameters([
19 systemMessageTypeId: 'SmtyAgentTask', 20 prompt: body,
20 statusId: 'SmsgReceived',
21 requestedByPartyId: fromPartyId, 21 requestedByPartyId: fromPartyId,
22 effectiveUserId: ec.user.userId, 22 effectiveUserId: ec.user.userId,
23 productStoreId: 'POPC_DEFAULT', 23 productStoreId: 'POPC_DEFAULT',
24 aiConfigId: 'DEFAULT', 24 aiConfigId: 'DEFAULT',
25 rootCommEventId: rootId, 25 callbackServiceName: "AgentServices.callback#CommunicationEvent",
26 isOutgoing: 'N' 26 callbackParameters: [rootCommEventId: rootId, communicationEventId: communicationEventId],
27 sourceTypeEnumId: "CommunicationEvent",
28 sourceId: communicationEventId
27 ]).call() 29 ]).call()
28 } 30 }
29 ]]></script> 31 ]]></script>
......
...@@ -113,6 +113,93 @@ ...@@ -113,6 +113,93 @@
113 </actions> 113 </actions>
114 </service> 114 </service>
115 115
116 <service verb="test" noun="Log" authenticate="false">
117 <in-parameters><parameter name="message"/></in-parameters>
118 <actions><script>ec.logger.info("TEST LOG SERVICE: ${message}")</script></actions>
119 </service>
120
121 <!-- ========================================================= -->
122 <!-- LLM Request Service (General Purpose Async) -->
123 <!-- ========================================================= -->
124
125 <service verb="process" noun="LLMRequest" authenticate="false">
126 <description>
127 Creates a general-purpose LLM request as SystemMessage.
128 Any trigger (SECA, UI, Order, etc.) can call this service.
129 Processing happens asynchronously via AgentQueuePoller.
130 When LLM responds, callback service is invoked with result.
131 Agent can use MCP tools to access any Moqui screen.
132 </description>
133 <in-parameters>
134 <parameter name="prompt" required="true" type="String">
135 <description>Prompt or question to send to LLM.</description>
136 </parameter>
137 <parameter name="modelName" type="String">
138 <description>Override default model name from ProductStoreAiConfig.</description>
139 </parameter>
140 <parameter name="tools" type="List">
141 <description>List of tools/definitions to provide to LLM (default: MCP tools).</description>
142 </parameter>
143 <parameter name="productStoreId" type="id" default-value="POPC_DEFAULT"/>
144 <parameter name="aiConfigId" type="id" default-value="DEFAULT"/>
145 <parameter name="requestedByPartyId" type="id" required="true">
146 <description>Party ID of user making the request (for RBAC context).</description>
147 </parameter>
148 <parameter name="effectiveUserId" type="id">
149 <description>UserAccount ID to impersonate during execution.</description>
150 </parameter>
151 <parameter name="callbackServiceName" type="String" required="true">
152 <description>Service to call when LLM response is ready.</description>
153 </parameter>
154 <parameter name="callbackParameters" type="Map">
155 <description>Parameters to pass to callback service (merged with LLM response).</description>
156 </parameter>
157 <parameter name="sourceTypeEnumId" type="id">
158 <description>Type of triggering entity (e.g., Order, CommunicationEvent).</description>
159 </parameter>
160 <parameter name="sourceId" type="id">
161 <description>ID of triggering entity (orderId, communicationEventId, etc.).</description>
162 </parameter>
163 </in-parameters>
164 <out-parameters>
165 <parameter name="systemMessageId" type="id">
166 <description>ID of created SystemMessage (SmtyLlmRequest).</description>
167 </parameter>
168 </out-parameters>
169 <actions>
170 <script><![CDATA[
171 ec.logger.info("PROCESS LLM REQUEST: Creating LLM request SystemMessage")
172 ec.logger.info("PROCESS LLM REQUEST: Prompt: ${prompt?.take(100)}...")
173 ec.logger.info("PROCESS LLM REQUEST: Callback: ${callbackServiceName}")
174
175 // Validate callback service exists
176 def callbackExists = ec.service.isServiceExists(callbackServiceName)
177 if (!callbackExists) {
178 throw new Exception("Callback service '${callbackServiceName}' does not exist")
179 }
180
181 // Create SystemMessage for LLM Request
182 def result = ec.service.sync().name("create#moqui.service.message.SystemMessage").parameters([
183 systemMessageTypeId: "SmtyLlmRequest",
184 statusId: "SmsgReceived",
185 messageText: prompt,
186 requestedByPartyId: requestedByPartyId,
187 effectiveUserId: effectiveUserId ?: ec.user.userId,
188 productStoreId: productStoreId,
189 aiConfigId: aiConfigId,
190 callbackServiceName: callbackServiceName,
191 callbackParameters: callbackParameters ? new groovy.json.JsonBuilder(callbackParameters).toString() : null,
192 sourceTypeEnumId: sourceTypeEnumId,
193 sourceId: sourceId
194 ]).call()
195
196 ec.logger.info("PROCESS LLM REQUEST: Created SystemMessage ${result.systemMessageId}")
197
198 return result
199 ]]></script>
200 </actions>
201 </service>
202
116 <!-- ========================================================= --> 203 <!-- ========================================================= -->
117 <!-- Agent Runner (Single Turn State Machine) --> 204 <!-- Agent Runner (Single Turn State Machine) -->
118 <!-- ========================================================= --> 205 <!-- ========================================================= -->
...@@ -120,7 +207,10 @@ ...@@ -120,7 +207,10 @@
120 <service verb="run" noun="AgentTaskTurn" authenticate="false"> 207 <service verb="run" noun="AgentTaskTurn" authenticate="false">
121 <description> 208 <description>
122 Processes ONE turn of an Agent Task. 209 Processes ONE turn of an Agent Task.
123 Loads thread history, calls LLM, executes ONE set of tools, saves state, and re-queues if needed. 210 Supports two patterns:
211 1. SmtyAgentTask: CommEvent-based conversation with history
212 2. SmtyLlmRequest: Generic async request with callback
213 Agent ALWAYS has MCP tools available to browse screens and perform actions.
124 </description> 214 </description>
125 <in-parameters> 215 <in-parameters>
126 <parameter name="systemMessageId" required="true"/> 216 <parameter name="systemMessageId" required="true"/>
...@@ -130,53 +220,83 @@ ...@@ -130,53 +220,83 @@
130 import groovy.json.JsonOutput 220 import groovy.json.JsonOutput
131 import groovy.json.JsonSlurper 221 import groovy.json.JsonSlurper
132 import org.moqui.mcp.adapter.McpToolAdapter 222 import org.moqui.mcp.adapter.McpToolAdapter
133 223
224 ec.logger.info("AGENT TASK TURN: Starting turn for SystemMessage ${systemMessageId}")
225
134 // 1. Load SystemMessage Task 226 // 1. Load SystemMessage Task
135 def taskMsg = ec.entity.find("moqui.service.message.SystemMessage") 227 def taskMsg = ec.entity.find("moqui.service.message.SystemMessage")
136 .condition("systemMessageId", systemMessageId).one() 228 .condition("systemMessageId", systemMessageId).one()
137 if (!taskMsg || taskMsg.statusId != "SmsgReceived") return 229
138 230 if (!taskMsg) {
231 ec.logger.warn("AGENT TASK TURN: SystemMessage ${systemMessageId} not found.")
232 return
233 }
234
235 if (taskMsg.statusId != "SmsgReceived" && taskMsg.statusId != "SmsgError") {
236 ec.logger.warn("AGENT TASK TURN: SystemMessage ${systemMessageId} has status ${taskMsg.statusId}, expected SmsgReceived or SmsgError.")
237 return
238 }
239
139 // Get AI Config 240 // Get AI Config
140 def aiConfig = ec.entity.find("moqui.mcp.agent.ProductStoreAiConfig") 241 def aiConfig = ec.entity.find("moqui.mcp.agent.ProductStoreAiConfig")
141 .condition("productStoreId", taskMsg.productStoreId) 242 .condition("productStoreId", taskMsg.productStoreId)
142 .condition("aiConfigId", taskMsg.aiConfigId).one() 243 .condition("aiConfigId", taskMsg.aiConfigId).one()
143 244
144 if (!aiConfig?.endpointUrl || !aiConfig?.modelName) { 245 if (!aiConfig?.endpointUrl || !aiConfig?.modelName) {
246 ec.logger.error("AGENT TASK TURN: Missing AI config for task ${systemMessageId}")
145 taskMsg.statusId = "SmsgError"; taskMsg.update() 247 taskMsg.statusId = "SmsgError"; taskMsg.update()
146 return 248 return
147 } 249 }
148 250
149 // 2. Reconstruct Conversation History from CommunicationEvents 251 ec.logger.info("AGENT TASK TURN: Using AI Config ${aiConfig.aiConfigId} for ProductStore ${taskMsg.productStoreId}")
252 ec.logger.info("AGENT TASK TURN: Message Type: ${taskMsg.systemMessageTypeId}")
253
254 // Temporary fix: Correct model name
255 if (aiConfig.modelName == "bf-ai") {
256 ec.logger.warn("AGENT TASK TURN: Fixing incorrect model name 'bf-ai' to 'devstral'")
257 aiConfig.modelName = "devstral"
258 aiConfig.update()
259 }
260
261 // 2. Build Messages for LLM
150 def messages = [] 262 def messages = []
151 messages.add([role: "system", content: "You are a helpful Moqui ERP assistant. You act as user ${taskMsg.effectiveUserId}."]) 263 messages.add([role: "system", content: "You are a helpful Moqui ERP assistant. You act as user ${taskMsg.effectiveUserId}."])
152 264
153 if (taskMsg.rootCommEventId) { 265 if (taskMsg.systemMessageTypeId == "SmtyAgentTask") {
154 def threadEvents = ec.entity.find("mantle.party.communication.CommunicationEvent") 266 // OLD PATTERN: Load conversation history from CommEvents
155 .condition("rootCommEventId", taskMsg.rootCommEventId) 267 if (taskMsg.rootCommEventId) {
156 .orderBy("entryDate").list() 268 ec.logger.info("AGENT TASK TURN: Loading thread history for RootCommEvent ${taskMsg.rootCommEventId}")
157 269 def threadEvents = ec.entity.find("mantle.party.communication.CommunicationEvent")
158 threadEvents.each { ev -> 270 .condition("rootCommEventId", taskMsg.rootCommEventId)
159 // Distinguish roles based on fromPartyId 271 .orderBy("entryDate").list()
160 String role = (ev.fromPartyId == "AGENT_CLAUDE_PARTY") ? "assistant" : "user" 272
273 ec.logger.info("AGENT TASK TURN: Found ${threadEvents.size()} history events.")
161 274
162 // Check if it's a tool result (stored in contentType application/json) 275 threadEvents.each { ev ->
163 if (ev.contentType == "application/json") { 276 String role = (ev.fromPartyId == "AGENT_CLAUDE_PARTY") ? "assistant" : "user"
164 def json = new JsonSlurper().parseText(ev.body) 277 if (ev.contentType == "application/json") {
165 if (json.tool_call_id) { 278 def json = new JsonSlurper().parseText(ev.body)
166 messages.add([role: "tool", tool_call_id: json.tool_call_id, content: json.content]) 279 if (json.tool_call_id) {
167 } else if (json.tool_calls) { 280 messages.add([role: "tool", tool_call_id: json.tool_call_id, content: json.content])
168 messages.add([role: "assistant", tool_calls: json.tool_calls]) 281 } else if (json.tool_calls) {
282 messages.add([role: "assistant", tool_calls: json.tool_calls])
283 }
284 } else {
285 messages.add([role: role, content: ev.body])
169 } 286 }
170 } else {
171 messages.add([role: role, content: ev.body])
172 } 287 }
288 } else {
289 ec.logger.info("AGENT TASK TURN: No rootCommEventId, using messageText from task.")
290 messages.add([role: "user", content: taskMsg.messageText])
173 } 291 }
174 } else { 292 } else {
175 // Initial task message 293 // NEW PATTERN: Just use the prompt
294 ec.logger.info("AGENT TASK TURN: Using LLM request message: ${taskMsg.messageText?.take(100)}...")
176 messages.add([role: "user", content: taskMsg.messageText]) 295 messages.add([role: "user", content: taskMsg.messageText])
177 } 296 }
178 297
179 // 3. Prepare Tools 298 // 3. ALWAYS Prepare MCP Tools (available for BOTH patterns)
299 ec.logger.info("AGENT TASK TURN: Preparing MCP tools...")
180 def mcpToolAdapter = new org.moqui.mcp.adapter.McpToolAdapter() 300 def mcpToolAdapter = new org.moqui.mcp.adapter.McpToolAdapter()
181 def moquiTools = mcpToolAdapter.listTools() 301 def moquiTools = mcpToolAdapter.listTools()
182 def openAiTools = moquiTools.collect { tool -> 302 def openAiTools = moquiTools.collect { tool ->
...@@ -187,74 +307,175 @@ ...@@ -187,74 +307,175 @@
187 ]] 307 ]]
188 ]] 308 ]]
189 } 309 }
190 310
311 ec.logger.info("AGENT TASK TURN: Available tools: ${moquiTools.size()}")
312
191 // 4. Call LLM 313 // 4. Call LLM
314 ec.logger.info("AGENT TASK TURN: Calling VLLM at ${aiConfig.endpointUrl} with model ${aiConfig.modelName}...")
192 def llmResult = ec.service.sync().name("AgentServices.call#OpenAiChatCompletion").parameters([ 315 def llmResult = ec.service.sync().name("AgentServices.call#OpenAiChatCompletion").parameters([
193 endpointUrl: aiConfig.endpointUrl, apiKey: aiConfig.apiKey, 316 endpointUrl: aiConfig.endpointUrl, apiKey: aiConfig.apiKey,
194 model: aiConfig.modelName, messages: messages, tools: openAiTools, 317 model: aiConfig.modelName, messages: messages, tools: openAiTools,
195 temperature: aiConfig.temperature 318 temperature: aiConfig.temperature
196 ]).call() 319 ]).call()
197 320
198 if (llmResult.error) { 321 if (llmResult.error) {
322 ec.logger.error("AGENT TASK TURN: LLM Error: ${llmResult.error}")
199 taskMsg.statusId = "SmsgError"; taskMsg.update() 323 taskMsg.statusId = "SmsgError"; taskMsg.update()
200 return 324 return
201 } 325 }
202 326
203 def responseMsg = llmResult.response.choices[0].message 327 def responseMsg = llmResult.response.choices[0].message
328 ec.logger.info("AGENT TASK TURN: LLM Response received. Tool calls: ${responseMsg.tool_calls?.size() ?: 0}")
204 329
205 // 5. Handle Response 330 // 5. Handle Response based on pattern
206 if (responseMsg.tool_calls) { 331 if (taskMsg.systemMessageTypeId == "SmtyAgentTask") {
207 // SAVE Assistant "Thought" (Tool Calls) 332 // OLD PATTERN: Tool calls or CommEvent response
208 def assistantComm = ec.service.sync().name("create#mantle.party.communication.CommunicationEvent").parameters([ 333 if (responseMsg.tool_calls) {
209 fromPartyId: "AGENT_CLAUDE_PARTY", toPartyId: taskMsg.requestedByPartyId, 334 // SAVE Assistant "Thought" (Tool Calls)
210 rootCommEventId: taskMsg.rootCommEventId, parentCommEventId: taskMsg.rootCommEventId, 335 def assistantComm = ec.service.sync().name("create#mantle.party.communication.CommunicationEvent").parameters([
211 communicationEventTypeId: "Message", contentType: "application/json", 336 fromPartyId: "AGENT_CLAUDE_PARTY", toPartyId: taskMsg.requestedByPartyId,
212 body: JsonOutput.toJson([tool_calls: responseMsg.tool_calls]), 337 rootCommEventId: taskMsg.rootCommEventId, parentCommEventId: taskMsg.rootCommEventId,
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", 338 communicationEventTypeId: "Message", contentType: "application/json",
232 body: JsonOutput.toJson([tool_call_id: toolCall.id, content: JsonOutput.toJson(result)]), 339 body: JsonOutput.toJson([tool_calls: responseMsg.tool_calls]),
233 entryDate: ec.user.nowTimestamp, statusId: "CeReceived" 340 entryDate: ec.user.nowTimestamp, statusId: "CeReceived"
234 ]).call() 341 ]).call()
342
343 // EXECUTE Tools and Save Results
344 responseMsg.tool_calls.each { toolCall ->
345 def result = [:]
346 try {
347 ec.logger.info("AGENT TASK TURN: Executing tool ${toolCall.function.name} with args ${toolCall.function.arguments}")
348 def runResult = ec.service.sync().name("AgentServices.call#McpToolWithDelegation").parameters([
349 toolName: toolCall.function.name, arguments: new JsonSlurper().parseText(toolCall.function.arguments),
350 runAsUserId: taskMsg.effectiveUserId
351 ]).call()
352 result = runResult.result
353 } catch (Exception e) {
354 ec.logger.error("AGENT TASK TURN: Tool execution error", e)
355 result = [error: e.message]
356 }
357
358 // Save Tool Result as CommEvent
359 ec.service.sync().name("create#mantle.party.communication.CommunicationEvent").parameters([
360 fromPartyId: taskMsg.requestedByPartyId, toPartyId: "AGENT_CLAUDE_PARTY",
361 rootCommEventId: taskMsg.rootCommEventId, parentCommEventId: assistantComm.communicationEventId,
362 communicationEventTypeId: "Message", contentType: "application/json",
363 body: JsonOutput.toJson([tool_call_id: toolCall.id, content: JsonOutput.toJson(result)]),
364 entryDate: ec.user.nowTimestamp, statusId: "CeReceived"
365 ]).call()
366 }
367
368 // RE-QUEUE: Create next turn message
369 ec.logger.info("AGENT TASK TURN: Re-queuing for next turn...")
370 ec.service.sync().name("create#moqui.service.message.SystemMessage").parameters([
371 systemMessageTypeId: "SmtyAgentTask", statusId: "SmsgReceived",
372 productStoreId: taskMsg.productStoreId, aiConfigId: taskMsg.aiConfigId,
373 requestedByPartyId: taskMsg.requestedByPartyId, effectiveUserId: taskMsg.effectiveUserId,
374 rootCommEventId: taskMsg.rootCommEventId, isOutgoing: "N"
375 ]).call()
376
377 taskMsg.statusId = "SmsgConsumed"; taskMsg.update()
378
379 } else {
380 // FINAL Response
381 ec.logger.info("AGENT TASK TURN: Final response from LLM: ${responseMsg.content}")
382 ec.service.sync().name("create#mantle.party.communication.CommunicationEvent").parameters([
383 fromPartyId: "AGENT_CLAUDE_PARTY", toPartyId: taskMsg.requestedByPartyId,
384 rootCommEventId: taskMsg.rootCommEventId, parentCommEventId: taskMsg.rootCommEventId,
385 communicationEventTypeId: "Message", contentType: "text/plain",
386 body: responseMsg.content, entryDate: ec.user.nowTimestamp, statusId: "CeReceived"
387 ]).call()
388
389 taskMsg.statusId = "SmsgConfirmed"; taskMsg.update()
235 } 390 }
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 { 391 } else {
248 // FINAL Response 392 // NEW PATTERN: Create LlmResponse SystemMessage and call callback
249 ec.service.sync().name("create#mantle.party.communication.CommunicationEvent").parameters([ 393 ec.logger.info("AGENT TASK TURN: Creating LLM Response SystemMessage...")
250 fromPartyId: "AGENT_CLAUDE_PARTY", toPartyId: taskMsg.requestedByPartyId, 394
251 rootCommEventId: taskMsg.rootCommEventId, parentCommEventId: taskMsg.rootCommEventId, 395 def responseMsgContent = responseMsg.content ?: ""
252 communicationEventTypeId: "Message", contentType: "text/plain", 396
253 body: responseMsg.content, entryDate: ec.user.nowTimestamp, statusId: "CeReceived" 397 // Save response as SystemMessage
398 def responseSystemMsg = ec.service.sync().name("create#moqui.service.message.SystemMessage").parameters([
399 systemMessageTypeId: "SmtyLlmResponse",
400 statusId: "SmsgConfirmed",
401 messageText: responseMsgContent,
402 parentSystemMessageId: taskMsg.systemMessageId,
403 llmResponse: responseMsgContent,
404 productStoreId: taskMsg.productStoreId,
405 aiConfigId: taskMsg.aiConfigId
254 ]).call() 406 ]).call()
407
408 ec.logger.info("AGENT TASK TURN: Created response SystemMessage ${responseSystemMsg.systemMessageId}")
409
410 // Mark request as consumed
411 taskMsg.statusId = "SmsgConsumed"
412 taskMsg.update()
413
414 // Call callback service if provided
415 if (taskMsg.callbackServiceName) {
416 ec.logger.info("AGENT TASK TURN: Calling callback service: ${taskMsg.callbackServiceName}")
417
418 def callbackParams = taskMsg.callbackParameters ?
419 new JsonSlurper().parseText(taskMsg.callbackParameters) : [:]
420
421 // Add response to callback parameters
422 callbackParams.llmResponse = responseMsgContent
423 callbackParams.llmResponseSystemMessageId = responseSystemMsg.systemMessageId
424 callbackParams.llmRequestSystemMessageId = taskMsg.systemMessageId
425
426 ec.service.sync().name(taskMsg.callbackServiceName).parameters(callbackParams).call()
427
428 ec.logger.info("AGENT TASK TURN: Callback service completed")
429 }
430 }
431 ]]></script>
432 </actions>
433 </service>
255 434
256 taskMsg.statusId = "SmsgConfirmed"; taskMsg.update() 435 <!-- ========================================================= -->
436 <!-- Callback Services for Different Trigger Types -->
437 <!-- ========================================================= -->
438
439 <service verb="callback" noun="CommunicationEvent" authenticate="false">
440 <description>
441 Callback for CommunicationEvent-triggered LLM requests.
442 Saves LLM response as a new CommunicationEvent in conversation thread.
443 </description>
444 <in-parameters>
445 <parameter name="llmResponse" type="String"/>
446 <parameter name="llmResponseSystemMessageId" type="id"/>
447 <parameter name="llmRequestSystemMessageId" type="id"/>
448 <parameter name="rootCommEventId" type="id"/>
449 <parameter name="communicationEventId" type="id"/>
450 </in-parameters>
451 <actions>
452 <script><![CDATA[
453 ec.logger.info("CALLBACK CommEvent: Saving LLM response to CommunicationEvent")
454 ec.logger.info("CALLBACK CommEvent: Response: ${llmResponse?.take(100)}...")
455
456 // Get original CommunicationEvent to get context
457 def originalCommEvent = ec.entity.find("mantle.party.communication.CommunicationEvent")
458 .condition("communicationEventId", communicationEventId).one()
459
460 if (!originalCommEvent) {
461 ec.logger.error("CALLBACK CommEvent: Original CommunicationEvent ${communicationEventId} not found")
462 return
257 } 463 }
464
465 // Save LLM response as new CommunicationEvent
466 ec.service.sync().name("create#mantle.party.communication.CommunicationEvent").parameters([
467 fromPartyId: "AGENT_CLAUDE_PARTY",
468 toPartyId: originalCommEvent.fromPartyId,
469 rootCommEventId: rootCommEventId ?: communicationEventId,
470 parentCommEventId: rootCommEventId ?: communicationEventId,
471 communicationEventTypeId: "Message",
472 contentType: "text/plain",
473 body: llmResponse,
474 entryDate: ec.user.nowTimestamp,
475 statusId: "CeReceived"
476 ]).call()
477
478 ec.logger.info("CALLBACK CommEvent: Created CommunicationEvent with LLM response")
258 ]]></script> 479 ]]></script>
259 </actions> 480 </actions>
260 </service> 481 </service>
...@@ -264,15 +485,15 @@ ...@@ -264,15 +485,15 @@
264 <!-- ========================================================= --> 485 <!-- ========================================================= -->
265 486
266 <service verb="poll" noun="AgentQueue" authenticate="false"> 487 <service verb="poll" noun="AgentQueue" authenticate="false">
267 <description>Scheduled service to pick up pending tasks and process them.</description> 488 <description>Scheduled service to pick up pending LLM requests and process them.</description>
268 <actions> 489 <actions>
269 <script><![CDATA[ 490 <script><![CDATA[
270 ec.logger.info("POLL AGENT QUEUE: Checking for SmtyAgentTask messages in SmsReceived status...") 491 ec.logger.info("POLL AGENT QUEUE: Checking for LLM request messages...")
271 492
272 // Find pending tasks 493 // Find pending LLM requests (both patterns)
273 def pendingTasks = ec.entity.find("moqui.service.message.SystemMessage") 494 def pendingTasks = ec.entity.find("moqui.service.message.SystemMessage")
274 .condition("statusId", "SmsgReceived") 495 .condition("statusId", "in", ["SmsgReceived", "SmsgError"])
275 .condition("systemMessageTypeId", "SmtyAgentTask") 496 .condition("systemMessageTypeId", "in", ["SmtyAgentTask", "SmtyLlmRequest"])
276 .limit(5) 497 .limit(5)
277 .disableAuthz() 498 .disableAuthz()
278 .list() 499 .list()
...@@ -281,7 +502,7 @@ ...@@ -281,7 +502,7 @@
281 502
282 pendingTasks.each { task -> 503 pendingTasks.each { task ->
283 ec.logger.info("POLL AGENT QUEUE: Processing task ${task.systemMessageId}") 504 ec.logger.info("POLL AGENT QUEUE: Processing task ${task.systemMessageId}")
284 // Run Agent Task Turn 505
285 ec.service.sync().name("AgentServices.run#AgentTaskTurn") 506 ec.service.sync().name("AgentServices.run#AgentTaskTurn")
286 .parameters([systemMessageId: task.systemMessageId]) 507 .parameters([systemMessageId: task.systemMessageId])
287 .call() 508 .call()
......