added ai-agent
This commit is contained in:
parent
b458e11e90
commit
c0e44d14ff
4 changed files with 373 additions and 27 deletions
|
|
@ -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());
|
||||
|
|
@ -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(" ", " ")
|
||||
.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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue