From ee23108659d2b07942dc3a9a8c1b7a690e50a154 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Wed, 1 May 2024 18:51:26 +0100 Subject: [PATCH] Add secure-fraud-detection demo --- pom.xml | 1 + samples/secure-fraud-detection/README.md | 118 +++++++++++++++ samples/secure-fraud-detection/pom.xml | 134 ++++++++++++++++++ .../langchain4j/sample/Customer.java | 16 +++ .../langchain4j/sample/CustomerConfig.java | 11 ++ .../sample/CustomerRepository.java | 19 +++ .../langchain4j/sample/FraudDetectionAi.java | 33 +++++ .../FraudDetectionContentRetriever.java | 51 +++++++ .../sample/FraudDetectionResource.java | 26 ++++ .../FraudDetectionRetrievalAugmentor.java | 20 +++ .../langchain4j/sample/LoginResource.java | 33 +++++ .../langchain4j/sample/LogoutResource.java | 29 ++++ .../sample/MissingCustomerException.java | 4 + .../MissingCustomerExceptionMapper.java | 18 +++ .../sample/MissingCustomerResource.java | 32 +++++ .../sample/SecureMemoryIdProvider.java | 78 ++++++++++ .../quarkiverse/langchain4j/sample/Setup.java | 51 +++++++ .../langchain4j/sample/Transaction.java | 26 ++++ .../sample/TransactionRepository.java | 19 +++ .../META-INF/resources/images/google.png | Bin 0 -> 1768 bytes .../resources/META-INF/resources/index.html | 133 +++++++++++++++++ ...se.langchain4j.spi.DefaultMemoryIdProvider | 1 + .../src/main/resources/application.properties | 14 ++ .../resources/templates/fraudDetection.html | 18 +++ .../resources/templates/missingCustomer.html | 18 +++ 25 files changed, 903 insertions(+) create mode 100644 samples/secure-fraud-detection/README.md create mode 100644 samples/secure-fraud-detection/pom.xml create mode 100644 samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/Customer.java create mode 100644 samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/CustomerConfig.java create mode 100644 samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/CustomerRepository.java create mode 100644 samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/FraudDetectionAi.java create mode 100644 samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/FraudDetectionContentRetriever.java create mode 100644 samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/FraudDetectionResource.java create mode 100644 samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/FraudDetectionRetrievalAugmentor.java create mode 100644 samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/LoginResource.java create mode 100644 samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/LogoutResource.java create mode 100644 samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/MissingCustomerException.java create mode 100644 samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/MissingCustomerExceptionMapper.java create mode 100644 samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/MissingCustomerResource.java create mode 100644 samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/SecureMemoryIdProvider.java create mode 100644 samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/Setup.java create mode 100644 samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/Transaction.java create mode 100644 samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/TransactionRepository.java create mode 100644 samples/secure-fraud-detection/src/main/resources/META-INF/resources/images/google.png create mode 100644 samples/secure-fraud-detection/src/main/resources/META-INF/resources/index.html create mode 100644 samples/secure-fraud-detection/src/main/resources/META-INF/services/io.quarkiverse.langchain4j.spi.DefaultMemoryIdProvider create mode 100644 samples/secure-fraud-detection/src/main/resources/application.properties create mode 100644 samples/secure-fraud-detection/src/main/resources/templates/fraudDetection.html create mode 100644 samples/secure-fraud-detection/src/main/resources/templates/missingCustomer.html diff --git a/pom.xml b/pom.xml index fbe66681e..b0cb2f964 100644 --- a/pom.xml +++ b/pom.xml @@ -198,6 +198,7 @@ samples/cli-translator samples/review-triage samples/fraud-detection + samples/secure-fraud-detection samples/chatbot samples/chatbot-easy-rag samples/sql-chatbot diff --git a/samples/secure-fraud-detection/README.md b/samples/secure-fraud-detection/README.md new file mode 100644 index 000000000..0fbb748fb --- /dev/null +++ b/samples/secure-fraud-detection/README.md @@ -0,0 +1,118 @@ +# Secure Fraud Detection Demo + +This demo showcases the implementation of a secure fraud detection system which is available only to users authenticated with Google. +It uses the `gpt-3.5-turbo` LLM, use `quarkus.langchain4j.openai.chat-model.model-name` property to select a different model. + +## The Demo + +### Setup + +The demo requires that your Google account's name and email are configured. +You can use system or env properties, see `Running the Demo` section below. + +When the application starts, 5 transactions with random amounts between 1 and 1000 are generated for the registered user. +A random city is also assigned to each transaction. + +The setup is defined in the [Setup.java](./src/main/java/io/quarkiverse/langchain4j/samples/Setup.java) class. + +The registered user and transactions are stored in a PostgreSQL database. When running the demo in dev mode (recommended), the database is automatically created and populated. + +### Content Retrieval + +To enable fraud detection, we provide the LLM with access to the custom [FraudDetectionContentRetriever](./src/main/java/io/quarkiverse/langchain4j/samples/FraudDetectionContentRetriever.java) content retriever. + +`FraudDetectionContentRetriever` is registered by [FraudDetectionRetrievalAugmentor](./src/main/java/io/quarkiverse/langchain4j/samples/FraudDetectionRetrievalAugmentor.java). + +It retrieves transaction data for the currently authenticated user through two Panache repositories: + +- [CustomerRepository.java](./src/main/java/io/quarkiverse/langchain4j/samples/CustomerRepository.java) +- [TransactionRepository.java](./src/main/java/io/quarkiverse/langchain4j/samples/TransactionRepository.java) + +It extracts the authenticated user's name and email from a custom memory id string representation. Memory id is created by [SecureMemoryIdProvider](./src/main/java/io/quarkiverse/langchain4j/samples/SecureMemoryIdProvider.java) from the authenticated security identity. `SecureMemoryIdProvider` is registered as a Java service provider. + +Currently, the memory id has the following format: `userName:userEmail#suffix` with an AI service specific `#suffix` added by the extension runtime in order to correctly segregate memory of concurrent requests to different AI services. + +### AI Service + +This demo leverages the AI service abstraction, with the interaction between the LLM and the application handled through the AIService interface. + +The `io.quarkiverse.langchain4j.sample.FraudDetectionAi` interface uses specific annotations to define the LLM: + +```java +@RegisterAiService(retrievalAugmentor = FraudDetectionRetrievalAugmentor.class) +``` + +For each message, the prompt is engineered to help the LLM understand the context and answer the request: + +```java + @SystemMessage(""" + You are a bank account fraud detection AI. You have to detect frauds in transactions. + """) + @UserMessage(""" + Your task is to detect whether a fraud was committed for the customer. + + Answer with a **single** JSON document containing: + - the customer name in the 'customer-name' key + - the transaction limit in the 'transaction-limit' key + - the computed sum of all transactions committed during the last 15 minutes in the 'total' key + - the 'fraud' key set to true if the computed sum of all transactions is greater than the transaction limit + - the 'transactions' key containing an array of JSON objects. Each object must have transaction 'amount', 'city' and formatted 'time' keys. + - the 'explanation' key containing an explanation of your answer. + - the 'email' key containing the customer email if the fraud was detected. + + Your response must be just the raw JSON document, without ```json, ``` or anything else. Do not use null JSON properties. + """) +@Timeout(value = 2, unit = ChronoUnit.MINUTES) +String detectAmountFraudForCustomer(); +``` + +_Note:_ You can also use fault tolerance annotations in combination with the prompt annotations. + +### Using the AI service + +Once defined, you can inject the AI service as a regular bean, and use it: + +```java +package io.quarkiverse.langchain4j.sample; + +import io.quarkus.security.Authenticated; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/fraud") +@Authenticated +public class FraudDetectionResource { + + private final FraudDetectionAi service; + + public FraudDetectionResource(FraudDetectionAi service) { + this.service = service; + } + + @GET + @Path("/amount") + public String detectBaseOnAmount() { + return service.detectAmountFraudForCustomer(); + } +} +``` + +`FraudDetectionResource` can only be accessed by authenticated users. + +## Google Authentication + +This demo requires users to authenticate with Google. +All you need to do is to register an application with Google, follow steps listed in the [Quarkus Google](https://quarkus.io/guides/security-openid-connect-providers#google) section. +Name your Google application as `Quarkus LangChain4j AI`, and make sure an allowed callback URL is set to `http://localhost:8080/login`. +Google will generate a client id and secret, use them to set `quarkus.oidc.client-id` and `quarkus.oidc.credentials.secret` properties. + +## Running the Demo + +To run the demo, use the following command: + +```shell +mvn quarkus:dev -Dname="Firstname Familyname" -Demail=someuser@gmail.com +``` + +Then, access `http://localhost:8080`, login to Google, and follow a provided application link to check the fraud. + diff --git a/samples/secure-fraud-detection/pom.xml b/samples/secure-fraud-detection/pom.xml new file mode 100644 index 000000000..fc976c2fc --- /dev/null +++ b/samples/secure-fraud-detection/pom.xml @@ -0,0 +1,134 @@ + + + 4.0.0 + + io.quarkiverse.langchain4j + quarkus-langchain4j-sample-secure-fraud-detection + Quarkus LangChain4j - Sample - Secure Fraud Detection + 1.0-SNAPSHOT + + + 3.13.0 + true + 17 + UTF-8 + UTF-8 + quarkus-bom + io.quarkus + 3.9.4 + true + 3.2.5 + 0.15.1 + + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + io.quarkus + quarkus-oidc + + + io.quarkiverse.langchain4j + quarkus-langchain4j-openai + ${quarkus-langchain4j.version} + + + io.quarkus + quarkus-smallrye-fault-tolerance + + + io.quarkus + quarkus-jdbc-postgresql + + + io.quarkus + quarkus-hibernate-orm-panache + + + io.quarkus + quarkus-resteasy-reactive-qute + + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus.platform.version} + + + + build + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + maven-surefire-plugin + 3.2.5 + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + native + + + native + + + + + + maven-failsafe-plugin + 3.2.5 + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + native + + + + diff --git a/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/Customer.java b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/Customer.java new file mode 100644 index 000000000..d67c3f02e --- /dev/null +++ b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/Customer.java @@ -0,0 +1,16 @@ +package io.quarkiverse.langchain4j.sample; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +@Entity +public class Customer { + + @Id + @GeneratedValue + public Long id; + public String name; + public String email; + public int transactionLimit; +} diff --git a/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/CustomerConfig.java b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/CustomerConfig.java new file mode 100644 index 000000000..43b15ccd3 --- /dev/null +++ b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/CustomerConfig.java @@ -0,0 +1,11 @@ +package io.quarkiverse.langchain4j.sample; + +import io.smallrye.config.ConfigMapping; + +@ConfigMapping(prefix = "customer") +public interface CustomerConfig { + + String name(); + + String email(); +} diff --git a/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/CustomerRepository.java b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/CustomerRepository.java new file mode 100644 index 000000000..ec25d536d --- /dev/null +++ b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/CustomerRepository.java @@ -0,0 +1,19 @@ +package io.quarkiverse.langchain4j.sample; + +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class CustomerRepository implements PanacheRepository { + + /* + * Transaction limit for the customer. + */ + public int getTransactionLimit(String customerName, String customerEmail) { + Customer customer = find("name = ?1 and email = ?2", customerName, customerEmail).firstResult(); + if (customer == null) { + throw new MissingCustomerException(); + } + return customer.transactionLimit; + } +} diff --git a/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/FraudDetectionAi.java b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/FraudDetectionAi.java new file mode 100644 index 000000000..4b82f8054 --- /dev/null +++ b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/FraudDetectionAi.java @@ -0,0 +1,33 @@ +package io.quarkiverse.langchain4j.sample; + +import java.time.temporal.ChronoUnit; + +import org.eclipse.microprofile.faulttolerance.Timeout; + +import dev.langchain4j.service.SystemMessage; +import dev.langchain4j.service.UserMessage; +import io.quarkiverse.langchain4j.RegisterAiService; + +@RegisterAiService(retrievalAugmentor = FraudDetectionRetrievalAugmentor.class) +public interface FraudDetectionAi { + + @SystemMessage(""" + You are a bank account fraud detection AI. You have to detect frauds in transactions. + """) + @UserMessage(""" + Your task is to detect whether a fraud was committed for the customer. + + Answer with a **single** JSON document containing: + - the customer name in the 'customer-name' key + - the transaction limit in the 'transaction-limit' key + - the computed sum of all transactions committed during the last 15 minutes in the 'total' key + - the 'fraud' key set to true if the computed sum of all transactions is greater than the transaction limit + - the 'transactions' key containing an array of JSON objects. Each object must have transaction 'amount', 'city' and formatted 'time' keys. + - the 'explanation' key containing an explanation of your answer. + - the 'email' key containing the customer email if the fraud was detected. + + Your response must be just the raw JSON document, without ```json, ``` or anything else. Do not use null JSON properties. + """) + @Timeout(value = 2, unit = ChronoUnit.MINUTES) + String detectAmountFraudForCustomer(); +} diff --git a/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/FraudDetectionContentRetriever.java b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/FraudDetectionContentRetriever.java new file mode 100644 index 000000000..66c1f36e7 --- /dev/null +++ b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/FraudDetectionContentRetriever.java @@ -0,0 +1,51 @@ +package io.quarkiverse.langchain4j.sample; + +import java.time.ZoneOffset; +import java.util.List; + +import org.jboss.logging.Logger; + +import dev.langchain4j.rag.content.Content; +import dev.langchain4j.rag.content.retriever.ContentRetriever; +import dev.langchain4j.rag.query.Query; +import io.quarkiverse.langchain4j.sample.SecureMemoryIdProvider.UserNameAndEmail; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.control.ActivateRequestContext; +import jakarta.inject.Inject; + +@ApplicationScoped +public class FraudDetectionContentRetriever implements ContentRetriever { + private static final Logger log = Logger.getLogger(FraudDetectionContentRetriever.class); + + @Inject + TransactionRepository transactionRepository; + + @Inject + CustomerRepository customerRepository; + + @ActivateRequestContext + @Override + public List retrieve(Query query) { + UserNameAndEmail userNameAndEmail = UserNameAndEmail.fromString((String) query.metadata().chatMemoryId()); + log.infof("Use customer name %s and email %s to retrieve content", userNameAndEmail.getName(), + userNameAndEmail.getEmail()); + + int transactionLimit = customerRepository.getTransactionLimit(userNameAndEmail.getName(), + userNameAndEmail.getEmail()); + + List transactions = transactionRepository.getTransactionsForCustomer(userNameAndEmail.getName(), + userNameAndEmail.getEmail()); + + JsonArray jsonTransactions = new JsonArray(); + for (Transaction t : transactions) { + jsonTransactions.add(JsonObject.of("customer-name", t.customerName, "customer-email", t.customerEmail, + "transaction-amount", t.amount, "transaction-city", t.city, + "transaction-time-in-seconds-from-the-epoch", t.time.toEpochSecond(ZoneOffset.UTC))); + } + + JsonObject json = JsonObject.of("transaction-limit", transactionLimit, "transactions", jsonTransactions); + return List.of(Content.from(json.toString())); + } +} diff --git a/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/FraudDetectionResource.java b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/FraudDetectionResource.java new file mode 100644 index 000000000..01afe8a47 --- /dev/null +++ b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/FraudDetectionResource.java @@ -0,0 +1,26 @@ +package io.quarkiverse.langchain4j.sample; + +import io.quarkus.security.Authenticated; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/fraud") +@Authenticated +public class FraudDetectionResource { + + private final FraudDetectionAi service; + + public FraudDetectionResource(FraudDetectionAi service) { + this.service = service; + } + + @GET + @Path("/amount") + public String detectBaseOnAmount() { + try { + return service.detectAmountFraudForCustomer(); + } catch (RuntimeException ex) { + throw (ex.getCause() instanceof MissingCustomerException) ? (MissingCustomerException) ex.getCause() : ex; + } + } +} diff --git a/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/FraudDetectionRetrievalAugmentor.java b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/FraudDetectionRetrievalAugmentor.java new file mode 100644 index 000000000..c6e4ddb19 --- /dev/null +++ b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/FraudDetectionRetrievalAugmentor.java @@ -0,0 +1,20 @@ +package io.quarkiverse.langchain4j.sample; + +import java.util.function.Supplier; + +import dev.langchain4j.rag.DefaultRetrievalAugmentor; +import dev.langchain4j.rag.RetrievalAugmentor; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class FraudDetectionRetrievalAugmentor implements Supplier { + + @Inject + FraudDetectionContentRetriever contentRetriever; + + @Override + public RetrievalAugmentor get() { + return DefaultRetrievalAugmentor.builder().contentRetriever(contentRetriever).build(); + } +} diff --git a/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/LoginResource.java b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/LoginResource.java new file mode 100644 index 000000000..abc0c76a0 --- /dev/null +++ b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/LoginResource.java @@ -0,0 +1,33 @@ +package io.quarkiverse.langchain4j.sample; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.oidc.IdToken; +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.security.Authenticated; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; + +/** + * Login resource which returns a fraud detection page to the authenticated user + */ +@Path("/login") +@Authenticated +public class LoginResource { + + @Inject + @IdToken + JsonWebToken idToken; + + @Inject + Template fraudDetection; + + @GET + @Produces("text/html") + public TemplateInstance fraudDetection() { + return fraudDetection.data("name", idToken.getName()); + } +} diff --git a/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/LogoutResource.java b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/LogoutResource.java new file mode 100644 index 000000000..16b481e71 --- /dev/null +++ b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/LogoutResource.java @@ -0,0 +1,29 @@ +package io.quarkiverse.langchain4j.sample; + +import io.quarkus.oidc.OidcSession; +import io.quarkus.security.Authenticated; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; + +/** + * Logout resource + */ +@Path("/logout") +@Authenticated +public class LogoutResource { + + @Inject + OidcSession session; + + @GET + public Response logout(@Context UriInfo uriInfo) { + // remove the local session cookie + session.logout().await().indefinitely(); + // redirect to the login page + return Response.seeOther(uriInfo.getBaseUriBuilder().path("login").build()).build(); + } +} diff --git a/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/MissingCustomerException.java b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/MissingCustomerException.java new file mode 100644 index 000000000..039d5e164 --- /dev/null +++ b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/MissingCustomerException.java @@ -0,0 +1,4 @@ +package io.quarkiverse.langchain4j.sample; + +public class MissingCustomerException extends RuntimeException { +} diff --git a/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/MissingCustomerExceptionMapper.java b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/MissingCustomerExceptionMapper.java new file mode 100644 index 000000000..325da329d --- /dev/null +++ b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/MissingCustomerExceptionMapper.java @@ -0,0 +1,18 @@ +package io.quarkiverse.langchain4j.sample; + +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +@Provider +public class MissingCustomerExceptionMapper implements ExceptionMapper { + @Context + UriInfo uriInfo; + + @Override + public Response toResponse(MissingCustomerException ex) { + return Response.seeOther(uriInfo.getBaseUriBuilder().path("missingCustomer").build()).build(); + } +} diff --git a/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/MissingCustomerResource.java b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/MissingCustomerResource.java new file mode 100644 index 000000000..997e7889d --- /dev/null +++ b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/MissingCustomerResource.java @@ -0,0 +1,32 @@ +package io.quarkiverse.langchain4j.sample; + +import org.eclipse.microprofile.jwt.Claims; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.oidc.IdToken; +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.security.Authenticated; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; + +@Path("/missingCustomer") +@Authenticated +public class MissingCustomerResource { + + @Inject + @IdToken + JsonWebToken idToken; + + @Inject + Template missingCustomer; + + @GET + @Produces("text/html") + public TemplateInstance missingCustomer() { + return missingCustomer.data("given_name", idToken.getClaim("given_name")).data("name", idToken.getName()) + .data("email", idToken.getClaim(Claims.email)); + } +} diff --git a/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/SecureMemoryIdProvider.java b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/SecureMemoryIdProvider.java new file mode 100644 index 000000000..64a9f94ce --- /dev/null +++ b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/SecureMemoryIdProvider.java @@ -0,0 +1,78 @@ +package io.quarkiverse.langchain4j.sample; + +import java.util.Objects; + +import org.eclipse.microprofile.jwt.Claims; +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.jboss.logging.Logger; + +import io.quarkiverse.langchain4j.spi.DefaultMemoryIdProvider; +import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.enterprise.context.ContextNotActiveException; + +public class SecureMemoryIdProvider implements DefaultMemoryIdProvider { + + private static final Logger log = Logger.getLogger(SecureMemoryIdProvider.class); + + @Override + public Object getMemoryId() { + InstanceHandle instance = Arc.container().instance(SecurityIdentity.class); + if (instance.isAvailable()) { + try { + JsonWebToken jwt = (JsonWebToken) instance.get().getPrincipal(); + return new UserNameAndEmail(jwt.getName(), jwt.getClaim(Claims.email)); + } catch (ContextNotActiveException ignored) { + log.info("CDI context is null"); + // this means that the request scope was not active, so we can't provide a value + return null; + } + } + log.info("SecurityIdentity is unavailable"); + return null; + } + + public static class UserNameAndEmail { + private final String name; + private final String email; + + UserNameAndEmail(String name, String email) { + this.name = Objects.requireNonNull(name); + this.email = Objects.requireNonNull(email); + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof UserNameAndEmail)) { + return false; + } + UserNameAndEmail otherMemoryId = (UserNameAndEmail) other; + return name.equals(otherMemoryId.name) && email.equals(otherMemoryId.email); + } + + @Override + public int hashCode() { + return Objects.hash(name, email); + } + + @Override + public String toString() { + return name + ":" + email; + } + + public static UserNameAndEmail fromString(String chatMemoryId) { + // name:email#suffix + String[] nameAndEmail = chatMemoryId.substring(0, chatMemoryId.indexOf('#')).split(":"); + return new UserNameAndEmail(nameAndEmail[0], nameAndEmail[1]); + } + } +} diff --git a/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/Setup.java b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/Setup.java new file mode 100644 index 000000000..2f6517467 --- /dev/null +++ b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/Setup.java @@ -0,0 +1,51 @@ +package io.quarkiverse.langchain4j.sample; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Random; + +import io.quarkus.runtime.StartupEvent; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +@ApplicationScoped +public class Setup { + + public static List CITIES = List.of("Paris", "Lyon", "Marseille", "Bordeaux", "Toulouse", "Nantes", "Brest", + "Clermont-Ferrand", "La Rochelle", "Lille", "Metz", "Strasbourg", "Nancy", "Valence", "Avignon", + "Montpellier", "Nime", "Arles", "Nice", "Cannes"); + + public static String getARandomCity() { + return CITIES.get(new Random().nextInt(CITIES.size())); + } + + @Inject + CustomerConfig config; + + @Transactional + public void init(@Observes StartupEvent ev, CustomerRepository customers, TransactionRepository transactions) { + customers.deleteAll(); + Random random = new Random(); + + var customer = new Customer(); + customer.name = config.name(); + customer.email = config.email(); + customer.transactionLimit = 1000; + customers.persist(customer); + + transactions.deleteAll(); // Delete all transactions + for (int i = 0; i < 5; i++) { + var transaction = new Transaction(); + transaction.customerName = customer.name; + transaction.customerEmail = customer.email; + transaction.amount = random.nextInt(1000) + 1; + transaction.time = LocalDateTime.now().minusMinutes(random.nextInt(20)); + transaction.city = getARandomCity(); + transactions.persist(transaction); + } + + System.out.println("Customer: " + customer.name + " - " + customer.email); + } +} diff --git a/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/Transaction.java b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/Transaction.java new file mode 100644 index 000000000..68637d504 --- /dev/null +++ b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/Transaction.java @@ -0,0 +1,26 @@ +package io.quarkiverse.langchain4j.sample; + +import java.time.LocalDateTime; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +@Entity +public class Transaction { + + @Id + @GeneratedValue + public Long id; + + public double amount; + + public String customerName; + + public String customerEmail; + + public String city; + + public LocalDateTime time; + +} diff --git a/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/TransactionRepository.java b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/TransactionRepository.java new file mode 100644 index 000000000..30132ecff --- /dev/null +++ b/samples/secure-fraud-detection/src/main/java/io/quarkiverse/langchain4j/sample/TransactionRepository.java @@ -0,0 +1,19 @@ +package io.quarkiverse.langchain4j.sample; + +import java.time.LocalDateTime; +import java.util.List; + +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class TransactionRepository implements PanacheRepository { + + /* + * List of transactions during the last 30 minutes. + */ + public List getTransactionsForCustomer(String customerName, String customerEmail) { + return find("customerName = ?1 and customerEmail = ?2 and time > ?3", customerName, customerEmail, + LocalDateTime.now().minusMinutes(30)).list(); + } +} diff --git a/samples/secure-fraud-detection/src/main/resources/META-INF/resources/images/google.png b/samples/secure-fraud-detection/src/main/resources/META-INF/resources/images/google.png new file mode 100644 index 0000000000000000000000000000000000000000..e2503df4e1444b9873941468508ccd199936d6c6 GIT binary patch literal 1768 zcmVP)Jou1o8{pKi)?@R z$HYuUmTlRh$s9G-NtJA%umqQ(h5F#3DAYk8%);GwE*34;-b+=oUs8I%bI*70k8{57 zeCM2U@Do{ic_|ojC2*W#5gR555P0^@-@R?+01Lob1P)Dr!dR-;8{r4Iu_zZ56!86a z`xbD**68SHJ~AQ_>>H8c5eNzjf?BN>^_)&8dU|@$W9>z2Yb!39FQKcm)52~Um7KCr zuh(1TzEc)Kk(k4tuU1dkx?uhS%+e)6qtRe2=6kSo5OaJ$gO^3 z^%EmU2vP@O!}@gSbh-pGh&LVOXaTu3_W9V?-dej3q9`($Os3;_7{Tnk z>NDPb4=R-k;|Z-+i}Ov5$%%=&+hDvD)U6ML=Ox(p}y%2s<8(B{-!2y#IeZ`-MBz-{{7LMTc8M2Ae&fcxx z4Hn2Xw5vrIll8RGTG{0KD?Z2VT{)8E#AkPP;-BJ=nWc|H4#?H>*@4jSv~Q)cdkv;6 zNllaW09)lg*ngS=}QtZ{$3Q#`r~! zpk-+s3ppt&mZ?OAYC@%FbpI@VJ(YG1ipc{QQf~4|?okmL9!Vi4p5kDXCRBR;2@cE1 zwR(0pl!@LK{n`pUq>#QK5KCY1eNw4|)a={0b4&-xeT+rYnPz z1n+X<75JZn2wEw6OMz2Id5nd=CishKlcg(-yPT{lrfPLMoRV(lLgFDfE#mbNhZbFy zHe^2ipx%j{!4Jr`f9n+8>PFaPX~$7%z_F^3G#lGH+9fA&QV^aaV>|n&K3Y5nazNIj?t>D_LK=%jQ(ax1 zbJmMVnm|=xG9|8Z5Tl90nwVEak}TmspfV7*oWo@;$zpiAzYO`wA3!%X7IHy`y6{)? z{$hP`C5`XZBm;v5tap;mN=}|q9l2OGBSri?4v_T>27`tAHg4HS*-2KMJ8$m5-xph6 zPD)BbfGPmv30huzzp8+9z_E>4&xp|k31>`a>SpQ^H2r=3$y7{_Ck`GugsWGtl$R8h z=p~g|*g08MT_ubc=v}OfcHSEouOZzvaOd^ zWaeg;QH$(W&n@9`<68SQvHiv@SLTa@($~$irnA53k`gc1)|wL7P&{d<)%z$Qs~L?( z96Wr8Wl@eDrNyNMzVv0afT{D~f~!Eoho)1UOiN9LtUBgCQ2Iot)o-<@G2nETGnW>4 z>j(Npa5Pa)P%H|M2#+GN)I<;EHK4l2ol}yPSdNMI&JMAO-oes2mYrBxL@%GHo>(S_ zm|q0%2Nl(9kEKxAN|hmoKlgry;5aMm4m7gmc8W5lQhlire&9b-ai9neJ9jz&0000< KMNUMnLSTYO@I>?g literal 0 HcmV?d00001 diff --git a/samples/secure-fraud-detection/src/main/resources/META-INF/resources/index.html b/samples/secure-fraud-detection/src/main/resources/META-INF/resources/index.html new file mode 100644 index 000000000..6fd3636ed --- /dev/null +++ b/samples/secure-fraud-detection/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,133 @@ + + + + + Secure Fraud Detection + + + + + + +
+
+
+

Login

+ + + + +
Login with Google
+ +
+
+
+ +
+ +
+ + + + diff --git a/samples/secure-fraud-detection/src/main/resources/META-INF/services/io.quarkiverse.langchain4j.spi.DefaultMemoryIdProvider b/samples/secure-fraud-detection/src/main/resources/META-INF/services/io.quarkiverse.langchain4j.spi.DefaultMemoryIdProvider new file mode 100644 index 000000000..4a8d26572 --- /dev/null +++ b/samples/secure-fraud-detection/src/main/resources/META-INF/services/io.quarkiverse.langchain4j.spi.DefaultMemoryIdProvider @@ -0,0 +1 @@ +io.quarkiverse.langchain4j.sample.SecureMemoryIdProvider \ No newline at end of file diff --git a/samples/secure-fraud-detection/src/main/resources/application.properties b/samples/secure-fraud-detection/src/main/resources/application.properties new file mode 100644 index 000000000..6c50ae0d2 --- /dev/null +++ b/samples/secure-fraud-detection/src/main/resources/application.properties @@ -0,0 +1,14 @@ +quarkus.langchain4j.openai.timeout=60s +quarkus.langchain4j.openai.chat-model.temperature=0 +quarkus.langchain4j.openai.api-key=${OPENAI_API_KEY} + +quarkus.oidc.provider=google +quarkus.oidc.client-id=${GOOGLE_CLIENT_ID} +quarkus.oidc.credentials.secret=${GOOGLE_CLIENT_SECRET} +quarkus.oidc.authentication.redirect-path=/login + +quarkus.langchain4j.openai.log-requests=true +quarkus.langchain4j.openai.log-responses=true + +customer.name=${name} +customer.email=${email} diff --git a/samples/secure-fraud-detection/src/main/resources/templates/fraudDetection.html b/samples/secure-fraud-detection/src/main/resources/templates/fraudDetection.html new file mode 100644 index 000000000..080a487af --- /dev/null +++ b/samples/secure-fraud-detection/src/main/resources/templates/fraudDetection.html @@ -0,0 +1,18 @@ + + + + +Secure Fraud Detection + + +

Hello {name}, please check fraud occurrences by amount:

+ + + + +
+ Detect fraud +
+ Logout + + diff --git a/samples/secure-fraud-detection/src/main/resources/templates/missingCustomer.html b/samples/secure-fraud-detection/src/main/resources/templates/missingCustomer.html new file mode 100644 index 000000000..12b1e4a8b --- /dev/null +++ b/samples/secure-fraud-detection/src/main/resources/templates/missingCustomer.html @@ -0,0 +1,18 @@ + + + + +Missing Customer + + + {given_name}, please make sure your Google account's full name and email are correctly registered at the startup using -Dname="{name}" and -Demail={email} system properties +

+ + + + +
+ Back to the main page +
+ +