diff --git a/doc/RAG-draft.md b/doc/RAG-draft.md deleted file mode 100644 index 8779553..0000000 --- a/doc/RAG-draft.md +++ /dev/null @@ -1,20 +0,0 @@ -# Dynamixs AI - Retrieval-Augmented-Generation (RAG) - -Retrieval Augmented Generation (RAG) is a term from the field of artificial intelligence and machine learning. - -- **Retrieval** - refers to the ability to quickly and efficiently access stored information or retrieve it from a database or other storage system. -- **Augmented** - refers to the idea of improving or expanding an existing function or capability by adding something new. -- **Generation** - refers to the creation of new content or results based on available information. - -In summary, Retrieval Augmented Generation (RAG) describes a method that uses artificial intelligence or machine learning to quickly and efficiently access stored information and use it to generate new content or results. This method can help improve the accuracy and relevance of generated content by drawing on a broader range of information. - -The following section give a general overview how the Retrieval-augmented generation (RAG) is realized in Dynamixs AI. - -## The Model - -The retrieval data in Dynamixs.AI is generated by the LLM [nomic-embed-text-v2-moe](https://huggingface.co/nomic-ai/nomic-embed-text-v2-moe-GGUF) which is -a [sentence-transformers model](https://www.sbert.net/) supporting more then 100 languages and trained on over 1.6B pairs. -The model is used in Q4 quantitation and a size of 328MiB which provides a good quality for most use cases. -The model maps sentences & paragraphs to a 768 dimensional dense vector space. It can be run on a CPU with adequate performance. - -## BPMN diff --git a/doc/RAG_SERVICE.md b/doc/RAG_SERVICE.md new file mode 100644 index 0000000..fbd466e --- /dev/null +++ b/doc/RAG_SERVICE.md @@ -0,0 +1,74 @@ +# The RAG ClusterService EJB + +With the EJB `ClusterService` clients can index and retrieve embeddings. + +## Index + +A text chunk can be index by: + +```java + clusterService.insertVector(workitem.getUniqueID(), + workitem.getModelVersion(), + workitem.getTaskID(), + chunk, + indexResult); +``` + +## Retrieval + +The retrieval can be based on text embedding only or refined by workflow metadata like the modelVersion or the TaskId + +```java + List retrievalResultList = clusterService.searchEmbeddings(embeddings, 5); + +``` + +You can refine the retrieval by providing additional workflow context + +1. Task ID Range: Suche alle Tasks zwischen 1000-1999 + +```java + +List rangeResults = clusterService.searchEmbeddingsWithTaskRange( + queryEmbedding, + 5, + null, // all Model Versions + 1000, + 1999 +); +``` + +2. Wildcard Model Version: all products by wildcard search + +```java +List productResults = clusterService.searchEmbeddingsWithModelWildcard( + queryEmbedding, + 5, + "product-de*", // product-de-1.0.0, product-de-2.0.0, etc. + null +); +``` + +3. Combination products and Task Range 1000-1999 + +```java +List combinedResults = clusterService.searchEmbeddings( + queryEmbedding, + 5, + Arrays.asList("product-de*"), + null, // no specific Task IDs + 1000, // Task Range Start + 1999 // Task Range End +); +``` + +4. Multiple Wildcards: all german models + +```java +List germanResults = clusterService.searchEmbeddings( + queryEmbedding, + 5, + Arrays.asList("product-de*", "offer-de*", "invoice-de*"), + null, null, null +); +``` diff --git a/docker/import_examples/lupi-product-export_clean.csv b/docker/import_examples/lupi-product-export_clean.csv index 99f3c1c..8d21ee5 100644 --- a/docker/import_examples/lupi-product-export_clean.csv +++ b/docker/import_examples/lupi-product-export_clean.csv @@ -39,16 +39,16 @@ ID;Name; Description;stock;price;category;Lieferzeit;EAN 1170;Wandkonsole für Plattentauscher B3-12-xx;Wandhalterung für eine schnelle Montage mit praktischem Klettverschlußband.;1;18,9;Wandhalterung;;2837107354495 1181;LP-375VA 115kW mit Wärmedämmung;Hochwertiger Schwimmbad-Wärmetauscher aus Edelstahl für Schwimmbadbeheizung mit Wärmedämmung.;0;459;Pool Wärmetauscher > Rohrförmig aus Edelstahl;;8247903213713 1188;Wandkonsole für isolierte Pooltauscher LP-xx;Wandhalterung für eine schnelle Montage mit praktischem Klettverschlußband.;1;19,9;Wandhalterung;;5234087663802 -1191;LP-65VA 20kW mit Wärmedämmung;Hochwertiger Schwimmbad-Wärmetauscher aus Edelstahl für Schwimmbadbeheizung mit Wärmedämmung.;1;219,9;Pool Wärmetauscher > Rohrförmig aus Edelstahl;;938364240247 +1191;LP-65VA 20kW mit Wärmedämmung;Hochwertiger Schwimmbad-Wärmetauscher aus Edelstahl für Schwimmbadbeheizung mit Wärmedämmung.;1;219,9;Pool Wärmetauscher > Rohrförmig aus Edelstahl;;938364240247 1194;LP-100VA 30kW mit Wärmedämmung;Hochwertiger Schwimmbad-Wärmetauscher aus Edelstahl für Schwimmbadbeheizung mit Wärmedämmung.;1;239,9;Pool Wärmetauscher > Rohrförmig aus Edelstahl;;450118214736 1196;LP-175VA 50kW mit Wärmedämmung;Hochwertiger Schwimmbad-Wärmetauscher aus Edelstahl für Schwimmbadbeheizung mit Wärmedämmung.;1;259,9;Pool Wärmetauscher > Rohrförmig aus Edelstahl;;655296358115 1198;LP-230VA 70kW mit Wärmedämmung;Hochwertiger Schwimmbad-Wärmetauscher aus Edelstahl für Schwimmbadbeheizung mit Wärmedämmung.;1;329,9;Pool Wärmetauscher > Rohrförmig aus Edelstahl;;8611986118980 1200;LP-325VA 95kW mit Wärmedämmung;Hochwertiger Schwimmbad-Wärmetauscher aus Edelstahl für Schwimmbadbeheizung mit Wärmedämmung.;0;389,9;Pool Wärmetauscher > Rohrförmig aus Edelstahl;;9005775281402 1204;Dämmschale für Plattentauscher M3-M;Hochwertige Dämmschale für Plattentauscher in verschiedenen Größen;1;89,9;Isolierung für Wärmetauscher;;5367012957872 -1205;Dämmschale für Plattentauscher M3-L;Hochwertige Dämmschale für Plattentauscher in verschiedenen Größen;1;99,9;Isolierung für Wärmetauscher;;6854117854742 +1205;Dämmschale für Plattentauscher M3-L;Hochwertige Dämmschale für Plattentauscher in verschiedenen Größen;1;99,9;Isolierung für Wärmetauscher;;6854117854742 1206;Dämmschale für Plattentauscher SP-S;Hochwertige Dämmschale für Plattentauscher in verschiedenen Größen;1;79,9;Isolierung für Wärmetauscher;;1382809012286 1207;Dämmschale für Plattentauscher SP-M;Hochwertige Dämmschale für Plattentauscher in verschiedenen Größen;1;89,9;Isolierung für Wärmetauscher;;5461960384619 -1252;Dämmschale für Plattentauscher M1-S;Hochwertige Dämmschale für Plattentauscher in verschiedenen Größen;1;79,9;Isolierung für Wärmetauscher;;3625664059934 +1252;Dämmschale für Plattentauscher M1-S;Hochwertige Dämmschale für Plattentauscher in verschiedenen Größen;1;79,9;Isolierung für Wärmetauscher;;3625664059934 1253;Dämmschale für Plattentauscher M1-M;Hochwertige Dämmschale für Plattentauscher in verschiedenen Größen;1;89,9;Isolierung für Wärmetauscher;;4894233116968 1254;Dämmschale für Plattentauscher M1-L;Hochwertige Dämmschale für Plattentauscher in verschiedenen Größen;1;99,9;Isolierung für Wärmetauscher;;8183922928019 1257;Wandbefestigung für Plattentauscher M1-53;Hochwertige Dämmschale für Plattentauscher in verschiedenen Größen;1;34,9;Wandhalterung;;8183313115592 @@ -62,7 +62,7 @@ ID;Name; Description;stock;price;category;Lieferzeit;EAN 1268;Edelstahl Plattenwärmetauscher Typ M4-56-45;Edelstahl Plattenwärmetauscher geeignet für Anwendungen mit geschlossenen Kreisläufen wie z.B. Heizungsanwendungen;1;499,9;Plattenwärmetauscher > M4-56 Baureihe / zerlegbar;;4811928042234 1269;Edelstahl Plattenwärmetauscher Typ M4-56-50;Edelstahl Plattenwärmetauscher geeignet für Anwendungen mit geschlossenen Kreisläufen wie z.B. Heizungsanwendungen;1;535,9;Plattenwärmetauscher > M4-56 Baureihe / zerlegbar;;3388615003133 1270;Edelstahl Plattenwärmetauscher Typ M4-56-60;Edelstahl Plattenwärmetauscher geeignet für Anwendungen mit geschlossenen Kreisläufen wie z.B. Heizungsanwendungen;1;599,9;Plattenwärmetauscher > M4-56 Baureihe / zerlegbar;;6261793125392 -1326;Abgaswärmetauscher AWT-7/160VA;Abgaswärmetauscher mit Edelstahl Tauscherspirale.Dieser Tauscher wurde speziell für Kamin und Kachelöfen entwickelt.;1;284,9;Abgaswärmetauscher;;6474143371206 +1326;Abgaswärmetauscher AWT-7/160VA a;Abgaswärmetauscher mit Edelstahl Tauscherspirale.Dieser Tauscher wurde speziell für Kamin und Kachelöfen entwickelt.;1;284,9;Abgaswärmetauscher;;6474143371206 1327;Abgaswärmetauscher AWT-7/160-Titan;Abgaswärmetauscher mit titanlegierter Edelstahl Tauscherspirale entwickelt für höchste Temperatur- und Feuchtebelastungen. Dieser Tauscher ist für extreme Langlebigkeit und Beständigkeit ausgelegt.;1;289,9;Abgaswärmetauscher;;4089415274929 1328;Abgaswärmetauscher AWT-7/180-Titan;Abgaswärmetauscher mit titanlegierter Edelstahl Tauscherspirale entwickelt für höchste Temperatur- und Feuchtebelastungen. Dieser Tauscher ist für extreme Langlebigkeit und Beständigkeit ausgelegt.;1;319,9;Abgaswärmetauscher;;4598541150958 1329;Abgaswärmetauscher AWT-7/180VA;Abgaswärmetauscher mit Edelstahl Tauscherspirale.Dieser Tauscher wurde speziell für Kamin und Kachelöfen entwickelt.;1;314,9;Abgaswärmetauscher;;9282813513019 diff --git a/dynamixs-ai-rag/src/main/java/ai/dynamixs/rag/ClusterService.java b/dynamixs-ai-rag/src/main/java/ai/dynamixs/rag/ClusterService.java index d92bb37..01e7c50 100644 --- a/dynamixs-ai-rag/src/main/java/ai/dynamixs/rag/ClusterService.java +++ b/dynamixs-ai-rag/src/main/java/ai/dynamixs/rag/ClusterService.java @@ -1,6 +1,9 @@ package ai.dynamixs.rag; import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -334,6 +337,8 @@ public class ClusterService { String query = "CREATE TABLE IF NOT EXISTS document_vectors (\n" + // " id text,\n" + // " chunk_id uuid,\n" + // + " model_version text,\n" + // + " task_id int,\n" + // " content_chunk text,\n" + // " content_vector VECTOR ,\n" + // " PRIMARY KEY (id, chunk_id)\n" + // @@ -343,8 +348,16 @@ public class ClusterService { logger.info("│ ├── create index..."); query = "CREATE INDEX IF NOT EXISTS edv_ann_index\n" + // " ON document_vectors(content_vector) USING 'sai';"; - session.execute(query); + + // Additional indexes for metadata filtering + query = "CREATE INDEX IF NOT EXISTS idx_model_version " + // + " ON document_vectors(model_version) USING 'sai';"; + session.execute(query); + query = "CREATE INDEX IF NOT EXISTS idx_task_id " + // + " ON document_vectors(task_id) USING 'sai';"; + session.execute(query); + logger.info("│ ├── ✅ database schema created."); } @@ -354,27 +367,30 @@ public class ClusterService { * data. * * @param uniqueID + * @param modelVersion + * @param taskId * @param content * @param vector * @throws ClusterException */ - public void insertVector(String uniqueID, String content, List vector) throws ClusterException { + public void insertVector(String uniqueID, String modelVersion, int taskId, String content, List vector) + throws ClusterException { // Prepare statement? if (insertVectorStmt == null) { String insertQuery = "INSERT INTO document_vectors " - + "(id, chunk_id, content_chunk, content_vector) VALUES (?, ?, ?, ?)"; + + "(id, chunk_id, model_version, task_id, content_chunk, content_vector) VALUES (?, ?, ?, ?, ?, ?)"; insertVectorStmt = session.prepare(insertQuery); } try { // Generiere UUID UUID chunk_id = UUID.randomUUID(); - - // CqlVector verwenden + // CqlVector CqlVector cqlVector = CqlVector.newInstance(vector); - BoundStatement boundStmt = insertVectorStmt.bind(uniqueID, chunk_id, content, cqlVector); + BoundStatement boundStmt = insertVectorStmt.bind(uniqueID, chunk_id, modelVersion, taskId, content, + cqlVector); session.execute(boundStmt); } catch (Exception e) { @@ -440,6 +456,9 @@ public class ClusterService { * provided query embedding. If multiple chunks exist for the same document ID, * only the chunk with the highest similarity score is returned. The results are * sorted by relevance score in descending order. + *

+ * The params 'modelVrsions' and 'taskIds' are optional and can be used to + * refine the search result to specific process instances only. * * @param queryEmbedding The embedding vector of the search query as a list of * float values. The dimensionality must match the @@ -455,41 +474,109 @@ public class ClusterService { * @see RetrievalResult * @since 1.0 */ - public List searchEmbeddings(List embedding, int maxResults) throws ClusterException { + public List searchEmbeddings(List embedding, + int maxResults, + List modelVersions, + List taskIds, + Integer taskIdRangeStart, + Integer taskIdRangeEnd) throws ClusterException { + if (maxResults <= 0) { throw new ClusterException(ClusterException.CLUSTER_ERROR, "maxResults must be greater than 0, but was: " + maxResults); } // Calculate a reasonable DB limit based on maxResults // Factor 3-4 times to have enough chunks for deduplication. - int dbLimit = Math.min(maxResults * 4, SEARCH_LIMIT_MAX); // Max 100 + int searchLimit = Math.min(maxResults * 4, SEARCH_LIMIT_MAX); // Max 100 - // Prepare statement if not already done (dynamisches Limit erfordert neue - // Statements) - String statementKey = "search_" + dbLimit; - PreparedStatement searchVectorStmt = searchVectorStatements.get(statementKey); + StringBuilder queryBuilder = new StringBuilder(); + queryBuilder.append("SELECT id, model_version, task_id, content_chunk, ") + .append("similarity_cosine(content_vector, ?) ") + .append("FROM document_vectors "); - // Prepare statement if not already done - if (searchVectorStmt == null) { - String searchQuery = "SELECT id,content_chunk,similarity_cosine(content_vector, ?) " + // - " FROM document_vectors " + // - " ORDER BY content_vector ANN OF ? " + // - " LIMIT " + dbLimit; - searchVectorStmt = session.prepare(searchQuery); + List whereConditions = new ArrayList<>(); + // Model version filtering (with wildcard support) + if (modelVersions != null && !modelVersions.isEmpty()) { + List modelConditions = new ArrayList<>(); + for (String modelVersion : modelVersions) { + if (modelVersion.endsWith("*")) { + // Wildcard search using LIKE + modelConditions.add("model_version LIKE ?"); + } else { + // Exact match + modelConditions.add("model_version = ?"); + } + } + whereConditions.add("(" + String.join(" OR ", modelConditions) + ")"); } + + // Task ID filtering - either list or range + if (taskIds != null && !taskIds.isEmpty()) { + whereConditions.add("task_id IN " + formatInClause(taskIds.size())); + } else if (taskIdRangeStart != null && taskIdRangeEnd != null) { + whereConditions.add("task_id >= ? AND task_id <= ?"); + } else if (taskIdRangeStart != null) { + whereConditions.add("task_id >= ?"); + } else if (taskIdRangeEnd != null) { + whereConditions.add("task_id <= ?"); + } + + if (!whereConditions.isEmpty()) { + queryBuilder.append("WHERE ").append(String.join(" AND ", whereConditions)).append(" "); + } + + queryBuilder.append("ORDER BY content_vector ANN OF ? LIMIT ").append(searchLimit); + // Prepare statement (with caching) + String statementKey = buildStatementKey(searchLimit, modelVersions, taskIds, + taskIdRangeStart, taskIdRangeEnd); + PreparedStatement searchStmt = searchVectorStatements.get(statementKey); + if (searchStmt == null) { + searchStmt = session.prepare(queryBuilder.toString()); + searchVectorStatements.put(statementKey, searchStmt); + } + try { // use CqlVector CqlVector cqlVector = CqlVector.newInstance(embedding); - ResultSet resultRows = session.execute(searchVectorStmt.bind( - cqlVector, - cqlVector)); + // Build parameters list + List params = new ArrayList<>(); + params.add(cqlVector); // First parameter for similarity calculation + + // Add model version parameters + if (modelVersions != null && !modelVersions.isEmpty()) { + for (String modelVersion : modelVersions) { + if (modelVersion.endsWith("*")) { + // Convert wildcard to LIKE pattern + params.add(modelVersion.substring(0, modelVersion.length() - 1) + "%"); + } else { + params.add(modelVersion); + } + } + } + + // Add task ID parameters + if (taskIds != null && !taskIds.isEmpty()) { + params.addAll(taskIds); + } else if (taskIdRangeStart != null && taskIdRangeEnd != null) { + params.add(taskIdRangeStart); + params.add(taskIdRangeEnd); + } else if (taskIdRangeStart != null) { + params.add(taskIdRangeStart); + } else if (taskIdRangeEnd != null) { + params.add(taskIdRangeEnd); + } + + params.add(cqlVector); + + ResultSet resultRows = session.execute(searchStmt.bind(params.toArray())); + // track best Similarity per uniqueID Map bestMatches = new HashMap<>(); for (Row row : resultRows) { String id = row.getString(0); - String contentChunk = row.getString(1); - Float similarity = row.getFloat(2); + String contentChunk = row.getString(3); + Float similarity = row.getFloat(4); // test if we already have a match RetrievalResult existingMatch = bestMatches.get(id); @@ -523,4 +610,70 @@ public class ClusterService { } } + public List searchEmbeddings(List embedding, + int maxResults) throws ClusterException { + + return searchEmbeddings(embedding, maxResults, null, null, null, null); + } + + // Range-based search + public List searchEmbeddingsWithTaskRange(List embedding, int maxResults, + List modelVersions, + int taskIdStart, int taskIdEnd) + throws ClusterException { + return searchEmbeddings(embedding, maxResults, modelVersions, null, taskIdStart, taskIdEnd); + } + + // Wildcard model search + public List searchEmbeddingsWithModelWildcard(List embedding, int maxResults, + String modelPattern, + List taskIds) + throws ClusterException { + return searchEmbeddings(embedding, maxResults, Arrays.asList(modelPattern), taskIds, null, null); + } + + /** + * Helper method to prepare a IN clause + * + * @param size + * @return + */ + private String formatInClause(int size) { + return "(" + String.join(",", Collections.nCopies(size, "?")) + ")"; + } + + /** + * Helper method to build a key for specific select statement with parameters. + * This key is used to cache prepared statements + * + * @param dbLimit + * @param modelVersions + * @param taskIds + * @param taskIdRangeStart + * @param taskIdRangeEnd + * @return + */ + private String buildStatementKey(int dbLimit, List modelVersions, List taskIds, + Integer taskIdRangeStart, Integer taskIdRangeEnd) { + StringBuilder keyBuilder = new StringBuilder("search_").append(dbLimit); + + if (modelVersions != null) { + keyBuilder.append("_mv").append(modelVersions.size()); + // Add wildcard indicator + long wildcardCount = modelVersions.stream().mapToLong(mv -> mv.endsWith("*") ? 1 : 0).sum(); + keyBuilder.append("w").append(wildcardCount); + } + + if (taskIds != null && !taskIds.isEmpty()) { + keyBuilder.append("_ti").append(taskIds.size()); + } else if (taskIdRangeStart != null || taskIdRangeEnd != null) { + keyBuilder.append("_tr"); + if (taskIdRangeStart != null) + keyBuilder.append("s"); + if (taskIdRangeEnd != null) + keyBuilder.append("e"); + } + + return keyBuilder.toString(); + } } diff --git a/dynamixs-ai-rag/src/main/java/ai/dynamixs/rag/RAGAdapter.java b/dynamixs-ai-rag/src/main/java/ai/dynamixs/rag/RAGAdapter.java index 0e01274..62bd288 100644 --- a/dynamixs-ai-rag/src/main/java/ai/dynamixs/rag/RAGAdapter.java +++ b/dynamixs-ai-rag/src/main/java/ai/dynamixs/rag/RAGAdapter.java @@ -96,13 +96,10 @@ public class RAGAdapter implements SignalAdapter { public static final String ML_ENTITY = "entity"; public static final String API_ERROR = "API_ERROR"; - public static final String LLM_PROMPT = "PROMPT"; - public static final String LLM_SUGGEST = "SUGGEST"; - public static final String LLM_INDEX = "INDEX"; - public static final String LLM_RETRIEVAL = "RETRIEVAL"; - public static final int API_EVENT_SUCCESS = 110; - public static final int API_EVENT_FAILURE = 90; + public static final String RAG_INDEX = "INDEX"; + public static final String RAG_DELETE = "DELETE"; + public static final String RAG_RETRIEVAL = "RETRIEVAL"; private static Logger logger = Logger.getLogger(RAGAdapter.class.getName()); @@ -156,9 +153,21 @@ public class RAGAdapter implements SignalAdapter { // read optional configuration form the model or imixs.properties.... List llmIndexDefinitions = workflowService.evalWorkflowResultXML(event, "imixs-ai", - LLM_INDEX, workitem, false); + RAG_INDEX, workitem, false); List llmRetrievalDefinitions = workflowService.evalWorkflowResultXML(event, "imixs-ai", - LLM_RETRIEVAL, workitem, false); + RAG_RETRIEVAL, workitem, false); + + List llmDeleteDefinitions = workflowService.evalWorkflowResultXML(event, "imixs-ai", + RAG_DELETE, workitem, false); + + // verify if we have an DELETE configuration + if (llmDeleteDefinitions != null && llmDeleteDefinitions.size() > 0) { + try { + clusterService.removeAllEmbeddings(workitem.getUniqueID()); + } catch (ClusterException e) { + throw new PluginException(RAGAdapter.class.getSimpleName(), API_ERROR, e.getMessage(), e); + } + } // verify if we have an INDEX configuration if (llmIndexDefinitions != null && llmIndexDefinitions.size() > 0) { @@ -227,7 +236,11 @@ public class RAGAdapter implements SignalAdapter { } List indexResult = llmService.postEmbedding(chunk, llmAPIEndpoint, llmAPIDebug); // write to cassandra - clusterService.insertVector(workitem.getUniqueID(), chunk, indexResult); + clusterService.insertVector(workitem.getUniqueID(), + workitem.getModelVersion(), + workitem.getTaskID(), + chunk, + indexResult); if (llmAPIDebug) { logger.info("│ ├── ⇨ " + indexResult.size() + " floats stored in RAG db."); } @@ -289,12 +302,12 @@ public class RAGAdapter implements SignalAdapter { } // retrieve prompt.... - List indexResult = llmService.postEmbedding(llmPrompt, llmAPIEndpoint, llmAPIDebug); + List embeddings = llmService.postEmbedding(llmPrompt, llmAPIEndpoint, llmAPIDebug); if (llmAPIDebug) { - logger.info("├── ⇨ " + indexResult.size() + " floats stored in RAG db."); + logger.info("├── ⇨ " + embeddings.size() + " floats stored in RAG db."); } // search cassandra - List retrievalResultList = clusterService.searchEmbeddings(indexResult, 5); + List retrievalResultList = clusterService.searchEmbeddings(embeddings, 5); List listOfIds = retrievalResultList.stream() .map(RetrievalResult::getUniqueId) .collect(Collectors.toList()); diff --git a/workflow/product-de-1.0.0.bpmn b/workflow/product-de-1.0.0.bpmn index da5887f..bd5ee03 100644 --- a/workflow/product-de-1.0.0.bpmn +++ b/workflow/product-de-1.0.0.bpmn @@ -50,7 +50,6 @@ Task_3 StartEvent_1 IntermediateCatchEvent_17 - ExclusiveGateway_2 IntermediateCatchEvent_9 Task_4 IntermediateCatchEvent_8 @@ -73,6 +72,8 @@ event_y0smQw event_qb0xXA event_jBz77w + gateway_cDqotg + event_k2i6EQ @@ -138,6 +139,7 @@ Color: _color sequenceFlow_OiqVHg sequenceFlow_4DUFiA sequenceFlow_crpgGw + SequenceFlow_21 @@ -253,9 +255,9 @@ Color: _color
$workflowgroup - $workflowstatus]]> SequenceFlow_17 - SequenceFlow_40 SequenceFlow_19 - SequenceFlow_25 + sequenceFlow_8XT0hw + SequenceFlow_26 @@ -299,7 +301,6 @@ Color: _color
$workflowgroup - $workflowstatus]]> - SequenceFlow_21 SequenceFlow_47
@@ -310,7 +311,7 @@ Color: _color
- + @@ -378,13 +379,10 @@ Betrag: _amount (Brutto € _amount_brutto - SequenceFlow_56 - sequenceFlow_HCxYvw + sequenceFlow_Hq0Ccg + sequenceFlow_8XT0hw - - - - + @@ -436,7 +434,8 @@ Betrag: _amount (Brutto € _amount_brutto - home]]> + home +]]> @@ -459,7 +458,7 @@ Betrag: _amount (Brutto € _amount_brutto - + @@ -506,9 +505,9 @@ Color: _color
$workflowgroup - $workflowstatus]]> - SequenceFlow_26 SequenceFlow_23 - SequenceFlow_36 + sequenceFlow_2pAPvA + sequenceFlow_RgOnDQ @@ -589,15 +588,11 @@ Betrag: _amount (Brutto € _amount_brutto - SequenceFlow_25 SequenceFlow_26 - - - - + @@ -671,8 +666,7 @@ Betrag: _amount (Brutto € _amount_brutto - SequenceFlow_36 - SequenceFlow_39 + sequenceFlow_2pAPvA
@@ -885,21 +879,6 @@ result.isValid=true; - - SequenceFlow_56 - SequenceFlow_39 - SequenceFlow_40 - - - - - - - - - - - @@ -1194,7 +1173,7 @@ result.isValid=true; - + @@ -1203,7 +1182,7 @@ result.isValid=true; - + @@ -1285,6 +1264,104 @@ result.isValid=true; + + + + + + sequenceFlow_HCxYvw + sequenceFlow_Hq0Ccg + sequenceFlow_Jkgs0A + + + + + + + + + + + + + + + + + _subject]]> + + + + + + + + + _amount (Brutto € _amount_brutto) +_description]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + home + + https://llama.cpp2.imixs.com/ + true + +]]> + + + + + + + + + + + + false + + + + sequenceFlow_Jkgs0A + sequenceFlow_RgOnDQ + + + + + + + + @@ -1297,10 +1374,10 @@ result.isValid=true; - + - + @@ -1339,9 +1416,9 @@ result.isValid=true; - + - + @@ -1351,48 +1428,42 @@ result.isValid=true; - + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - + @@ -1418,89 +1489,65 @@ result.isValid=true; - - - - - - + - - + + + + - - - - - - + - - + + - - - + + + - - - + + + + - + - - - + + + - - + + - - - - - - - - - - - - - - - - - - + + - - + + - @@ -1510,7 +1557,7 @@ result.isValid=true; - + @@ -1570,20 +1617,20 @@ result.isValid=true; - + - + - - - - + + + + - - + + @@ -1594,8 +1641,8 @@ result.isValid=true; - - + + @@ -1603,6 +1650,41 @@ result.isValid=true; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +