AI Agent draft impl
This commit is contained in:
parent
5f15f7cfeb
commit
b458e11e90
4 changed files with 1375 additions and 0 deletions
140
dynamixs-ai-app/src/main/java/ai/dynamixs/AIAgentController.java
Normal file
140
dynamixs-ai-app/src/main/java/ai/dynamixs/AIAgentController.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
1080
workflow/demo/vacation-request-en-1.0.0.bpmn
Normal file
1080
workflow/demo/vacation-request-en-1.0.0.bpmn
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue