Skip to content

Commit

Permalink
Modify create_bpmn prompt, fix bug with emtpy EG paths
Browse files Browse the repository at this point in the history
  • Loading branch information
jtlicardo committed Dec 16, 2024
1 parent d9bc940 commit 4b37142
Show file tree
Hide file tree
Showing 5 changed files with 262 additions and 8 deletions.
10 changes: 9 additions & 1 deletion src/bpmn_assistant/prompts/create_bpmn.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
The BPMN JSON representation uses a sequence of elements to describe the process. Each element is executed in order based on its position in the "process" array unless gateways (exclusive or parallel) specify branching paths.

# Representation of various BPMN elements

## Tasks
Expand Down Expand Up @@ -25,11 +27,15 @@ Specify the event type in the 'type' field. Only "startEvent" and "endEvent" opt

## Gateways

Gateways determine process flow based on conditions or parallel tasks.

### Exclusive gateway

Each branch has a condition and a path of elements that are executed if the condition is met.
Each branch must include a condition and an array of elements that are executed if the condition is met.
If a branch has an empty "path", it leads to the first element after the exclusive gateway.
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.

```json
{
Expand All @@ -55,6 +61,8 @@ If the branch leads to the next element in the process, do not specify the next

### Parallel gateway

Specify "branches" as an array of arrays, where each sub-array lists elements executed in parallel.

```json
{
"type": String = "parallelGateway",
Expand Down
11 changes: 8 additions & 3 deletions src/bpmn_assistant/services/bpmn_modeling_service.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import traceback
from importlib import resources

Expand All @@ -8,7 +9,7 @@
BpmnEditorService,
define_change_request,
)
from bpmn_assistant.utils import prepare_prompt, message_history_to_string
from bpmn_assistant.utils import message_history_to_string, prepare_prompt


class BpmnModelingService:
Expand Down Expand Up @@ -46,8 +47,12 @@ def create_bpmn(
response = llm_facade.call(prompt)

try:
self._validate_bpmn(response["process"])
return response["process"] # Return the process if it's valid
process = response["process"]
self._validate_bpmn(process)
logger.debug(
f"Generated BPMN process:\n{json.dumps(process, indent=2)}"
)
return process # Return the process if it's valid
except Exception as e:
error_type = (
"LLM call failed" if response is None else "Invalid process"
Expand Down
16 changes: 15 additions & 1 deletion src/bpmn_assistant/services/bpmn_process_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ def handle_exclusive_gateway(
)

for branch in element["branches"]:

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),
}
)
continue # Skip further processing for empty branches

branch_structure = self.transform(
branch["path"], join_gateway_id or next_element_id
)
Expand Down Expand Up @@ -175,7 +189,7 @@ def handle_parallel_gateway(element: dict) -> str:
"condition": None,
}
)
elif 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(
{
Expand Down
45 changes: 42 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
from dotenv import load_dotenv

from bpmn_assistant.core import LLMFacade, MessageItem
from bpmn_assistant.core.enums import Provider, AnthropicModels, OpenAIModels
from bpmn_assistant.core.enums import AnthropicModels, OpenAIModels, Provider
from tests.fixtures.bpmn_loader import load_bpmn


@pytest.fixture
def anthropic_facade():
load_dotenv(override=True)
api_key = os.getenv("ANTHROPIC_API_KEY")
return LLMFacade(Provider.ANTHROPIC, api_key, AnthropicModels.HAIKU.value)
return LLMFacade(Provider.ANTHROPIC, api_key, AnthropicModels.HAIKU_3_5.value)


@pytest.fixture
Expand Down Expand Up @@ -47,13 +47,52 @@ def empty_gateway_path_process():
}
],
},
{"condition": "Condition B", "path": [], "next": "end"},
{"condition": "Condition B", "path": []},
],
},
{"type": "endEvent", "id": "end"},
]


@pytest.fixture
def eg_end_event_in_path_process():
"""
Description: A process that contains an exclusive gateway with an end event in one of the paths.
"""
return [
{"type": "startEvent", "id": "start"},
{
"type": "exclusiveGateway",
"id": "exclusive1",
"label": "Decision Point",
"has_join": False,
"branches": [
{
"condition": "Condition A",
"path": [
{
"type": "task",
"id": "task1",
"label": "Perform the first task",
}
],
},
{
"condition": "Condition B",
"path": [
{
"type": "endEvent",
"id": "end1",
}
],
},
],
},
{"type": "task", "id": "task2", "label": "Perform the second task"},
{"type": "endEvent", "id": "end2"},
]


@pytest.fixture
def order_process():
"""
Expand Down
188 changes: 188 additions & 0 deletions tests/services/test_bpmn_process_transformer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
from bpmn_assistant.services.bpmn_process_transformer import BpmnProcessTransformer


class TestBpmnProcessTransformer:

def test_transform_exclusive_gateway_with_empty_path(
self, empty_gateway_path_process
):

self.transformer = BpmnProcessTransformer()

result = self.transformer.transform(empty_gateway_path_process)

expected = {
"elements": [
{
"id": "start",
"type": "startEvent",
"label": None,
"incoming": [],
"outgoing": ["start-task1"],
},
{
"id": "task1",
"type": "task",
"label": "Perform a simple task",
"incoming": ["start-task1"],
"outgoing": ["task1-task2"],
},
{
"id": "task2",
"type": "task",
"label": "Perform a second task",
"incoming": ["task1-task2"],
"outgoing": ["task2-exclusive1"],
},
{
"id": "exclusive1",
"type": "exclusiveGateway",
"label": "Decision Point",
"incoming": ["task2-exclusive1"],
"outgoing": ["exclusive1-task3", "exclusive1-end"],
},
{
"id": "task3",
"type": "task",
"label": "Perform a third task",
"incoming": ["exclusive1-task3"],
"outgoing": ["task3-end"],
},
{
"id": "end",
"type": "endEvent",
"label": None,
"incoming": ["task3-end", "exclusive1-end"],
"outgoing": [],
},
],
"flows": [
{
"id": "start-task1",
"sourceRef": "start",
"targetRef": "task1",
"condition": None,
},
{
"id": "task1-task2",
"sourceRef": "task1",
"targetRef": "task2",
"condition": None,
},
{
"id": "task2-exclusive1",
"sourceRef": "task2",
"targetRef": "exclusive1",
"condition": None,
},
{
"id": "task3-end",
"sourceRef": "task3",
"targetRef": "end",
"condition": None,
},
{
"id": "exclusive1-task3",
"sourceRef": "exclusive1",
"targetRef": "task3",
"condition": "Condition A",
},
{
"id": "exclusive1-end",
"sourceRef": "exclusive1",
"targetRef": "end",
"condition": "Condition B",
},
],
}

assert result == expected

def test_transform_exclusive_gateway_with_end_event_in_path(
self, eg_end_event_in_path_process
):

self.transformer = BpmnProcessTransformer()

result = self.transformer.transform(eg_end_event_in_path_process)

expected = {
"elements": [
{
"id": "start",
"type": "startEvent",
"label": None,
"incoming": [],
"outgoing": ["start-exclusive1"],
},
{
"id": "exclusive1",
"type": "exclusiveGateway",
"label": "Decision Point",
"incoming": ["start-exclusive1"],
"outgoing": ["exclusive1-task1", "exclusive1-end1"],
},
{
"id": "task1",
"type": "task",
"label": "Perform the first task",
"incoming": ["exclusive1-task1"],
"outgoing": ["task1-task2"],
},
{
"id": "end1",
"type": "endEvent",
"label": None,
"incoming": ["exclusive1-end1"],
"outgoing": [],
},
{
"id": "task2",
"type": "task",
"label": "Perform the second task",
"incoming": ["task1-task2"],
"outgoing": ["task2-end2"],
},
{
"id": "end2",
"type": "endEvent",
"label": None,
"incoming": ["task2-end2"],
"outgoing": [],
},
],
"flows": [
{
"id": "start-exclusive1",
"sourceRef": "start",
"targetRef": "exclusive1",
"condition": None,
},
{
"id": "task1-task2",
"sourceRef": "task1",
"targetRef": "task2",
"condition": None,
},
{
"id": "exclusive1-task1",
"sourceRef": "exclusive1",
"targetRef": "task1",
"condition": "Condition A",
},
{
"id": "exclusive1-end1",
"sourceRef": "exclusive1",
"targetRef": "end1",
"condition": "Condition B",
},
{
"id": "task2-end2",
"sourceRef": "task2",
"targetRef": "end2",
"condition": None,
},
],
}

assert result == expected

0 comments on commit 4b37142

Please sign in to comment.