improved ConditionalAIAdapter class (added caching)

This commit is contained in:
Ralph Soika 2025-12-01 23:42:36 +01:00
parent dc7d0a5998
commit e00739d564
3 changed files with 303 additions and 157 deletions

View file

@ -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
* <p>
* The Adapter defines a Default Expression template for LLMs. The BPMN
* Configuration must only include the user prompt. See the following example:
* <p>
*
* <pre>
* {@code
<imixs-ai name="CONDITION">
<debug>true</debug>
<endpoint><propertyvalue>llm.service.endpoint</propertyvalue></endpoint>
<result-item>my.condition</result-item>
<prompt><![CDATA[
Is Germany an EU member country? ]]>
</prompt>
</imixs-ai>
* }
* </pre>
*
* Caching: The ConditionalAIAdapter implements a caching mechnaism. The adapter
* class stores the hash value in to the item <result-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<Long, String> promptResultCache = new HashMap<>();
@Inject
protected WorkflowService workflowService;
@Inject
protected ImixsAIPromptService imixsAIPromptService;
@Inject
protected OpenAIAPIService llmService;
@Inject
ImixsAIContextHandler imixsAIContextHandler;
public static String DEFAULT_EXPRESSION_TEMPLATE = "<imixs-ai name=\"CONDITION\">\n" + //
" <debug>true</debug>\n" + //
" <endpoint><propertyvalue>llm.service.endpoint</propertyvalue></endpoint>\n" + //
" <result-event>BOOLEAN</result-event>\n" + //
" <PromptDefinition>\n" + //
" <prompt_options>{\"n_predict\": 16, \"temperature\": 0 }</prompt_options>\n" + //
" <prompt role=\"system\"><![CDATA[\n" + //
" Evaluate the following condition to 'true' or 'false'. Do only answer with 'true' or 'false'. Do not add any additional context into your answer.]]>\n"
+ //
" </prompt>\n" + //
" </PromptDefinition>\n" + //
"</imixs-ai>";
/**
* 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("<imixs-ai")) {
long l = System.currentTimeMillis();
List<ItemCollection> 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");
}
}
}
}
}

View file

@ -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
*
* <pre>
* {@code
<imixs-ai name="CONDITION">
<debug>true</debug>
<endpoint><propertyvalue>llm.service.endpoint</propertyvalue></endpoint>
<result-event>BOOLEAN</result-event>
<PromptDefinition>
<prompt_options>{"n_predict": 16, "temperature": 0 }</prompt_options>
<prompt role="system"><![CDATA[
You are a sales expert. You evaluate the following condition to 'true' or 'false'. ]]>
</prompt>
<prompt role="user"><![CDATA[
Is Germany an EU member country? ]]>
</prompt>
</PromptDefinition>
</imixs-ai>
* }
* </pre>
*
*/
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<ItemCollection> 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>" + promptDefinition.getItemValueString("PromptDefinition")
+ "</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
*
* <pre>
* {@code
<imixs-ai name="CONDITION">
<endpoint><propertyvalue>llm.service.endpoint</propertyvalue></endpoint>
<result-event>BOOLEAN</result-event>
</imixs-ai>
* }
* </pre>
*
* @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);
}
}
}

View file

@ -121,6 +121,7 @@ th {
<bpmn2:flowNodeRef>event_HBu5SQ</bpmn2:flowNodeRef> <bpmn2:flowNodeRef>event_HBu5SQ</bpmn2:flowNodeRef>
<bpmn2:flowNodeRef>event_ykc34Q</bpmn2:flowNodeRef> <bpmn2:flowNodeRef>event_ykc34Q</bpmn2:flowNodeRef>
<bpmn2:flowNodeRef>event_kXkdQw</bpmn2:flowNodeRef> <bpmn2:flowNodeRef>event_kXkdQw</bpmn2:flowNodeRef>
<bpmn2:flowNodeRef>event_jP2H0w</bpmn2:flowNodeRef>
</bpmn2:lane> </bpmn2:lane>
<bpmn2:lane id="Lane_4" name="Accounting"> <bpmn2:lane id="Lane_4" name="Accounting">
<bpmn2:flowNodeRef>IntermediateCatchEvent_8</bpmn2:flowNodeRef> <bpmn2:flowNodeRef>IntermediateCatchEvent_8</bpmn2:flowNodeRef>
@ -1385,6 +1386,7 @@ Bei Fragen wenden Sie sich bitte an info@imixs.com
</bpmn2:extensionElements> </bpmn2:extensionElements>
<bpmn2:documentation id="documentation_GFtuYw"><![CDATA[<textblock><itemvalue>$workflowgroup</itemvalue> - <itemvalue>$workflowstatus</itemvalue></textblock>]]></bpmn2:documentation> <bpmn2:documentation id="documentation_GFtuYw"><![CDATA[<textblock><itemvalue>$workflowgroup</itemvalue> - <itemvalue>$workflowstatus</itemvalue></textblock>]]></bpmn2:documentation>
<bpmn2:incoming>SequenceFlow_31</bpmn2:incoming> <bpmn2:incoming>SequenceFlow_31</bpmn2:incoming>
<bpmn2:incoming>sequenceFlow_rTkelw</bpmn2:incoming>
<bpmn2:outgoing>sequenceFlow_b9RMyw</bpmn2:outgoing> <bpmn2:outgoing>sequenceFlow_b9RMyw</bpmn2:outgoing>
<bpmn2:incoming>SequenceFlow_7</bpmn2:incoming> <bpmn2:incoming>SequenceFlow_7</bpmn2:incoming>
<bpmn2:outgoing>sequenceFlow_KW7K3Q</bpmn2:outgoing> <bpmn2:outgoing>sequenceFlow_KW7K3Q</bpmn2:outgoing>
@ -1601,7 +1603,6 @@ Debitor: <itemvalue>customer.name</itemvalue> (<itemvalue>customer.number</itemv
<bpmn2:documentation id="documentation_96I0BA"><![CDATA[Review Rejected]]></bpmn2:documentation> <bpmn2:documentation id="documentation_96I0BA"><![CDATA[Review Rejected]]></bpmn2:documentation>
<bpmn2:incoming>sequenceFlow_JClvDg</bpmn2:incoming> <bpmn2:incoming>sequenceFlow_JClvDg</bpmn2:incoming>
<bpmn2:incoming>sequenceFlow_PL3AfA</bpmn2:incoming> <bpmn2:incoming>sequenceFlow_PL3AfA</bpmn2:incoming>
<bpmn2:incoming>sequenceFlow_tZVF2w</bpmn2:incoming>
<bpmn2:outgoing>sequenceFlow_hbRxpg</bpmn2:outgoing> <bpmn2:outgoing>sequenceFlow_hbRxpg</bpmn2:outgoing>
<bpmn2:dataOutput id="dataOutput_Gel0Bw" name="Signal_6_Output"/> <bpmn2:dataOutput id="dataOutput_Gel0Bw" name="Signal_6_Output"/>
<bpmn2:dataOutputAssociation id="dataOutputAssociation_Tgv2nA"> <bpmn2:dataOutputAssociation id="dataOutputAssociation_Tgv2nA">
@ -1867,19 +1868,13 @@ Debitor: <itemvalue>customer.name</itemvalue> (<itemvalue>customer.number</itemv
</bpmn2:task> </bpmn2:task>
<bpmn2:sequenceFlow id="sequenceFlow_kWtVWw" name="check" sourceRef="gateway_YmVj8g" targetRef="event_kXkdQw"> <bpmn2:sequenceFlow id="sequenceFlow_kWtVWw" name="check" sourceRef="gateway_YmVj8g" targetRef="event_kXkdQw">
<bpmn2:documentation id="documentation_0f3Qyw"/> <bpmn2:documentation id="documentation_0f3Qyw"/>
<bpmn2:conditionExpression id="formalExpression_laNJyA" xsi:type="bpmn2:tFormalExpression"><![CDATA[<imixs-ai name="CONDITION"> <bpmn2:conditionExpression id="formalExpression_hy83hg" xsi:type="bpmn2:tFormalExpression"><![CDATA[<imixs-ai name="CONDITION">
<debug>true</debug> <debug>true</debug>
<endpoint><propertyvalue>llm.service.endpoint</propertyvalue></endpoint> <endpoint><propertyvalue>llm.service.endpoint</propertyvalue></endpoint>
<result-event>BOOLEAN</result-event> <result-item>compliance.check</result-item>
<PromptDefinition> <prompt><![CDATA[
<prompt_options>{"n_predict": 16, "temperature": 0 }</prompt_options> Are Germany and Italy EU member countries? ]]]]><![CDATA[>
<prompt role="system"><![CDATA[
You are a sales expert. You evaluate the following condition to 'true' or 'false'. ]]]]><![CDATA[>
</prompt> </prompt>
<prompt role="user"><![CDATA[
Is Germany an EU member country? ]]]]><![CDATA[>
</prompt>
</PromptDefinition>
</imixs-ai> </imixs-ai>
]]></bpmn2:conditionExpression> ]]></bpmn2:conditionExpression>
</bpmn2:sequenceFlow> </bpmn2:sequenceFlow>
@ -2111,9 +2106,103 @@ Debitor: <itemvalue>customer.name</itemvalue> (<itemvalue>customer.number</itemv
<bpmn2:sequenceFlow id="sequenceFlow_0qwg1A" sourceRef="event_kXkdQw" targetRef="task_omZN4A"> <bpmn2:sequenceFlow id="sequenceFlow_0qwg1A" sourceRef="event_kXkdQw" targetRef="task_omZN4A">
<bpmn2:documentation id="documentation_pe890Q"/> <bpmn2:documentation id="documentation_pe890Q"/>
</bpmn2:sequenceFlow> </bpmn2:sequenceFlow>
<bpmn2:sequenceFlow id="sequenceFlow_tZVF2w" sourceRef="Task_3" targetRef="event_07075Q"> <bpmn2:sequenceFlow id="sequenceFlow_tZVF2w" sourceRef="Task_3" targetRef="event_jP2H0w">
<bpmn2:documentation id="documentation_0WuhyQ"/> <bpmn2:documentation id="documentation_0WuhyQ"/>
</bpmn2:sequenceFlow> </bpmn2:sequenceFlow>
<bpmn2:intermediateCatchEvent id="event_jP2H0w" imixs:activityid="37" name="DEBUG COndition">
<bpmn2:extensionElements>
<imixs:item name="keylogtimeformat" type="xs:string">
<imixs:value><![CDATA[2]]></imixs:value>
</imixs:item>
<imixs:item name="keyarchive" type="xs:string">
<imixs:value><![CDATA[0]]></imixs:value>
</imixs:item>
<imixs:item name="txtmailsubject" type="xs:string">
<imixs:value/>
</imixs:item>
<imixs:item name="keyaccessmode" type="xs:string">
<imixs:value><![CDATA[0]]></imixs:value>
</imixs:item>
<imixs:item name="keyscheduledactivity" type="xs:string">
<imixs:value><![CDATA[0]]></imixs:value>
</imixs:item>
<imixs:item name="rtfmailbody" type="xs:string">
<imixs:value/>
</imixs:item>
<imixs:item name="keyversion" type="xs:string">
<imixs:value><![CDATA[0]]></imixs:value>
</imixs:item>
<imixs:item name="keyaddwritefields" type="xs:string">
<imixs:value><![CDATA[namCreator]]></imixs:value>
<imixs:value><![CDATA[namCurrentEditor]]></imixs:value>
<imixs:value><![CDATA[namprocessmanager]]></imixs:value>
<imixs:value><![CDATA[namprocessteam]]></imixs:value>
</imixs:item>
<imixs:item name="numnextactivityid" type="xs:int">
<imixs:value><![CDATA[0]]></imixs:value>
</imixs:item>
<imixs:item name="keyfollowup" type="xs:string">
<imixs:value><![CDATA[2]]></imixs:value>
</imixs:item>
<imixs:item name="keypublicresult" type="xs:string">
<imixs:value><![CDATA[1]]></imixs:value>
</imixs:item>
<imixs:item name="keylogdateformat" type="xs:string">
<imixs:value><![CDATA[2]]></imixs:value>
</imixs:item>
<imixs:item name="keyownershipfields" type="xs:string">
<imixs:value><![CDATA[namCurrentEditor]]></imixs:value>
<imixs:value><![CDATA[namCreator]]></imixs:value>
</imixs:item>
<imixs:item name="numnextid" type="xs:int">
<imixs:value><![CDATA[5000]]></imixs:value>
</imixs:item>
<imixs:item name="txtnextprocesstree" type="xs:string">
<imixs:value/>
</imixs:item>
<imixs:item name="txtactivityresult" type="xs:string">
<imixs:value><![CDATA[<poi-copy name="filename"><itemvalue>sequencenumber</itemvalue>.*\.xlsx</poi-copy>
<poi-copy name="copy">
<find>INVOICE_TOTAL</find>
<itemname>invoice.total</itemname>
<type>number</type>
</poi-copy>
<poi-copy name="copy">
<find>INVOICE_CURRENCY</find>
<itemname>invoice.currency</itemname>
</poi-copy>
]]></imixs:value>
</imixs:item>
<imixs:item name="txtname" type="xs:string">
<imixs:value><![CDATA[Speichern]]></imixs:value>
</imixs:item>
<imixs:item name="keyownershipmode" type="xs:string">
<imixs:value><![CDATA[0]]></imixs:value>
</imixs:item>
<imixs:item name="rtfresultlog" type="xs:string">
<imixs:value><![CDATA[Invoice rejected]]></imixs:value>
</imixs:item>
<imixs:item name="keyupdateacl" type="xs:boolean">
<imixs:value>false</imixs:value>
</imixs:item>
<imixs:item name="keymailreceiverfields" type="xs:string"/>
</bpmn2:extensionElements>
<bpmn2:documentation id="documentation_aWOrGw"><![CDATA[Review Rejected]]></bpmn2:documentation>
<bpmn2:incoming>sequenceFlow_PL3AfA</bpmn2:incoming>
<bpmn2:outgoing>sequenceFlow_rTkelw</bpmn2:outgoing>
<bpmn2:dataOutput id="dataOutput_cajPZg" name="Signal_6_Output"/>
<bpmn2:dataOutputAssociation id="dataOutputAssociation_R1frQA">
<bpmn2:sourceRef>DataOutput_5</bpmn2:sourceRef>
</bpmn2:dataOutputAssociation>
<bpmn2:outputSet id="outputSet_3eLIFA" name="Output Set 6">
<bpmn2:dataOutputRefs>DataOutput_5</bpmn2:dataOutputRefs>
</bpmn2:outputSet>
<bpmn2:incoming>sequenceFlow_tZVF2w</bpmn2:incoming>
</bpmn2:intermediateCatchEvent>
<bpmn2:sequenceFlow id="sequenceFlow_rTkelw" sourceRef="event_jP2H0w" targetRef="task_VRC0OA">
<bpmn2:documentation id="documentation_Rm6j4g"/>
</bpmn2:sequenceFlow>
</bpmn2:process> </bpmn2:process>
<bpmn2:process id="process_2" name="Default Process" processType="Public"> <bpmn2:process id="process_2" name="Default Process" processType="Public">
<bpmn2:documentation id="documentation_6zIncA"/> <bpmn2:documentation id="documentation_6zIncA"/>
@ -2971,6 +3060,12 @@ th { font-weight: bold;}
<dc:Bounds height="20.0" width="100.0" x="1635.0" y="326.0"/> <dc:Bounds height="20.0" width="100.0" x="1635.0" y="326.0"/>
</bpmndi:BPMNLabel> </bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="event_jP2H0w" id="BPMNShape_1Kd85Q">
<dc:Bounds height="36.0" width="36.0" x="1187.0" y="377.0"/>
<bpmndi:BPMNLabel id="BPMNLabel_ZVtzzA">
<dc:Bounds height="20.0" width="100.0" x="1155.0" y="416.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="Association_2" id="BPMNEdge_Association_2" sourceElement="BPMNShape_DataObject_1" targetElement="BPMNShape_Task_5"> <bpmndi:BPMNEdge bpmnElement="Association_2" id="BPMNEdge_Association_2" sourceElement="BPMNShape_DataObject_1" targetElement="BPMNShape_Task_5">
<di:waypoint x="235.0" y="196.0"/> <di:waypoint x="235.0" y="196.0"/>
<di:waypoint x="335.0" y="196.0"/> <di:waypoint x="335.0" y="196.0"/>
@ -3208,14 +3303,17 @@ th { font-weight: bold;}
<di:waypoint x="1703.0" y="305.0"/> <di:waypoint x="1703.0" y="305.0"/>
<di:waypoint x="1780.0" y="305.0"/> <di:waypoint x="1780.0" y="305.0"/>
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sequenceFlow_tZVF2w" id="BPMNEdge_tNGMVQ" sourceElement="BPMNShape_Task_9" targetElement="BPMNShape_zJI73Q"> <bpmndi:BPMNEdge bpmnElement="sequenceFlow_tZVF2w" id="BPMNEdge_tNGMVQ" sourceElement="BPMNShape_Task_9" targetElement="BPMNShape_1Kd85Q">
<di:waypoint x="850.0" y="799.0"/> <di:waypoint x="839.0" y="790.0"/>
<di:waypoint x="876.0" y="799.0"/> <di:waypoint x="839.0" y="648.0"/>
<di:waypoint x="876.0" y="642.0"/> <di:waypoint x="1018.0" y="648.0"/>
<di:waypoint x="1089.0" y="642.0"/> <di:waypoint x="1018.0" y="458.0"/>
<di:waypoint x="1089.0" y="135.0"/> <di:waypoint x="1204.0" y="458.0"/>
<di:waypoint x="1345.0" y="135.0"/> <di:waypoint x="1204.0" y="413.0"/>
<di:waypoint x="1345.0" y="187.0"/> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sequenceFlow_rTkelw" id="BPMNEdge_1FMdMQ" sourceElement="BPMNShape_1Kd85Q" targetElement="BPMNShape_lxN0AA">
<di:waypoint x="1205.0" y="377.0"/>
<di:waypoint x="1205.0" y="330.0"/>
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
</bpmndi:BPMNPlane> </bpmndi:BPMNPlane>
<bpmndi:BPMNLabelStyle id="BPMNLabelStyle_1"> <bpmndi:BPMNLabelStyle id="BPMNLabelStyle_1">