diff --git a/dynamixs-ai-app/src/main/java/ai/dynamixs/AIAgentController.java b/dynamixs-ai-app/src/main/java/ai/dynamixs/agent/AIAgentController.java
similarity index 58%
rename from dynamixs-ai-app/src/main/java/ai/dynamixs/AIAgentController.java
rename to dynamixs-ai-app/src/main/java/ai/dynamixs/agent/AIAgentController.java
index 08fb5ff..25b037b 100644
--- a/dynamixs-ai-app/src/main/java/ai/dynamixs/AIAgentController.java
+++ b/dynamixs-ai-app/src/main/java/ai/dynamixs/agent/AIAgentController.java
@@ -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());
diff --git a/dynamixs-ai-app/src/main/java/ai/dynamixs/agent/HtmlStripper.java b/dynamixs-ai-app/src/main/java/ai/dynamixs/agent/HtmlStripper.java
new file mode 100644
index 0000000..bbb6157
--- /dev/null
+++ b/dynamixs-ai-app/src/main/java/ai/dynamixs/agent/HtmlStripper.java
@@ -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(
+ "]*>(.*?)", 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(
+ "
]*>(.*?)", 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(" ", " ")
+ .replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace(""", "\"")
+ .replace("'", "'")
+ .replace("ä", "ä").replace("Ä", "Ä")
+ .replace("ö", "ö").replace("Ö", "Ö")
+ .replace("ü", "ü").replace("Ü", "Ü")
+ .replace("ß", "ß")
+ .replace("é", "é").replace("É", "É")
+ .replace("è", "è").replace("È", "È")
+ .replace("ê", "ê").replace("Ê", "Ê")
+ .replace("à", "à").replace("À", "À")
+ .replace("â", "â").replace("Â", "Â")
+ .replace("ô", "ô").replace("Ô", "Ô")
+ .replace("û", "û").replace("Û", "Û")
+ .replace("ç", "ç").replace("Ç", "Ç")
+ .replace("ñ", "ñ").replace("Ñ", "Ñ")
+ .replace("©", "©")
+ .replace("®", "®")
+ .replace("™", "™")
+ .replace("—", "—")
+ .replace("–", "–")
+ .replace("…", "…")
+ .replace("«", "«").replace("»", "»")
+ .replace("•", "•")
+ .replace("€", "€");
+
+ return s;
+ }
+
+ private static final Pattern P_DEC_ENTITY = Pattern.compile("(\\d+);");
+ private static final Pattern P_HEX_ENTITY = Pattern.compile("([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();
+ }
+}
\ No newline at end of file
diff --git a/dynamixs-ai-app/src/main/java/ai/dynamixs/agent/SkillController.java b/dynamixs-ai-app/src/main/java/ai/dynamixs/agent/SkillController.java
new file mode 100644
index 0000000..befc260
--- /dev/null
+++ b/dynamixs-ai-app/src/main/java/ai/dynamixs/agent/SkillController.java
@@ -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 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 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 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();
+ }
+
+ }
+
+}
diff --git a/dynamixs-ai-app/src/main/webapp/layout/servicenav_custom.xhtml b/dynamixs-ai-app/src/main/webapp/layout/servicenav_custom.xhtml
index 9b8548e..0487e81 100644
--- a/dynamixs-ai-app/src/main/webapp/layout/servicenav_custom.xhtml
+++ b/dynamixs-ai-app/src/main/webapp/layout/servicenav_custom.xhtml
@@ -6,9 +6,9 @@
-
+ value="#{aiAgentController.userMessage}" />
+