new cognito service
This commit is contained in:
parent
6743a9d716
commit
23e09d4600
9 changed files with 1095 additions and 7 deletions
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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 });
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
|
@ -53,3 +53,64 @@
|
||||||
background-color: transparent !important;
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
79
dynamixs-ai-app/src/main/webapp/layout/workitem_icon.xhtml
Normal file
79
dynamixs-ai-app/src/main/webapp/layout/workitem_icon.xhtml
Normal 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>
|
||||||
|
|
@ -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&key=#{qrprafix}#{workflowController.workitem.uniqueID}">
|
||||||
|
</h:graphicImage>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</ui:fragment>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</ui:composition>
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
<imixs:value><![CDATA[org.imixs.marty.profile.MailPlugin]]></imixs:value>
|
<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.IntervalPlugin]]></imixs:value>
|
||||||
<imixs:value><![CDATA[org.imixs.workflow.engine.plugins.SplitAndJoinPlugin]]></imixs:value>
|
<imixs:value><![CDATA[org.imixs.workflow.engine.plugins.SplitAndJoinPlugin]]></imixs:value>
|
||||||
|
<imixs:value><![CDATA[ai.dynamixs.cognition.CognitoPlugin]]></imixs:value>
|
||||||
</imixs:item>
|
</imixs:item>
|
||||||
<open-bpmn:auto-align>true</open-bpmn:auto-align>
|
<open-bpmn:auto-align>true</open-bpmn:auto-align>
|
||||||
</bpmn2:extensionElements>
|
</bpmn2:extensionElements>
|
||||||
|
|
@ -216,11 +217,7 @@ Address: <itemvalue>customer.country</itemvalue> <itemvalue>customer.zip</itemva
|
||||||
<imixs:value><![CDATA[workitem]]></imixs:value>
|
<imixs:value><![CDATA[workitem]]></imixs:value>
|
||||||
</imixs:item>
|
</imixs:item>
|
||||||
<imixs:item name="txtworkflowabstract" type="xs:string">
|
<imixs:item name="txtworkflowabstract" type="xs:string">
|
||||||
<imixs:value><![CDATA[<strong>Customer: <itemvalue>customer.name</itemvalue></strong><br />
|
<imixs:value><![CDATA[
|
||||||
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>
|
]]></imixs:value>
|
||||||
</imixs:item>
|
</imixs:item>
|
||||||
</bpmn2:extensionElements>
|
</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:association id="association_jW6RnQ" sourceRef="event_KwpMKw" targetRef="dataObject_iq4agg">
|
||||||
<bpmn2:documentation id="documentation_XBh77Q"/>
|
<bpmn2:documentation id="documentation_XBh77Q"/>
|
||||||
</bpmn2:association>
|
</bpmn2:association>
|
||||||
<bpmn2:intermediateCatchEvent id="event_oOnn8A" imixs:activityid="42" name="AI Assist 💡︎">
|
<bpmn2:intermediateCatchEvent id="event_oOnn8A" imixs:activityid="42" name="AI Cognito 💡︎">
|
||||||
<bpmn2:extensionElements>
|
<bpmn2:extensionElements>
|
||||||
<imixs:item name="keylogtimeformat" type="xs:string">
|
<imixs:item name="keylogtimeformat" type="xs:string">
|
||||||
<imixs:value><![CDATA[2]]></imixs:value>
|
<imixs:value><![CDATA[2]]></imixs:value>
|
||||||
|
|
@ -1965,7 +1962,10 @@ Address: <itemvalue>customer.country</itemvalue> <itemvalue>customer.zip</itemva
|
||||||
<imixs:value/>
|
<imixs:value/>
|
||||||
</imixs:item>
|
</imixs:item>
|
||||||
<imixs:item name="txtactivityresult" type="xs:string">
|
<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>
|
||||||
<imixs:item name="txtname" type="xs:string">
|
<imixs:item name="txtname" type="xs:string">
|
||||||
<imixs:value><![CDATA[Speichern]]></imixs:value>
|
<imixs:value><![CDATA[Speichern]]></imixs:value>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue