Skip to content

Commit

Permalink
Add a Secure SQL ChatBot demo
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin authored and jmartisk committed Nov 18, 2024
1 parent 106d259 commit 55c9c69
Show file tree
Hide file tree
Showing 34 changed files with 1,482 additions and 1 deletion.
1 change: 1 addition & 0 deletions samples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<module>review-triage</module>
<module>secure-fraud-detection</module>
<module>secure-poem-multiple-models</module>
<module>secure-sql-chatbot</module>
<module>sql-chatbot</module>
</modules>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ 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();
return Response.seeOther(uriInfo.getBaseUriBuilder().build()).build();
}
}
101 changes: 101 additions & 0 deletions samples/secure-sql-chatbot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Secure chatbot using advanced RAG and a SQL database

## Secure Movie Muse

This example demonstrates how to create a secure chatbot with RAG using
`quarkus-langchain4j`. This chatbot internally uses LLM-generated SQL
queries to retrieve the relevant information from a PostgreSQL database.

### Setup

The demo requires that your Google account's full name and email are configured.
You can use system or env properties, see `Running the example` section below.

When the application starts, a registered user, the movie watcher, is allocated a random preferred movie genre .

The setup is defined in the [Setup.java](./src/main/java/io/quarkiverse/langchain4j/samples/chatbot/Setup.java) class.

The registered movie watchers are stored in a PostgreSQL database. When running the demo in dev mode (recommended), the database is automatically created and populated.

The genre preferred by the registered movie watcher is used by a Movie `ContentRetriever` to sort the list of movies.

## Running the example

Users must authenticate with Google before they can start working with the Movie Muse.
They also must register their OpenAI API key.

### Google Authentication

In order 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 `https://localhost:8443/login`.
Google will generate a client id and secret, use them to set `quarkus.oidc.client-id` and `quarkus.oidc.credentials.secret` properties.

### OpenAI API key

You must provide your OpenAI API key:

```
export QUARKUS_LANGCHAIN4J_OPENAI_API_KEY=<your-openai-api-key>
```

Then, simply run the project in Dev mode:

```shell
mvn quarkus:dev -Dname="Firstname Familyname" [email protected]
```

Note, you should use double quotes to register your Google account's full name.

## Using the example

Open your browser and navigate to `https://localhost:8443`, accept the demo certificate.
Now, choose a `Login with Google` option.
Once logged in, click an orange robot icon in the bottom right corner to open a chat window.
Ask questions such as `Give me movies with a rating higher than 8`.

The chatbot is available at the secure `wss:` scheme.

It uses a SQL database with information about movies with their
basic metadata (the database is populated with data from
`src/main/resources/data/movies.csv` at startup). When you ask a question, an
LLM is used to generate SQL queries necessary for answering your question.
Check the application's log, the SQL queries and the retrieved data will be
printed there.

The chatbot will refer to you by your name during the conversation.

## Security Considerations

### HTTPS and WSS

This demo requires the use of secure HTTPS and WSS WebSockets protocols only.

### Chatbot is accessible to authenticated users only

Only users who have authenticated with Google will see a page which contains a chatbot icon.
It eliminates a risk of non-authenticated users attempting to trick LLM.

### CORS

Even though browsers do not enforce Single Origin Policy (SOP) for WebSockets HTTP upgrade requests, enabling
CORS origin check can add an extra protection in combination with verifying the expected authentication credentials.

For example, attackers can set `Origin` themselves but they will not have the HTTPS bound authentication session cookie
which can be used to authenticate a WSS WebSockets upgrade request.
Or if the authenticated user is tricked into visiting an unfriendly web site, then a WSS WebSockets upgrade request will fail
at the Quarkus CORS check level.

### Custom WebSocket ticket system

In addition to requiring authentication over secure HTTPS, in combination with the CORS constraint for
the HTTP upgrade to succeed, this demo shows a simple WebSockets ticket system to verify that the current HTTP upgrade request
was made from the page allocated to authenticated users only.

When the user completes the OpenId Connect Google authentication, a dynamically generated HTML page will contain a WSS HTTP upgrade link
with a randomly generated ticket added at the authentication time, for example: `wss://localhost/chatbot?ticket=random_ticket_value`.

HTTP upgrade checker will ensure that a matching ticket is found before permitting an upgrade.

### User identity is visible to AI services and tools

AI service and tools can access an ID `JsonWebToken` token which represents a successful user authentication and use it to complete its work.
200 changes: 200 additions & 0 deletions samples/secure-sql-chatbot/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-sample-secure-sql-chatbot</artifactId>
<name>Quarkus LangChain4j - Sample - Secure Chatbot &amp; RAG from a SQL database</name>
<version>1.0-SNAPSHOT</version>

<properties>
<compiler-plugin.version>3.13.0</compiler-plugin.version>
<maven.compiler.parameters>true</maven.compiler.parameters>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
<quarkus.platform.version>3.15.1</quarkus.platform.version>
<skipITs>true</skipITs>
<surefire-plugin.version>3.2.5</surefire-plugin.version>
<quarkus-langchain4j.version>0.21.0</quarkus-langchain4j.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>${quarkus.platform.artifact-id}</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-websockets-next</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-qute</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-openai</artifactId>
<version>${quarkus-langchain4j.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-csv</artifactId>
</dependency>


<!-- UI -->
<dependency>
<groupId>io.mvnpm</groupId>
<artifactId>importmap</artifactId>
<version>1.0.11</version>
</dependency>
<dependency>
<groupId>org.mvnpm</groupId>
<artifactId>lit</artifactId>
<version>3.2.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mvnpm</groupId>
<artifactId>wc-chatbot</artifactId>
<version>0.2.0</version>
<scope>runtime</scope>
</dependency>

<!-- Minimal dependencies to constrain the build -->
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-openai-deployment</artifactId>
<version>${quarkus-langchain4j.version}</version>
<scope>test</scope>
<type>pom</type>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
<executions>
<execution>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>

<profiles>
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<systemPropertyVariables>
<native.image.path>
${project.build.directory}/${project.build.finalName}-runner
</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager
</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<quarkus.package.type>native</quarkus.package.type>
</properties>
</profile>

<profile>
<id>mvnpm</id>
<repositories>
<repository>
<id>central</id>
<name>central</name>
<url>https://repo.maven.apache.org/maven2</url>
</repository>
<repository>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>mvnpm.org</id>
<name>mvnpm</name>
<url>https://repo.mvnpm.org/maven2</url>
</repository>
</repositories>
</profile>
</profiles>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.quarkiverse.langchain4j.sample.chatbot;

import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;

import io.quarkus.logging.Log;
import io.quarkus.oidc.IdToken;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;

import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;

@WebSocket(path = "/chatbot")
@Authenticated
public class ChatBotWebSocket {

private final MovieMuse bot;

@Inject
@IdToken
JsonWebToken idToken;

public ChatBotWebSocket(MovieMuse bot) {
this.bot = bot;
}

@OnOpen
public String onOpen() {
return "Hello, " + idToken.getName() + ", I'm MovieMuse, how can I help you?";
}

@OnTextMessage
public String onMessage(String message) {
try {
return idToken.getName() + ", " + bot.chat(message);
} catch (MissingMovieWatcherException ex) {
Log.error(ex);
return """
Sorry, %s, looks like you did not register your name and email correctly.
Please use '-Dname="%s"' (keep double quotes around your name) and '-Demail=%s' system properties at startup
"""
.formatted(idToken.getName(), idToken.getName(), idToken.getClaim(Claims.email));
} catch (Throwable ex) {
Log.error(ex);
return "Sorry, " + idToken.getName()
+ ", an unexpected error occurred, can you please ask your question again ?";
}
}
}
Loading

0 comments on commit 55c9c69

Please sign in to comment.