AI Agent draft impl

This commit is contained in:
Ralph Soika 2026-02-22 11:16:50 +01:00
parent 5f15f7cfeb
commit b458e11e90
4 changed files with 1375 additions and 0 deletions

View file

@ -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;
}
}

View file

@ -0,0 +1,15 @@
<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:a="http://xmlns.jcp.org/jsf/passthrough"
xmlns:i="http://xmlns.jcp.org/jsf/composite/imixs">
<f:view>
<div class="toolbar agent">
<h:inputText a:data-id="nav_input_phrase" style="margin-right: 10px;" a:placeholder="What do you need?"
value="#{aiAgent.userMessage}" />
<h:commandLink action="#{aiAgent.chat()}" a:data-id="nav_search_link"><span class="typcn typcn-lightbulb"
style="font-size:1.4em;"></span>
</h:commandLink>
</div>
</f:view>
</ui:composition>

View file

@ -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);
}
}

File diff suppressed because it is too large Load diff