From e00739d564a64e4c2aad0934743ac5780cd6daee Mon Sep 17 00:00:00 2001 From: Ralph Soika Date: Mon, 1 Dec 2025 23:42:36 +0100 Subject: [PATCH] improved ConditionalAIAdapter class (added caching) --- .../ai/dynamixs/ConditionalAIAdapter.java | 184 ++++++++++++++++++ .../ai/dynamixs/ConditionalDebugAdapter.java | 136 ------------- workflow/en/invoice-demo-c-1.0.0.bpmn | 140 +++++++++++-- 3 files changed, 303 insertions(+), 157 deletions(-) create mode 100644 dynamixs-ai-app/src/main/java/ai/dynamixs/ConditionalAIAdapter.java delete mode 100644 dynamixs-ai-app/src/main/java/ai/dynamixs/ConditionalDebugAdapter.java diff --git a/dynamixs-ai-app/src/main/java/ai/dynamixs/ConditionalAIAdapter.java b/dynamixs-ai-app/src/main/java/ai/dynamixs/ConditionalAIAdapter.java new file mode 100644 index 0000000..f9d6a3c --- /dev/null +++ b/dynamixs-ai-app/src/main/java/ai/dynamixs/ConditionalAIAdapter.java @@ -0,0 +1,184 @@ +package ai.dynamixs; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import org.imixs.ai.ImixsAIContextHandler; +import org.imixs.ai.workflow.ImixsAIPromptService; +import org.imixs.ai.workflow.OpenAIAPIService; +import org.imixs.workflow.ItemCollection; +import org.imixs.workflow.engine.ConditionalExpressionEvent; +import org.imixs.workflow.engine.WorkflowService; +import org.imixs.workflow.exceptions.AdapterException; +import org.imixs.workflow.exceptions.PluginException; + +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; +import jakarta.json.JsonObject; +import jakarta.transaction.TransactionScoped; + +/** + * The ConditionalAIAdapter reacts on CDI Events of type BPMNConditionEvent and + * evaluates a condition against an LLM + *

+ * The Adapter defines a Default Expression template for LLMs. The BPMN + * Configuration must only include the user prompt. See the following example: + *

+ * + *

+* {@code
+
+    true
+    llm.service.endpoint
+    my.condition
+    
+    
+
+
+* }
+* 
+ * + * Caching: The ConditionalAIAdapter implements a caching mechnaism. The adapter + * class stores the hash value in to the item .hash to avoid + * duplicate calls against the llm with the same prompt in one processing cycle! + */ +@TransactionScoped +public class ConditionalAIAdapter implements Serializable { + + private static final Logger logger = Logger.getLogger(ConditionalAIAdapter.class.getName()); + + // Transaction-scoped cache - survives across multiple eval() calls + private Map promptResultCache = new HashMap<>(); + + @Inject + protected WorkflowService workflowService; + + @Inject + protected ImixsAIPromptService imixsAIPromptService; + + @Inject + protected OpenAIAPIService llmService; + + @Inject + ImixsAIContextHandler imixsAIContextHandler; + + public static String DEFAULT_EXPRESSION_TEMPLATE = "\n" + // + " true\n" + // + " llm.service.endpoint\n" + // + " BOOLEAN\n" + // + " \n" + // + " {\"n_predict\": 16, \"temperature\": 0 }\n" + // + " \n" + + // + " \n" + // + " \n" + // + ""; + + /** + * Test method + * + * @param conditionEvent + * @throws PluginException + * @throws AdapterException + */ + public void onConditionEvent(@Observes ConditionalExpressionEvent conditionalEvent) + throws PluginException, AdapterException { + + // run only if we have a prompt definition + String condition = conditionalEvent.getCondition(); + if (condition != null && condition.contains(" llmConditionDefinitions; + boolean llmAPIDebug = false; + + condition = condition.trim(); + llmConditionDefinitions = workflowService.evalXMLExpressionList( + conditionalEvent.getCondition(), "imixs-ai", "CONDITION", conditionalEvent.getWorkitem(), true); + + for (ItemCollection promptDefinition : llmConditionDefinitions) { + String llmAPIEndpoint = imixsAIPromptService.parseLLMEndpointByBPMN(promptDefinition); + String userPrompt = promptDefinition.getItemValueString("prompt"); + String llmAPIResultItem = promptDefinition.getItemValueString("result-item"); + if (llmAPIResultItem.isEmpty()) { + throw new PluginException(OpenAIAPIService.class.getSimpleName(), + OpenAIAPIService.ERROR_PROMPT_TEMPLATE, + "Prompt Definition must include the tag 'result-item'."); + } + + if ("true".equalsIgnoreCase(promptDefinition.getItemValueString("debug"))) { + llmAPIDebug = true; + } + if (llmAPIDebug) { + logger.info("├── evaluate conditional expression"); + } + // set DEFAULT_EXPRESSION_TEMPLATE. + imixsAIContextHandler.setWorkItem(conditionalEvent.getWorkitem()); + imixsAIContextHandler.addPromptDefinition(DEFAULT_EXPRESSION_TEMPLATE); + + // add the user prompt! + imixsAIContextHandler.addMessage(ImixsAIContextHandler.ROLE_USER, userPrompt, + workflowService.getSessionContext().getCallerPrincipal().getName(), null); + + // POST PromptCompletion + JsonObject jsonPrompt = imixsAIContextHandler.getOpenAIMessageObject(); + if (llmAPIDebug) { + logger.info("│ ├── conditional prompt:"); + logger.info(jsonPrompt.toString()); + } + + // Caching: + // if jsonPrompt (hash) was already stored, we do not call the LLM + long hashPrompt = jsonPrompt.hashCode(); + // Check transaction-scoped cache + if (promptResultCache.containsKey(hashPrompt)) { + if (llmAPIDebug) { + logger.info("│ ├── reuse cached result from transaction cache - hash=" + hashPrompt); + logger.info("└── ✓ evaluation time: " + (System.currentTimeMillis() - l) + "ms"); + } + conditionalEvent.setCondition(promptResultCache.get(hashPrompt)); + + return; + } + + String completionResult = llmService.postPromptCompletion(jsonPrompt, llmAPIEndpoint); + if (llmAPIDebug) { + logger.info("│ ├── ⚙ Completion Request conditional expression..."); + } + String conditionalExpressionResult = llmService.processPromptResult(completionResult, "BOOLEAN", + conditionalEvent.getWorkitem()); + + // store the result message + if (conditionalExpressionResult != null && !conditionalExpressionResult.isBlank()) { + // trim result + conditionalExpressionResult = conditionalExpressionResult.trim().toLowerCase(); + + if (!"true".equals(conditionalExpressionResult) && !"false".equals(conditionalExpressionResult)) { + logger.warning( + "│ ├── ⚠ Unexpected conditional expression result: " + conditionalExpressionResult); + throw new PluginException(OpenAIAPIService.class.getSimpleName(), + OpenAIAPIService.ERROR_PROMPT_INFERENCE, + "Conditional expression should result into 'true' or 'false' only"); + } + if (llmAPIDebug) { + logger.info("│ ├── ⚐ Conditional expression result: " + conditionalExpressionResult); + } + // store result + boolean booleanExpressionResult = Boolean.parseBoolean(conditionalExpressionResult.trim()); + conditionalEvent.getWorkitem().setItemValue(llmAPIResultItem, booleanExpressionResult); + + // Store in transaction-scoped cache + promptResultCache.put(hashPrompt, "" + booleanExpressionResult); + conditionalEvent.setCondition("" + booleanExpressionResult); + logger.info("└── ✓ evaluation time: " + (System.currentTimeMillis() - l) + "ms"); + } + } + } + + } + +} \ No newline at end of file diff --git a/dynamixs-ai-app/src/main/java/ai/dynamixs/ConditionalDebugAdapter.java b/dynamixs-ai-app/src/main/java/ai/dynamixs/ConditionalDebugAdapter.java deleted file mode 100644 index d14dbb7..0000000 --- a/dynamixs-ai-app/src/main/java/ai/dynamixs/ConditionalDebugAdapter.java +++ /dev/null @@ -1,136 +0,0 @@ -package ai.dynamixs; - -import java.io.Serializable; -import java.util.List; -import java.util.logging.Logger; - -import org.imixs.ai.ImixsAIContextHandler; -import org.imixs.ai.workflow.ImixsAIPromptService; -import org.imixs.ai.workflow.ImixsAIResultEvent; -import org.imixs.ai.workflow.OpenAIAPIService; -import org.imixs.workflow.ItemCollection; -import org.imixs.workflow.engine.ConditionalExpressionEvent; -import org.imixs.workflow.engine.WorkflowService; -import org.imixs.workflow.exceptions.AdapterException; -import org.imixs.workflow.exceptions.PluginException; -import org.imixs.workflow.faces.data.WorkflowEvent; - -import jakarta.enterprise.event.Observes; -import jakarta.inject.Inject; - -/** - * Debug class to test CDI Events of type BPMNConditionEvent - * - * - * Note: To run this plugin the conditional configuration should look like this - * - *
-* {@code
-    
-    true
-    llm.service.endpoint
-    BOOLEAN
-    
-        {"n_predict": 16, "temperature": 0 }
-        
-        
-        
-        
-    
-    
-* }
-* 
- * - */ -public class ConditionalDebugAdapter implements Serializable { - - private static final Logger logger = Logger.getLogger(ConditionalDebugAdapter.class.getName()); - - @Inject - protected WorkflowService workflowService; - - @Inject - protected ImixsAIPromptService imixsAIPromptService; - - @Inject - protected OpenAIAPIService llmService; - - @Inject - ImixsAIContextHandler imixsAIContextHandler; - - /** - * Test method - * - * @param conditionEvent - * @throws PluginException - * @throws AdapterException - */ - public void onConditionEvent(@Observes ConditionalExpressionEvent conditionalEvent) - throws PluginException, AdapterException { - - List llmConditionDefinitions; - boolean llmAPIDebug = false; - - llmConditionDefinitions = workflowService.evalXMLExpressionList( - conditionalEvent.getCondition(), "imixs-ai", "CONDITION", conditionalEvent.getWorkitem(), true); - - for (ItemCollection promptDefinition : llmConditionDefinitions) { - String llmAPIEndpoint = imixsAIPromptService.parseLLMEndpointByBPMN(promptDefinition); - String llmAPIResultEvent = promptDefinition.getItemValueString("result-event"); - String llmAPIResultItem = promptDefinition.getItemValueString("result-item"); - if ("true".equalsIgnoreCase(promptDefinition.getItemValueString("debug"))) { - llmAPIDebug = true; - } - - // get PromptDefintion template... - String prompt = "" + promptDefinition.getItemValueString("PromptDefinition") - + ""; - logger.info("PromptDefinition=" + prompt); - - imixsAIContextHandler.setWorkItem(conditionalEvent.getWorkitem()); - imixsAIContextHandler.addPromptDefinition(prompt); - - } - - } - - public void onWorkflowEvent(@Observes WorkflowEvent conditionEvent) { - - logger.info("We observe a WorkflowEvent"); - - } - - /** - * This observer method reacts on a ImixsAI Result event and evaluates a boolean - * value - * - * Note: To run this adapter the configuration should look like this - * - *
-    * {@code
-         
-             llm.service.endpoint
-             BOOLEAN
-         
-    * }
-    * 
- * - * @param event - */ - public void onEvent(@Observes ImixsAIResultEvent event) { - if (event.getWorkitem() == null) { - return; - } - - // we only adapt the result in case the eventType===JSON - if ("BOOLEAN".equalsIgnoreCase(event.getEventType())) { - // get result string - String booleanString = event.getPromptResult(); - - logger.info("Prompt Boolean Result= " + booleanString); - - } - } -} \ No newline at end of file diff --git a/workflow/en/invoice-demo-c-1.0.0.bpmn b/workflow/en/invoice-demo-c-1.0.0.bpmn index 4fb3f08..d4c01fd 100644 --- a/workflow/en/invoice-demo-c-1.0.0.bpmn +++ b/workflow/en/invoice-demo-c-1.0.0.bpmn @@ -121,6 +121,7 @@ th { event_HBu5SQ event_ykc34Q event_kXkdQw + event_jP2H0w IntermediateCatchEvent_8 @@ -1385,6 +1386,7 @@ Bei Fragen wenden Sie sich bitte an info@imixs.com $workflowgroup - $workflowstatus]]> SequenceFlow_31 + sequenceFlow_rTkelw sequenceFlow_b9RMyw SequenceFlow_7 sequenceFlow_KW7K3Q @@ -1601,7 +1603,6 @@ Debitor: customer.name (customer.number sequenceFlow_JClvDg sequenceFlow_PL3AfA - sequenceFlow_tZVF2w sequenceFlow_hbRxpg @@ -1867,19 +1868,13 @@ Debitor: customer.name (customer.number - + true llm.service.endpoint - BOOLEAN - - {"n_predict": 16, "temperature": 0 } - - - - - + compliance.check + + ]]> @@ -2111,9 +2106,103 @@ Debitor: customer.name (customer.number - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + sequencenumber.*\.xlsx + + INVOICE_TOTAL + invoice.total + number + + + INVOICE_CURRENCY + invoice.currency + + +]]> + + + + + + + + + + + + false + + + + + sequenceFlow_PL3AfA + sequenceFlow_rTkelw + + + DataOutput_5 + + + DataOutput_5 + + sequenceFlow_tZVF2w + + + + @@ -2971,6 +3060,12 @@ th { font-weight: bold;} + + + + + + @@ -3208,14 +3303,17 @@ th { font-weight: bold;} - - - - - - - - + + + + + + + + + + +