Skip to content

A simple process driven application to manage restaurant complaints

Notifications You must be signed in to change notification settings

d135-1r43/restaurant-complaints

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

51 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Kogito/Quarkus PDA Example: Restaurant Complaints

This repository contains a basic Process Driven Applications (PDA) demo application created using Quarkus and the Kogito process engine. The purpose of this demo is to demonstrate key concepts of Process Driven Applications.

The application includes three services:

  1. Complaints Service (de.thi.complaints): A Quarkus service that handles restaurant complaint submissions.
  2. Sentiment Analysis Service (de.thi.sentiment): A Quarkus service that assesses the sentiment of the complaint. It uses OpenAI (ChatGPT) and a User Task as a fallback.
  3. Archive Service (de.thi.archiv, optional): A Quarkus service with a simple REST API to store complaints. This service can be integrated into the Complaints Service.

πŸ—£οΈ I have showcased this project at a Meetup of the Quarkus User Group Munich on Aug. 31st 2023. The accompanying slides are available here: meetup-kogito.pdf

Prior Knowledge

To understand this walkthrough you should already be able to:

You must

  • have Docker running
  • have a working developer environment for Java 17 with Maven and an IDE of your choice (VS Code preferred)

Walkthrough

Complaint Business Process

πŸ‘‰ Open the file https://github.com/d135-1r43/restaurant-complaints/blob/master/de.thi.complaints/src/main/resources/complaints.bpmn in VS Code and analyse the properties of the process, especially the process variables under 'Process Data' to get a better understanding.

πŸ‘‰ Run the Complaints Service and start a complaint with the Swagger API. It should be available at http://localhost:8080/q/swagger-ui/. Remove the sentiment value, just leave it out.

Complaint Process in Swagger

The Complaint Process uses several best-practice patterns. Let's have a look at them…

Asynchronous implementation via Intermediate Message Event

ℹ️ Sentiment Analysis is the process of analyzing digital text to determine if the emotional tone of the message is positive, negative, or neutral. In our case, we have a scale from 0 'very angry' to 10 'extremly happy'.

In 'Ask for Sentiment' the process will throw a message and will immediately wait for the returned message in 'Get Sentiment'. The process is agnostic of the actual implementation. The sentiment definition is done by OpenAI. There is an event listener on an error event, in that case a user task is started. As there is no API key for OpenAI defined on default, the User Task will always be triggered.

To achieve decoupling, we will have to define several things:

  • In 'Ask for Sentiment' we will have to define a message. We do this in the BPMN editor UI and enter the free text name text in the field 'Message' in the dropdown 'Implementation/Execution'.
  • In 'Data Assignments' we will define the variable complaintText to be assigned to the message.

πŸ‘‰ Analyze the properties of 'Ask for Sentiment' now in VS Code.

As we are developing in a cloud world, the implementation of the sentiment analysis will run in a complete different service, in the Sentiment Analysis Service (de.thi.sentiment). In order to make the two services communicate with each other, we will use Apache Kafka. Thankfully, Kogito and Quarkus abstract away all the nitty-gritty details. Yet we want to understand them.

In order for Kogito to use Quarkus, we have some dependencies in our pom.xml:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-reactive-messaging-kafka</artifactId>
</dependency>
<dependency>
    <groupId>org.kie.kogito</groupId>
    <artifactId>kogito-addons-quarkus-messaging</artifactId>
</dependency>

That is enough for the magic to happen: 'Ask for Sentiment' will create a Kafka event with the standard Cloudevents.io. This event will be sent to the Microprofile messaging channel text, which is defined as a Kafka channel in our application.properties. If you run Quarkus in Dev mode, it will start a Kafka automatically as a Dev Service. Also, if you start another Quarkus service in Dev mode, it will connect to the same Kafka. Learn more about Dev Services at Dev Services Overview.

mp.messaging.outgoing.text.connector=smallrye-kafka
mp.messaging.outgoing.text.topic=de-thi-sentiments-in
mp.messaging.outgoing.text.value.serializer=org.apache.kafka.common.serialization.StringSerializer
#
mp.messaging.incoming.sentiment.connector=smallrye-kafka
mp.messaging.incoming.sentiment.topic=de-thi-sentiments-out
mp.messaging.incoming.sentiment.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer

πŸ‘‰ Analyze the application.properties and understand on which topic the BPMN event will be sent to. In the Dev UI, look for the Kafka UI and try to find your cloud event. Understand, what has happenend. Also, try to add a random event with the plus button.

Kafka UI

The Sentiment Analysis Service (de.thi.sentiment) will implement the sentiment analysis. For the sake of simplicity, it is just a User Task to type in a value between 0 and 10.

πŸ‘‰ Open the BPMN https://github.com/d135-1r43/restaurant-complaints/blob/master/de.thi.sentiment/src/main/resources/sentiment.bpmn in VS Code. Understand the BPMN, analyze the variables and the properties of the start event, the user task and the end event.

πŸ‘‰ Start the Sentiment Analysis Service and understand that it connects to the same Kafka Dev Service and to the same Kogito Data Index Dev Service. Open its Dev UI at http://localhost:8081/q/dev.

Sentiment Process

We want the Sentiment process to start at the event. So we have to make sure…

  • that 'Get Text' is a Message Start event
  • that it has the same message name under 'Implementation/Execution' as the corresponding Intermediate Throw Event
  • that the mapping of the variables under 'Data Assignment' makes sense
  • that we configure the topic correctly in the application.properties. The incoming channel has the key mp.messaging.incoming.kogito_incoming_stream, the outgoing is called mp.messaging.outgoing.kogito_outgoing_stream.

mp.messaging.incoming.kogito_incoming_stream.connector=smallrye-kafka
mp.messaging.incoming.kogito_incoming_stream.topic=de-thi-sentiments-in
mp.messaging.incoming.kogito_incoming_stream.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
#
mp.messaging.outgoing.kogito_outgoing_stream.connector=smallrye-kafka
mp.messaging.outgoing.kogito_outgoing_stream.topic=de-thi-sentiments-out
mp.messaging.outgoing.kogito_outgoing_stream.value.serializer=org.apache.kafka.common.serialization.StringSerializer

πŸ‘‰ Make a complaint at the Complaints Service via Swagger UI. Switch to the Sentiment Analysis Service and run the user task to define the sentiment in the Dev UI. Understand why and how now the Complaints Service will catch the event at 'Get Sentiment'. Use the log files, the Kafka UI and the Kogito Management Console to deepen your understanding.

πŸ‘‰ Configure an OpenAI API key under the config openai.api.key and rerun the process. Now the sentiment will be determined by OpenAI. Understand the Java Code that runs the Completion Request agains OpenAI.

Synchronous Implementation of Archiving the Complaints

The Complaints Service will ask for a response in a user task and, if the sentiment is bad and angry (>5), it will get the acknowledgement of the manager. After this, the next important pattern is implemented: A reuseable call activity. The call activity 'Archive Complaint Resolution' will call one of two processes:

πŸ‘‰ Open complaints.bpmn and understand which call activity (REST or DB) is used. Run the complaint. Switch the call activity and experiment. Try to assert after the process, that the complaint has been saved either in the Archive Service or in the internal database. Understand, how the parameters are assigend from the process to the Java class.

image

ℹ️ Under the hood, Kogito generates Java classes for each process. That is why the filename, the id and the name should not contain special characters.

Understanding the external REST service

Archive Service is a very simple REST service. It basically follows the guide at Simplified Hibernate ORM with Panache. It uses a Postgres to store the complaints. It starts at http://localhost:8082/q/swagger-ui/.

So how does the Complaints Service make the REST request? The magic happens in two classes.

The interface ComplaintRestClient uses Microprofile REST Client to auto-implement a full feature REST client. There is no need to actually care about the HTTP requests, it is all abstracted away.

@ApplicationScoped
@RegisterRestClient(configKey = "complaint")
public interface ComplaintRestClient
{
@GET
List<Complaint> all();
@GET
@Path("{id}")
Complaint byId(@PathParam("id") Long id);
@POST
void post(Complaint complaint);
}

In order to know the URL of the service, it has to be configured in the application.properties:

%dev.quarkus.rest-client.complaint.url=http://localhost:8082/complaints

The class RestArchiver serves as the Java implementation of the Service Task in the process.

@ApplicationScoped
public class RestArchiver
{
private static final Logger LOG = LoggerFactory.getLogger(RestArchiver.class);
@Inject
@RestClient
ComplaintRestClient complaintRestClient;
public void archive(String complaintText, String responseText, Integer sentiment)
{
Complaint complaint = new Complaint(complaintText, responseText, sentiment);
complaintRestClient.post(complaint);
LOG.info("Complaint '{}' successfully POSTed", complaintText);
}
}

We are using RESTAssured, a nice test framework, to write unit tests against the REST service. Have a look at ComplaintResourceTests.java.

public void shouldPersistAComplaint()
{
String json = """
{
"complaintText": "Too little salt",
"responseText": "We are sorry",
"sentiment": 2
}
""";
// when
given()
.contentType(ContentType.JSON)
.body(json)
.when().post()
.then()
.statusCode(204);
// then
List<Complaint> complaints = complaintRepository.listAll();
assertThat(complaints, hasSize(1));
assertThat(complaints, contains(hasProperty("complaintText", is("Too little salt"))));
}

πŸ‘‰ Run the unit tests in your IDE. Write a third unit test to understand how testing REST APIs works.

Understanding the internal database

The alternative call activity archivedb.bpmn will not call an external service, yet it will save the complaint inside of the Complaints Service. It uses (like the Archive Service) Hibernate Panache, based on the JPA standard.

πŸ‘‰ Find out if we are using the Repository pattern or the Active Record pattern by analyzing the code starting from DbArchiver.java.

Testing the processes with JUnit

ℹ️ Testing with unit tests allows us to make sure that specific parts of the works and to identify the source of errors early on.

At the end of the day, the process itself must be tested as well. It is best practice to test each Service Task individually and only do a rough integration test against the process.

An example of such an integration test is ArchiveProcessTests.java.

@QuarkusTest
public class ArchiveProcessTests
{
@Inject
@Named("archive")
Process<? extends Model> archiveProcess;
@Inject
ComplaintRepository complaintRepository;
@Test
public void shouldArchiveInDatabase()
{
// given
assertNotNull(archiveProcess);
Map<String, Object> parameters = new HashMap<>();
parameters.put("complaintText", "Too much parsley.");
parameters.put("responseText", "Thank You. We will reduce the parsley");
parameters.put("sentiment", Integer.valueOf(3));
Model model = archiveProcess.createModel();
model.fromMap(parameters);
// when
ProcessInstance<?> processInstance = archiveProcess.createInstance(model);
processInstance.start();
// then
List<Complaint> complaintsFromDatabase = complaintRepository.findAll().list();
assertThat(complaintsFromDatabase, hasSize(1));
assertThat(complaintsFromDatabase, contains(hasProperty("complaintText", is("Too much parsley."))));
}
}

πŸ‘‰ Run the test. Understand how you can inject the process and how to start it in the test. Understand, how the start parameters are provided.

About

A simple process driven application to manage restaurant complaints

Resources

Stars

Watchers

Forks