new cognito service

This commit is contained in:
Ralph Soika 2026-01-04 16:42:19 +01:00
parent 6743a9d716
commit 23e09d4600
9 changed files with 1095 additions and 7 deletions

View file

@ -0,0 +1,130 @@
/****************************************************************************
* 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.cognition;
import static org.imixs.workflow.engine.AsyncEventSchedulerConfig.ASYNCEVENT_PROCESSOR_DEADLOCK;
import static org.imixs.workflow.engine.AsyncEventSchedulerConfig.ASYNCEVENT_PROCESSOR_ENABLED;
import static org.imixs.workflow.engine.AsyncEventSchedulerConfig.ASYNCEVENT_PROCESSOR_INITIALDELAY;
import static org.imixs.workflow.engine.AsyncEventSchedulerConfig.ASYNCEVENT_PROCESSOR_INTERVAL;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.imixs.workflow.engine.AsyncEventService;
import org.imixs.workflow.engine.EventLogService;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import jakarta.annotation.security.DeclareRoles;
import jakarta.annotation.security.RunAs;
import jakarta.ejb.Singleton;
import jakarta.ejb.Startup;
import jakarta.ejb.Timeout;
import jakarta.ejb.Timer;
import jakarta.ejb.TimerConfig;
import jakarta.ejb.TimerService;
import jakarta.inject.Inject;
/**
* The CognitoEventScheduler starts a scheduler service to process cognition
* events in an asynchronous way by calling the CognitoEventService.
* <p>
* The Scheduler runs on a non-persistent ejb timer with the same interval
* settings as the AsyncEventProcessor
* <p>
* Configuration is read from the environment variables
* 'ASYNCEVENT_PROCESSOR_INTERVAL' and an optional delay defined by
* 'ASYNCEVENT_PROCESSOR_INITIALDELAY'. To enable the processor
* 'ASYNCEVENT_PROCESSOR_ENABLED' must be set to true (default=false).
* 'ASYNCEVENT_PROCESSOR_DEADLOCK' deadlock timeout
* <p>
* In a clustered environment this timer runs in each cluster member that
* contains the EJB. So this means the non-persistent EJB Timer scales
* horizontal within a clustered environment – e.g. a Kubernetes cluster.
*
* @see AsyncEventService
* @version 1.1
* @author rsoika
*
*/
@DeclareRoles({ "org.imixs.ACCESSLEVEL.MANAGERACCESS" })
@RunAs("org.imixs.ACCESSLEVEL.MANAGERACCESS")
@Startup
@Singleton
public class CognitoEventScheduler {
// enabled
@Inject
@ConfigProperty(name = ASYNCEVENT_PROCESSOR_ENABLED, defaultValue = "false")
boolean enabled;
// timeout interval in ms
@Inject
@ConfigProperty(name = ASYNCEVENT_PROCESSOR_INTERVAL, defaultValue = "1000")
long interval;
// initial delay in ms
@Inject
@ConfigProperty(name = ASYNCEVENT_PROCESSOR_INITIALDELAY, defaultValue = "0")
long initialDelay;
// deadlock timeout interval in ms
@Inject
@ConfigProperty(name = ASYNCEVENT_PROCESSOR_DEADLOCK, defaultValue = "60000")
long deadLockInterval;
private static final Logger logger = Logger.getLogger(CognitoEventScheduler.class.getName());
@Resource
TimerService timerService;
@Inject
CognitoEventService cognitoEventService;
@Inject
EventLogService eventLogService;
@PostConstruct
public void init() {
if (enabled) {
// Registering a non-persistent Timer Service.
final TimerConfig timerConfig = new TimerConfig();
timerConfig.setInfo("Dynamixs AI CognitoEventScheduler");
timerConfig.setPersistent(false);
timerService.createIntervalTimer(initialDelay, interval, timerConfig);
logger.log(Level.INFO, "├── ✅ Started CognitoEventScheduler - initalDelay={0} inverval={1}",
new Object[] { initialDelay, interval });
}
}
/**
* The method delegates the event processing to the stateless ejb
* AsyncEventProcessor.
* <p>
* Before processing the eventLog the method releases possible dead locks first.
* Both methods are running in separate transactions
*
*/
@Timeout
public void run(Timer timer) {
eventLogService.releaseDeadLocks(deadLockInterval,
CognitoEventService.EVENTLOG_TOPIC_COGNITO_PROMPT,
CognitoEventService.EVENTLOG_TOPIC_COGNITO_CACHE);
cognitoEventService.processEventLog();
}
}

View file

@ -0,0 +1,243 @@
/****************************************************************************
* 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.cognition;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.imixs.ai.ImixsAIContextHandler;
import org.imixs.ai.workflow.OpenAIAPIConnector;
import org.imixs.ai.workflow.OpenAIAPIService;
import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.engine.AsyncEventScheduler;
import org.imixs.workflow.engine.EventLogService;
import org.imixs.workflow.engine.WorkflowService;
import org.imixs.workflow.engine.jpa.EventLog;
import org.imixs.workflow.exceptions.AdapterException;
import org.imixs.workflow.exceptions.PluginException;
import jakarta.annotation.security.DeclareRoles;
import jakarta.annotation.security.RunAs;
import jakarta.ejb.LocalBean;
import jakarta.ejb.Stateless;
import jakarta.ejb.TransactionAttribute;
import jakarta.ejb.TransactionAttributeType;
import jakarta.inject.Inject;
import jakarta.json.JsonObject;
import jakarta.persistence.OptimisticLockException;
/**
* The CognitoEventService is responsible to process Dynamixs-AI Cognito events
* in an asynchronous batch process. The CognitoEventService automatically
* lookup eventLog entries of the topic "rag.event.*". The RAGEventService is
* called only by the RAGEventScheduler which is implementing a
* ManagedScheduledExecutorService.
* <p>
* The CognitoEventService reacts on events from the following types:
* <ul>
* <li>cognito.event.prompt - resoles the llm prompt based on the given workitem
* and creates a cache event</li>
* <li>cognito.event.cache - indicates that this entry is still cached in the
* event log database</li>
* </ul>
* The processor look up the workItem by the $uniqueId.
* <p>
* To prevent concurrent processes to handle the same workitems the batch
* process uses a Optimistic lock strategy. After fetching new event log entries
* the processor updates the eventLog entry in a new transaction and set the
* topic to 'batch.process.lock'. After that update we can be sure that no other
* process is dealing with these entries. After completing the processing step
* the eventlog entry will be removed.
* <p>
* To avoid ad deadlock the processor set an expiration time on the lock, so the
* lock will be auto-released after 1 minute (batch.processor.deadlock).
*
* @see AsyncEventScheduler
* @version 1.0
* @author rsoika
*
*/
@DeclareRoles({ "org.imixs.ACCESSLEVEL.MANAGERACCESS" })
@RunAs("org.imixs.ACCESSLEVEL.MANAGERACCESS")
@Stateless
@LocalBean
public class CognitoEventService {
public static final String EVENTLOG_TOPIC_COGNITO_PROMPT = "cognito.event.prompt";
public static final String EVENTLOG_TOPIC_COGNITO_CACHE = "cognito.event.cache";
public static final String ITEM_PROMPT_DEFINITION = "prompt.definition";
private static final Logger logger = Logger.getLogger(CognitoEventService.class.getName());
@Inject
EventLogService eventLogService;
@Inject
private WorkflowService workflowService;
@Inject
private OpenAIAPIService llmService;
@Inject
ImixsAIContextHandler imixsAIContextHandler;
@Inject
CognitoLockHandler cognitoLockHandler;
@Inject
@ConfigProperty(name = OpenAIAPIConnector.ENV_LLM_SERVICE_ENDPOINT)
Optional<String> serviceEndpoint;
/**
* The method lookups for RAG event log entries and updates the RAG index
* information
* <p>
* Each eventLogEntry provides optional a prompt template
*
*/
@TransactionAttribute(value = TransactionAttributeType.REQUIRES_NEW)
public void processEventLog() {
long l = System.currentTimeMillis();
// test for new event log entries by timeout...
List<EventLog> events = eventLogService.findEventsByTimeout(100,
CognitoEventService.EVENTLOG_TOPIC_COGNITO_PROMPT, CognitoEventService.EVENTLOG_TOPIC_COGNITO_CACHE);
if (events.size() > 0) {
logger.log(Level.INFO, "├── 🔃 processing {0} Cognito events....", events.size());
// do we know the endpoint?
if (!serviceEndpoint.isPresent()) {
logger.log(Level.WARNING,
"├── ⚠️ Missing environment var 'llm.service.endpoint' - can not process cognito events!");
return;
}
for (EventLog eventLogEntry : events) {
try {
boolean llmAPIDebug = true;
logger.log(Level.INFO,
"│ ├── Event: " + eventLogEntry.getTopic() + " - " + eventLogEntry.getRef());
// first try to lock the eventLog entry....
if (eventLogService.lock(eventLogEntry)) {
// Index / Update event?
ItemCollection workitem = workflowService.getWorkItem(eventLogEntry.getRef());
if (workitem == null) {
logger.log(Level.WARNING, "│ ├── ⚠️ unable to read workitem: " + eventLogEntry.getRef());
eventLogService.removeEvent(eventLogEntry.getId());
continue;
}
ItemCollection promptItemCol = new ItemCollection(eventLogEntry.getData());
switch (eventLogEntry.getTopic()) {
case CognitoEventService.EVENTLOG_TOPIC_COGNITO_PROMPT + ".lock":
// load the promptDefinition from the event data object
String promptTemplate = promptItemCol.getItemValueString(ITEM_PROMPT_DEFINITION);
imixsAIContextHandler.setWorkItem(workitem);
imixsAIContextHandler.addPromptDefinition(promptTemplate);
String llmPrompt = imixsAIContextHandler.toString();
// if we have a prompt we call the llm api endpoint
if (llmPrompt != null && !llmPrompt.isBlank()) {
String llmAPIEndpoint = serviceEndpoint.get();
// postPromptCompletion
JsonObject jsonPrompt = llmService.buildJsonPromptObjectV1(llmPrompt, false,
workitem.getItemValueString("ai.prompt.prompt_options"));
String completionResult = llmService.postPromptCompletion(jsonPrompt, llmAPIEndpoint);
// process the ai.result....
if (llmAPIDebug) {
logger.info("===> Completion Result: ");
logger.info(completionResult);
}
String resultMessage = llmService.processPromptResult(completionResult, "", workitem);
// if the workitem is not locked we can safety update the $workflowabstarct
if (!cognitoLockHandler.isLocked(workitem.getUniqueID())) {
logger.log(Level.INFO,
"│ ├── ✓ workitem not locked - updating $workflowabstract...");
workitem.setItemValue("$workflowabstract", resultMessage);
workflowService.getDocumentService().save(workitem);
} else {
// workitem is still in edit mode - so we create a cache event that just stores
// the result. This is an async caching mechanism for the front end.
logger.log(Level.INFO,
"│ ├── ⚐ workitem locked - create COGNITO_CACHE event...");
promptItemCol.setItemValue("$workflowabstract", resultMessage);
// create a new event
eventLogService.createEvent(CognitoEventService.EVENTLOG_TOPIC_COGNITO_CACHE,
workitem.getUniqueID(),
promptItemCol);
}
} else {
logger.finest(
"......no prompt definition found for " + workitem.getUniqueID());
}
// finally remove the event log entry...
eventLogService.removeEvent(eventLogEntry.getId());
break;
case CognitoEventService.EVENTLOG_TOPIC_COGNITO_CACHE + ".lock":
logger.log(Level.INFO,
"│ ├── process COGNITO_CACHE event...");
if (!cognitoLockHandler.isLocked(workitem.getUniqueID())) {
logger.log(Level.INFO,
"│ ├── ✓ workitem not locked - updating $workflowabstract...");
// ItemCollection promptItemCol = new ItemCollection(eventLogEntry.getData());
String resultMessage = promptItemCol.getItemValueString("$workflowabstract");
workitem.setItemValue("$workflowabstract", resultMessage);
workflowService.getDocumentService().save(workitem);
// finally remove the event log entry...
eventLogService.removeEvent(eventLogEntry.getId());
} else {
// repeat event until workitem is unlocked
eventLogService.unlock((eventLogEntry));
}
break;
default:
// no op
}
}
} catch (OptimisticLockException e) {
// lock was not possible - continue....
logger.log(Level.INFO, "│ ├── ⚠️ unable to lock AsyncEvent: {0}", e.getMessage());
} catch (PluginException | AdapterException e) {
logger.log(Level.WARNING, e.getErrorCode() + " - " + e.getMessage());
// remove the event log entry...
eventLogService.removeEvent(eventLogEntry.getId());
}
}
logger.log(Level.INFO, "├── ✅ {0} CognitoEvents processed in {1}ms",
new Object[] { events.size(), System.currentTimeMillis() - l });
}
}
}

View file

@ -0,0 +1,274 @@
/****************************************************************************
* Copyright (c) 2022-2025 Imixs Software Solutions GmbH and others.
* https://www.imixs.com
*
* 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.cognition;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.imixs.workflow.exceptions.PluginException;
import org.imixs.workflow.faces.data.WorkflowEvent;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.interceptor.Interceptor;
/**
* The CognitoLockHandler is an application scoped CDI bean that holds a list of
* workitem uniqueIds currently in edit mode. This caching mechanism is used by
* the {@code CognitoEventService} to avoid optimistic lock exceptions.
* <p>
* The IDs are stored in the lock cache. The CognitoEventService looks up an ID
* in this cache before updating a workitem in a safe way.
* <p>
* It may happen that a user switches into edit mode
* ({@link WorkflowEvent#DOCUMENT_CHANGED}) without updating the workitem. For
* this reason a locked ID will be released after {@value #LOCK_TIMEOUT_SECONDS}
* seconds automatically.
* <p>
* <strong>Thread Safety:</strong> This implementation uses a
* {@link ConcurrentHashMap} to ensure thread-safe operations in a
* multi-user-environment where the same workitem can be opened by different
* users in parallel.
*
* @author rsoika
* @version 1.0
*/
@ApplicationScoped
public class CognitoLockHandler {
private static final Logger logger = Logger.getLogger(CognitoLockHandler.class.getName());
/**
* Lock timeout in seconds. After this duration, a lock is considered stale and
* will be automatically released.
*/
public static final long LOCK_TIMEOUT_SECONDS = 120;
/**
* Thread-safe cache storing workitem uniqueIds mapped to their lock timestamp.
* The timestamp is used to determine if a lock has expired.
*/
private final Map<String, Instant> lockCache = new ConcurrentHashMap<>();
/**
* WorkflowEvent listener that manages the lock cache based on document
* lifecycle events.
* <p>
* <ul>
* <li>{@link WorkflowEvent#DOCUMENT_CHANGED}: Adds the workitem's uniqueID to
* the lock cache (lock operation)</li>
* <li>{@link WorkflowEvent#DOCUMENT_AFTER_SAVE}: Removes the workitem's
* uniqueID from the lock cache (unlock operation)</li>
* </ul>
* <p>
* This observer is executed with {@link Interceptor.Priority#LIBRARY_BEFORE}
* priority to ensure it runs before other observers.
*
* @param workflowEvent the workflow event containing the workitem and event
* type
* @throws PluginException if an error occurs during event processing
*/
public void onWorkflowEvent(@Observes WorkflowEvent workflowEvent)
throws PluginException {
int eventType = workflowEvent.getEventType();
String uniqueId = extractUniqueId(workflowEvent);
if (uniqueId == null || uniqueId.isEmpty()) {
logger.log(Level.FINE, "Skipping lock operation - workitem has no uniqueId");
return;
}
// Lock the workitem when user enters edit mode
if (WorkflowEvent.WORKITEM_CHANGED == eventType) {
lock(uniqueId);
logger.log(Level.INFO, "Workitem locked: uniqueId={0}, active locks={1}",
new Object[] { uniqueId, lockCache.size() });
}
// Unlock the workitem after successful save
if (WorkflowEvent.WORKITEM_AFTER_PROCESS == eventType) {
boolean wasLocked = unlock(uniqueId);
if (wasLocked) {
logger.log(Level.INFO, "Workitem unlocked after process: uniqueId={0}, remaining locks={1}",
new Object[] { uniqueId, lockCache.size() });
} else {
logger.log(Level.INFO, "Workitem was not locked (possibly expired): uniqueId={0}",
uniqueId);
}
}
}
/**
* Checks if the given workitem uniqueID is currently locked (in edit mode).
* <p>
* This method also performs cleanup of stale locks. If the lock for the given
* ID has exceeded the timeout ({@value #LOCK_TIMEOUT_SECONDS} seconds), it will
* be automatically removed and this method returns {@code false}.
*
* @param id the unique identifier of the workitem to check
* @return {@code true} if the workitem is currently locked and the lock has not
* expired, {@code false} otherwise
*/
public boolean isLocked(String id) {
if (id == null || id.isEmpty()) {
logger.log(Level.FINE, "isLocked called with null or empty id - returning false");
return false;
}
Instant lockTimestamp = lockCache.get(id);
if (lockTimestamp == null) {
logger.log(Level.FINE, "Workitem not locked: uniqueId={0}", id);
return false;
}
// Check if lock has expired
if (isLockExpired(lockTimestamp)) {
logger.log(Level.INFO, "Lock expired and removed: uniqueId={0}, locked since={1}",
new Object[] { id, lockTimestamp });
lockCache.remove(id);
return false;
}
long lockedSeconds = java.time.Duration.between(lockTimestamp, Instant.now()).getSeconds();
logger.log(Level.FINE, "Workitem is locked: uniqueId={0}, locked for {1}s",
new Object[] { id, lockedSeconds });
return true;
}
/**
* Adds a workitem uniqueID to the lock cache with the current timestamp.
* <p>
* If the ID is already in the cache, the timestamp will be updated (lock
* refresh).
*
* @param id the unique identifier of the workitem to lock
*/
public void lock(String id) {
if (id == null || id.isEmpty()) {
logger.log(Level.WARNING, "Attempted to lock with null or empty id - operation ignored");
return;
}
Instant previousLock = lockCache.put(id, Instant.now());
if (previousLock != null) {
logger.log(Level.FINE, "Lock refreshed: uniqueId={0}, previous lock from={1}",
new Object[] { id, previousLock });
} else {
logger.log(Level.FINE, "New lock created: uniqueId={0}", id);
}
// Opportunistic cleanup of expired locks
cleanupExpiredLocks();
}
/**
* Removes a workitem uniqueID from the lock cache.
*
* @param id the unique identifier of the workitem to unlock
* @return {@code true} if the workitem was locked and is now unlocked,
* {@code false} if the workitem was not locked
*/
public boolean unlock(String id) {
if (id == null || id.isEmpty()) {
logger.log(Level.WARNING, "Attempted to unlock with null or empty id - operation ignored");
return false;
}
Instant removed = lockCache.remove(id);
if (removed != null) {
long lockedDuration = java.time.Duration.between(removed, Instant.now()).getSeconds();
logger.log(Level.FINE, "Lock removed: uniqueId={0}, was locked for {1}s",
new Object[] { id, lockedDuration });
return true;
}
return false;
}
/**
* Returns the current number of active locks in the cache.
* <p>
* <strong>Note:</strong> This count may include expired locks that have not yet
* been cleaned up.
*
* @return the number of entries in the lock cache
*/
public int getLockCount() {
return lockCache.size();
}
/**
* Removes all expired locks from the cache.
* <p>
* This method is called opportunistically during lock operations to prevent
* unbounded cache growth.
*/
public void cleanupExpiredLocks() {
int initialSize = lockCache.size();
lockCache.entrySet().removeIf(entry -> {
boolean expired = isLockExpired(entry.getValue());
if (expired) {
logger.log(Level.FINE, "Cleaning up expired lock: uniqueId={0}, locked since={1}",
new Object[] { entry.getKey(), entry.getValue() });
}
return expired;
});
int removed = initialSize - lockCache.size();
if (removed > 0) {
logger.log(Level.INFO, "Expired locks cleanup completed: removed={0}, remaining={1}",
new Object[] { removed, lockCache.size() });
}
}
/**
* Checks if a lock timestamp has exceeded the timeout threshold.
*
* @param lockTimestamp the timestamp when the lock was created
* @return {@code true} if the lock is expired, {@code false} otherwise
*/
private boolean isLockExpired(Instant lockTimestamp) {
return lockTimestamp.plusSeconds(LOCK_TIMEOUT_SECONDS).isBefore(Instant.now());
}
/**
* Extracts the uniqueId from the workitem contained in the workflow event.
*
* @param workflowEvent the workflow event
* @return the uniqueId of the workitem, or {@code null} if not available
*/
private String extractUniqueId(WorkflowEvent workflowEvent) {
if (workflowEvent == null || workflowEvent.getWorkitem() == null) {
logger.log(Level.WARNING, "WorkflowEvent or workitem is null - cannot extract uniqueId");
return null;
}
String uniqueId = workflowEvent.getWorkitem().getUniqueID();
if (uniqueId == null || uniqueId.isEmpty()) {
logger.log(Level.FINE, "Workitem has no uniqueId (possibly a new document)");
}
return uniqueId;
}
}

View file

@ -0,0 +1,137 @@
/****************************************************************************
* 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.cognition;
import java.util.List;
import java.util.logging.Logger;
import org.imixs.ai.workflow.ImixsAIPromptService;
import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.engine.EventLogService;
import org.imixs.workflow.engine.WorkflowService;
import org.imixs.workflow.exceptions.PluginException;
import ai.dynamixs.ChronicleTextBuilder;
import jakarta.inject.Inject;
/**
* The CognitoPlugin is a generic plugin to generate an cognito.create.event.
* This event is processed by the `CognitoEventService` to generate an AI based
* workflow abstract
* <p>
* The plugin can be activated by the following workflow result tag
*
* <pre>
{@code
<dynamixs-ai name="COGNITO" />
}
* </pre>
* <p>
* The embedded tag 'debug' is optional
*
* <p>
* The prompt definition is either loaded from the corresponding BPMN event
* (DataObject) or generated as generic default template in case that no
* PromptDefinition is assigned to the current event.
*
*
* @author rsoika
*
*/
public class CognitoPlugin extends org.imixs.ai.rag.RAGIndexPlugin {
private static final Logger logger = Logger.getLogger(CognitoPlugin.class.getName());
@Inject
private WorkflowService workflowService;
@Inject
private EventLogService eventLogService;
@Inject
private ImixsAIPromptService imixsAIPromptService;
@Override
public ItemCollection run(ItemCollection workitem, ItemCollection event) throws PluginException {
// do only run on type = workitem!
if (!workitem.getType().equals("workitem")) {
// no op!
return workitem;
}
// read optional configuration form the model or imixs.properties....
List<ItemCollection> abstarctAIDefinitions = workflowService.evalWorkflowResultXML(event, "dynamixs-ai",
"COGNITO", workitem, false);
// INDEX Mode?
if (abstarctAIDefinitions != null && abstarctAIDefinitions.size() > 0) {
logger.info("├── running Dynamixs.AI Cognito Plugin");
// test if the event provides a Prompt template!
String promptTemplate = imixsAIPromptService.loadPromptTemplateByModelElement(event);
if (promptTemplate != null && !promptTemplate.isBlank()) {
logger.info("│ ├── load custom cognito prompt template from event");
} else {
promptTemplate = buildDefaultPromptTemplate(workitem);
}
// we create a data object that holds the template
ItemCollection promptItemCol = new ItemCollection();
promptItemCol.setItemValue(CognitoEventService.ITEM_PROMPT_DEFINITION, promptTemplate);
eventLogService.createEvent(CognitoEventService.EVENTLOG_TOPIC_COGNITO_PROMPT, workitem.getUniqueID(),
promptItemCol);
}
return workitem;
}
/**
* This method generates a generic prompt definition including a prompt template
* including the workflow chronicle data.
*
* @param workitem
* @return
*/
private String buildDefaultPromptTemplate(ItemCollection workitem) {
// generate default template
ChronicleTextBuilder chronicleTextBuilder = new ChronicleTextBuilder(workitem);
String promptTemplate = chronicleTextBuilder.build();
String result = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + //
"<PromptDefinition>\n" + //
"\t<prompt_options>{\"n_predict\": 4096, \"temperature\": 0.0}</prompt_options>\n" + //
"\n" + //
" <prompt role=\"system\"><![CDATA[\n" + //
"You are a process analyst. A workflow has just been completed.\n" + //
"\n" + //
"Create a brief project insight:\n" + //
"- What specifically happened? (Include relevant names from people, companies, products when mentioned.)\n"
+ //
"- Is there anything unusual or important to remember for the future? (Pay attention to user comments - they often contain important warnings or decisions.)\n"
+ //
"\n" + //
"No general advice. Only project-specific facts and insights.]]>\n" + //
" </prompt>\n" + //
" <prompt role=\"user\"><![CDATA[\n" + promptTemplate + "]]>\n" + //
" </prompt>\n" + //
"\n" + //
"</PromptDefinition> ";
return result;
}
}

View file

@ -0,0 +1,63 @@
// custom scripts
/**
* This method renders $workflowabstarct as markup
*/
$(document).ready(function() {
});
function showTooltip(trigger) {
const tooltip = trigger.nextElementSibling;
const rect = trigger.getBoundingClientRect();
// Position relativ zum Viewport berechnen
tooltip.style.left = rect.left + 'px';
tooltip.style.top = (rect.bottom + 5) + 'px';
// Markdown nur einmal parsen (lazy loading)
const markdownContainer = tooltip.querySelector('.imixs-workflowabstract');
if (markdownContainer && !markdownContainer.dataset.parsed) {
const rawMarkdown = markdownContainer.textContent;
markdownContainer.innerHTML = marked.parse(rawMarkdown);
markdownContainer.dataset.parsed = 'true'; // Flag setzen
}
tooltip.classList.add('visible');
// Optional: Prüfen ob Tooltip aus dem Viewport ragt
const tooltipRect = tooltip.getBoundingClientRect();
if (tooltipRect.right > window.innerWidth) {
tooltip.style.left = (window.innerWidth - tooltipRect.width - 10) + 'px';
}
if (tooltipRect.bottom > window.innerHeight) {
tooltip.style.top = (rect.top - tooltipRect.height - 5) + 'px';
}
}
function hideTooltip(trigger) {
const tooltip = trigger.nextElementSibling;
tooltip.classList.remove('visible');
}
/**
* Helper method to trigger the workflowabstract element in the form
* @param {} trigger
*/
function toggleWorkflowAbstract(trigger) {
const content = trigger.nextElementSibling;
// Markdown nur einmal parsen (lazy loading)
const markdownContainer = content.querySelector('.imixs-markdown-output');
if (markdownContainer && !markdownContainer.dataset.parsed) {
const rawMarkdown = markdownContainer.textContent;
markdownContainer.innerHTML = marked.parse(rawMarkdown);
markdownContainer.dataset.parsed = 'true';
}
content.classList.toggle('hidden');
}

View file

@ -53,3 +53,64 @@
background-color: transparent !important;
}
/* Neues Abstract markup */
.tooltip-container {
position: relative;
display: inline-block;
}
.tooltip-trigger {
cursor: pointer;
color: #007bff;
}
.tooltip-content {
display: none;
position: fixed;
font-size: 0.85rem;
line-height: 1.75;
left: 0;
top: 100%;
z-index: 1200;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
min-width: 420px;
max-width: 650px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
background-color: #ffffff;
padding: 10px;
transition: opacity 0.5s ease-in-out, visibility 0.3s ease-in-out;
}
.tooltip-container:hover .tooltip-content {
display: block;
}
.imixs-workflowabstract-toggle-icon {
margin-left: auto;
align-self: flex-end;
cursor: pointer;
padding: 10px;
font-size: 1.25rem;
color: #599191;
}
.imixs-workflowabstract-content {
flex-basis: 100%;
padding: 0 15px;
border: 1px solid #599191;
margin: 0 15px;
}

View file

@ -0,0 +1,79 @@
<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:pt="http://xmlns.jcp.org/jsf/passthrough"
xmlns:fn="http://xmlns.jcp.org/jsp/jstl/functions" xmlns:marty="http://xmlns.jcp.org/jsf/composite/marty"
xmlns:i="http://xmlns.jcp.org/jsf/composite/imixs">
<!-- Build with link ? -->
<span class="imixs-icon tooltip-container" title=" ">
<span class="tooltip-trigger" onmouseenter="showTooltip(this)" onmouseleave="hideTooltip(this)">
<ui:fragment rendered="#{showlink}">
<h:link outcome="/pages/workitems/workitem">
<!-- typcn -->
<ui:fragment rendered="#{fn:startsWith(workitem.item['txtWorkflowImageURL'],'typcn')}">
<span class="imixs-typicon" data-typicon="#{workitem.item['txtWorkflowImageURL']}"></span>
</ui:fragment>
<!-- Image -->
<ui:fragment rendered="#{!fn:startsWith(workitem.item['txtWorkflowImageURL'],'typcn')}">
<h:graphicImage rendered="#{!empty workitem.item['txtWorkflowImageURL']}"
url="#{workitem.item['txtWorkflowImageURL']}" style="max-height: 24px; margin: 4px;">
</h:graphicImage>
<h:graphicImage rendered="#{empty workitem.item['txtWorkflowImageURL']}"
value="/layout/icons/vwicn070.png" style="max-height: 24px; margin: 4px;"></h:graphicImage>
</ui:fragment>
<f:param name="id" value="#{workitem.item['$uniqueid']}" />
</h:link>
</ui:fragment>
<!-- Build without link -->
<ui:fragment rendered="#{!showlink}">
<!-- typcn -->
<ui:fragment rendered="#{fn:startsWith(workitem.item['txtWorkflowImageURL'],'typcn')}">
<span class="imixs-typicon" data-typicon="#{workitem.item['txtWorkflowImageURL']}"></span>
</ui:fragment>
<!-- Image -->
<ui:fragment rendered="#{!fn:startsWith(workitem.item['txtWorkflowImageURL'],'typcn')}">
<h:graphicImage rendered="#{!empty workitem.item['txtWorkflowImageURL']}"
url="#{workitem.item['txtWorkflowImageURL']}" style="max-height: 24px; margin: 4px;">
</h:graphicImage>
<h:graphicImage rendered="#{empty workitem.item['txtWorkflowImageURL']}"
value="/layout/icons/vwicn070.png" style="max-height: 24px; margin: 4px;"></h:graphicImage>
</ui:fragment>
</ui:fragment>
</span>
<h:panelGroup class="tooltip-content">
<h:outputText value="#{message.created}: " style="font-weight:bold;" />
<h:outputText value="#{workitem.item['$created']}">
<f:convertDateTime timeZone="#{message.timeZone}" type="both" pattern="#{message.dateTimePattern}" />
</h:outputText>
<h:outputText value=" #{message.by} #{userController.getUserName(workitem.item['$creator'])}" />
<!-- Modified -->
<br />
<span>
<h:outputText style="font-weight: bold;" value="#{message.edited}: " />
<h:outputText value="#{workitem.item['$lasteventdate']}" style="font-weight: normal;">
<f:convertDateTime timeZone="#{message.timeZone}" pattern="#{message.dateTimePattern}" />
</h:outputText>
<h:outputText value=" #{message.by} #{userController.getUserName(workitem.item['$editor'])}" />
</span>
<hr />
<!-- Markup -->
<div class="imixs-workflowabstract">
<h:outputText styleClass="imixs-markdown-output" value="#{workitem.item['$WorkflowAbstract']}"
escape="false" />
</div>
</h:panelGroup>
</span>
</ui:composition>

View file

@ -0,0 +1,101 @@
<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:pt="http://xmlns.jcp.org/jsf/passthrough"
xmlns:marty="http://xmlns.jcp.org/jsf/composite/marty" xmlns:i="http://xmlns.jcp.org/jsf/composite/imixs">
<div style="display: flex;">
<div style="flex:1;">
<h1>
<h:outputText value="#{workflowController.workitem.item['$workflowgroup']}" />
»
<h:outputText value=" #{workflowController.workitem.item['$workflowstatus']}" />
<!-- Display Deleted status -->
<h:outputText rendered="#{workflowController.workitem.item['type'] eq 'workitemdeleted'}"
value="- #{message.deleted}!" style="color:red;" />
</h1>
<!-- Workitem Info Header -->
<div class="imixs-form-header">
<!-- *** Image *** -->
<div class="image-block">
<ui:include src="/layout/workitem_icon.xhtml">
<ui:param name="workitem" value="#{workflowController.workitem}" />
<ui:param name="hidetooltip" value="#{true}" />
</ui:include>
</div>
<ui:remove><!-- *** Standard Info *** --></ui:remove>
<div class="content-block">
<h2>
<h:outputText escape="false"
rendered="#{empty workflowController.workitem.item['$WorkflowSummary']}"
value="#{message.new_workitem}" />
<h:outputText escape="false"
rendered="#{!empty workflowController.workitem.item['$WorkflowSummary']}"
value="#{workflowController.workitem.item['$WorkflowSummary']}" />
</h2>
<h:outputLabel value="#{message['process.title']}: " />
<h:link outcome="/pages/notes_board?processref=#{workflowController.workitem.item['process.Ref']}">
<h:outputText value="#{workflowController.workitem.item['Process.Name']}" />
</h:link>
<ui:remove><!-- *** Owners *** --></ui:remove>
<h:panelGroup styleClass="imixs-no-print" layout="block">
<ui:repeat value="#{workflowController.workitem.itemList['$owner']}" var="name">
<ui:param name="profile" value="#{userController.getProfile(name)}"></ui:param>
<ui:fragment rendered="#{! empty profile}">
<h:graphicImage styleClass="imixs-workitem-chronicle-usericon"
title="#{profile.item['txtusername']}"
rendered="#{! empty profile.item['txtusericon']}"
value="/api/snapshot/#{profile.item['$uniqueid']}/file/#{profile.item['txtusericon']}">
</h:graphicImage>
<!-- optional - user initials -->
<h:panelGroup layout="block" rendered="#{empty profile.item['txtusericon']}">
<h:outputText styleClass="imixs-workitem-chronicle-usericon"
title="#{profile.item['txtusername']}" value="#{profile.item['txtinitials']}" />
</h:panelGroup>
</ui:fragment>
</ui:repeat>
</h:panelGroup>
</div>
<!-- *** Comment Toggle *** -->
<div class="imixs-workflowabstract-toggle-icon" onclick="toggleWorkflowAbstract(this)">
<i class="fa-regular fa-comment-dots"></i>
</div>
<!-- Extra Content - volle Breite -->
<div class="imixs-workflowabstract-content hidden">
<h:outputText styleClass="imixs-markdown-output"
value="#{workflowController.workitem.item['$WorkflowAbstract']}" escape="false" />
</div>
</div>
</div>
<!-- QR-Code displayed only if property 'qr-code.url.prafix' is filled -->
<ui:param name=" qrprafix" value="#{propertyController.getProperty('qr-code.url.prafix')}">
</ui:param>
<ui:fragment
rendered="#{(! empty workflowController.workitem.uniqueID) and (!userController.mobileUser) and (!empty qrprafix)}">
<div style="flex-grow: 1;"></div>
<div>
<a href="#" onclick="imixsOfficeWorkitem.printQRCode();">
<h:graphicImage styleClass="imixsQrCode"
url="/api/qr-code?size=130&amp;key=#{qrprafix}#{workflowController.workitem.uniqueID}">
</h:graphicImage>
</a>
</div>
</ui:fragment>
</div>
</ui:composition>

View file

@ -37,6 +37,7 @@
<imixs:value><![CDATA[org.imixs.marty.profile.MailPlugin]]></imixs:value>
<imixs:value><![CDATA[org.imixs.workflow.engine.plugins.IntervalPlugin]]></imixs:value>
<imixs:value><![CDATA[org.imixs.workflow.engine.plugins.SplitAndJoinPlugin]]></imixs:value>
<imixs:value><![CDATA[ai.dynamixs.cognition.CognitoPlugin]]></imixs:value>
</imixs:item>
<open-bpmn:auto-align>true</open-bpmn:auto-align>
</bpmn2:extensionElements>
@ -216,11 +217,7 @@ Address: <itemvalue>customer.country</itemvalue> <itemvalue>customer.zip</itemva
<imixs:value><![CDATA[workitem]]></imixs:value>
</imixs:item>
<imixs:item name="txtworkflowabstract" type="xs:string">
<imixs:value><![CDATA[<strong>Customer: <itemvalue>customer.name</itemvalue></strong><br />
E-Mail: <itemvalue>customer.email</itemvalue><br />
Address: <itemvalue>customer.country</itemvalue> <itemvalue>customer.zip</itemvalue>-<itemvalue>customer.city</itemvalue> <itemvalue>customer.address</itemvalue><br />
<hr />
<imixs:value><![CDATA[
]]></imixs:value>
</imixs:item>
</bpmn2:extensionElements>
@ -1913,7 +1910,7 @@ Address: <itemvalue>customer.country</itemvalue> <itemvalue>customer.zip</itemva
<bpmn2:association id="association_jW6RnQ" sourceRef="event_KwpMKw" targetRef="dataObject_iq4agg">
<bpmn2:documentation id="documentation_XBh77Q"/>
</bpmn2:association>
<bpmn2:intermediateCatchEvent id="event_oOnn8A" imixs:activityid="42" name="AI Assist &#128161;︎">
<bpmn2:intermediateCatchEvent id="event_oOnn8A" imixs:activityid="42" name="AI Cognito &#128161;︎">
<bpmn2:extensionElements>
<imixs:item name="keylogtimeformat" type="xs:string">
<imixs:value><![CDATA[2]]></imixs:value>
@ -1965,7 +1962,10 @@ Address: <itemvalue>customer.country</itemvalue> <itemvalue>customer.zip</itemva
<imixs:value/>
</imixs:item>
<imixs:item name="txtactivityresult" type="xs:string">
<imixs:value><![CDATA[<item name="comment" ignore="true"/>]]></imixs:value>
<imixs:value><![CDATA[<item name="action">home</item>
<dynamixs-ai name="COGNITO" >
<debug>true</debug>
</dynamixs-ai>]]></imixs:value>
</imixs:item>
<imixs:item name="txtname" type="xs:string">
<imixs:value><![CDATA[Speichern]]></imixs:value>