From b458e11e9037a8ddbedd9844bb574f8a7b1dbcac Mon Sep 17 00:00:00 2001 From: Ralph Soika Date: Sun, 22 Feb 2026 11:16:50 +0100 Subject: [PATCH] AI Agent draft impl --- .../java/ai/dynamixs/AIAgentController.java | 140 +++ .../webapp/layout/servicenav_custom.xhtml | 15 + .../java/ai/dynamixs/agent/TestAgentCall.java | 140 +++ workflow/demo/vacation-request-en-1.0.0.bpmn | 1080 +++++++++++++++++ 4 files changed, 1375 insertions(+) create mode 100644 dynamixs-ai-app/src/main/java/ai/dynamixs/AIAgentController.java create mode 100644 dynamixs-ai-app/src/main/webapp/layout/servicenav_custom.xhtml create mode 100644 dynamixs-ai-app/src/test/java/ai/dynamixs/agent/TestAgentCall.java create mode 100644 workflow/demo/vacation-request-en-1.0.0.bpmn diff --git a/dynamixs-ai-app/src/main/java/ai/dynamixs/AIAgentController.java b/dynamixs-ai-app/src/main/java/ai/dynamixs/AIAgentController.java new file mode 100644 index 0000000..08fb5ff --- /dev/null +++ b/dynamixs-ai-app/src/main/java/ai/dynamixs/AIAgentController.java @@ -0,0 +1,140 @@ +package ai.dynamixs; + +import java.io.Serializable; +import java.util.logging.Logger; + +import org.imixs.ai.ImixsAIContextHandler; +import org.imixs.ai.workflow.ImixsAIToolCallEvent; +import org.imixs.ai.workflow.OpenAIAPIService; +import org.imixs.workflow.exceptions.ModelException; +import org.imixs.workflow.exceptions.PluginException; +import org.imixs.workflow.faces.data.WorkflowController; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.Conversation; +import jakarta.enterprise.context.ConversationScoped; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.event.Reception; +import jakarta.inject.Inject; +import jakarta.inject.Named; + +@Named("aiAgent") +@ConversationScoped +public class AIAgentController implements Serializable { + + private static final long serialVersionUID = 1L; + private static Logger logger = Logger.getLogger(AIAgentController.class.getName()); + + @Inject + private Conversation conversation; + + @Inject + private OpenAIAPIService openAIAPIService; + + @Inject + private ImixsAIContextHandler contextHandler; + + @Inject + protected WorkflowController workflowController; + + private String userMessage; + + private String agentResponse; + private String redirectUrl; + + private static final int MAX_ITERATIONS = 10; // prevent infinite loops + + /** + * Starts the conversation scope and initializes the context. + */ + @PostConstruct + public void init(String systemPrompt) { + if (conversation.isTransient()) { + conversation.begin(); + } + + try { + contextHandler.init(); + contextHandler.addSystemMessage(systemPrompt); + } catch (PluginException e) { + logger.warning("Failed to init AIAgent: " + e.getMessage()); + } + } + + /** + * Main entry point for the frontend. Runs the agent loop and returns either a + * text response or a redirect URL. + */ + public String chat() throws PluginException { + redirectUrl = null; + agentResponse = null; + + logger.info("├── Starting AI Agent"); + logger.info("│ ├── User Question: " + userMessage); + contextHandler.addQuestion(userMessage, null, null); + + // Agent Loop + int iterations = 0; + while (iterations < MAX_ITERATIONS) { + iterations++; + + String response = openAIAPIService.postPromptCompletion( + contextHandler, null, true); + + // Tool call? → processToolCallResult handles CDI events + boolean wasToolCall = openAIAPIService.processToolCallResult( + response, contextHandler); + + if (!wasToolCall) { + // Normal text response → done + agentResponse = openAIAPIService.processPromptResult( + response, null, null); + return null; // stay on current page + } + + // Check if a tool call produced a redirect URL + if (redirectUrl != null) { + conversation.end(); + return redirectUrl; // JSF redirect + } + } + + throw new PluginException(AIAgentController.class.getSimpleName(), + "AGENT_LOOP_ERROR", "Max iterations reached!"); + } + + public void onToolCall(@Observes(notifyObserver = Reception.IF_EXISTS) ImixsAIToolCallEvent event) + throws PluginException { + try { + if ("start_process".equals(event.getToolName())) { + String modelVersion = event.getArguments().getString("$modelversion"); + int taskId = event.getArguments().getInt("$taskid"); + String processRef = event.getArguments().getString("process.ref"); + + workflowController.create(modelVersion, taskId, processRef); + + String url = "/pages/workitems/workitem/" + ""; + // event.setRedirectUrl(url); + event.setResult("Process started: " + taskId); + } + } catch (ModelException e) { + throw new PluginException(e.getErrorContext(), e.getErrorCode(), e.getMessage()); + } + } + + public void setRedirectUrl(String url) { + this.redirectUrl = url; + } + + public String getUserMessage() { + return userMessage; + } + + public void setUserMessage(String userMessage) { + this.userMessage = userMessage; + } + + public String getAgentResponse() { + return agentResponse; + } +} \ No newline at end of file diff --git a/dynamixs-ai-app/src/main/webapp/layout/servicenav_custom.xhtml b/dynamixs-ai-app/src/main/webapp/layout/servicenav_custom.xhtml new file mode 100644 index 0000000..9b8548e --- /dev/null +++ b/dynamixs-ai-app/src/main/webapp/layout/servicenav_custom.xhtml @@ -0,0 +1,15 @@ + + + +
+ + + +
+
+
\ No newline at end of file diff --git a/dynamixs-ai-app/src/test/java/ai/dynamixs/agent/TestAgentCall.java b/dynamixs-ai-app/src/test/java/ai/dynamixs/agent/TestAgentCall.java new file mode 100644 index 0000000..4d9ffa1 --- /dev/null +++ b/dynamixs-ai-app/src/test/java/ai/dynamixs/agent/TestAgentCall.java @@ -0,0 +1,140 @@ +/**************************************************************************** + * Copyright (c) 2022-2025 Imixs Software Solutions GmbH and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0 + * + * This Source Code may also be made available under the terms of the + * GNU General Public License, version 2 or later (GPL-2.0-or-later), + * which is available at https://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later + ****************************************************************************/ + +package ai.dynamixs.agent; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Field; +import java.util.Date; +import java.util.Optional; + +import org.imixs.ai.ImixsAIContextHandler; +import org.imixs.ai.workflow.OpenAIAPIConnector; +import org.imixs.ai.workflow.OpenAIAPIService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Test class to test a function call + * + * + * @author rsoika + */ +@ExtendWith(MockitoExtension.class) +public class TestAgentCall { + + private static final String MISTRAL_ENDPOINT = "https://api.llama.cpp.imixs.com/"; + public static final String MISTRAL_API_KEY = "7fdb663fa87ea16731c1e0ece8ad3214e358423eabf2403188110c731ce07321"; + + private OpenAIAPIService service; + private OpenAIAPIConnector connector; + + @BeforeEach + void setUp() throws Exception { + connector = new OpenAIAPIConnector(); + connector.setServiceEndpointApiKey(Optional.of(MISTRAL_API_KEY)); + connector.setServiceEndpointUser(Optional.empty()); + connector.setServiceEndpointPassword(Optional.empty()); + injectField(connector, "serviceTimeout", 30000); + injectField(connector, "serviceEndpoint", Optional.of(MISTRAL_ENDPOINT)); + + service = new OpenAIAPIService(); + injectField(service, "openAIAPIConnector", connector); + } + + @Test + void testHelloWorld() throws Exception { + // Build a simple "Hello World" prompt + ImixsAIContextHandler contextHandler = new ImixsAIContextHandler(); + contextHandler.addQuestion("Say hello world!", null, null); + + String response = service.postPromptCompletion(contextHandler, MISTRAL_ENDPOINT, true); + + assertNotNull(response); + System.out.println("Response: " + response); + } + + @Test + void testFunctionCalling() throws Exception { + ImixsAIContextHandler contextHandler = new ImixsAIContextHandler(); + + // Define system context + contextHandler.addSystemMessage(""" + You are a workflow assistant integrated into the Imixs-Office-Workflow suite. + The following BPMN processes are available: + - urlaubsantrag: Vacation request process + - reisekostenabrechnung: Travel expense process + - eingangsrechnung: Incoming invoice process + + When a user wants to start a process, always call the load_skill tool + with the matching process_id. Never ask the user for the process ID. + """); + + // Define the load_skill function + contextHandler.addFunction( + "load_skill", + "Loads details about an available BPMN workflow process", + """ + { + "type": "object", + "properties": { + "process_id": { + "type": "string", + "description": "The ID of the BPMN process, e.g. urlaubsantrag" + } + }, + "required": ["process_id"] + } + """); + + contextHandler.addQuestion( + "I need next week off. Can you start the vacation request process?", + "tom.clancy", new Date()); + + // Print the full request JSON to verify it looks correct + System.out.println("Request: " + contextHandler.toString()); + + String response = service.postPromptCompletion(contextHandler, MISTRAL_ENDPOINT, true); + assertNotNull(response); + System.out.println("Response: " + response); + + // Check if Mistral responded with a tool call + assertTrue(response.contains("load_skill"), + "Expected Mistral to call load_skill tool"); + + // Verify Mistral responded with a tool call + assertTrue(response.contains("\"finish_reason\":\"tool_calls\"")); + assertTrue(response.contains("\"name\":\"load_skill\"")); + assertTrue(response.contains("urlaubsantrag")); + } + + private void injectField(Object target, String fieldName, Object value) throws Exception { + Class clazz = target.getClass(); + while (clazz != null) { + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + return; + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } + throw new NoSuchFieldException(fieldName); + } +} \ No newline at end of file diff --git a/workflow/demo/vacation-request-en-1.0.0.bpmn b/workflow/demo/vacation-request-en-1.0.0.bpmn new file mode 100644 index 0000000..1de18a0 --- /dev/null +++ b/workflow/demo/vacation-request-en-1.0.0.bpmn @@ -0,0 +1,1080 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + StartEvent_1 + + DataObject_2 + event_z9ZfGA + task_oHf3cQ + task_4ju0eg + gateway_0OamEQ + event_WuKdrA + event_wDw9RA + Task_6 + EndEvent_2 + + + + Task_3 + EventBasedGateway_1 + event_1yYSEQ + + + + Task_5 + IntermediateCatchEvent_6 + event_81scHA + task_HL0KNA + IntermediateCatchEvent_17 + event_QwIjiA + + + + + sequenceFlow_lagafw + + + + + + + + + + + + + +]]> + + + + + + + + + + + + request.subject]]> + + + true + + + + + + + + + + + + + + + + + SequenceFlow_23 + sequenceFlow_4EDjTw + + + + + + + + + + + _name (EAN: _ean) Preis: _price _currency]]> + + + true + + + + + + + + + + + + + + Customer: customer.name
+E-Mail: customer.email
+Address: customer.country customer.zip-customer.city customer.address
+ +
+]]>
+
+
+ $workflowgroup - $workflowstatus]]> + sequenceFlow_28Et9A + sequenceFlow_bc8XPw +
+ + SequenceFlow_22 + + + + + + + + + + + + _subject]]> + + + + + + + + + _amount (Brutto € _amount_brutto) +_description]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + home +]]> + + + + + + + + + + + + false + + + + sequenceFlow_HCxYvw + sequenceFlow_t7vAEA + + + + + + + + + + + + _subject]]> + + + + + + + + + _amount (Brutto € _amount_brutto) +_description]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + home]]> + + + + + + + + + order.delivery]]> + + + false + + + + + + + + + + + + + + + + sequenceFlow_ptQjOA + sequenceFlow_28Et9A + + + + + + + + + + + + _name (EAN: _ean) Preis: _price _currency]]> + + + true + + + + + + + + + + + + + + Customer: customer.name
+E-Mail: customer.email
+Address: customer.country customer.zip-customer.city customer.address
+ +
+]]>
+
+
+ $workflowgroup - $workflowstatus]]> + sequenceFlow_gTAnUw + SequenceFlow_22 +
+ + + + + SequenceFlow_23 + + sequenceFlow_HCxYvw + sequenceFlow_5y8UKw + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + home]]> + + + + sequenceFlow_cAQRKg + sequenceFlow_4EDjTw + + + + + + + + + + + + + + request.subject]]> + + + true + + + + + + + + + + + + + + + + + + Customer: customer.name
+E-Mail: customer.email
+Address: customer.country customer.zip-customer.city customer.address
+ +
+]]>
+
+
+ $workflowgroup - $workflowstatus]]> + sequenceFlow_WRPdIA + sequenceFlow_bCmSJg + sequenceFlow_t7vAEA + sequenceFlow_ptQjOA +
+ + + + + + + + + + + + + + request.subject]]> + + + true + + + + + + + + + + + + + + + + + + + sequenceFlow_WRPdIA + sequenceFlow_hkBSvg + sequenceFlow_lagafw + sequenceFlow_F06l0g + + + + + + + + + + + + + + + + + + + sequenceFlow_bc8XPw + + + + + + + + + + + + + + + + + _subject]]> + + + + + + + + + _amount (Brutto € _amount_brutto) +_description]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + home + +]]> + + + + + + + + + + + + false + + + + sequenceFlow_5y8UKw + sequenceFlow_gTAnUw + + + + + + + + + + + + + + + + + + + + + request.subject]]> + + + true + + + + + + + + + + + + + + + + + + Customer: customer.name
+E-Mail: customer.email
+Address: customer.country customer.zip-customer.city customer.address
+ +
+]]>
+
+
+ + sequenceFlow_WRPdIA + sequenceFlow_hkBSvg + sequenceFlow_gc3sAw + sequenceFlow_lN0z0w +
+ + + sequenceFlow_5y8UKw + sequenceFlow_F06l0g + sequenceFlow_cAQRKg + sequenceFlow_lxIpQw + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + home]]> + + + + sequenceFlow_lxIpQw + sequenceFlow_gc3sAw + + + + sequenceFlow_lN0z0w + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + + sequenceFlow_InfPdQ + sequenceFlow_bCmSJg + + + + + + + +
+ + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +