diff --git a/samples/pom.xml b/samples/pom.xml index 529e06ce6..c2acc9c41 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -20,6 +20,7 @@ review-triage secure-fraud-detection secure-poem-multiple-models + secure-sql-chatbot sql-chatbot 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 index 7db2ad4c9..19660a95f 100644 --- 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 @@ -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(); } } diff --git a/samples/secure-sql-chatbot/README.md b/samples/secure-sql-chatbot/README.md new file mode 100644 index 000000000..358337982 --- /dev/null +++ b/samples/secure-sql-chatbot/README.md @@ -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= +``` + +Then, simply run the project in Dev mode: + +```shell +mvn quarkus:dev -Dname="Firstname Familyname" -Demail=someuser@gmail.com +``` + +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. diff --git a/samples/secure-sql-chatbot/pom.xml b/samples/secure-sql-chatbot/pom.xml new file mode 100644 index 000000000..16c90f548 --- /dev/null +++ b/samples/secure-sql-chatbot/pom.xml @@ -0,0 +1,200 @@ + + + 4.0.0 + + io.quarkiverse.langchain4j + quarkus-langchain4j-sample-secure-sql-chatbot + Quarkus LangChain4j - Sample - Secure Chatbot & RAG from a SQL database + 1.0-SNAPSHOT + + + 3.13.0 + true + 17 + UTF-8 + UTF-8 + quarkus-bom + io.quarkus + 3.15.1 + true + 3.2.5 + 0.21.0 + + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + + io.quarkus + quarkus-rest-jackson + + + io.quarkus + quarkus-websockets-next + + + io.quarkus + quarkus-oidc + + + io.quarkus + quarkus-jdbc-postgresql + + + io.quarkus + quarkus-hibernate-orm + + + io.quarkus + quarkus-hibernate-orm-panache + + + io.quarkus + quarkus-rest-qute + + + io.quarkiverse.langchain4j + quarkus-langchain4j-openai + ${quarkus-langchain4j.version} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-csv + + + + + + io.mvnpm + importmap + 1.0.11 + + + org.mvnpm + lit + 3.2.0 + runtime + + + org.mvnpm + wc-chatbot + 0.2.0 + runtime + + + + + io.quarkiverse.langchain4j + quarkus-langchain4j-openai-deployment + ${quarkus-langchain4j.version} + test + pom + + + * + * + + + + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus.platform.version} + + + + build + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + maven-surefire-plugin + 3.5.1 + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + native + + + native + + + + + + maven-failsafe-plugin + 3.5.1 + + + + integration-test + verify + + + + + ${project.build.directory}/${project.build.finalName}-runner + + org.jboss.logmanager.LogManager + + ${maven.home} + + + + + + + + + native + + + + + mvnpm + + + central + central + https://repo.maven.apache.org/maven2 + + + + false + + mvnpm.org + mvnpm + https://repo.mvnpm.org/maven2 + + + + + + diff --git a/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ChatBotWebSocket.java b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ChatBotWebSocket.java new file mode 100644 index 000000000..874f627fe --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ChatBotWebSocket.java @@ -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 ?"; + } + } +} diff --git a/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/CsvIngestorExample.java b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/CsvIngestorExample.java new file mode 100644 index 000000000..575a541f4 --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/CsvIngestorExample.java @@ -0,0 +1,45 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import jakarta.enterprise.event.Observes; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.hibernate.StatelessSession; +import org.hibernate.Transaction; + +import com.fasterxml.jackson.databind.MappingIterator; +import com.fasterxml.jackson.dataformat.csv.CsvMapper; +import com.fasterxml.jackson.dataformat.csv.CsvSchema; + +import io.quarkus.logging.Log; +import io.quarkus.runtime.StartupEvent; + +public class CsvIngestorExample { + + private final File file; + private final StatelessSession session; + + public CsvIngestorExample(@ConfigProperty(name = "csv.file") File file, + StatelessSession session) { + this.file = file; + this.session = session; + } + + public void ingest(@Observes StartupEvent event) throws IOException { + try (MappingIterator it = new CsvMapper() + .readerFor(Movie.class) + .with(CsvSchema.emptySchema().withHeader()) + .readValues(file)) { + List movies = it.readAll(); + Transaction transaction = session.beginTransaction(); + for (Movie movie : movies) { + session.insert(movie); + } + transaction.commit(); + Log.infof("Ingested %d movies.%n", movies.size()); + } + } +} diff --git a/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ImportmapResource.java b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ImportmapResource.java new file mode 100644 index 000000000..f8270adee --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ImportmapResource.java @@ -0,0 +1,54 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import io.mvnpm.importmap.model.Imports; +import io.quarkus.runtime.annotations.RegisterForReflection; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; + +import io.mvnpm.importmap.Aggregator; + +/** + * Dynamically create the import map + */ +@ApplicationScoped +@Path("/_importmap") +@RegisterForReflection(targets = {Aggregator.class, Imports.class}) +public class ImportmapResource { + private String importmap; + + // See https://github.com/WICG/import-maps/issues/235 + // This does not seem to be supported by browsers yet... + @GET + @Path("/dynamic.importmap") + @Produces("application/importmap+json") + public String importMap() { + return this.importmap; + } + + @GET + @Path("/dynamic-importmap.js") + @Produces("application/javascript") + public String importMapJson() { + return JAVASCRIPT_CODE.formatted(this.importmap); + } + + @PostConstruct + void init() { + Aggregator aggregator = new Aggregator(); + // Add our own mappings + aggregator.addMapping("icons/", "/icons/"); + aggregator.addMapping("components/", "/components/"); + aggregator.addMapping("fonts/", "/fonts/"); + this.importmap = aggregator.aggregateAsJson(); + } + + private static final String JAVASCRIPT_CODE = """ + const im = document.createElement('script'); + im.type = 'importmap'; + im.textContent = JSON.stringify(%s); + document.currentScript.after(im); + """; +} diff --git a/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/LoginResource.java b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/LoginResource.java new file mode 100644 index 000000000..26427fea0 --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/LoginResource.java @@ -0,0 +1,35 @@ +package io.quarkiverse.langchain4j.sample.chatbot; +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 movie muse page to the authenticated user + */ +@Path("/login") +@Authenticated +public class LoginResource { + @Inject + @IdToken + JsonWebToken idToken; + + @Inject + WebSocketTickets tickets; + + @Inject + Template movieMuse; + + @GET + @Produces("text/html") + public TemplateInstance movieMuse() { + return movieMuse.data("name", idToken.getName()).data("ticket", tickets.getTicket()); + } +} diff --git a/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/LogoutResource.java b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/LogoutResource.java new file mode 100644 index 000000000..ca96e1baa --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/LogoutResource.java @@ -0,0 +1,29 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +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().build()).build(); + } +} diff --git a/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MissingMovieWatcherException.java b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MissingMovieWatcherException.java new file mode 100644 index 000000000..02af0341e --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MissingMovieWatcherException.java @@ -0,0 +1,4 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +public class MissingMovieWatcherException extends RuntimeException { +} diff --git a/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/Movie.java b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/Movie.java new file mode 100644 index 000000000..3d5606335 --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/Movie.java @@ -0,0 +1,135 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import org.hibernate.annotations.Comment; + +import com.fasterxml.jackson.annotation.JsonProperty; + +@Entity +@Table(name = "movie", schema = "public") +public class Movie { + + @Id + @GeneratedValue + private int id; + + @Column(name = "index") + private int index; + + @Column(name = "movie_name") + @JsonProperty("movie_name") + private String movieName; + + @Column(name = "year_of_release") + @JsonProperty("year_of_release") + private int yearOfRelease; + + @Column(name = "category") + private String category; + + @Column(name = "run_time") + @JsonProperty("run_time") + @Comment("in minutes") + private int runTime; + + @Column(name = "genre") + @Comment("this is a comma-separated list of genres") + private String genre; + + @Column(name = "imdb_rating") + @JsonProperty("imdb_rating") + private float imdbRating; + + @Column(name = "votes") + private Integer votes; + + @Column(name = "gross_total") + @JsonProperty("gross_total") + @Comment("in millions of US dollars") + private float grossTotal; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public String getMovieName() { + return movieName; + } + + public void setMovieName(String movieName) { + this.movieName = movieName; + } + + public int getYearOfRelease() { + return yearOfRelease; + } + + public void setYearOfRelease(int yearOfRelease) { + this.yearOfRelease = yearOfRelease; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public int getRunTime() { + return runTime; + } + + public void setRunTime(int runTime) { + this.runTime = runTime; + } + + public String getGenre() { + return genre; + } + + public void setGenre(String genre) { + this.genre = genre; + } + + public float getImdbRating() { + return imdbRating; + } + + public void setImdbRating(float imdbRating) { + this.imdbRating = imdbRating; + } + + public Integer getVotes() { + return votes; + } + + public void setVotes(Integer votes) { + this.votes = votes; + } + + public float getGrossTotal() { + return grossTotal; + } + + public void setGrossTotal(float grossTotal) { + this.grossTotal = grossTotal; + } +} diff --git a/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieDatabaseContentRetriever.java b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieDatabaseContentRetriever.java new file mode 100644 index 000000000..0329a4330 --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieDatabaseContentRetriever.java @@ -0,0 +1,76 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +import javax.sql.DataSource; + +import org.eclipse.microprofile.jwt.Claims; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import dev.langchain4j.rag.content.Content; +import dev.langchain4j.rag.content.retriever.ContentRetriever; +import dev.langchain4j.rag.query.Query; +import io.quarkus.logging.Log; +import io.quarkus.oidc.IdToken; +import io.quarkus.security.Authenticated; +import io.vertx.core.json.JsonObject; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +@Authenticated +public class MovieDatabaseContentRetriever implements ContentRetriever { + + @Inject + MovieMuseSupport support; + + @Inject + DataSource dataSource; + + @Inject + MovieWatcherRepository movieWatchers; + + @Inject + @IdToken + JsonWebToken idToken; + + @Override + public List retrieve(Query query) { + String question = query.text(); + String preferredGenre = movieWatchers.findPreferredGenre(idToken.getName(), idToken.getClaim(Claims.email)); + Log.infof("%s prefers the %s movies", idToken.getName(), preferredGenre); + + String sqlQuery = support.createSqlQuery(question, MovieSchemaSupport.getSchemaString(), preferredGenre); + if (sqlQuery.contains("```sql")) { // strip the formatting if it's there + sqlQuery = sqlQuery.substring(sqlQuery.indexOf("```sql") + 6, sqlQuery.lastIndexOf("```")); + } + Log.infof("%s asked a question %s: ", idToken.getName(), question); + Log.info("Supporting SQL query: " + sqlQuery); + List results = new ArrayList<>(); + Log.info("Retrieved relevant movie data: "); + try (Connection connection = dataSource.getConnection()) { + try (Statement statement = connection.createStatement()) { + try (ResultSet resultSet = statement.executeQuery(sqlQuery)) { + while (resultSet.next()) { + JsonObject json = new JsonObject(); + int columnCount = resultSet.getMetaData().getColumnCount(); + for (int i = 1; i <= columnCount; i++) { + String columnName = resultSet.getMetaData().getColumnName(i); + json.put(columnName, resultSet.getObject(i)); + } + Log.info("Item: " + json); + results.add(Content.from(json.toString())); + } + } + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + return results; + } +} diff --git a/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieMuse.java b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieMuse.java new file mode 100644 index 000000000..a5c682d4d --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieMuse.java @@ -0,0 +1,18 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import dev.langchain4j.service.SystemMessage; +import dev.langchain4j.service.UserMessage; +import io.quarkiverse.langchain4j.RegisterAiService; +import jakarta.enterprise.context.SessionScoped; + +@RegisterAiService(retrievalAugmentor = MovieMuseRetrievalAugmentor.class) +@SessionScoped +public interface MovieMuse { + + @SystemMessage(""" + You are MovieMuse, an AI answering questions about the top 100 movies from IMDB. + Your response must be polite, use the same language as the question, and be relevant to the question. + Don't use any knowledge that is not in the database. + """) + String chat(@UserMessage String question); +} diff --git a/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieMuseRetrievalAugmentor.java b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieMuseRetrievalAugmentor.java new file mode 100644 index 000000000..5882d4196 --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieMuseRetrievalAugmentor.java @@ -0,0 +1,46 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import java.util.function.Supplier; + +import org.eclipse.microprofile.context.ManagedExecutor; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import dev.langchain4j.model.chat.ChatLanguageModel; +import dev.langchain4j.rag.DefaultRetrievalAugmentor; +import dev.langchain4j.rag.RetrievalAugmentor; +import dev.langchain4j.rag.query.transformer.CompressingQueryTransformer; + +@ApplicationScoped +public class MovieMuseRetrievalAugmentor implements Supplier { + + @Inject + ChatLanguageModel model; + + @Inject + MovieDatabaseContentRetriever contentRetriever; + + @Inject + ManagedExecutor executor; + + @Override + public RetrievalAugmentor get() { + return DefaultRetrievalAugmentor.builder() + // The compressing transformer compresses the history of + // chat messages to add the necessary context + // to the current query. For example, if the user asks + // "What is the length of Inception?" and then the next query is + // "When was it released?", then the transformer will compress + // the second query to something like "Release year of Inception", + // which is then used by the content retriever to generate + // the necessary SQL query. + .queryTransformer(new CompressingQueryTransformer(model)) + // The content retriever takes a user's question (including + // the context provided by the transformer in the previous step), asks a LLM + // to generate a SQL query from it, then executes the query and + // provides the resulting data back to the LLM, so it can + // generate a proper text response from it. + .contentRetriever(contentRetriever).executor(executor).build(); + } +} diff --git a/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieMuseSupport.java b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieMuseSupport.java new file mode 100644 index 000000000..b7b64e123 --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieMuseSupport.java @@ -0,0 +1,33 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import jakarta.inject.Singleton; + +import dev.langchain4j.service.SystemMessage; +import dev.langchain4j.service.UserMessage; +import io.quarkiverse.langchain4j.RegisterAiService; + +@RegisterAiService +@Singleton +public interface MovieMuseSupport { + + @SystemMessage(""" + Create a SQL query to retrieve the data necessary to + answer the user's question using data from the database. + The database contains information about top rated movies from IMDB. + The dialect is PostgreSQL and the relevant table is called 'movie'. + Always include `movie_name` in the SELECT clause. + + The user might have not provided the movie name exactly, in that case + try to correct it to the official movie name, or match it using a LIKE clause. + + The table has the following columns: + {schemaStr} + + Sort the list of movies by the genre preferred by the movie watcher: + {preferredGenre} + + Answer only with the query and nothing else. + """) + String createSqlQuery(@UserMessage String question, String schemaStr, String preferredGenre); + +} diff --git a/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieSchemaSupport.java b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieSchemaSupport.java new file mode 100644 index 000000000..5d9beb9e5 --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieSchemaSupport.java @@ -0,0 +1,12 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +public class MovieSchemaSupport { + + public static String getSchemaString() { + String columnsStr = MovieTableIntegrator.schemaStr; + if (columnsStr.isEmpty()) { + throw new IllegalStateException("MovieSchemaSupport#getSchemaString called too early"); + } + return columnsStr; + } +} diff --git a/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieTableIntegrator.java b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieTableIntegrator.java new file mode 100644 index 000000000..8ac91037d --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieTableIntegrator.java @@ -0,0 +1,59 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import java.util.Collection; + +import org.hibernate.boot.Metadata; +import org.hibernate.boot.spi.BootstrapContext; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.mapping.Column; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Table; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; + +public class MovieTableIntegrator implements Integrator { + + static volatile String schemaStr = ""; + + @Override + public void integrate( + Metadata metadata, + BootstrapContext bootstrapContext, + SessionFactoryImplementor sessionFactory) { + PersistentClass moviePC = null; + for (PersistentClass entityBinding : metadata.getEntityBindings()) { + if (Movie.class.getName().equals(entityBinding.getClassName())) { + moviePC = entityBinding; + break; + } + } + if (moviePC == null) { + throw new IllegalStateException("Unable to determine metadata of Movie"); + } + + Table table = moviePC.getTable(); + Collection columns = table.getColumns(); + StringBuilder result = new StringBuilder(); + boolean first = true; + for (Column column : columns) { + if (first) { + first = false; + } else { + result.append("\n - "); + } + StringBuilder sb = new StringBuilder(column.getName()).append(" (").append(column.getSqlType(metadata)) + .append(")"); + if (column.getComment() != null) { + sb.append(" - ").append(column.getComment()); + } + result.append(sb); + } + schemaStr = result.toString(); + } + + @Override + public void disintegrate(SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + + } +} diff --git a/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieWatcher.java b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieWatcher.java new file mode 100644 index 000000000..097022346 --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieWatcher.java @@ -0,0 +1,16 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +@Entity +public class MovieWatcher { + + @Id + @GeneratedValue + public Long id; + public String name; + public String email; + public String preferredGenre; +} diff --git a/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieWatcherConfig.java b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieWatcherConfig.java new file mode 100644 index 000000000..6d74da799 --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieWatcherConfig.java @@ -0,0 +1,11 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import io.smallrye.config.ConfigMapping; + +@ConfigMapping(prefix = "movie.watcher") +public interface MovieWatcherConfig { + + String name(); + + String email(); +} diff --git a/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieWatcherRepository.java b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieWatcherRepository.java new file mode 100644 index 000000000..c187e52ba --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieWatcherRepository.java @@ -0,0 +1,16 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class MovieWatcherRepository implements PanacheRepository { + + public String findPreferredGenre(String movieWatcherName, String movieWatcherEmail) { + MovieWatcher movieWatcher = find("name = ?1 and email = ?2", movieWatcherName, movieWatcherEmail).firstResult(); + if (movieWatcher == null) { + throw new MissingMovieWatcherException(); + } + return movieWatcher.preferredGenre; + } +} diff --git a/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/Setup.java b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/Setup.java new file mode 100644 index 000000000..e0dc4f596 --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/Setup.java @@ -0,0 +1,39 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import java.util.List; +import java.util.Random; + +import io.quarkus.logging.Log; +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 GENRES = List.of("Crime", "Drama", "Thriller", "Action", "Adventure", "Fantasy", + "Horror", "Romance", "Sci-Fi", "Biography", "History", "Mystery", "War", "Animation", + "Comedy"); + + static String getARandomGenre() { + return GENRES.get(new Random().nextInt(GENRES.size())); + } + + @Inject + MovieWatcherConfig config; + + @Transactional + public void init(@Observes StartupEvent ev, MovieWatcherRepository movieWatchers) { + movieWatchers.deleteAll(); + + var movieWatcher = new MovieWatcher(); + movieWatcher.name = config.name(); + movieWatcher.email = config.email(); + movieWatcher.preferredGenre = getARandomGenre(); + movieWatchers.persist(movieWatcher); + + Log.infof("Movie watcher name: %s, email: %s, preferred genre: %s", movieWatcher.name, movieWatcher.email, movieWatcher.preferredGenre); + } +} diff --git a/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/TicketHttpUpgradeCheck.java b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/TicketHttpUpgradeCheck.java new file mode 100644 index 000000000..b3579b541 --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/TicketHttpUpgradeCheck.java @@ -0,0 +1,32 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import io.quarkus.websockets.next.HttpUpgradeCheck; +import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import io.quarkus.logging.Log; + +@ApplicationScoped +public class TicketHttpUpgradeCheck implements HttpUpgradeCheck { + + @Inject + WebSocketTickets tickets; + + @Override + public Uni perform(HttpUpgradeContext context) { + String ticket = context.httpRequest().getParam("ticket"); + if (ticket == null) { + Log.warn("WebSocket ticket is null"); + return Uni.createFrom().item(CheckResult.rejectUpgradeSync(401)); + } + if (tickets.removeTicket(ticket) == null) { + Log.warnf("WebSocket ticket %s is invalid", ticket); + return Uni.createFrom().item(CheckResult.rejectUpgradeSync(401)); + } + // If the ticket was backed up by a ticket cookie then it can be checked here as + // well. + Log.infof("WebSocket ticket %s is valid", ticket); + return Uni.createFrom().item(CheckResult.permitUpgradeSync()); + } + +} diff --git a/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/WebSocketTickets.java b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/WebSocketTickets.java new file mode 100644 index 000000000..3b54c2eac --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/WebSocketTickets.java @@ -0,0 +1,24 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import io.quarkus.logging.Log; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class WebSocketTickets { + ConcurrentHashMap tickets = new ConcurrentHashMap<>(); + + public String getTicket() { + String ticket = UUID.randomUUID().toString(); + tickets.put(ticket, ticket); + Log.infof("New WebSocket ticket %s", ticket); + // This ticket may also be backed up by a ticket cookie + return ticket; + } + + public String removeTicket(String ticket) { + return tickets.get(ticket); + } +} diff --git a/samples/secure-sql-chatbot/src/main/resources/META-INF/resources/components/demo-title.js b/samples/secure-sql-chatbot/src/main/resources/META-INF/resources/components/demo-title.js new file mode 100644 index 000000000..0b61eea50 --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/resources/META-INF/resources/components/demo-title.js @@ -0,0 +1,72 @@ +import {LitElement, html, css} from 'lit'; + +export class DemoTitle extends LitElement { + + static styles = css` + h1 { + font-family: "Red Hat Mono", monospace; + font-size: 60px; + font-style: normal; + font-variant: normal; + font-weight: 700; + line-height: 26.4px; + color: var(--main-highlight-text-color); + } + + .title { + text-align: center; + padding: 1em; + background: var(--main-bg-color); + } + + .explanation { + margin-left: auto; + margin-right: auto; + width: 50%; + text-align: justify; + font-size: 20px; + } + + .explanation img { + max-width: 60%; + display: block; + float:left; + margin-right: 2em; + margin-top: 1em; + } + ` + + render() { + return html` +
+

Secure Movie Muse

+
+
+ This demo shows how to build a chatbot powered by GPT 3.5 and advanced retrieval augmented generation. + The application ingests a CSV file listing the top 100 movies from IMDB. + You can now ask questions about the movies and the application will answer. + For example What is the rating of the movie Inception? or What comedy movies are in the database? +
+ +
+ +
+ +
+
    + You must login with Google first before you can interact with the chatbot +
+
+ +
+ + + + +
Login with Google
+
+ ` + } +} + +customElements.define('demo-title', DemoTitle); \ No newline at end of file diff --git a/samples/secure-sql-chatbot/src/main/resources/META-INF/resources/components/demo-welcome.js b/samples/secure-sql-chatbot/src/main/resources/META-INF/resources/components/demo-welcome.js new file mode 100644 index 000000000..484b6b89a --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/resources/META-INF/resources/components/demo-welcome.js @@ -0,0 +1,67 @@ +import {LitElement, html, css} from 'lit'; + +export class DemoWelcome extends LitElement { + + static styles = css` + h1 { + font-family: "Red Hat Mono", monospace; + font-size: 60px; + font-style: normal; + font-variant: normal; + font-weight: 700; + line-height: 26.4px; + color: var(--main-highlight-text-color); + } + + .title { + text-align: center; + padding: 1em; + background: var(--main-bg-color); + } + + .explanation { + margin-left: auto; + margin-right: auto; + width: 50%; + text-align: justify; + font-size: 20px; + } + + .explanation img { + max-width: 60%; + display: block; + float:left; + margin-right: 2em; + margin-top: 1em; + } + ` + + render() { + return html` +
+

Welcome to Secure Movie Muse

+
+ +
+ +
+ +
+
    +
  1. Click the orange robot in the bottom right corner to open the chat window.
  2. +
  3. Ask a question about movies
  4. +
  5. The question and previous chat history go through a query compressing transformer to extract a concise question with all the relevant context.
  6. +
  7. LLM is used to perform the compression.
  8. +
  9. Now we need to generate a SQL query to fetch the relevant data.
  10. +
  11. LLM is asked to generate the SQL query.
  12. +
  13. The content retriever executes the generated SQL query.
  14. +
  15. The fetched data is added back to the original question as a set of JSON objects.
  16. +
  17. The question + fetched data are submitted to the LLM.
  18. +
  19. The LLM's response is sent back to you.
  20. +
+
+ ` + } +} + +customElements.define('demo-welcome', DemoWelcome); \ No newline at end of file diff --git a/samples/secure-sql-chatbot/src/main/resources/META-INF/resources/fonts/red-hat-font.min.css b/samples/secure-sql-chatbot/src/main/resources/META-INF/resources/fonts/red-hat-font.min.css new file mode 100644 index 000000000..f03010775 --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/resources/META-INF/resources/fonts/red-hat-font.min.css @@ -0,0 +1 @@ +@font-face{font-family:"Red Hat Display";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/redhatdisplay/v7/8vIQ7wUr0m80wwYf0QCXZzYzUoTg8z6hR4jNCH5Z.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:"Red Hat Display";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/redhatdisplay/v7/8vIQ7wUr0m80wwYf0QCXZzYzUoTg_T6hR4jNCA.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:"Red Hat Display";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/redhatdisplay/v7/8vIQ7wUr0m80wwYf0QCXZzYzUoTg8z6hR4jNCH5Z.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:"Red Hat Display";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/redhatdisplay/v7/8vIQ7wUr0m80wwYf0QCXZzYzUoTg_T6hR4jNCA.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:"Red Hat Display";font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/redhatdisplay/v7/8vIQ7wUr0m80wwYf0QCXZzYzUoTg8z6hR4jNCH5Z.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:"Red Hat Display";font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/redhatdisplay/v7/8vIQ7wUr0m80wwYf0QCXZzYzUoTg_T6hR4jNCA.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:"Red Hat Text";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/redhattext/v6/RrQXbohi_ic6B3yVSzGBrMxQZqctMc-JPWCN.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:"Red Hat Text";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/redhattext/v6/RrQXbohi_ic6B3yVSzGBrMxQaKctMc-JPQ.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:"Red Hat Text";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/redhattext/v6/RrQXbohi_ic6B3yVSzGBrMxQZqctMc-JPWCN.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:"Red Hat Text";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/redhattext/v6/RrQXbohi_ic6B3yVSzGBrMxQaKctMc-JPQ.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}/*# sourceMappingURL=red-hat-font.css.map */ \ No newline at end of file diff --git a/samples/secure-sql-chatbot/src/main/resources/META-INF/resources/images/chatbot-architecture.png b/samples/secure-sql-chatbot/src/main/resources/META-INF/resources/images/chatbot-architecture.png new file mode 100644 index 000000000..432ddd870 Binary files /dev/null and b/samples/secure-sql-chatbot/src/main/resources/META-INF/resources/images/chatbot-architecture.png differ diff --git a/samples/secure-sql-chatbot/src/main/resources/META-INF/resources/images/google.png b/samples/secure-sql-chatbot/src/main/resources/META-INF/resources/images/google.png new file mode 100644 index 000000000..e2503df4e Binary files /dev/null and b/samples/secure-sql-chatbot/src/main/resources/META-INF/resources/images/google.png differ diff --git a/samples/secure-sql-chatbot/src/main/resources/META-INF/resources/index.html b/samples/secure-sql-chatbot/src/main/resources/META-INF/resources/index.html new file mode 100644 index 000000000..86fc2a85e --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + MovieMuse + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/secure-sql-chatbot/src/main/resources/META-INF/services/org.hibernate.integrator.spi.Integrator b/samples/secure-sql-chatbot/src/main/resources/META-INF/services/org.hibernate.integrator.spi.Integrator new file mode 100644 index 000000000..bc4bb9112 --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/resources/META-INF/services/org.hibernate.integrator.spi.Integrator @@ -0,0 +1 @@ +io.quarkiverse.langchain4j.sample.chatbot.MovieTableIntegrator diff --git a/samples/secure-sql-chatbot/src/main/resources/application.properties b/samples/secure-sql-chatbot/src/main/resources/application.properties new file mode 100644 index 000000000..d3ca947af --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/resources/application.properties @@ -0,0 +1,30 @@ +# Model configuration +quarkus.langchain4j.openai.timeout=60s +quarkus.langchain4j.openai.chat-model.temperature=0 +quarkus.langchain4j.openai.api-key=${OPENAI_API_KEY} + +# OIDC authentication configuration +quarkus.oidc.provider=google +quarkus.oidc.client-id=${GOOGLE_CLIENT_ID} +quarkus.oidc.credentials.secret=${GOOGLE_CLIENT_SECRET} +quarkus.oidc.authentication.redirect-path=/login + +# HTTPS configuration which also supports a secure `wss:` WebSockets HTTP upgrade request. + +quarkus.http.insecure-requests=disabled +quarkus.http.tls-configuration-name=secure-websockets +quarkus.tls.secure-websockets.key-store.p12.path=server-keystore.p12 +quarkus.tls.secure-websockets.key-store.p12.password=password + +# CORS +# Same origin SOP by default: WSS upgrade request can only be made from the page produced by this endpoint. +quarkus.http.cors=true + + +# Movies data +csv.file=src/main/resources/data/movies.csv +quarkus.hibernate-orm.database.generation=drop-and-create + +# Movie watcher name and email +movie.watcher.name=${name} +movie.watcher.email=${email} \ No newline at end of file diff --git a/samples/secure-sql-chatbot/src/main/resources/data/movies.csv b/samples/secure-sql-chatbot/src/main/resources/data/movies.csv new file mode 100644 index 000000000..1eff1d75e --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/resources/data/movies.csv @@ -0,0 +1,100 @@ +index,movie_name,year_of_release,category,run_time,genre,imdb_rating,votes,gross_total +1,The Godfather,1972,R,175,"Crime, Drama",9.2,1860471,134.97 +2,The Silence of the Lambs,1991,R,118,"Crime, Drama, Thriller",8.6,1435344,130.74 +3,Star Wars: Episode V - The Empire Strikes Back,1980,PG,124,"Action, Adventure, Fantasy",8.7,1294805,290.48 +4,The Shawshank Redemption,1994,R,142,Drama,9.3,2683302,28.34 +5,The Shining,1980,R,146,"Drama, Horror",8.4,1025560,44.02 +6,Casablanca,1942,PG,102,"Drama, Romance, War",8.5,574092,1.02 +7,One Flew Over the Cuckoo's Nest,1975,R,133,Drama,8.7,1010102,112 +8,Indiana Jones and the Raiders of the Lost Ark,1981,PG,115,"Action, Adventure",8.4,969143,248.16 +9,The Lord of the Rings: The Return of the King,2003,PG-13,201,"Action, Adventure, Drama",9,1849082,377.85 +10,Star Wars: Episode IV - A New Hope,1977,PG,121,"Action, Adventure, Fantasy",8.6,1367430,322.74 +11,The Dark Knight,2008,PG-13,152,"Action, Crime, Drama",9,2656768,534.86 +12,The Godfather: Part II,1974,R,202,"Crime, Drama",9,1273349,57.3 +13,Aliens,1986,R,137,"Action, Adventure, Sci-Fi",8.4,720623,85.16 +14,Schindler's List,1993,R,195,"Biography, Drama, History",9,1357621,96.9 +15,Inception,2010,PG-13,148,"Action, Adventure, Sci-Fi",8.8,2356293,292.58 +16,The Lord of the Rings: The Fellowship of the Ring,2001,PG-13,178,"Action, Adventure, Drama",8.8,1878557,315.54 +17,Alien,1979,R,117,"Horror, Sci-Fi",8.5,885635,78.9 +18,Some Like It Hot,1959,Passed,121,"Comedy, Music, Romance",8.2,269346,25 +19,Blade Runner,1982,R,117,"Action, Drama, Sci-Fi",8.1,773425,32.87 +20,Se7en,1995,R,127,"Crime, Drama, Mystery",8.6,1655745,100.13 +21,Apocalypse Now,1979,R,147,"Drama, Mystery, War",8.5,669994,83.47 +22,12 Angry Men,1957,Approved,96,"Crime, Drama",9,792729,4.36 +23,The Lord of the Rings: The Two Towers,2002,PG-13,179,"Action, Adventure, Drama",8.8,1669715,342.55 +24,Terminator 2: Judgment Day,1991,R,137,"Action, Sci-Fi",8.6,1101850,204.84 +25,Star Wars: Episode VI - Return of the Jedi,1983,PG,131,"Action, Adventure, Fantasy",8.3,1056750,309.13 +26,Die Hard,1988,R,132,"Action, Thriller",8.2,887967,83.01 +27,Gone with the Wind,1939,Passed,238,"Drama, Romance, War",8.2,317621,198.68 +28,Taxi Driver,1976,R,114,"Crime, Drama",8.2,836871,28.26 +29,Pulp Fiction,1994,R,154,"Crime, Drama",8.9,2058574,107.93 +30,The Bridge on the River Kwai,1957,PG,161,"Adventure, Drama, War",8.2,222540,44.91 +31,The Lion King,1994,G,88,"Animation, Adventure, Drama",8.5,1060900,422.78 +32,North by Northwest,1959,Approved,136,"Action, Adventure, Mystery",8.3,330232,13.28 +33,Rear Window,1954,PG,112,"Mystery, Thriller",8.5,493926,36.76 +34,Léon: The Professional,1994,R,110,"Action, Crime, Drama",8.5,1164087,19.5 +35,Back to the Future,1985,PG,116,"Adventure, Comedy, Sci-Fi",8.5,1208582,210.61 +36,Citizen Kane,1941,PG,119,"Drama, Mystery",8.3,444359,1.59 +37,Goodfellas,1990,R,145,"Biography, Crime, Drama",8.7,1164128,46.84 +38,Memento,2000,R,113,"Mystery, Thriller",8.4,1241252,25.54 +39,American Beauty,1999,R,122,Drama,8.4,1157536,130.1 +40,As Good as It Gets,1997,PG-13,139,"Comedy, Drama, Romance",7.7,301756,148.48 +41,Forrest Gump,1994,PG-13,142,"Drama, Romance",8.8,2082477,330.25 +42,Singin' in the Rain,1952,G,103,"Comedy, Musical, Romance",8.3,244548,8.82 +43,Braveheart,1995,R,178,"Biography, Drama, History",8.4,1040416,75.6 +44,Saving Private Ryan,1998,R,169,"Drama, War",8.6,1394262,216.54 +45,Rain Man,1988,R,133,Drama,8,517528,178.8 +46,The King's Speech,2010,R,118,"Biography, Drama, History",8,683379,138.8 +47,2001: A Space Odyssey,1968,G,149,"Adventure, Sci-Fi",8.3,671980,56.95 +48,Kill Bill: Vol. 1,2003,R,111,"Action, Crime, Drama",8.2,1119120,70.1 +49,Avanti!,1972,R,144,"Comedy, Romance",7.2,10748,3.3 +50,"The Good, the Bad and the Ugly",1966,Approved,178,"Adventure, Western",8.8,763678,6.1 +51,Amélie,2001,R,122,"Comedy, Romance",8.3,759411,33.23 +52,Modern Times,1936,G,87,"Comedy, Drama, Romance",8.5,244162,0.16 +53,Lost in Translation,2003,R,102,"Comedy, Drama",7.7,458613,44.59 +54,Full Metal Jacket,1987,R,116,"Drama, War",8.3,745546,46.36 +55,Requiem for a Dream,2000,R,102,Drama,8.3,845362,3.64 +56,Fight Club,1999,R,139,Drama,8.8,2128902,37.03 +57,No Country for Old Men,2007,R,122,"Crime, Drama, Thriller",8.2,977336,74.28 +58,Django Unchained,2012,R,165,"Drama, Western",8.4,1557890,162.81 +59,Children of Men,2006,R,109,"Action, Drama, Sci-Fi",7.9,503642,35.55 +60,Ratatouille,2007,G,111,"Animation, Adventure, Comedy",8.1,741322,206.45 +61,The Lives of Others,2006,R,137,"Drama, Mystery, Thriller",8.4,391480,11.29 +62,The Prestige,2006,PG-13,130,"Drama, Mystery, Sci-Fi",8.5,1336235,53.09 +63,V for Vendetta,2005,R,132,"Action, Drama, Sci-Fi",8.2,1125038,70.51 +64,Chinatown,1974,R,130,"Drama, Mystery, Thriller",8.2,329110,8.49 +65,City of God,2002,R,130,"Crime, Drama",8.6,758914,7.56 +66,To Have and Have Not,1944,Passed,100,"Adventure, Comedy, Film-Noir",7.8,35528,1 +67,Fargo,1996,R,98,"Crime, Thriller",8.1,681256,24.61 +68,Life of Pi,2012,PG,127,"Adventure, Drama, Fantasy",7.9,634357,124.99 +69,Slumdog Millionaire,2008,R,120,"Crime, Drama, Romance",8,848344,141.32 +70,Vertigo,1958,PG,128,"Mystery, Romance, Thriller",8.3,404626,3.2 +71,Trainspotting,1996,R,93,Drama,8.1,690138,16.5 +72,Interstellar,2014,PG-13,169,"Adventure, Drama, Sci-Fi",8.6,1835790,188.02 +73,The Thing,1982,R,109,"Horror, Mystery, Sci-Fi",8.2,428474,13.78 +74,The Third Man,1949,Approved,93,"Film-Noir, Mystery, Thriller",8.1,173206,0.45 +75,12 Monkeys,1995,R,129,"Mystery, Sci-Fi, Thriller",8,621205,57.14 +76,Life Is Beautiful,1997,PG-13,116,"Comedy, Drama, Romance",8.6,697226,57.6 +77,The Pianist,2002,R,150,"Biography, Drama, Music",8.5,834842,32.57 +78,Magnolia,1999,R,188,Drama,8,315037,22.46 +79,The Dark Knight Rises,2012,PG-13,164,"Action, Drama",8.4,1708002,448.14 +80,Star Wars: Episode VII - The Force Awakens,2015,PG-13,138,"Action, Adventure, Sci-Fi",7.8,933771,936.66 +81,The Hobbit: The Desolation of Smaug,2013,PG-13,161,"Adventure, Fantasy",7.8,667864,258.37 +82,Mad Max: Fury Road,2015,R,120,"Action, Adventure, Sci-Fi",8.1,1006158,154.06 +83,12 Years a Slave,2013,R,134,"Biography, Drama, History",8.1,703824,56.67 +84,Indiana Jones and the Last Crusade,1989,PG-13,127,"Action, Adventure",8.2,758057,197.17 +85,"O Brother, Where Art Thou?",2000,PG-13,107,"Adventure, Comedy, Crime",7.7,315173,45.51 +86,Inglourious Basterds,2009,R,153,"Adventure, Drama, War",8.3,1453288,120.54 +87,The Departed,2006,R,151,"Crime, Drama, Thriller",8.5,1328252,132.38 +88,A Beautifuld,2001,PG-13,135,"Biography, Drama",8.2,935549,170.74 +89,District 9,2009,R,112,"Action, Sci-Fi, Thriller",7.9,685403,115.65 +90,The Piano,1993,R,121,"Drama, Music, Romance",7.5,89819,40.16 +91,Mystic River,2003,R,138,"Crime, Drama, Mystery",7.9,459918,90.14 +92,The Insider,1999,R,157,"Biography, Drama, Thriller",7.8,172759,28.97 +93,L.A. Confidential,1997,R,138,"Crime, Drama, Mystery",8.2,585555,64.62 +94,Heat,1995,R,170,"Action, Crime, Drama",8.3,658033,67.44 +95,The Usual Suspects,1995,R,106,"Crime, Drama, Mystery",8.5,1087832,23.34 +96,Cool Hand Luke,1967,GP,127,"Crime, Drama",8.1,178888,16.22 +97,Eternal Sunshine of the Spotlessd,2004,R,108,"Drama, Romance, Sci-Fi",8.3,1011004,34.4 +98,City Lights,1931,G,87,"Comedy, Drama, Romance",8.5,186059,0.02 +99,The Matrix,1999,R,136,"Action, Sci-Fi",8.7,1916083,171.48 diff --git a/samples/secure-sql-chatbot/src/main/resources/server-keystore.p12 b/samples/secure-sql-chatbot/src/main/resources/server-keystore.p12 new file mode 100644 index 000000000..6e476f513 Binary files /dev/null and b/samples/secure-sql-chatbot/src/main/resources/server-keystore.p12 differ diff --git a/samples/secure-sql-chatbot/src/main/resources/templates/movieMuse.html b/samples/secure-sql-chatbot/src/main/resources/templates/movieMuse.html new file mode 100644 index 000000000..1d919f6db --- /dev/null +++ b/samples/secure-sql-chatbot/src/main/resources/templates/movieMuse.html @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + MovieMuse + + + + + + +
+

Hi, {name}

+
+ + + +
+ + + + +
+ +
+Logout +
+ + + \ No newline at end of file