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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* 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.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 💡︎">
|
||||
<bpmn2:intermediateCatchEvent id="event_oOnn8A" imixs:activityid="42" name="AI Cognito 💡︎">
|
||||
<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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue