added ai-agent

This commit is contained in:
Ralph Soika 2026-02-22 16:16:29 +01:00
parent b458e11e90
commit c0e44d14ff
4 changed files with 373 additions and 27 deletions

View file

@ -1,4 +1,4 @@
package ai.dynamixs;
package ai.dynamixs.agent;
import java.io.Serializable;
import java.util.logging.Logger;
@ -10,24 +10,19 @@ 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.context.SessionScoped;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.event.Reception;
import jakarta.inject.Inject;
import jakarta.inject.Named;
@Named("aiAgent")
@ConversationScoped
@Named("aiAgentController")
@SessionScoped
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;
@ -37,6 +32,9 @@ public class AIAgentController implements Serializable {
@Inject
protected WorkflowController workflowController;
@Inject
protected SkillController skillController;
private String userMessage;
private String agentResponse;
@ -47,15 +45,46 @@ public class AIAgentController implements Serializable {
/**
* Starts the conversation scope and initializes the context.
*/
@PostConstruct
public void init(String systemPrompt) {
if (conversation.isTransient()) {
conversation.begin();
}
public void init() {
logger.info("├── Initialize AI Agent");
redirectUrl = null;
agentResponse = null;
contextHandler.init();
// Build system prompt with skill headers
try {
contextHandler.init();
contextHandler.addSystemMessage(systemPrompt);
contextHandler.addSystemMessage(
"You are a workflow assistant in Imixs-Office-Workflow. " +
"When a user wants to start a process, call start_process with the " +
"matching modelversion and taskId from the list below.\n\n" +
"Do NOT explain what you are doing. Just call the tool directly.\n\n" +
skillController.getSkillHeaders());
// Register start_process function
contextHandler.addFunction(
"start_process",
"Starts a BPMN workflow process for the user",
"""
{
"type": "object",
"properties": {
"$modelversion": {
"type": "string",
"description": "The model version of the process"
},
"$taskid": {
"type": "integer",
"description": "The task ID of the initial task"
},
"process.ref": {
"type": "string",
"description": "The unique ID referring of the process"
}
},
"required": ["$modelversion", "$taskid", "process.ref"]
}
""");
} catch (PluginException e) {
logger.warning("Failed to init AIAgent: " + e.getMessage());
}
@ -65,11 +94,11 @@ public class AIAgentController implements Serializable {
* 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;
public String startChat() throws PluginException {
logger.info("├── Starting AI Agent");
init();
logger.info("├── Starting new chat");
logger.info("│ ├── User Question: " + userMessage);
contextHandler.addQuestion(userMessage, null, null);
@ -94,7 +123,6 @@ public class AIAgentController implements Serializable {
// Check if a tool call produced a redirect URL
if (redirectUrl != null) {
conversation.end();
return redirectUrl; // JSF redirect
}
}
@ -113,9 +141,12 @@ public class AIAgentController implements Serializable {
workflowController.create(modelVersion, taskId, processRef);
String url = "/pages/workitems/workitem/" + "";
// event.setRedirectUrl(url);
event.setResult("Process started: " + taskId);
// Set redirect URL directly on this bean - startChat() will pick it up
this.redirectUrl = "/pages/workitems/workitem.xhtml?id="
+ workflowController.getWorkitem().getUniqueID()
+ "&faces-redirect=true";
event.setResult("New Process started: " + workflowController.getWorkitem().getUniqueID());
}
} catch (ModelException e) {
throw new PluginException(e.getErrorContext(), e.getErrorCode(), e.getMessage());

View file

@ -0,0 +1,155 @@
package ai.dynamixs.agent;
import java.util.regex.Pattern;
/**
* Utility class to convert HTML content into clean, LLM-friendly markup text.
*/
public class HtmlStripper {
// Precompiled patterns for performance
private static final Pattern P_SCRIPT_STYLE = Pattern.compile(
"<(script|style)[^>]*>[\\s\\S]*?</\\1>", Pattern.CASE_INSENSITIVE);
private static final Pattern P_HEADING = Pattern.compile(
"<h[1-5][^>]*>(.*?)</h[1-5]>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
private static final Pattern P_STRONG = Pattern.compile(
"<(strong|b)[^>]*>(.*?)</(strong|b)>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
private static final Pattern P_EM = Pattern.compile(
"<(em|i)[^>]*>(.*?)</(em|i)>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
private static final Pattern P_LI = Pattern.compile(
"<li[^>]*>(.*?)</li>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
private static final Pattern P_BLOCK = Pattern.compile(
"<(p|div|br|tr|blockquote|pre|ul|ol|table)[^>]*/?>",
Pattern.CASE_INSENSITIVE);
private static final Pattern P_CLOSE_BLOCK = Pattern.compile(
"</(p|div|tr|blockquote|pre|ul|ol|table)>",
Pattern.CASE_INSENSITIVE);
private static final Pattern P_TAGS = Pattern.compile("<[^>]+>");
private static final Pattern P_MULTI_NEWLINE = Pattern.compile("\\n{3,}");
private static final Pattern P_BLANK_LINE = Pattern.compile("(?m)^[ \\t]+$");
/**
* Converts an HTML string into clean, LLM-readable markdown-style text. Handles
* headings, bold/italic, lists, images, HTML entities and removes all remaining
* tags. Designed to be minimal and allocation-efficient.
*
* @param content raw HTML input (may be null)
* @return clean markup text, or empty string if input is null/blank
*/
public static String stripHtml(String content) {
if (content == null || content.isBlank()) {
return "";
}
String s = content;
// 1. Normalize line endings
s = s.replace("\r\n", "\n").replace("\r", "\n");
// 2. Remove script / style blocks entirely
s = P_SCRIPT_STYLE.matcher(s).replaceAll("");
// 3. Images → [Image: alt] or drop silently
// s = P_IMG.matcher(s).replaceAll("[Image: $1]");
// s = P_IMG_NO_ALT.matcher(s).replaceAll("");
// 4. Headings → Markdown headings
s = P_HEADING.matcher(s).replaceAll("\n**$1**\n");
// 5. Bold / Italic → Markdown
s = P_STRONG.matcher(s).replaceAll("**$2**");
s = P_EM.matcher(s).replaceAll("*$2*");
// 6. List items → Markdown bullets
s = P_LI.matcher(s).replaceAll("\n- $1");
// 7. Block-level elements → newlines
s = P_BLOCK.matcher(s).replaceAll("\n");
s = P_CLOSE_BLOCK.matcher(s).replaceAll("\n");
// 8. Strip all remaining tags
s = P_TAGS.matcher(s).replaceAll("");
// 9. Decode HTML entities
s = decodeEntities(s);
// 10. Clean up whitespace
s = P_BLANK_LINE.matcher(s).replaceAll("");
s = P_MULTI_NEWLINE.matcher(s).replaceAll("\n\n");
return s.strip();
}
/**
* Decodes common named and numeric HTML entities. Covers the full ISO-8859-1 /
* HTML4 named entity set plus numeric references.
*/
private static String decodeEntities(String s) {
// Use a simple regex-replace loop for correctness
s = decodeNumericEntities(s);
// Named entities – most frequent ones in German/English business docs
s = s
.replace("&nbsp;", " ")
.replace("&amp;", "&")
.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("&quot;", "\"")
.replace("&apos;", "'")
.replace("&auml;", "ä").replace("&Auml;", "Ä")
.replace("&ouml;", "ö").replace("&Ouml;", "Ö")
.replace("&uuml;", "ü").replace("&Uuml;", "Ü")
.replace("&szlig;", "ß")
.replace("&eacute;", "é").replace("&Eacute;", "É")
.replace("&egrave;", "è").replace("&Egrave;", "È")
.replace("&ecirc;", "ê").replace("&Ecirc;", "Ê")
.replace("&agrave;", "à").replace("&Agrave;", "À")
.replace("&acirc;", "â").replace("&Acirc;", "Â")
.replace("&ocirc;", "ô").replace("&Ocirc;", "Ô")
.replace("&ucirc;", "û").replace("&Ucirc;", "Û")
.replace("&ccedil;", "ç").replace("&Ccedil;", "Ç")
.replace("&ntilde;", "ñ").replace("&Ntilde;", "Ñ")
.replace("&copy;", "©")
.replace("&reg;", "®")
.replace("&trade;", "™")
.replace("&mdash;", "—")
.replace("&ndash;", "–")
.replace("&hellip;", "…")
.replace("&laquo;", "«").replace("&raquo;", "»")
.replace("&bull;", "•")
.replace("&euro;", "€");
return s;
}
private static final Pattern P_DEC_ENTITY = Pattern.compile("&#(\\d+);");
private static final Pattern P_HEX_ENTITY = Pattern.compile("&#x([0-9a-fA-F]+);");
/**
* Replaces numeric HTML entities (decimal and hex) with their Unicode
* characters.
*/
private static String decodeNumericEntities(String s) {
// Hex
var m = P_HEX_ENTITY.matcher(s);
var sb = new StringBuilder(); // ← StringBuilder statt StringBuffer
while (m.find()) {
int cp = Integer.parseInt(m.group(1), 16);
m.appendReplacement(sb, new String(Character.toChars(cp)));
}
m.appendTail(sb);
s = sb.toString();
// Decimal
m = P_DEC_ENTITY.matcher(s);
sb = new StringBuilder(); // ← StringBuilder
while (m.find()) {
int cp = Integer.parseInt(m.group(1));
m.appendReplacement(sb, new String(Character.toChars(cp)));
}
m.appendTail(sb);
return sb.toString();
}
}

View file

@ -0,0 +1,160 @@
/*******************************************************************************
* Imixs Workflow
* Copyright (C) 2001, 2011 Imixs Software Solutions GmbH,
* http://www.imixs.com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You can receive a copy of the GNU General Public
* License at http://www.gnu.org/licenses/gpl.html
*
* Project:
* http://www.imixs.org
* http://java.net/projects/imixs-workflow
*
* Contributors:
* Imixs Software Solutions GmbH - initial API and implementation
* Ralph Soika - Software Developer
*******************************************************************************/
package ai.dynamixs.agent;
import java.io.Serializable;
import java.util.List;
import org.imixs.marty.team.TeamController;
import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.engine.DocumentEvent;
import org.imixs.workflow.office.model.ModelController;
import jakarta.annotation.Resource;
import jakarta.ejb.SessionContext;
import jakarta.enterprise.context.SessionScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
/**
* The SkillController is a CDI bean handling workflow skills from the current
* process models.
*
* @author rsoika
*/
@SessionScoped
public class SkillController implements Serializable {
private static final long serialVersionUID = 1L;
@Inject
TeamController teamController;
@Inject
ModelController modelController;
@Resource
SessionContext ctx;
String skillHeaders = null;
/**
* This method refreshes the cache of skillHeaders
*
* @param event
*/
public void onDocumentEvent(@Observes DocumentEvent event) {
String type = event.getDocument().getType();
if ((type.startsWith("process") || type.startsWith("model"))
&& (event.getEventType() == DocumentEvent.ON_DOCUMENT_SAVE
|| event.getEventType() == DocumentEvent.ON_DOCUMENT_DELETE)) {
skillHeaders = null;
}
}
/**
* Return the current skill headers. The method supports a lazy loading
* mechanism.
*
* @return
*/
public String getSkillHeaders() {
loadSkillHeaders();
return skillHeaders;
}
/**
* Helper method to load the skill headers for all available processes and
* assigned workflows
*
* @return
*/
private void loadSkillHeaders() {
if (skillHeaders == null) {
StringBuilder skillHeaderBuilder = new StringBuilder();
skillHeaderBuilder.append("Available Workflow Processes:\n\n");
List<ItemCollection> processList = teamController.getProcessList();
for (ItemCollection process : processList) {
if (process.getTaskID() == 301 || teamController.isManagerOf(process.getUniqueID())) {
String processName = process.getItemValueString("name");
String processDescription = HtmlStripper
.stripHtml(process.getItemValueString("txtdescription"));
List<String> workflowList = process.getItemValueList("txtWorkflowList", String.class);
// Collect workflow entries first - skip process if none found
StringBuilder workflowEntries = new StringBuilder();
for (String group : workflowList) {
String modelVersion = modelController.getVersionByGroup(group);
List<ItemCollection> startTasks = modelController.findAllStartTasksByGroup(modelVersion, group);
if (!startTasks.isEmpty()) {
ItemCollection initialTask = startTasks.get(0);
int taskId = initialTask.getItemValueInteger("taskid");
String taskDescription = HtmlStripper.stripHtml(
modelController.getProcessDescriptionByInitialTask(initialTask, modelVersion,
group));
workflowEntries.append("- ").append(group).append(": ");
if (taskDescription != null && !taskDescription.isBlank()) {
workflowEntries.append(taskDescription);
} else if (processDescription != null && !processDescription.isBlank()) {
workflowEntries.append(processDescription);
}
workflowEntries.append(" (model: ").append(modelVersion)
.append(", taskid: ").append(taskId)
.append(", process.ref: ").append(process.getUniqueID())
.append(")\n");
}
}
// Only add process section if it has at least one workflow
if (workflowEntries.length() > 0) {
skillHeaderBuilder.append("[").append(processName).append("]");
if (processDescription != null && !processDescription.isBlank()) {
skillHeaderBuilder.append(" - ").append(processDescription);
}
skillHeaderBuilder.append("\n");
skillHeaderBuilder.append(workflowEntries);
skillHeaderBuilder.append("\n");
}
}
}
skillHeaders = skillHeaderBuilder.toString();
}
}
}

View file

@ -6,9 +6,9 @@
<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>
value="#{aiAgentController.userMessage}" />
<h:commandLink action="#{aiAgentController.startChat()}" a:data-id="nav_search_link"><span
class="typcn typcn-lightbulb" style="font-size:1.4em;"></span>
</h:commandLink>
</div>
</f:view>