diff --git a/src/bpmn_assistant/prompts/create_bpmn.txt b/src/bpmn_assistant/prompts/create_bpmn.txt index 485c472..c426fba 100644 --- a/src/bpmn_assistant/prompts/create_bpmn.txt +++ b/src/bpmn_assistant/prompts/create_bpmn.txt @@ -36,6 +36,8 @@ If a branch has an empty "path", it leads to the first element after the exclusi If the branch does not lead to the next element in the process (for example, it goes back to a previous element), specify the next element id. If the branch leads to the next element in the process, do not specify the next element id. If the process needs to end under a specific condition, you must explicitly include an end event in that branch's "path". If no end event is provided, the process will automatically continue to the next task in the sequence. +If the process description does not explicitly mention the 'else' branch or specify the outcome for an unmet condition, assume it leads to an end event. + ```json { diff --git a/src/bpmn_assistant/services/bpmn_process_transformer.py b/src/bpmn_assistant/services/bpmn_process_transformer.py index c2ad759..7425366 100644 --- a/src/bpmn_assistant/services/bpmn_process_transformer.py +++ b/src/bpmn_assistant/services/bpmn_process_transformer.py @@ -38,6 +38,24 @@ def transform( elements = [] flows = [] + def add_flow(source_ref, target_ref, flow_id=None, condition=None): + """ + Helper function to append a flow to the flows list. + """ + for flow in flows: + if flow["sourceRef"] == source_ref and flow["targetRef"] == target_ref: + return + + flow_id = flow_id or f"{source_ref}-{target_ref}" + flows.append( + { + "id": flow_id, + "sourceRef": source_ref, + "targetRef": target_ref, + "condition": condition, + } + ) + def handle_exclusive_gateway( element: dict, next_element_id: Optional[str] = None ) -> Optional[str]: @@ -58,13 +76,10 @@ def handle_exclusive_gateway( if not branch.get("path"): # Connect the exclusive gateway to the next element in the process if next_element_id: - flows.append( - { - "id": f"{element['id']}-{next_element_id}", - "sourceRef": element["id"], - "targetRef": next_element_id, - "condition": branch.get("condition", None), - } + add_flow( + element["id"], + next_element_id, + condition=branch.get("condition", None), ) continue # Skip further processing for empty branches @@ -81,14 +96,7 @@ def handle_exclusive_gateway( source_ref = branch_structure["elements"][-1]["id"] condition = None - flows.append( - { - "id": f"{source_ref}-{branch['next']}", - "sourceRef": source_ref, - "targetRef": branch["next"], - "condition": condition, - } - ) + add_flow(source_ref, branch["next"], condition=condition) # Add the flow from the exclusive gateway to the first element in the branch first_element = ( @@ -97,13 +105,10 @@ def handle_exclusive_gateway( else None ) if first_element: - flows.append( - { - "id": f"{element['id']}-{first_element['id']}", - "sourceRef": element["id"], - "targetRef": first_element["id"], - "condition": branch["condition"], - } + add_flow( + element["id"], + first_element["id"], + condition=branch["condition"], ) return join_gateway_id @@ -120,31 +125,17 @@ def handle_parallel_gateway(element: dict) -> str: ) for branch in element["branches"]: - branch_structure = self.transform(branch) + branch_structure = self.transform(branch, join_gateway_id) elements.extend(branch_structure["elements"]) flows.extend(branch_structure["flows"]) # Add the flow from the parallel gateway to the first element in the branch first_element = branch_structure["elements"][0] - flows.append( - { - "id": f"{element['id']}-{first_element['id']}", - "sourceRef": element["id"], - "targetRef": first_element["id"], - "condition": None, - } - ) + add_flow(element["id"], first_element["id"]) # Add the flow from the last element in the branch to the join gateway last_element = branch_structure["elements"][-1] - flows.append( - { - "id": f"{last_element['id']}-{join_gateway_id}", - "sourceRef": last_element["id"], - "targetRef": join_gateway_id, - "condition": None, - } - ) + add_flow(last_element["id"], join_gateway_id) return join_gateway_id @@ -168,37 +159,16 @@ def handle_parallel_gateway(element: dict) -> str: # Connect the join gateway to the next element in the process if join_gateway_id and next_element_id: - flows.append( - { - "id": f"{join_gateway_id}-{next_element_id}", - "sourceRef": join_gateway_id, - "targetRef": next_element_id, - "condition": None, - } - ) + add_flow(join_gateway_id, next_element_id) elif element["type"] == "parallelGateway": join_gateway_id = handle_parallel_gateway(element) # Connect the join gateway to the next element in the process if next_element_id: - flows.append( - { - "id": f"{join_gateway_id}-{next_element_id}", - "sourceRef": join_gateway_id, - "targetRef": next_element_id, - "condition": None, - } - ) + add_flow(join_gateway_id, next_element_id) elif next_element_id and element["type"] != "endEvent": # Add the flow between the current element and the next element in the process - flows.append( - { - "id": f"{element['id']}-{next_element_id}", - "sourceRef": element["id"], - "targetRef": next_element_id, - "condition": None, - } - ) + add_flow(element["id"], next_element_id) # Add incoming and outgoing flows to each element for element in elements: diff --git a/tests/services/test_bpmn_xml_generator.py b/tests/services/test_bpmn_xml_generator.py index 65f6234..0cf38b9 100644 --- a/tests/services/test_bpmn_xml_generator.py +++ b/tests/services/test_bpmn_xml_generator.py @@ -1,34 +1,81 @@ +from xml.etree import ElementTree as ET + from bpmn_assistant.services import BpmnXmlGenerator +def elements_equal(e1: ET.Element, e2: ET.Element) -> bool: + """Recursively compares two XML elements, ignoring the order of child elements.""" + if e1.tag != e2.tag: + print(f"Tags do not match: {e1.tag} != {e2.tag}") + return False + if (e1.text or "").strip() != (e2.text or "").strip(): + print(f"Texts do not match in tag {e1.tag}: '{e1.text}' != '{e2.text}'") + return False + if e1.attrib != e2.attrib: + print(f"Attributes do not match in tag {e1.tag}: {e1.attrib} != {e2.attrib}") + return False + if len(e1) != len(e2): + print( + f"Number of children do not match in tag {e1.tag}: {len(e1)} != {len(e2)}" + ) + return False + + # Create a list of child elements for e2 to track matched elements + children2 = list(e2) + + for child1 in e1: + match_found = False + for child2 in children2: + if elements_equal(child1, child2): + match_found = True + children2.remove(child2) + break + if not match_found: + print( + f"No matching element found for {ET.tostring(child1, encoding='unicode')}" + ) + return False + return True + + class TestBpmnXmlGenerator: def test_create_bpmn_xml_parallel(self, procurement_process): xml_generator = BpmnXmlGenerator() result = xml_generator.create_bpmn_xml(procurement_process) expected_xml = 'start1-parallel1start1-parallel1parallel1-task1parallel1-task3task2-parallel1-jointask4-parallel1-joinparallel1-join-end1parallel1-task1task1-task2task1-task2task2-parallel1-joinparallel1-task3task3-task4task3-task4task4-parallel1-joinparallel1-join-end1' - assert result == expected_xml + result_tree = ET.ElementTree(ET.fromstring(result)) + expected_tree = ET.ElementTree(ET.fromstring(expected_xml)) + assert elements_equal(result_tree.getroot(), expected_tree.getroot()) def test_create_bpmn_xml_linear(self, linear_process): xml_generator = BpmnXmlGenerator() result = xml_generator.create_bpmn_xml(linear_process) expected_xml = 'start1-task1start1-task1task1-task2task1-task2task2-task3task2-task3task3-task4task3-task4task4-task5task4-task5task5-end1task5-end1' - assert result == expected_xml + result_tree = ET.ElementTree(ET.fromstring(result)) + expected_tree = ET.ElementTree(ET.fromstring(expected_xml)) + assert elements_equal(result_tree.getroot(), expected_tree.getroot()) def test_create_bpmn_exclusive(self, order_process): xml_generator = BpmnXmlGenerator() result = xml_generator.create_bpmn_xml(order_process) expected_xml = 'start1-task1start1-task1task1-exclusive1task1-exclusive1exclusive1-task2exclusive1-exclusive2exclusive1-task2task2-end1exclusive1-exclusive2exclusive2-task3exclusive2-task5exclusive2-task3task3-task4task3-task4task4-end1exclusive2-task5task5-end1task2-end1task4-end1task5-end1' - assert result == expected_xml + result_tree = ET.ElementTree(ET.fromstring(result)) + expected_tree = ET.ElementTree(ET.fromstring(expected_xml)) + assert elements_equal(result_tree.getroot(), expected_tree.getroot()) def test_create_bpmn_xml_pg_inside_eg(self, pg_inside_eg_process): xml_generator = BpmnXmlGenerator() result = xml_generator.create_bpmn_xml(pg_inside_eg_process) expected_xml = 'start1-exclusive1start1-exclusive1exclusive1-task2exclusive1-parallel1task2-exclusive1-joinparallel1-join-exclusive1-joinexclusive1-join-end1exclusive1-task2task2-exclusive1-joinexclusive1-parallel1parallel1-task3parallel1-task4task3-parallel1-jointask4-parallel1-joinparallel1-join-exclusive1-joinparallel1-task3task3-parallel1-joinparallel1-task4task4-parallel1-joinexclusive1-join-end1' - assert result == expected_xml + result_tree = ET.ElementTree(ET.fromstring(result)) + expected_tree = ET.ElementTree(ET.fromstring(expected_xml)) + assert elements_equal(result_tree.getroot(), expected_tree.getroot()) def test_create_bpmn_xml_empty_gateway_paths(self, empty_gateway_path_process): xml_generator = BpmnXmlGenerator() result = xml_generator.create_bpmn_xml(empty_gateway_path_process) expected_xml = 'start-task1start-task1task1-task2task1-task2task2-exclusive1task2-exclusive1exclusive1-task3exclusive1-endexclusive1-task3task3-endtask3-endexclusive1-end' - assert result == expected_xml + result_tree = ET.ElementTree(ET.fromstring(result)) + expected_tree = ET.ElementTree(ET.fromstring(expected_xml)) + assert elements_equal(result_tree.getroot(), expected_tree.getroot())