Add agent runtime infrastructure
- AgentEntities.xml: Entity extensions for agent tasks - AgentServices.xml: Agent runner, client, and queue services - AgentData.xml: Scheduled job, user, and AI config seed data
Showing
3 changed files
with
475 additions
and
0 deletions
data/AgentData.xml
0 → 100644
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 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"> | ||
| 4 | |||
| 5 | <!-- ========================================================= --> | ||
| 6 | <!-- Agent Runtime Scheduled Job --> | ||
| 7 | <!-- ========================================================= --> | ||
| 8 | |||
| 9 | <moqui.service.ScheduledJob | ||
| 10 | jobName="AgentQueuePoller" | ||
| 11 | description="Polls Agent Queue and processes pending tasks" | ||
| 12 | serviceName="AgentServices.poll#AgentQueue" | ||
| 13 | cronExpression="0/30 * * * * ?" | ||
| 14 | runAsUser="ADMIN" | ||
| 15 | paused="N"/> | ||
| 16 | |||
| 17 | <!-- ========================================================= --> | ||
| 18 | <!-- Agent User Account (for authentication) --> | ||
| 19 | <!-- ========================================================= --> | ||
| 20 | |||
| 21 | <moqui.security.UserAccount | ||
| 22 | userId="AGENT_CLAUDE" | ||
| 23 | username="agent-claude" | ||
| 24 | currentPassword="16ac58bbfa332c1c55bd98b53e60720bfa90d394" | ||
| 25 | passwordHashType="SHA" | ||
| 26 | enabled="Y" | ||
| 27 | description="Agent user for AI runtime"/> | ||
| 28 | |||
| 29 | <moqui.security.UserGroup userGroupId="AgentUsers" description="AI Agent Users"/> | ||
| 30 | <moqui.security.UserGroupMember userGroupId="AgentUsers" userId="AGENT_CLAUDE" fromDate="2026-02-04 00:00:00.000"/> | ||
| 31 | |||
| 32 | <!-- Agent users have permission to execute the delegation service --> | ||
| 33 | <moqui.security.ArtifactGroup artifactGroupId="AgentDelegationServices" description="Agent Tool Delegation Services"/> | ||
| 34 | <moqui.security.ArtifactGroupMember artifactGroupId="AgentDelegationServices" artifactName="AgentServices.call#McpToolWithDelegation" artifactTypeEnumId="AT_SERVICE"/> | ||
| 35 | <moqui.security.ArtifactAuthz userGroupId="AgentUsers" artifactGroupId="AgentDelegationServices" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/> | ||
| 36 | |||
| 37 | <!-- ========================================================= --> | ||
| 38 | <!-- Sample AI Configuration (for testing) --> | ||
| 39 | <!-- ========================================================= --> | ||
| 40 | |||
| 41 | <!-- Using localhost:11434 for Ollama (if available) --> | ||
| 42 | <!-- Or configure for your VLLM/OpenAI endpoint --> | ||
| 43 | <moqui.mcp.agent.ProductStoreAiConfig | ||
| 44 | productStoreId="POPCOMMERCE_RETAIL" | ||
| 45 | aiConfigId="DEFAULT" | ||
| 46 | serviceTypeEnumId="AistOllama" | ||
| 47 | description="Default AI Config for Testing (Ollama)" | ||
| 48 | endpointUrl="http://localhost:11434/v1" | ||
| 49 | modelName="llama3.2:3b" | ||
| 50 | temperature="0.7" | ||
| 51 | maxTokens="2048" | ||
| 52 | /> | ||
| 53 | |||
| 54 | </entity-facade-xml> |
entity/AgentEntities.xml
0 → 100644
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| 3 | xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/entity-definition-3.xsd"> | ||
| 4 | |||
| 5 | <!-- ========================================================= --> | ||
| 6 | <!-- System Message Extensions for Agent Tasks --> | ||
| 7 | <!-- ========================================================= --> | ||
| 8 | |||
| 9 | <extend-entity entity-name="SystemMessage" package="moqui.service.message"> | ||
| 10 | <field name="requestedByPartyId" type="id"> | ||
| 11 | <description>The human user (Party) who requested this agent task.</description> | ||
| 12 | </field> | ||
| 13 | <field name="effectiveUserId" type="id"> | ||
| 14 | <description>The UserAccount ID to impersonate during execution (RBAC context).</description> | ||
| 15 | </field> | ||
| 16 | <field name="productStoreId" type="id"> | ||
| 17 | <description>The context ProductStore for this task (determines AI config).</description> | ||
| 18 | </field> | ||
| 19 | <field name="aiConfigId" type="id"> | ||
| 20 | <description>Specific AI configuration used for this task.</description> | ||
| 21 | </field> | ||
| 22 | |||
| 23 | <relationship type="one" title="RequestedBy" related="mantle.party.Party"> | ||
| 24 | <key-map field-name="requestedByPartyId" related="partyId"/> | ||
| 25 | </relationship> | ||
| 26 | <!-- Note: effectiveUserId links to UserAccount, but we don't force FK to allow system users --> | ||
| 27 | <relationship type="one" related="mantle.product.store.ProductStore"> | ||
| 28 | <key-map field-name="productStoreId"/> | ||
| 29 | </relationship> | ||
| 30 | <relationship type="one" related="moqui.mcp.agent.ProductStoreAiConfig"> | ||
| 31 | <key-map field-name="productStoreId"/> | ||
| 32 | <key-map field-name="aiConfigId"/> | ||
| 33 | </relationship> | ||
| 34 | </extend-entity> | ||
| 35 | |||
| 36 | <!-- ========================================================= --> | ||
| 37 | <!-- AI Gateway Configuration (Per Store) --> | ||
| 38 | <!-- ========================================================= --> | ||
| 39 | |||
| 40 | <entity entity-name="ProductStoreAiConfig" package="moqui.mcp.agent"> | ||
| 41 | <description>Configures AI Service Providers (OpenAI, VLLM, etc.) per Product Store.</description> | ||
| 42 | |||
| 43 | <field name="productStoreId" type="id" is-pk="true"/> | ||
| 44 | <field name="aiConfigId" type="id" is-pk="true"/> | ||
| 45 | |||
| 46 | <field name="serviceTypeEnumId" type="id"/> | ||
| 47 | <field name="description" type="text-medium"/> | ||
| 48 | |||
| 49 | <!-- Connection Details --> | ||
| 50 | <field name="endpointUrl" type="text-medium"/> | ||
| 51 | <field name="apiKey" type="text-medium" encrypt="true"/> | ||
| 52 | <field name="organizationId" type="text-short"/> | ||
| 53 | |||
| 54 | <!-- Model Parameters --> | ||
| 55 | <field name="modelName" type="text-short"/> | ||
| 56 | <field name="temperature" type="number-decimal"/> | ||
| 57 | <field name="maxTokens" type="number-integer"/> | ||
| 58 | |||
| 59 | <!-- System Prompt Template --> | ||
| 60 | <field name="systemMessageId" type="id"> | ||
| 61 | <description>Template for the system prompt (instruction set)</description> | ||
| 62 | </field> | ||
| 63 | |||
| 64 | <relationship type="one" related="mantle.product.store.ProductStore"> | ||
| 65 | <key-map field-name="productStoreId"/> | ||
| 66 | </relationship> | ||
| 67 | <relationship type="one" title="AiServiceType" related="moqui.basic.Enumeration"> | ||
| 68 | <key-map field-name="serviceTypeEnumId" related="enumId"/> | ||
| 69 | </relationship> | ||
| 70 | <relationship type="one" title="PromptTemplate" related="moqui.service.message.SystemMessage"> | ||
| 71 | <key-map field-name="systemMessageId"/> | ||
| 72 | </relationship> | ||
| 73 | |||
| 74 | <seed-data> | ||
| 75 | <!-- AI Service Types --> | ||
| 76 | <moqui.basic.EnumerationType description="AI Service Type" enumTypeId="AiServiceType"/> | ||
| 77 | <moqui.basic.Enumeration enumId="AistOpenAi" description="OpenAI" enumTypeId="AiServiceType"/> | ||
| 78 | <moqui.basic.Enumeration enumId="AistVllm" description="VLLM (OpenAI Compatible)" enumTypeId="AiServiceType"/> | ||
| 79 | <moqui.basic.Enumeration enumId="AistAnthropic" description="Anthropic" enumTypeId="AiServiceType"/> | ||
| 80 | <moqui.basic.Enumeration enumId="AistOllama" description="Ollama" enumTypeId="AiServiceType"/> | ||
| 81 | |||
| 82 | <!-- Agent Task Message Type --> | ||
| 83 | <moqui.basic.Enumeration enumId="SmtyAgentTask" description="Agent Task" enumTypeId="SystemMessageType"/> | ||
| 84 | |||
| 85 | <!-- AI Product Store Settings --> | ||
| 86 | <moqui.basic.Enumeration enumId="AiEndpointUrl" description="AI Endpoint URL" enumTypeId="ProductStoreSettingType"/> | ||
| 87 | <moqui.basic.Enumeration enumId="AiApiKey" description="AI API Key" enumTypeId="ProductStoreSettingType"/> | ||
| 88 | <moqui.basic.Enumeration enumId="AiModelName" description="AI Model Name" enumTypeId="ProductStoreSettingType"/> | ||
| 89 | <moqui.basic.Enumeration enumId="AiTemperature" description="AI Temperature" enumTypeId="ProductStoreSettingType"/> | ||
| 90 | </seed-data> | ||
| 91 | </entity> | ||
| 92 | |||
| 93 | </entities> |
service/AgentServices.xml
0 → 100644
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <services xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| 3 | xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-definition-3.xsd"> | ||
| 4 | |||
| 5 | <!-- ========================================================= --> | ||
| 6 | <!-- Agent Tool Bridge (The Secure Gateway) --> | ||
| 7 | <!-- ========================================================= --> | ||
| 8 | |||
| 9 | <service verb="call" noun="McpToolWithDelegation"> | ||
| 10 | <description> | ||
| 11 | Securely executes an MCP tool by impersonating the target user (runAsUserId). | ||
| 12 | The calling agent must have permission to use this service, but the | ||
| 13 | tool execution itself is subject to the target user's permissions. | ||
| 14 | </description> | ||
| 15 | <in-parameters> | ||
| 16 | <parameter name="toolName" required="true"/> | ||
| 17 | <parameter name="arguments" type="Map"/> | ||
| 18 | <parameter name="runAsUserId" required="true"> | ||
| 19 | <description>The UserAccount ID to impersonate.</description> | ||
| 20 | </parameter> | ||
| 21 | </in-parameters> | ||
| 22 | <out-parameters> | ||
| 23 | <parameter name="result" type="Map"/> | ||
| 24 | </out-parameters> | ||
| 25 | <actions> | ||
| 26 | <script><![CDATA[ | ||
| 27 | import org.moqui.mcp.adapter.McpToolAdapter | ||
| 28 | import org.moqui.context.ArtifactAuthorizationException | ||
| 29 | |||
| 30 | // 1. Capture current agent identity | ||
| 31 | String agentUsername = ec.user.username | ||
| 32 | |||
| 33 | try { | ||
| 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) | ||
| 37 | if (!loggedIn) throw new Exception("Could not switch to user ${runAsUserId}") | ||
| 38 | |||
| 39 | ec.logger.info("Agent ${agentUsername} executing ${toolName} AS ${ec.user.username} (${runAsUserId})") | ||
| 40 | |||
| 41 | // 3. Execute Tool (Standard RBAC applies to this user) | ||
| 42 | McpToolAdapter adapter = new McpToolAdapter() | ||
| 43 | |||
| 44 | // The adapter MUST NOT disableAuthz internally for this to be secure | ||
| 45 | result = adapter.callTool(ec, toolName, arguments) | ||
| 46 | |||
| 47 | } finally { | ||
| 48 | // 4. Restore Agent Identity | ||
| 49 | if (agentUsername) { | ||
| 50 | ec.user.internalLoginUser(agentUsername, false) | ||
| 51 | } | ||
| 52 | } | ||
| 53 | ]]></script> | ||
| 54 | </actions> | ||
| 55 | </service> | ||
| 56 | |||
| 57 | <!-- ========================================================= --> | ||
| 58 | <!-- Agent Client (OpenAI-Compatible API Wrapper) --> | ||
| 59 | <!-- ========================================================= --> | ||
| 60 | |||
| 61 | <service verb="call" noun="OpenAiChatCompletion"> | ||
| 62 | <description>Generic wrapper for OpenAI-compatible chat completions (VLLM, OpenAI, etc.)</description> | ||
| 63 | <in-parameters> | ||
| 64 | <parameter name="endpointUrl" required="true"/> | ||
| 65 | <parameter name="apiKey"/> | ||
| 66 | <parameter name="model" required="true"/> | ||
| 67 | <parameter name="messages" type="List" required="true"/> | ||
| 68 | <parameter name="tools" type="List"/> | ||
| 69 | <parameter name="temperature" type="BigDecimal" default="0.7"/> | ||
| 70 | <parameter name="maxTokens" type="Integer"/> | ||
| 71 | </in-parameters> | ||
| 72 | <out-parameters> | ||
| 73 | <parameter name="response" type="Map"/> | ||
| 74 | <parameter name="httpStatus" type="Integer"/> | ||
| 75 | <parameter name="error" type="String"/> | ||
| 76 | </out-parameters> | ||
| 77 | <actions> | ||
| 78 | <script><![CDATA[ | ||
| 79 | import groovy.json.JsonBuilder | ||
| 80 | import groovy.json.JsonSlurper | ||
| 81 | |||
| 82 | // Construct payload | ||
| 83 | def payloadMap = [ | ||
| 84 | model: model, | ||
| 85 | messages: messages, | ||
| 86 | temperature: temperature, | ||
| 87 | stream: false | ||
| 88 | ] | ||
| 89 | |||
| 90 | if (maxTokens) payloadMap.maxTokens = maxTokens | ||
| 91 | if (tools) payloadMap.tools = tools | ||
| 92 | |||
| 93 | String jsonPayload = new JsonBuilder(payloadMap).toString() | ||
| 94 | |||
| 95 | // Setup connection | ||
| 96 | URL url = new URL(endpointUrl + "/chat/completions") | ||
| 97 | HttpURLConnection conn = (HttpURLConnection) url.openConnection() | ||
| 98 | conn.setRequestMethod("POST") | ||
| 99 | conn.setRequestProperty("Content-Type", "application/json") | ||
| 100 | if (apiKey) conn.setRequestProperty("Authorization", "Bearer " + apiKey) | ||
| 101 | conn.setDoOutput(true) | ||
| 102 | conn.setConnectTimeout(10000) // 10s connect | ||
| 103 | conn.setReadTimeout(60000) // 60s read (LLMs are slow) | ||
| 104 | |||
| 105 | try { | ||
| 106 | conn.outputStream.write(jsonPayload.getBytes("UTF-8")) | ||
| 107 | |||
| 108 | httpStatus = conn.responseCode | ||
| 109 | |||
| 110 | InputStream is = (httpStatus >= 200 && httpStatus < 300) ? conn.inputStream : conn.errorStream | ||
| 111 | String responseText = is?.text | ||
| 112 | |||
| 113 | if (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) { | ||
| 123 | error = e.message | ||
| 124 | httpStatus = 500 | ||
| 125 | ec.logger.error("OpenAI Client Exception", e) | ||
| 126 | } | ||
| 127 | ]]></script> | ||
| 128 | </actions> | ||
| 129 | </service> | ||
| 130 | |||
| 131 | <!-- ========================================================= --> | ||
| 132 | <!-- Agent Runner (The Loop) --> | ||
| 133 | <!-- ========================================================= --> | ||
| 134 | |||
| 135 | <service verb="run" noun="AgentTask"> | ||
| 136 | <description> | ||
| 137 | Processes a single Agent Task SystemMessage. | ||
| 138 | Handles the loop of: Prompt -> LLM -> Tool Call -> Tool Execution -> Prompt. | ||
| 139 | </description> | ||
| 140 | <in-parameters> | ||
| 141 | <parameter name="systemMessageId" required="true"/> | ||
| 142 | </in-parameters> | ||
| 143 | <actions> | ||
| 144 | <script><![CDATA[ | ||
| 145 | import groovy.json.JsonOutput | ||
| 146 | import groovy.json.JsonSlurper | ||
| 147 | |||
| 148 | // 1. Load SystemMessage and Config | ||
| 149 | def taskMsg = ec.entity.find("moqui.service.message.SystemMessage") | ||
| 150 | .condition("systemMessageId", systemMessageId) | ||
| 151 | .one() | ||
| 152 | |||
| 153 | if (!taskMsg) return | ||
| 154 | |||
| 155 | // Get AI Config | ||
| 156 | def aiConfig = ec.entity.find("moqui.mcp.agent.ProductStoreAiConfig") | ||
| 157 | .condition("productStoreId", taskMsg.productStoreId) | ||
| 158 | .condition("aiConfigId", taskMsg.aiConfigId) | ||
| 159 | .one() | ||
| 160 | |||
| 161 | // Fallback to ProductStoreSetting if no specific AI Config found | ||
| 162 | def endpointUrl, apiKey, modelName, temperature | ||
| 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 | ||
| 188 | } | ||
| 189 | |||
| 190 | // Default temperature if missing | ||
| 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 | |||
| 197 | // Filter out dangerous tools if needed? For now, we rely on RBAC delegation. | ||
| 198 | |||
| 199 | def openAiTools = moquiTools.collect { tool -> | ||
| 200 | [ | ||
| 201 | type: "function", | ||
| 202 | function: [ | ||
| 203 | name: tool.name, | ||
| 204 | description: tool.description, | ||
| 205 | // Helper to build schema (simplified for now, ideally strictly typed) | ||
| 206 | parameters: [ | ||
| 207 | type: "object", | ||
| 208 | properties: [ | ||
| 209 | path: [type: "string", description: "Screen path or resource URI"], | ||
| 210 | action: [type: "string", description: "Action to perform (create, update, etc)"], | ||
| 211 | parameters: [type: "object", description: "Key-value pairs for the action"] | ||
| 212 | ] | ||
| 213 | ] | ||
| 214 | ] | ||
| 215 | ] | ||
| 216 | } | ||
| 217 | |||
| 218 | // 3. Build Conversation History | ||
| 219 | // TODO: Load history if this is a continuation. For now, simple start. | ||
| 220 | def messages = [ | ||
| 221 | [role: "system", content: "You are a helpful Moqui ERP assistant. You act as user ${taskMsg.requestedByUserId}."], | ||
| 222 | [role: "user", content: taskMsg.messageText] | ||
| 223 | ] | ||
| 224 | |||
| 225 | // 4. The Loop (Max 5 turns for safety) | ||
| 226 | int maxTurns = 5 | ||
| 227 | int currentTurn = 0 | ||
| 228 | boolean taskComplete = false | ||
| 229 | |||
| 230 | while (currentTurn < maxTurns && !taskComplete) { | ||
| 231 | currentTurn++ | ||
| 232 | |||
| 233 | // Call LLM | ||
| 234 | def llmResult = ec.service.sync().name("AgentServices.call#OpenAiChatCompletion").parameters([ | ||
| 235 | endpointUrl: aiConfig.endpointUrl, | ||
| 236 | apiKey: aiConfig.apiKey, // Decrypt if needed | ||
| 237 | model: aiConfig.modelName, | ||
| 238 | messages: messages, | ||
| 239 | tools: openAiTools, | ||
| 240 | temperature: aiConfig.temperature | ||
| 241 | ]).call() | ||
| 242 | |||
| 243 | if (llmResult.error) { | ||
| 244 | taskMsg.statusId = "SmsError" | ||
| 245 | taskMsg.messageText += "\nError: ${llmResult.error}" | ||
| 246 | taskMsg.update() | ||
| 247 | return | ||
| 248 | } | ||
| 249 | |||
| 250 | def responseMsg = llmResult.response.choices[0].message | ||
| 251 | messages.add(responseMsg) // Add assistant response to history | ||
| 252 | |||
| 253 | // Check for Tool Calls | ||
| 254 | if (responseMsg.tool_calls) { | ||
| 255 | ec.logger.info("Agent requesting ${responseMsg.tool_calls.size()} tools") | ||
| 256 | |||
| 257 | responseMsg.tool_calls.each { toolCall -> | ||
| 258 | def functionName = toolCall.function.name | ||
| 259 | def functionArgs = new JsonSlurper().parseText(toolCall.function.arguments) | ||
| 260 | def toolCallId = toolCall.id | ||
| 261 | |||
| 262 | // EXECUTE TOOL via Secure Bridge | ||
| 263 | def executionResult = [:] | ||
| 264 | try { | ||
| 265 | def runResult = ec.service.sync().name("AgentServices.call#McpToolWithDelegation").parameters([ | ||
| 266 | toolName: functionName, | ||
| 267 | arguments: functionArgs, | ||
| 268 | runAsUserId: taskMsg.effectiveUserId // DELEGATION! | ||
| 269 | ]).call() | ||
| 270 | |||
| 271 | executionResult = runResult.result | ||
| 272 | } catch (Exception e) { | ||
| 273 | executionResult = [error: e.message] | ||
| 274 | } | ||
| 275 | |||
| 276 | // Add result to history | ||
| 277 | messages.add([ | ||
| 278 | role: "tool", | ||
| 279 | tool_call_id: toolCallId, | ||
| 280 | content: JsonOutput.toJson(executionResult) | ||
| 281 | ]) | ||
| 282 | } | ||
| 283 | // Loop continues to let LLM see results | ||
| 284 | } else { | ||
| 285 | // No tool calls = Final Response | ||
| 286 | taskComplete = true | ||
| 287 | taskMsg.statusId = "SmsProcessed" | ||
| 288 | taskMsg.messageText += "\n\n=== RESPONSE ===\n${responseMsg.content}" | ||
| 289 | taskMsg.update() | ||
| 290 | } | ||
| 291 | } | ||
| 292 | ]]></script> | ||
| 293 | </actions> | ||
| 294 | </service> | ||
| 295 | |||
| 296 | <!-- ========================================================= --> | ||
| 297 | <!-- Task Scheduler (Polls Queue) --> | ||
| 298 | <!-- ========================================================= --> | ||
| 299 | |||
| 300 | <service verb="poll" noun="AgentQueue"> | ||
| 301 | <description>Scheduled service to pick up pending tasks.</description> | ||
| 302 | <actions> | ||
| 303 | <script><![CDATA[ | ||
| 304 | import org.moqui.entity.EntityCondition | ||
| 305 | |||
| 306 | // Find pending tasks | ||
| 307 | def pendingTasks = ec.entity.find("moqui.service.message.SystemMessage") | ||
| 308 | .condition("statusId", "SmsReceived") // Or generic 'Pending' | ||
| 309 | .condition("systemMessageTypeId", "SmtyAgentTask") | ||
| 310 | .limit(5) // Batch size | ||
| 311 | .disableAuthz() // System service needs to see all tasks | ||
| 312 | .list() | ||
| 313 | |||
| 314 | pendingTasks.each { task -> | ||
| 315 | // Mark as In Progress | ||
| 316 | task.statusId = "SmsConsumed" // Or 'In Progress' | ||
| 317 | task.update() | ||
| 318 | |||
| 319 | // Run Async | ||
| 320 | ec.service.async().name("AgentServices.run#AgentTask") | ||
| 321 | .parameters([systemMessageId: task.systemMessageId]) | ||
| 322 | .call() | ||
| 323 | } | ||
| 324 | ]]></script> | ||
| 325 | </actions> | ||
| 326 | </service> | ||
| 327 | |||
| 328 | </services> |
-
Please register or sign in to post a comment