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]*?", 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)[^>]*>(.*?)", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + private static final Pattern P_EM = Pattern.compile( + "<(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( + "", + 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("&#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(); + } +} \ 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}" /> +