diff --git a/streaming-chat/.gitignore b/streaming-chat/.gitignore new file mode 100644 index 0000000..a36ad47 --- /dev/null +++ b/streaming-chat/.gitignore @@ -0,0 +1,16 @@ +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + +.DS_Store +.project +.classpath +.settings diff --git a/streaming-chat/.mvn/wrapper/maven-wrapper.properties b/streaming-chat/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..16f1c0a --- /dev/null +++ b/streaming-chat/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar diff --git a/streaming-chat/README.md b/streaming-chat/README.md new file mode 100644 index 0000000..b91d553 --- /dev/null +++ b/streaming-chat/README.md @@ -0,0 +1,118 @@ +# LangChain4j in Jakarta EE and MicroProfile + +This example demonstrates LangChain4J in a Jakarta EE / MicroProfile application on Open Liberty. The application is a chatbot built with LangChain4J and uses Jakarta CDI, Jakarta RESTful Web Services, Jakarta WebSocket, MicroProfile Config, MicroProfile Metrics, and MicroProfile OpenAPI features. The application allows to use models from either Github, Ollama, or Hugging Face. + +## Prerequisites: + +- [Java 21](https://developer.ibm.com/languages/java/semeru-runtimes/downloads) +- Either one of the following model providers: + - Github + - Sign up and sign in to https://github.com. + - Go to your [Settings/Developer Settings/Personal access tokens](https://github.com/settings/personal-access-tokens). + - Generate a new token with the `models` account permission. + - Ollama + - Download and install [Ollama](https://ollama.com/download) + - see the [README.md](https://github.com/ollama/ollama/blob/main/README.md#ollama) + - Pull the following models + - `ollama pull llama3.2` + - Mistral AI + - Sign up and log in to https://console.mistral.ai/home. + - Go to [Your API keys](https://console.mistral.ai/api-keys). + - Create a new key. + - Hugging Face + - Sign up and log in to https://huggingface.co. + - Go to [Access Tokens](https://huggingface.co/settings/tokens). + - Create a new access token with `read` role. + +## Environment Set Up + +To run this example application, navigate to the `streaming-chat` directory: + +``` +cd sample-langchain4j/streaming-chat +``` + +Set the `JAVA_HOME` environment variable: + +``` +export JAVA_HOME= +``` + +Set the `GITHUB_API_KEY` environment variable if using Github. + +``` +unset HUGGING_FACE_API_KEY +unset OLLAMA_BASE_URL +unset MISTRAL_AI_API_KEY +export GITHUB_API_KEY= +``` + +Set the `OLLAMA_BASE_URL` environment variable if using Ollama. Use your Ollama URL if not using the default. + +``` +unset HUGGING_FACE_API_KEY +unset GITHUB_API_KEY +unset MISTRAL_AI_API_KEY +export OLLAMA_BASE_URL=http://localhost:11434 +``` + +Set the `MISTRAL_AI_API_KEY` environment variable if using Mistral AI. + +``` +unset HUGGING_FACE_API_KEY +unset GITHUB_API_KEY +unset OLLAMA_BASE_URL +export MISTRAL_AI_API_KEY= +``` + +Set the `HUGGING_FACE_API_KEY` environment variable if using Hugging Face. + +``` +unset GITHUB_API_KEY +unset OLLAMA_BASE_URL +unset MISTRAL_AI_API_KEY +export HUGGING_FACE_API_KEY= +``` + +## Start the application + +Use the Maven wrapper to start the application by using the [Liberty dev mode](https://openliberty.io/docs/latest/development-mode.html): + +``` +./mvnw liberty:dev +``` + +## Try out the streaming chat application + +- Navigate to http://localhost:9080 +- At the prompt, try the following message examples: + - ``` + What are large language models? + ``` + - ``` + Which are the most used models? + ``` + - ``` + show me the documentation + ``` + +## Running the tests + +Because you started Liberty in dev mode, you can run the provided tests by pressing the `enter/return` key from the command-line session where you started dev mode. + +If the tests pass, you see a similar output to the following example: + +``` +[INFO] ------------------------------------------------------- +[INFO] T E S T S +[INFO] ------------------------------------------------------- +[INFO] Running it.dev.langchan4j.example.StreamingChatServiceIT +[INFO] ... +[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.101 s... +[INFO] +[INFO] Results: +[INFO] +[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 +``` + +When you are done checking out the service, exit dev mode by pressing `Ctrl+C` in the command-line session where you ran Liberty, or by typing `q` and then pressing the `enter/return` key. diff --git a/streaming-chat/mvnw b/streaming-chat/mvnw new file mode 100755 index 0000000..b7f0646 --- /dev/null +++ b/streaming-chat/mvnw @@ -0,0 +1,287 @@ +#!/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.1.1 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="`/usr/libexec/java_home`"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + printf '%s' "$(cd "$basedir"; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname $0)") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) wrapperUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $wrapperUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + QUIET="--quiet" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + QUIET="" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" + fi + [ $? -eq 0 ] || rm -f "$wrapperJarPath" + elif command -v curl > /dev/null; then + QUIET="--silent" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + QUIET="" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L + fi + [ $? -eq 0 ] || rm -f "$wrapperJarPath" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaSource="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=`cygpath --path --windows "$javaSource"` + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/streaming-chat/mvnw.cmd b/streaming-chat/mvnw.cmd new file mode 100644 index 0000000..cba1f04 --- /dev/null +++ b/streaming-chat/mvnw.cmd @@ -0,0 +1,187 @@ +@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.1.1 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/streaming-chat/pom.xml b/streaming-chat/pom.xml new file mode 100644 index 0000000..662d27b --- /dev/null +++ b/streaming-chat/pom.xml @@ -0,0 +1,128 @@ + + + 4.0.0 + + io.openliberty.sample.langchain4j + jakartaee-microprofile-streaming-example + 1.0-SNAPSHOT + war + + + 21 + 21 + UTF-8 + + + + + jakarta.platform + jakarta.jakartaee-api + 10.0.0 + provided + + + org.eclipse.microprofile + microprofile + 7.0 + pom + provided + + + dev.langchain4j + langchain4j + 1.1.0 + + + dev.langchain4j + langchain4j-hugging-face + 1.1.0-beta7 + + + dev.langchain4j + langchain4j-github-models + 1.1.0-beta7 + + + dev.langchain4j + langchain4j-ollama + 1.1.0-rc1 + + + dev.langchain4j + langchain4j-mistral-ai + 1.1.0-rc1 + + + org.slf4j + slf4j-reload4j + 2.0.17 + + + org.slf4j + slf4j-api + 2.0.17 + + + + org.junit.jupiter + junit-jupiter + 5.13.0 + test + + + org.jboss.resteasy + resteasy-client + 6.2.12.Final + test + + + org.jboss.resteasy + resteasy-json-binding-provider + 6.2.12.Final + test + + + org.glassfish + jakarta.json + 2.0.1 + test + + + org.eclipse.jetty.websocket + websocket-jakarta-client + 11.0.25 + test + + + + + ${project.artifactId} + + + + org.apache.maven.plugins + maven-war-plugin + 3.4.0 + + + io.openliberty.tools + liberty-maven-plugin + 3.11.3 + + + + + + io.openliberty.tools + liberty-maven-plugin + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.3 + + + + diff --git a/streaming-chat/src/main/java/io/openliberty/sample/langchain4j/streaming/chat/StreamingChatAgent.java b/streaming-chat/src/main/java/io/openliberty/sample/langchain4j/streaming/chat/StreamingChatAgent.java new file mode 100644 index 0000000..d7b3824 --- /dev/null +++ b/streaming-chat/src/main/java/io/openliberty/sample/langchain4j/streaming/chat/StreamingChatAgent.java @@ -0,0 +1,69 @@ +package io.openliberty.sample.langchain4j.streaming.chat; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import dev.langchain4j.memory.chat.MessageWindowChatMemory; +import dev.langchain4j.model.chat.StreamingChatModel; +import dev.langchain4j.model.chat.response.ChatResponse; +import dev.langchain4j.model.output.FinishReason; +import dev.langchain4j.service.AiServices; +import dev.langchain4j.service.MemoryId; +import dev.langchain4j.service.TokenStream; +import dev.langchain4j.service.UserMessage; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.openliberty.sample.langchain4j.util.ModelBuilder; + +@ApplicationScoped +public class StreamingChatAgent { + + @Inject + private ModelBuilder modelBuilder; + + @Inject + @ConfigProperty(name = "chat.memory.max.messages") + private Integer MAX_MESSAGES; + + @FunctionalInterface + interface PartialResponseHandler { + void onPartialResponse(String token) throws Exception; + } + + interface StreamingAssistant { + TokenStream streamingChat(@MemoryId String sessionId, @UserMessage String userMessage); + } + + private StreamingAssistant assistant = null; + + public StreamingAssistant getStreamingAssistant() throws Exception { + if (assistant == null) { + StreamingChatModel streamingModel = modelBuilder.getStreamingChatModel(); + assistant = AiServices.builder(StreamingAssistant.class) + .streamingChatModel(streamingModel) + .chatMemoryProvider( + sessionId -> MessageWindowChatMemory.withMaxMessages(MAX_MESSAGES)) + .build(); + } + return assistant; + } + + public FinishReason streamingChat(String sessionId, String message, PartialResponseHandler handler) throws Exception { + CompletableFuture future = new CompletableFuture<>(); + getStreamingAssistant().streamingChat(sessionId, message) + .onPartialResponse(token -> { + try { + handler.onPartialResponse(token); + } catch (Throwable t) { + future.completeExceptionally(t); + } + }) + .onCompleteResponse(future::complete) + .onError(future::completeExceptionally) + .start(); + return future.get().finishReason(); + } + +} diff --git a/streaming-chat/src/main/java/io/openliberty/sample/langchain4j/streaming/chat/StreamingChatService.java b/streaming-chat/src/main/java/io/openliberty/sample/langchain4j/streaming/chat/StreamingChatService.java new file mode 100644 index 0000000..de06953 --- /dev/null +++ b/streaming-chat/src/main/java/io/openliberty/sample/langchain4j/streaming/chat/StreamingChatService.java @@ -0,0 +1,74 @@ +package io.openliberty.sample.langchain4j.streaming.chat; + +import java.util.logging.Logger; + +import org.eclipse.microprofile.metrics.annotation.Timed; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.websocket.CloseReason; +import jakarta.websocket.OnClose; +import jakarta.websocket.OnError; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.RemoteEndpoint; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpoint; + +@ApplicationScoped +@ServerEndpoint("/streamingchat") +public class StreamingChatService { + + private static Logger logger = Logger.getLogger(StreamingChatService.class.getName()); + + @Inject + StreamingChatAgent agent = null; + + @OnOpen + public void onOpen(Session session) { + logger.info("Server connected to session: " + session.getId()); + } + + @OnMessage + @Timed(name = "chatProcessingTime", absolute = true, + description = "Time needed chatting to the agent.") + public void onMessage(String message, Session session) throws Exception { + + logger.info("Server received message \"" + message + "\" " + + "from session: " + session.getId()); + + RemoteEndpoint.Basic remote = session.getBasicRemote(); + + try { + String sessionId = session.getId(); + switch (agent.streamingChat(sessionId, message, token -> { + remote.sendText(token); + Thread.sleep(100); + })) { + case STOP: + break; + default: + remote.sendText(" ..."); + } + } catch (Exception e) { + remote.sendText("My failure reason is:\n\n" + e.getMessage()); + } + + remote.sendText(""); + logger.info("Server finished response to session: " + session.getId()); + + } + + @OnClose + public void onClose(Session session, CloseReason closeReason) { + logger.info("Session " + session.getId() + + " was closed with reason " + closeReason.getCloseCode()); + } + + @OnError + public void onError(Session session, Throwable throwable) { + logger.info("WebSocket error for " + session.getId() + " " + + throwable.getMessage()); + } + +} diff --git a/streaming-chat/src/main/java/io/openliberty/sample/langchain4j/util/ModelBuilder.java b/streaming-chat/src/main/java/io/openliberty/sample/langchain4j/util/ModelBuilder.java new file mode 100644 index 0000000..d816a95 --- /dev/null +++ b/streaming-chat/src/main/java/io/openliberty/sample/langchain4j/util/ModelBuilder.java @@ -0,0 +1,121 @@ +package io.openliberty.sample.langchain4j.util; + +import static java.time.Duration.ofSeconds; + +import java.util.logging.Logger; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import dev.langchain4j.model.chat.StreamingChatModel; +import dev.langchain4j.model.github.GitHubModelsStreamingChatModel; +import dev.langchain4j.model.mistralai.MistralAiStreamingChatModel; +import dev.langchain4j.model.ollama.OllamaStreamingChatModel; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class ModelBuilder { + + private static Logger logger = Logger.getLogger(ModelBuilder.class.getName()); + + @Inject + @ConfigProperty(name = "hugging.face.api.key") + private String HUGGING_FACE_API_KEY; + + @Inject + @ConfigProperty(name = "hugging.face.chat.model.id") + private String HUGGING_FACE_CHAT_MODEL_ID; + + @Inject + @ConfigProperty(name = "github.api.key") + private String GITHUB_API_KEY; + + @Inject + @ConfigProperty(name = "github.chat.model.id") + private String GITHUB_CHAT_MODEL_ID; + + @Inject + @ConfigProperty(name = "ollama.base.url") + private String OLLAMA_BASE_URL; + + @Inject + @ConfigProperty(name = "ollama.chat.model.id") + private String OLLAMA_CHAT_MODEL_ID; + + @Inject + @ConfigProperty(name = "mistral.ai.api.key") + private String MISTRAL_AI_API_KEY; + + @Inject + @ConfigProperty(name = "mistral.ai.chat.model.id") + private String MISTRAL_AI_MISTRAL_CHAT_MODEL_ID; + + @Inject + @ConfigProperty(name = "chat.model.timeout") + private Integer TIMEOUT; + + @Inject + @ConfigProperty(name = "chat.model.max.token") + private Integer MAX_NEW_TOKEN; + + @Inject + @ConfigProperty(name = "chat.model.temperature") + private Double TEMPERATURE; + + private StreamingChatModel streamingChatModel = null; + + public boolean usingGithub() { + return GITHUB_API_KEY.startsWith("ghp_") || GITHUB_API_KEY.startsWith("github_pat_"); + } + + public boolean usingOllama() { + return OLLAMA_BASE_URL.startsWith("http"); + } + + public boolean usingMistralAi() { + return MISTRAL_AI_API_KEY.length() > 30; + } + + public boolean usingHuggingFace() { + return HUGGING_FACE_API_KEY.startsWith("hf_"); + } + + public StreamingChatModel getStreamingChatModel() throws Exception { + if (streamingChatModel == null) { + if (usingGithub()) { + streamingChatModel = GitHubModelsStreamingChatModel.builder() + .gitHubToken(GITHUB_API_KEY) + .modelName(GITHUB_CHAT_MODEL_ID) + .timeout(ofSeconds(TIMEOUT)) + .temperature(TEMPERATURE) + .maxTokens(MAX_NEW_TOKEN) + .build(); + logger.info("using Github " + GITHUB_CHAT_MODEL_ID + " streaming chat model for the web"); + } else if (usingOllama()) { + streamingChatModel = OllamaStreamingChatModel.builder() + .baseUrl(OLLAMA_BASE_URL) + .modelName(OLLAMA_CHAT_MODEL_ID) + .timeout(ofSeconds(TIMEOUT)) + .temperature(TEMPERATURE) + .numPredict(MAX_NEW_TOKEN) + .build(); + logger.info("using Ollama " + OLLAMA_CHAT_MODEL_ID + " streaming chat model for the web"); + } else if (usingMistralAi()) { + streamingChatModel = MistralAiStreamingChatModel.builder() + .apiKey(MISTRAL_AI_API_KEY) + .modelName(MISTRAL_AI_MISTRAL_CHAT_MODEL_ID) + .timeout(ofSeconds(TIMEOUT)) + .temperature(TEMPERATURE) + .maxTokens(MAX_NEW_TOKEN) + .build(); + logger.info("using Mistral AI " + MISTRAL_AI_MISTRAL_CHAT_MODEL_ID + " streaming chat model for the web"); + } else if (usingHuggingFace()) { + throw new Exception("LangChain4J Hugging Face APIs do not support streaming chat model"); + } else { + throw new Exception("No available platform to access model"); + } + } + return streamingChatModel; + } + +} diff --git a/streaming-chat/src/main/liberty/config/server.xml b/streaming-chat/src/main/liberty/config/server.xml new file mode 100644 index 0000000..89bb20d --- /dev/null +++ b/streaming-chat/src/main/liberty/config/server.xml @@ -0,0 +1,24 @@ + + + + + jakartaee-10.0 + microprofile-7.0 + cdi + mpConfig + mpMetrics + websocket + + + + + + + + + + + + + + diff --git a/streaming-chat/src/main/resources/META-INF/microprofile-config.properties b/streaming-chat/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 0000000..a558a9e --- /dev/null +++ b/streaming-chat/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,16 @@ +github.api.key=set it by env variable +github.chat.model.id=gpt-4o-mini + +ollama.base.url=set it by env variable +ollama.chat.model.id=llama3.2 + +mistral.ai.api.key=set it by env variable +mistral.ai.chat.model.id=mistral-small-latest + +hugging.face.api.key=set it by env variable +hugging.face.chat.model.id=HuggingFaceH4/zephyr-7b-beta + +chat.model.timeout=120 +chat.model.max.token=200 +chat.model.temperature=1.0 +chat.memory.max.messages=20 diff --git a/streaming-chat/src/main/webapp/WEB-INF/web.xml b/streaming-chat/src/main/webapp/WEB-INF/web.xml new file mode 100755 index 0000000..da7f948 --- /dev/null +++ b/streaming-chat/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,11 @@ + + + Liberty Project + + + streamingChat.html + + + diff --git a/streaming-chat/src/main/webapp/css/styles.css b/streaming-chat/src/main/webapp/css/styles.css new file mode 100644 index 0000000..2e55635 --- /dev/null +++ b/streaming-chat/src/main/webapp/css/styles.css @@ -0,0 +1,544 @@ +@import url("https://fonts.googleapis.com/css?family=Asap:300,400,500"); + +body{ + font-family:Asap; + font-size: 16px; + color:#24243b; + background-color: white; + margin: 0px; +} + +section { + padding-top: 55px; + padding-left: 8%; + padding-right: 8%; + /* font-weight: 400; */ + letter-spacing:0; + text-align:left; +} + +.line { + margin-right: 200px; + height: 1px; + background-color: #C8D3D3; +} + +.headerImage { + background-image: url(/img/header_ufo.png); + background-repeat: no-repeat; + background-position: top 20px right 15px; + height: 103px; + margin-top: -94px; +} + +#whereTo { + padding-bottom: 80px; + width: 50%; +} + +p { + line-height: 22px; + margin-top: 0px; +} +h1 { + font-family:BunueloSemiBold; + font-size: 40px; + font-weight: 400; + letter-spacing:0; + text-align:left; +} +h2 { + font-size: 24px; + font-weight: 400; +} +h4 { + margin-top: 52px; +} +a { + text-decoration: none; +} + +#appIntro { + background-image:linear-gradient(#141427 0%, #2c2e50 100%); + background-size: 100% calc(100% - 70px); + background-repeat: no-repeat; +} + +#titleSection { + color: white; + margin-bottom: 80px; +} + +#appTitle { + font-family:BunueloLight; + font-size:55px; +} + +.headerRow { + height: 100px; + position:relative; + z-index:2; + box-shadow: 0 2px 4px 0 rgba(0,0,0,0.50); +} +.headerRow > div { + display: inline-block; +} + +.collapsibleRow { + transition: border 400ms ease-out, box-shadow 200ms linear; + cursor: pointer; +} +.collapsibleRow:hover .headerTitle { + background-color: #f4f4f4; + transition: background-color 0.1s; +} +.collapsed .collapsibleRow { + box-shadow: none; + border-bottom: 4px solid; +} +.collapsed#healthSection > .headerRow { + border-bottom-color: #D6D9E4; +} +.collapsed#configSection > .headerRow { + border-bottom-color: #F8D7C1; +} +.collapsed#metricsSection > .headerRow { + border-bottom-color: #EEF3C3; +} + +.collapsed .collapsibleContent { /* collapsing animation */ + transition: all 400ms ease-out, opacity 300ms ease-in; +} +.expanded .collapsibleContent { /* expanding animation */ + transition: all 400ms ease-out, opacity 450ms ease-out; +} +.collapsed .collapsibleContent { + opacity: 0; + max-height: 0; + visibility: hidden; +} +.expanded .collapsibleContent { + opacity: 1; + max-height: 1000px; + visibility: visible; +} + +.headerIcon { + width: 160px; + height: 100%; + float: left; + background-color: #E8EAEF; +} +.headerIcon img { + display:block; + margin:auto; + margin-top: 20px; +} + +#healthSection .headerIcon { + background-color: #E8EAEF; +} +#configSection .headerIcon { + background-color: #FDE4D1; +} +#metricsSection .headerIcon { + background-color: #F5F8DA; +} + +.headerTitle { + background-color: white; + color:#5d6a8e; + letter-spacing:0; + text-align:left; + padding-left: 40px; + padding-top: 10px; + width: calc(100% - 200px); /* 160 from icon, 40 from padding */ +} +#healthSection h2 { + color: #5D6A8E; +} +#configSection h2 { + color: #E57000; +} +#metricsSection h2 { + color: #4F6700; +} + +#sysPropTitle { + padding-top: 28px; +} + +.headerTitle > h2 { + font-family: BunueloLight; + font-size:40px; + margin: 0; +} + +.caret { + position: absolute; + right: 45px; + top: 45px; +} + +.collapsed#configSection .caret { + background-image: url("../img/carets/caret_down_orange.svg") +} +.expanded#configSection .caret { + background-image: url("../img/carets/caret_up_orange.svg") +} + +.msSection { + background: white; + box-shadow: 0 2px 4px 0 rgba(63,70,89,0.31); +} + +.sectionContent { + margin-left: 160px; +} + +#messagesTable { + padding-left: 160px; + background: white; +} + +button { + border-radius:100px; + height:44px; + color:#24253a; + text-align:center; + font-family: Asap; + margin-top: 25px; + margin-bottom: 70px; + cursor: pointer; + border: none; +} + +button a { + text-decoration: none; + color:#F4914D; +} + +#guidesButton { + background-color:#abd155; + width:269px; + font-weight: 500; + font-size:16px; + transition: background-color .2s; +} +#guidesButton:hover { + background-color: #C7EE63; +} + +section#openLibertyAndMp { + background:#f4f4f5; + background-size: 100% calc(100% - 70px); + background-repeat: no-repeat; +} + +#healthBox { + text-align: left; + display: table-cell; + vertical-align: middle; + width: 47%; +} + +#healthBox > div { + display: table-cell; + vertical-align: middle; +} + +#healthIcon { + padding-left: 73px; + padding-top: 56px; + padding-bottom: 56px; +} +#healthStatusIcon { + width: 104px; + height: 104px; +} + +#healthText { + padding: 50px; +} + +#serviceStatus { + font-size: 50px; + font-family:BunueloLight; + margin-top: 30px; +} + +#healthNote { + text-align: left; + display: table-cell; + vertical-align: middle; + padding-left: 43px; + line-height: 26px; + width: 53%; +} + +table { + width: 100%; + font-size: 14px; + text-align: left; + border-collapse: collapse; +} + +th { + height: 63px; + padding-left: 41px; + font-size: 16px; +} +tr { + height: 45px; +} +td { + padding-left: 41px; +} +#messagesTable tr:first-child { + background: #D6D9E4; +} +#configTable tr:first-child { + background: #F8D7C1;; +} +#metricsTable tr:first-child { + background: #EEF3C3; +} + +#messagesTable tr:nth-child(2n+3) { + background: #EEEFF3; +} +#configTable tr:nth-child(2n+2) { + background: #FEF8F4; +} +#metricsTable tr:nth-child(2n+2) { + background: #FBFCEE; +} + +#messagesTable .sourceRow, +#healthTable .sourceRow { + border-top: 4px solid #D6D9E4; +} +#messagesTable .sourceRow a, +#healthTable .sourceRow a { + color: #5D6A8E; +} +#configTable .sourceRow { + border-top: 4px solid #F8D7C1; +} +#configTable .sourceRow a { + color: #E57000; +} +#metricsTable .sourceRow { + border-top: 4px solid #EEF3C3; +} +#metricsTable .sourceRow a { + color: #4F6700; +} +.sourceRow a { + font-weight: 500; +} + +#learnMore { + margin-top: 120px; + padding: 0px 200px 100px; +} + +#learnMore > h2 { + color:#5e6b8d; +} + +.bodyFooter { + padding: 5px 8%; + background-repeat: no-repeat; + background-position: top 20px right 110px; + margin-bottom: 40px; + margin-top: 50px; + color: #3F4659; +} + +.bodyFooterLink { + font-family: Asap; + font-weight: 300; + font-size: 14px; + letter-spacing: 0; + height: 60px; + margin-top: 30px; + margin-left: 10px; + margin-right: 10px; + padding-top: 10px; + padding-bottom: 10px; + padding-left: 20px; + padding-right: 20px; + text-align: right; + background: #F9F9F9; +} + +.my_message_container { + font-family: Asap; + font-weight: 300; + font-size: 14px; + letter-spacing: 0; + margin-top: 30px; + margin-right: 130px; + padding-bottom: 5px; + padding-right: 50px; + text-align: right; +} + +.bodyFooterLink > a { + text-decoration: none; + padding: 10px; + color: #96bc32; +} + +#licenseLink { + color: #5E6B8D; + text-align: left; +} + +#footer_text { + margin-top: 4px; + margin-bottom: 4px; + font-size: 16px; +} + +#myMsgLabel { + margin-top: 4px; + margin-bottom: 4px; + font-size: 18px; + margin-right: 10px; +} + +.refreshSection { + padding-top: 20px; + padding-left: 160px; + color: #3A73B4; +} + +label { + margin-right: 30px; +} + +#sendButton { + background-color:#abd155; + width:80px; + height:30px; + font-weight: 500; + font-size:16px; + transition: background-color .2s; + margin-left: 15px; +} + +#sendButton:hover { + background-color: #C7EE63; +} + +.agent-msg { + border: 1px solid black; + border-radius: 20px; + padding-top: 10px; + padding-bottom: 10px; + padding-left: 10px; + padding-right: 10px; +} + +.my-msg { + border-radius: 20px; + padding-top: 10px; + padding-bottom: 10px; + padding-left: 20px; + padding-right: 10px; + margin-left: 60%; + background-color: #F0F0F0; + text-align: left; +} + +.thinking-msg { + border: 1px solid black; + border-radius: 20px; + padding-top: 10px; + padding-bottom: 10px; + padding-left: 10px; + padding-right: 10px; + margin-right: 60%; +} + +.footer_round_btn { + display: inline-block; + width: 20px; + height: 20px; + background-repeat: no-repeat; + background-size: cover; +} + +#footer_github_link { + background-image: url("/img/Footer_GitCat.svg"); + &:hover { + background-image: url("/img/Footer_GitCat_Hover.svg"); + } +} + +#footer_twitter_link { + background-image: url("/img/Footer_TwitterBird.svg"); + &:hover { + background-image: url("/img/Footer_TwitterBird_Hover.svg"); + } +} + +#footer_groupsio_link { + background-image: url("/img/Footer_GroupsIO.svg"); + &:hover { + background-image: url("/img/Footer_GroupsIO_Hover.svg"); + } +} + +#footer_gitter_link { + background-image: url("/img/footer_gitter.svg"); + &:hover { + background-image: url("/img/footer_gitter_hover.svg"); + } +} + +#footer_project_container { + overflow: hidden; + float: left; +} + +#footer_copyright { + font-size: 13px; + padding-left: 30px; + float: left; + clear: left; +} + +.footer_ol_io { + font-size: 15px; + padding-right: 220px; + float: right; +} + +.footer_ol_io_others { + font-size: 15px; + padding-right: 20px; + float: right; +} + +#footer_project { + font-weight: 500; + font-size: 16px; + color: #6A7070; + letter-spacing: 0; + float: left; + clear: left; +} + + +#footer_open_liberty { + width: 100px; + padding: 25px 25px 10px 25px; + background-image: url(/img/small_logo_dark_gray.svg); + background-repeat: no-repeat; + transition: background-image .2s; + float: left; + clear: left; +} diff --git a/streaming-chat/src/main/webapp/favicon.ico b/streaming-chat/src/main/webapp/favicon.ico new file mode 100755 index 0000000..c8652f3 Binary files /dev/null and b/streaming-chat/src/main/webapp/favicon.ico differ diff --git a/streaming-chat/src/main/webapp/fonts/BunueloCleanPro-Light.otf b/streaming-chat/src/main/webapp/fonts/BunueloCleanPro-Light.otf new file mode 100755 index 0000000..bcb8cfb Binary files /dev/null and b/streaming-chat/src/main/webapp/fonts/BunueloCleanPro-Light.otf differ diff --git a/streaming-chat/src/main/webapp/fonts/BunueloCleanPro-SemiBold.otf b/streaming-chat/src/main/webapp/fonts/BunueloCleanPro-SemiBold.otf new file mode 100755 index 0000000..6d85daf Binary files /dev/null and b/streaming-chat/src/main/webapp/fonts/BunueloCleanPro-SemiBold.otf differ diff --git a/streaming-chat/src/main/webapp/img/Footer_GitCat.svg b/streaming-chat/src/main/webapp/img/Footer_GitCat.svg new file mode 100644 index 0000000..325a82d --- /dev/null +++ b/streaming-chat/src/main/webapp/img/Footer_GitCat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/streaming-chat/src/main/webapp/img/Footer_GitCat_Hover.svg b/streaming-chat/src/main/webapp/img/Footer_GitCat_Hover.svg new file mode 100644 index 0000000..dddcf5d --- /dev/null +++ b/streaming-chat/src/main/webapp/img/Footer_GitCat_Hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/streaming-chat/src/main/webapp/img/Footer_GroupsIO.svg b/streaming-chat/src/main/webapp/img/Footer_GroupsIO.svg new file mode 100644 index 0000000..434e3a0 --- /dev/null +++ b/streaming-chat/src/main/webapp/img/Footer_GroupsIO.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/streaming-chat/src/main/webapp/img/Footer_GroupsIO_Hover.svg b/streaming-chat/src/main/webapp/img/Footer_GroupsIO_Hover.svg new file mode 100644 index 0000000..bffd272 --- /dev/null +++ b/streaming-chat/src/main/webapp/img/Footer_GroupsIO_Hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/streaming-chat/src/main/webapp/img/Footer_TwitterBird.svg b/streaming-chat/src/main/webapp/img/Footer_TwitterBird.svg new file mode 100644 index 0000000..897a874 --- /dev/null +++ b/streaming-chat/src/main/webapp/img/Footer_TwitterBird.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/streaming-chat/src/main/webapp/img/Footer_TwitterBird_Hover.svg b/streaming-chat/src/main/webapp/img/Footer_TwitterBird_Hover.svg new file mode 100644 index 0000000..98b1fab --- /dev/null +++ b/streaming-chat/src/main/webapp/img/Footer_TwitterBird_Hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/streaming-chat/src/main/webapp/img/footer_gitter.svg b/streaming-chat/src/main/webapp/img/footer_gitter.svg new file mode 100644 index 0000000..e07d595 --- /dev/null +++ b/streaming-chat/src/main/webapp/img/footer_gitter.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/streaming-chat/src/main/webapp/img/footer_gitter_hover.svg b/streaming-chat/src/main/webapp/img/footer_gitter_hover.svg new file mode 100644 index 0000000..caa06db --- /dev/null +++ b/streaming-chat/src/main/webapp/img/footer_gitter_hover.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/streaming-chat/src/main/webapp/img/footer_main.png b/streaming-chat/src/main/webapp/img/footer_main.png new file mode 100755 index 0000000..1194702 Binary files /dev/null and b/streaming-chat/src/main/webapp/img/footer_main.png differ diff --git a/streaming-chat/src/main/webapp/img/header_ufo.png b/streaming-chat/src/main/webapp/img/header_ufo.png new file mode 100755 index 0000000..b7fce7d Binary files /dev/null and b/streaming-chat/src/main/webapp/img/header_ufo.png differ diff --git a/streaming-chat/src/main/webapp/img/small_logo_dark_gray.svg b/streaming-chat/src/main/webapp/img/small_logo_dark_gray.svg new file mode 100644 index 0000000..fd8501e --- /dev/null +++ b/streaming-chat/src/main/webapp/img/small_logo_dark_gray.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/streaming-chat/src/main/webapp/img/sysProps.svg b/streaming-chat/src/main/webapp/img/sysProps.svg new file mode 100644 index 0000000..3ba129f --- /dev/null +++ b/streaming-chat/src/main/webapp/img/sysProps.svg @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/streaming-chat/src/main/webapp/streamingChat.html b/streaming-chat/src/main/webapp/streamingChat.html new file mode 100755 index 0000000..a9fefe6 --- /dev/null +++ b/streaming-chat/src/main/webapp/streamingChat.html @@ -0,0 +1,76 @@ + + + + + Chat Room - streaming chat example + + + + + + +
+
+

Chat Room - streaming chat example

+
+
+
+
+
+
+ +
+
+

Conversation with an AI Agent

+
+
+
+ + + + + + + + +
Chat MessagesTime
+
+
+
+
+ + + +
+ + + + diff --git a/streaming-chat/src/main/webapp/streamingChat.js b/streaming-chat/src/main/webapp/streamingChat.js new file mode 100644 index 0000000..44371f0 --- /dev/null +++ b/streaming-chat/src/main/webapp/streamingChat.js @@ -0,0 +1,67 @@ + var messagesTableBody = document.getElementById('messagesTableBody'); + var thinkingRow = document.createElement('tr'); + thinkingRow.setAttribute('id', 'thinking'); + thinkingRow.innerHTML = '

thinking...

' + + ''; + + function getTime() { + var now = new Date(); + var hours = now.getHours(); + hours = hours < 10 ? '0' + hours : hours; + var minutes = now.getMinutes(); + minutes = minutes < 10 ? '0' + minutes : minutes; + var seconds = now.getSeconds(); + seconds = seconds < 10 ? '0' + seconds : seconds; + var time = hours + ":" + minutes + ":" + seconds; + return time; + } + + function sendMessage() { + var myMessageRow = document.createElement('tr'); + var myMessage = document.getElementById('myMessage').value; + myMessageRow.innerHTML = '

' + myMessage + '

' + + '' + getTime() + ''; + messagesTableBody.appendChild(myMessageRow); + messagesTableBody.appendChild(thinkingRow); + webSocket.send(myMessage); + document.getElementById('myMessage').value = ""; + document.getElementById('sendButton').disabled = true; + } + + // Getting the used url from browser + var loc = window.location, uri; + if (loc.protocol === "https:") { + uri = "wss:"; + } else { + uri = "ws:"; + } + uri += "//" + loc.host; + uri += "/" + "streamingchat"; + // buildign websocket + const webSocket = new WebSocket(uri); + + webSocket.onopen = function (event) { + console.log(event); + }; + + webSocket.onmessage = function (event) { + var data = event.data; + if (data === "") { // messages are ended with an empty string + document.getElementById('sendButton').disabled = false; + return; + } + if (!thinkingRow.parentNode) { // if a token has been sent already + var existing = messagesTableBody.lastChild.firstChild.firstChild; + existing.innerHTML += data.replaceAll("\n", "
"); + return; + } + messagesTableBody.removeChild(thinkingRow); + var agentMessageRow = document.createElement('tr'); + agentMessageRow.innerHTML = '

' + data + '

' + + '' + getTime() + ''; + messagesTableBody.appendChild(agentMessageRow); + }; + + webSocket.onerror = function (event) { + console.log(event); + }; diff --git a/streaming-chat/src/test/java/io/openliberty/sample/langchain4j/StreamingChatClient.java b/streaming-chat/src/test/java/io/openliberty/sample/langchain4j/StreamingChatClient.java new file mode 100644 index 0000000..fd4621c --- /dev/null +++ b/streaming-chat/src/test/java/io/openliberty/sample/langchain4j/StreamingChatClient.java @@ -0,0 +1,44 @@ +package io.openliberty.sample.langchain4j; + +import java.net.URI; + +import jakarta.websocket.ClientEndpoint; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.OnMessage; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.WebSocketContainer; + +@ClientEndpoint() +public class StreamingChatClient { + + private Session session; + + public StreamingChatClient(URI endpoint) { + try { + WebSocketContainer container = ContainerProvider.getWebSocketContainer(); + container.connectToServer(this, endpoint); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @OnOpen + public void onOpen(Session session) { + this.session = session; + } + + @OnMessage + public void onMessage(String message, Session session) throws Exception { + StreamingChatServiceIT.verify(message); + } + + public void sendMessage(String message) { + session.getAsyncRemote().sendText(message); + } + + public void close() throws Exception { + session.close(); + } + +} diff --git a/streaming-chat/src/test/java/io/openliberty/sample/langchain4j/StreamingChatServiceIT.java b/streaming-chat/src/test/java/io/openliberty/sample/langchain4j/StreamingChatServiceIT.java new file mode 100644 index 0000000..8ea0de5 --- /dev/null +++ b/streaming-chat/src/test/java/io/openliberty/sample/langchain4j/StreamingChatServiceIT.java @@ -0,0 +1,51 @@ +package io.openliberty.sample.langchain4j; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +@TestMethodOrder(OrderAnnotation.class) +public class StreamingChatServiceIT { + + private static StringBuilder builder; + private static CompletableFuture future; + + @Test + public void testChat() throws Exception { + if (Util.usingHuggingFace()) { + return; + } + URI uri = new URI("ws://localhost:9080/streamingchat"); + StreamingChatClient client = new StreamingChatClient(uri); + future = new CompletableFuture<>(); + builder = new StringBuilder(); + client.sendMessage("When was the LangChain4j launched?"); + String message; + try { + message = future.get(20, TimeUnit.SECONDS); + } catch (TimeoutException e) { + message = builder.toString(); + } + client.close(); + assertNotNull(message); + assertTrue(message.contains("2020") || message.contains("2021") || + message.contains("2022") || message.contains("2023"), + message); + } + + public static void verify(String message) { + if (message.equals("")) { + future.complete(builder.toString()); + } + builder.append(message); + } + +} diff --git a/streaming-chat/src/test/java/io/openliberty/sample/langchain4j/Util.java b/streaming-chat/src/test/java/io/openliberty/sample/langchain4j/Util.java new file mode 100644 index 0000000..366a2d9 --- /dev/null +++ b/streaming-chat/src/test/java/io/openliberty/sample/langchain4j/Util.java @@ -0,0 +1,29 @@ +package io.openliberty.sample.langchain4j; + +public class Util { + + private static String hfApiKey = System.getenv("HUGGING_FACE_API_KEY"); + private static String githubApiKey = System.getenv("GITHUB_API_KEY"); + private static String ollamaBaseUrl = System.getenv("OLLAMA_BASE_URL"); + private static String mistralAiApiKey = System.getenv("MISTRAL_AI_API_KEY"); + + public static boolean usingHuggingFace() { + return hfApiKey != null && hfApiKey.startsWith("hf_"); + } + + public static boolean usingGithub() { + return githubApiKey != null && ( + githubApiKey.startsWith("ghp_") || + githubApiKey.startsWith("github_pat_") + ); + } + + public static boolean usingOllama() { + return ollamaBaseUrl != null && ollamaBaseUrl.startsWith("http"); + } + + public static boolean usingMistralAi() { + return mistralAiApiKey != null && mistralAiApiKey.length() > 30; + } + +} diff --git a/streaming-chat/src/test/resources/log4j.properties b/streaming-chat/src/test/resources/log4j.properties new file mode 100644 index 0000000..9ad612e --- /dev/null +++ b/streaming-chat/src/test/resources/log4j.properties @@ -0,0 +1,10 @@ +log4j.rootLogger=INFO, stdout + +log4j.appender=org.apache.log4j.ConsoleAppender +log4j.appender.layout=org.apache.log4j.PatternLayout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%r %p %c %x - %m%n + +log4j.logger.io.openliberty.sample.langchain4j=DEBUG