diff --git a/.gitignore b/.gitignore index 920bc902..b54f4332 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ training/cloud/examples training/instructlab/instructlab vector_dbs/milvus/volumes/milvus/* .idea +target diff --git a/recipes/natural_language_processing/chatbot-java-quarkus-rag/README.md b/recipes/natural_language_processing/chatbot-java-quarkus-rag/README.md new file mode 100644 index 00000000..55ddc33d --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/README.md @@ -0,0 +1,15 @@ +# Java-based chatbot application with SQL-based RAG - Quarkus + +This application implements a chatbot with SQL RAG functionality backed by +Quarkus and its LangChain4j extension. The UI communicates with the backend +application via web sockets and the backend uses the OpenAI API to talk to +the model served by Podman AI Lab. + +The RAG data is stored in a PostgreSQL database. The application always asks +the model in the background to generate a SQL query that yields data +necessary to ask a question. Then, the application executes that query and +passes the result, encoded as a JSON object, to the model along with +the original user prompt. + +Documentation for Quarkus+LangChain4j can be found at +https://docs.quarkiverse.io/quarkus-langchain4j/dev/. diff --git a/recipes/natural_language_processing/chatbot-java-quarkus-rag/ai-lab.yaml b/recipes/natural_language_processing/chatbot-java-quarkus-rag/ai-lab.yaml new file mode 100644 index 00000000..b8900037 --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/ai-lab.yaml @@ -0,0 +1,35 @@ +version: v1.0 +application: + type: language + name: ChatBot_Java_Quarkus_SQL_RAG + description: Chatbot sample with SQL-based RAG based on Quarkus + containers: + - name: llamacpp-server + contextdir: ../../../model_servers/llamacpp_python + containerfile: ./base/Containerfile + model-service: true + backend: + - llama-cpp + arch: + - arm64 + - amd64 + ports: + - 8001 + image: quay.io/ai-lab/llamacpp_python:latest + - name: postgresql-server + contextdir: ../../../relational_dbs/postgresql + containerfile: Containerfile + arch: + - arm64 + - amd64 + ports: + - 5432 + image: quay.io/ai-lab/postgresql:latest + - name: quarkus-chat-app + contextdir: app + containerfile: Containerfile + arch: + - arm64 + - amd64 + ports: + - 8080 diff --git a/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/.mvn/wrapper/maven-wrapper.properties b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..d58dfb70 --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/Containerfile b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/Containerfile new file mode 100644 index 00000000..a246f5ae --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/Containerfile @@ -0,0 +1,12 @@ +FROM registry.access.redhat.com/ubi8/openjdk-21:latest +WORKDIR /deployments +COPY --chown=185 . . +RUN mvn package +EXPOSE 8080 +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/target/quarkus-app/quarkus-run.jar" +# FIXME: the sleep is to allow the postgresql container to start first and +# should be replaced with a mechanism to declare that the application +# container depends on the database container, once something like that is +# supported... +CMD echo "sleeping for 10 seconds..."; sleep 10; /usr/local/s2i/run \ No newline at end of file diff --git a/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/mvnw b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/mvnw new file mode 100755 index 00000000..19529ddf --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/mvnw.cmd b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/mvnw.cmd new file mode 100644 index 00000000..249bdf38 --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/pom.xml b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/pom.xml new file mode 100644 index 00000000..b44689d4 --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/pom.xml @@ -0,0 +1,150 @@ + + + 4.0.0 + + org.example + podman-ai-sample-chatbot-quarkus-rag + Quarkus-based chatbot sample with RAG + 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.quarkiverse.langchain4j + quarkus-langchain4j-openai + ${quarkus-langchain4j.version} + + + io.quarkus + quarkus-jdbc-postgresql + + + io.quarkus + quarkus-hibernate-orm + + + 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.quarkus + quarkus-maven-plugin + ${quarkus.platform.version} + + + + build + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + maven-surefire-plugin + 3.5.0 + + + 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 + + + + + diff --git a/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ChatBotWebSocket.java b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ChatBotWebSocket.java new file mode 100644 index 00000000..74bb1d6c --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ChatBotWebSocket.java @@ -0,0 +1,26 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import io.quarkus.logging.Log; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.smallrye.mutiny.Multi; + +@WebSocket(path = "/chatbot") +public class ChatBotWebSocket { + + private final MovieMuse bot; + + public ChatBotWebSocket(MovieMuse bot) { + this.bot = bot; + } + + @OnTextMessage + public Multi onMessage(String message) { + return bot.chat(message).onFailure().recoverWithItem(t -> { + Log.warn(t); + return "There was an error in communicating with the model server"; + }); + } + +} diff --git a/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/CsvIngestorExample.java b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/CsvIngestorExample.java new file mode 100644 index 00000000..575a541f --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/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/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ImportmapResource.java b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ImportmapResource.java new file mode 100644 index 00000000..88d0ee21 --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/ImportmapResource.java @@ -0,0 +1,51 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +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") +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/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/Movie.java b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/Movie.java new file mode 100644 index 00000000..3d560633 --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/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/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieDatabaseContentRetriever.java b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieDatabaseContentRetriever.java new file mode 100644 index 00000000..deee979b --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieDatabaseContentRetriever.java @@ -0,0 +1,61 @@ +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 jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +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.vertx.core.json.JsonObject; + +@ApplicationScoped +public class MovieDatabaseContentRetriever implements ContentRetriever { + + @Inject + MovieMuseSupport support; + + @Inject + DataSource dataSource; + + @Override + public List retrieve(Query query) { + String question = query.text(); + String sqlQuery = support.createSqlQuery(question, MovieSchemaSupport.getSchemaString()); + if (sqlQuery.contains("```sql")) { // strip the formatting if it's there + sqlQuery = sqlQuery.substring(sqlQuery.indexOf("```sql") + 6, sqlQuery.lastIndexOf("```")); + } + Log.info("Question to answer: " + 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/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieMuse.java b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieMuse.java new file mode 100644 index 00000000..e53ca500 --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieMuse.java @@ -0,0 +1,19 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import dev.langchain4j.service.SystemMessage; +import dev.langchain4j.service.UserMessage; +import io.quarkiverse.langchain4j.RegisterAiService; +import io.smallrye.mutiny.Multi; +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. + """) + Multi chat(@UserMessage String question); +} diff --git a/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieMuseRetrievalAugmentor.java b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieMuseRetrievalAugmentor.java new file mode 100644 index 00000000..bea4e6ab --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieMuseRetrievalAugmentor.java @@ -0,0 +1,42 @@ +package io.quarkiverse.langchain4j.sample.chatbot; + +import java.util.function.Supplier; + +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; + + @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) + .build(); + } +} diff --git a/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieMuseSupport.java b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieMuseSupport.java new file mode 100644 index 00000000..880b9705 --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieMuseSupport.java @@ -0,0 +1,31 @@ +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} + + Answer only with the raw query string and nothing else. + Don't execute the query. + """) + String createSqlQuery(@UserMessage String question, String schemaStr); + +} diff --git a/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieSchemaSupport.java b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieSchemaSupport.java new file mode 100644 index 00000000..5d9beb9e --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/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/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieTableIntegrator.java b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/java/io/quarkiverse/langchain4j/sample/chatbot/MovieTableIntegrator.java new file mode 100644 index 00000000..8ac91037 --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/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/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/META-INF/resources/components/demo-chat.js b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/META-INF/resources/components/demo-chat.js new file mode 100644 index 00000000..1b7823ba --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/META-INF/resources/components/demo-chat.js @@ -0,0 +1,64 @@ +import {css, LitElement} from 'lit'; + +export class DemoChat extends LitElement { + + _stripHtml(html) { + const div = document.createElement("div"); + div.innerHTML = html; + return div.textContent || div.innerText || ""; + } + + connectedCallback() { + const chatBot = document.getElementsByTagName("chat-bot")[0]; + + const protocol = (window.location.protocol === 'https:') ? 'wss' : 'ws'; + const socket = new WebSocket(protocol + '://' + window.location.host + '/chatbot'); + + const that = this; + socket.onmessage = function (event) { + chatBot.hideLastLoading(); + // LLM response + let lastMessage; + if (chatBot.messages.length > 0) { + lastMessage = chatBot.messages[chatBot.messages.length - 1]; + } + if (lastMessage && lastMessage.sender.name === "Bot" && ! lastMessage.loading) { + if (! lastMessage.msg) { + lastMessage.msg = ""; + } + lastMessage.msg += event.data; + let bubbles = chatBot.shadowRoot.querySelectorAll("chat-bubble"); + let bubble = bubbles.item(bubbles.length - 1); + if (lastMessage.message) { + bubble.innerHTML = that._stripHtml(lastMessage.message) + lastMessage.msg; + } else { + bubble.innerHTML = lastMessage.msg; + } + chatBot.body.scrollTo({ top: chatBot.body.scrollHeight, behavior: 'smooth' }) + } else { + chatBot.sendMessage(event.data, { + right: false, + sender: { + name: "Bot" + } + }); + } + } + + chatBot.addEventListener("sent", function (e) { + if (e.detail.message.sender.name !== "Bot") { + // User message + const msg = that._stripHtml(e.detail.message.message); + socket.send(msg); + chatBot.sendMessage("", { + right: false, + loading: true + }); + } + }); + } + + +} + +customElements.define('demo-chat', DemoChat); \ No newline at end of file diff --git a/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/META-INF/resources/components/demo-title.js b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/META-INF/resources/components/demo-title.js new file mode 100644 index 00000000..bf289fe5 --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/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` +
+

Movie Muse

+
+
+ This demo shows how to build a chatbot powered by GPT 3.5 and advanced retrieval augmented generation. + The application ingested a CSV files 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? +
+ +
+ +
+ +
+
    +
  1. The user sends a question.
  2. +
  3. The question and previous chat history go through a query compressing transformer to extract a concise question with all the relevant context.
  4. +
  5. LLM is used to perform the compression.
  6. +
  7. Now we need to generate a SQL query to fetch the relevant data.
  8. +
  9. LLM is asked to generate the SQL query.
  10. +
  11. The content retriever executes the generated SQL query.
  12. +
  13. The fetched data is added back to the original question as a set of JSON objects.
  14. +
  15. The question + fetched data are submitted to the LLM.
  16. +
  17. The LLM's response is sent back to the user.
  18. +
+
+ ` + } +} + +customElements.define('demo-title', DemoTitle); \ No newline at end of file diff --git a/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/META-INF/resources/fonts/red-hat-font.min.css b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/META-INF/resources/fonts/red-hat-font.min.css new file mode 100644 index 00000000..f0301077 --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/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/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/META-INF/resources/images/chatbot-architecture.png b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/META-INF/resources/images/chatbot-architecture.png new file mode 100644 index 00000000..432ddd87 Binary files /dev/null and b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/META-INF/resources/images/chatbot-architecture.png differ diff --git a/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/META-INF/resources/index.html b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/META-INF/resources/index.html new file mode 100644 index 00000000..fe2f79aa --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + MovieMuse + + + + + + + + +
+ + + +
+ + + + \ No newline at end of file diff --git a/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/META-INF/services/org.hibernate.integrator.spi.Integrator b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/META-INF/services/org.hibernate.integrator.spi.Integrator new file mode 100644 index 00000000..bc4bb911 --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/META-INF/services/org.hibernate.integrator.spi.Integrator @@ -0,0 +1 @@ +io.quarkiverse.langchain4j.sample.chatbot.MovieTableIntegrator diff --git a/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/application.properties b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/application.properties new file mode 100644 index 00000000..55bf5b51 --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/application.properties @@ -0,0 +1,14 @@ +quarkus.langchain4j.timeout=120s +quarkus.langchain4j.openai.api-key=NOT-NECESSARY +quarkus.langchain4j.openai.base-url=${MODEL_ENDPOINT:http://0.0.0.0:8001}/v1/ + +# the movie database +%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/rag +%prod.quarkus.datasource.username=user +%prod.quarkus.datasource.password=password +csv.file=src/main/resources/data/movies.csv +quarkus.hibernate-orm.database.generation=drop-and-create + +# to see the LLM requests and responses in the logs +quarkus.langchain4j.log-requests=true +quarkus.langchain4j.log-responses=true \ No newline at end of file diff --git a/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/data/movies.csv b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/src/main/resources/data/movies.csv new file mode 100644 index 00000000..1eff1d75 --- /dev/null +++ b/recipes/natural_language_processing/chatbot-java-quarkus-rag/app/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/relational_dbs/README.md b/relational_dbs/README.md new file mode 100644 index 00000000..3cde0a58 --- /dev/null +++ b/relational_dbs/README.md @@ -0,0 +1,5 @@ +# Directory to store relational_dbs files + +## PostgreSQL +PostgreSQL is currently used by the Quarkus SQL-based RAG recipe +(`recipes/natural_language_processing/chatbot-java-quarkus-rag`). diff --git a/relational_dbs/postgresql/Containerfile b/relational_dbs/postgresql/Containerfile new file mode 100644 index 00000000..d123b27c --- /dev/null +++ b/relational_dbs/postgresql/Containerfile @@ -0,0 +1,4 @@ +FROM docker.io/postgres:17 +ENV POSTGRES_PASSWORD=password +ENV POSTGRES_USER=user +ENV POSTGRES_DB=rag \ No newline at end of file diff --git a/relational_dbs/postgresql/Makefile b/relational_dbs/postgresql/Makefile new file mode 100644 index 00000000..2664e0a2 --- /dev/null +++ b/relational_dbs/postgresql/Makefile @@ -0,0 +1,7 @@ +CONTAINER_TOOL ?= podman +APP ?= postgresql +APPIMAGE ?= quay.io/ai-lab/${APP}:latest + +.PHONY: build +build: + "${CONTAINER_TOOL}" build -f Containerfile -t ${APPIMAGE} .