Categories
Database Development Linux

Performance-Analyse

Performance-Analyse einer PostgreSQL-Datenbank mit pg_stat_statements

Bei einer Webanwendung mit einer PostgreSQL-Datenbank stießen wir auf Performance-Probleme. Hier beschreibe ich, wie man Performance-Probleme in PostgreSQL mittels der Erweiterung pg_stat_statements analysieren kann. Da die Erweiterung nicht auf der Produktivdatenbank installiert ist, zeige ich, wie man eine lokale Testumgebung aufsetzt.

Überblick der Vorgehensweise

  1. Backup der Produktivdatenbank erstellen
  2. Lokale Testdatenbank mit Docker aufsetzen
  3. pg_stat_statements installieren und konfigurieren
  4. Performance-Analyse durchführen

Vorbereitung

Zuerst prüfen wir die Version der Datenbank, um die passende Docker-Umgebung zu wählen:

SELECT version();
-- PostgreSQL 13.4


Backup erstellen

Das Backup erfolgt mittels Docker, um eine konsistente Umgebung zu gewährleisten. Hier das Script backup.sh:

targetfile=backup_`date +%Y-%m-%d`.sql

docker run --rm \
  --network=host \
  -e PGPASSWORD=PASSWORD \
  postgres:13.4-bullseye \
  pg_dump -h localhost -U myuser myschema > $targetfile

gzip $targetfile

Die Backup-Datei wird auf den lokalen Rechner übertragen.

Lokale Testdatenbank aufsetzen

Zuerst entfernen wir eine eventuell vorhandene alte Testinstanz und starten dann einen neuen Container:

docker stop mydb-performancetest && docker rm mydb-performancetest

docker run -d \
  --name mydb-performancetest \
  -e POSTGRES_USER=myuser \
  -e POSTGRES_PASSWORD=PASSWORD \
  -e POSTGRES_DB=myuser \
  -p 6432:5432 \
  postgres:13.4-bullseye

Verwendet wird Port 6432, um Konflikte mit einer möglicherweise laufenden lokalen PostgreSQL-Instanz zu vermeiden.

Daten importieren

Das Backup wird dann in die lokale Datenbank importiert, über das Script import.sh:

#!/bin/bash

# Check if arguments were passed
if [ $# -eq 0 ]; then
	    echo "Error: No arguments provided"
	        echo "Usage: $0 <filename>"
		    exit 1
fi

# Check if file exists
if [ ! -f "$1" ]; then
	  echo "Error: $1 is not a valid file."
	    exit 1
fi
echo "File found: $1"

importfile=$(readlink -f "$1")
server=localhost
port=6432
username=myuser
password=PASSWORD
databasename=myuser

echo psql -h $server -U $username -d $databasename -f $importfile

docker run --rm \
  --network=host \
  -v $importfile:/script.sql \
  -e PGPASSWORD=$password \
  postgres:13.4-bullseye \
  psql -h $server -p $port -U $username -d $databasename -f script.sql

pg_stat_statements installieren

Um pg_stat_statements zu aktivieren, müssen wir die PostgreSQL-Konfiguration anpassen. Dazu bearbeiten wir die postgresql.conf. Da in dem Postgres Container kein Editor enthalten ist, ex- und importieren wir die Datei, um sie bearbeiten zu können:

# Export & bearbeiten:
docker cp mydb-performancetest:/var/lib/postgresql/data/postgresql.conf .
## shared_preload_libraries = 'pg_stat_statements' in einem Editor hinzufügen

# Re-Import & Neustart:
docker cp postgresql.conf mydb-performancetest:/var/lib/postgresql/data/postgresql.conf
docker restart mydb-performancetest

Danach aktivieren wir die Erweiterung in der Datenbank:

CREATE EXTENSION pg_stat_statements;

Performance-Analyse

Nach dem Ausführen der kritischen Anwendungsfälle können wir die problematischen Queries identifizieren:

SELECT
    query,
    calls,
    total_exec_time,
    min_exec_time,
    max_exec_time,
    mean_exec_time,
    rows,
    (total_exec_time / calls) AS avg_time_per_call
FROM
    pg_stat_statements
ORDER BY
    total_exec_time DESC
LIMIT 10;

Beispiel-Ergebnisse

Hier ein anonymisiertes Beispiel der Ausgabe:

query                              | calls | total_exec_time | min_exec_time | max_exec_time | mean_exec_time | rows  | avg_time_per_call
----------------------------------+-------+----------------+---------------+---------------+----------------+-------+-------------------
SELECT * FROM large_table WHERE... | 1500  | 350000.23      | 150.32       | 890.45        | 233.33         | 15000 | 233.33
UPDATE complex_table SET...       | 500   | 180000.45      | 250.12       | 750.89        | 360.00         | 500   | 360.00

Die Ergebnisse zeigen:

  • Anzahl der Aufrufe (calls)
  • Gesamtausführungszeit in ms (total_exec_time)
  • Minimale und maximale Ausführungszeit (min/max_exec_time)
  • Durchschnittliche Ausführungszeit (mean_exec_time)
  • Anzahl der betroffenen Zeilen (rows)

Besonders interessant sind Queries mit:

  • Hoher Gesamtausführungszeit
  • Hoher durchschnittlicher Ausführungszeit
  • Großer Differenz zwischen minimaler und maximaler Ausführungszeit
  • Unerwartet hoher Anzahl von Aufrufen

Reset

SELECT pg_stat_statements_reset();

Fazit

Mit dieser Methode können wir Performance-Probleme systematisch analysieren, ohne die Produktivumgebung zu beeinflussen.

Die Ergebnisse aus pg_stat_statements geben uns wichtige Hinweise, welche Queries optimiert werden sollten.

Categories
Development

X/Twitter Simple Sample

Simple Sample for X/Twitter access with Python and https://www.tweepy.org

# Python virtual Environment
python3 -m venv twitter
## Activate
source twitter/bin/activate

# Install tweepy
pip install -r requirements.txt

# run sample
python tweepyTest.py
dotenv
tweepy
# Your X API credentials
consumer_key = "SECRET_CREDENTIAL"
consumer_secret = "SECRET_CREDENTIAL"
access_token = "SECRET_CREDENTIAL"
access_token_secret = "SECRET_CREDENTIAL"
bearer_token = "SECRET_CREDENTIAL"
import os
from dotenv import load_dotenv
import tweepy

# load .env-Cile
load_dotenv()

# Create a client instance for Twitter API v2
client = tweepy.Client(bearer_token=os.getenv("bearer_token"))

# Fetch a user by username
username = "IngoKaulbachQS"
user = client.get_user(username=username)

# Access the user information
print(f"User ID: {user.data.id}")
print(f"Username: {user.data.username}")
print(f"Name: {user.data.name}")
print(f"User ID: {user}")
Categories
Development

OpenAI: Text to Speech

Example how to listen to an article from Heise.

MP3 from Text with OpenAI

Create an .env file with an API Key for OpenAI:

OPENAI_API_KEY=mySecretKey

According to OpenAPI documentation this is a sample code to generate Speech from Text:

from pathlib import Path
from openai import OpenAI
client = OpenAI()

speech_file_path = Path(__file__).parent / "speech.mp3"
response = client.audio.speech.create(
  model="tts-1",
  voice="alloy",
  input="Today is a wonderful day to build something people love!"
)

response.stream_to_file(speech_file_path)

To execute this sample I have to install openai first:

pip install openai

To play the mp3-file I have to install ffmpeg first:

sudo apt install ffmpeg

Create mp3 and play it:

# run sample code
python sample.py
# play soundfile
ffplay speech.mp3

Play MP3 with Python

Install pygame:

pip install pygame
from pathlib import Path
import pygame

def play_mp3(file_path):
    pygame.mixer.init()
    pygame.mixer.music.load(file_path)
    pygame.mixer.music.play()

    # Keep the program running while the music plays
    while pygame.mixer.music.get_busy():
        pygame.time.Clock().tick(10)

# Usage
speech_file_path = Path(__file__).parent / "speech.mp3"
play_mp3(speech_file_path)
python playmp3.py

Read Heise Article

from dotenv import load_dotenv
from pathlib import Path
from openai import OpenAI
import selenium.webdriver as webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from bs4 import BeautifulSoup
import pygame

def scrape_website(website):
    print("Launching chrome browser...")
    service = Service()
    options = Options()
    options.headless = True  # Headless-Modus aktivieren, um den Browser unsichtbar zu machen

    driver = webdriver.Chrome(service=service, options=options)

    try:
        driver.get(website)
        print("Page loaded...")
        html = driver.page_source
        return html
    finally:
        driver.quit()


def split_dom_content(dom_content, max_length=6000):
    return [
        dom_content[i : i + max_length] for i in range(0, len(dom_content), max_length)
    ]


def scrape_heise_website(website):
    html  = scrape_website(website)

    # BeautifulSoup zum Parsen des HTML-Codes verwenden
    soup = BeautifulSoup(html, 'html.parser')

    # Artikel-Header und -Inhalt extrahieren
    # Der Header ist oft in einem <h1>-Tag zu finden
    header_title = soup.find('h1', {'class': 'a-article-header__title'}).get_text().strip()
    header_lead  = soup.find('p',  {'class': 'a-article-header__lead'}).get_text().strip()

    # Der eigentliche Artikelinhalt befindet sich oft in einem <div>-Tag mit der Klasse 'article-content'
    article_div = soup.find('div', {'class': 'article-content'})
    paragraphs = article_div.find_all('p') if article_div else []
    # 'redakteurskuerzel' entfernen
    for para in paragraphs:
        spans_to_remove = para.find_all('span', {'class': 'redakteurskuerzel'})
        for span in spans_to_remove:
            span.decompose()  # Entfernt den Tag vollständig aus dem Baum

    article_content = "\n".join([para.get_text().strip() for para in paragraphs])

    return article_content
    # Header und Artikelinhalt ausgeben
    #result = "Header Title:" + header_title + "\nHeader Lead:" + header_lead + "\nContent:" + article_content
    #return result

def article_to_mp3(article_content):
    client = OpenAI()

    speech_file_path = Path(__file__).parent / "speech.mp3"
    response = client.audio.speech.create(
        model="tts-1",
        voice="alloy",
        input=article_content
    )

    response.stream_to_file(speech_file_path)

def play_mp3():
    speech_file_path = Path(__file__).parent / "speech.mp3"
    pygame.mixer.init()
    pygame.mixer.music.load(speech_file_path)
    pygame.mixer.music.play()

    # Keep the program running while the music plays
    while pygame.mixer.music.get_busy():
        pygame.time.Clock().tick(10)


# .env-Datei laden#
load_dotenv()
article_content = scrape_heise_website("https://www.heise.de/news/Streit-ueber-Kosten-Meta-kappt-Leitungen-zur-Telekom-9953162.html")
article_to_mp3(article_content)
play_mp3()
Categories
Development

Python environment variables

For my Python applications I want to use variables from an .env file, so I can store this in a local file for configuration flexibility.

Example:

OPENAI_API_KEY=mySecretKey
import os
from dotenv import load_dotenv

# .env-Datei laden
load_dotenv()
print("OPENAI_API_KEY: " + os.getenv("OPENAI_API_KEY"))
python envexample.py
OPENAI_API_KEY: mySecretKey
Categories
Development

Web Scraping

Web Scraping with Python on my WSL environment.

Python virtual Environment

Open WSL Terminal, switch into the folder for my Web Scraping project and create a virtual environment first.

# Python virtual Environment
## Install
### On Debian/Ubuntu systems, you need to install the python3-venv package
sudo apt install python3.10-venv -y
python3 -m venv ai
## Activate
source ai/bin/activate

Visual Code

Open Visual Code IDE with code .

Change Python Interpreter to the one of virtual Environment

When working in a Terminal window in VS then the virtual environment has also to be activated in this terminal window: source ai/bin/activate

Requirements

I put all required external libraries and their specific versions my project relies on in a seperate file: requirements.txt.
In Python projects this is considered best practice.

selenium

Installation:

pip install -r requirements.txt

Selenium

Selenium is a powerful automation tool for web browsers. It allows you to control web browsers programmatically, simulating user interactions like clicking buttons, filling out forms, and navigating between pages. This makes it ideal for tasks such as web testing, web scraping, and browser automation.

As of Selenium 4.6, Selenium downloads the correct driver for you. You shouldn’t need to do anything. If you are using the latest version of Selenium and you are getting an error, please turn on logging and file a bug report with that information. Quelle

So installation of Google Chrome and Google Chrome Webdriver is not required anymore.
But I had to install some additional libraries on WSL:

sudo apt install libnss3 libgbm1 libasound2

Exkurs: Google Chrome

To find missing libraries I downloaded Google Chrome and tried to start it until all missing libraries were installed.

Page to find download link:

https://googlechromelabs.github.io/chrome-for-testing/#stable

## Google Chrome
wget https://storage.googleapis.com/chrome-for-testing-public/129.0.6668.70/linux64/chrome-linux64.zip
unzip chrome-linux64.zip
mv chrome-linux64 chrome

## Google Chrome Webdriver
wget https://storage.googleapis.com/chrome-for-testing-public/129.0.6668.70/linux64/chromedriver-linux64.zip
unzip chromedriver-linux64.zip
mv chromedriver-linux64 chromedriver
cp chromedriver/chromedriver chrome/chromedriver

cd chrome
./chromedriver

Scrape a single page

import selenium.webdriver as webdriver
from selenium.webdriver.chrome.service import Service

def scrape_website(website):
    print("Launching chrome browser...")
    driver= webdriver.Chrome()

    try:
        driver.get(website)
        print("Page loaded...")
        html = driver.page_source
        return html
    finally:
        driver.quit()

print(scrape_website("https://www.selenium.dev/"))
python scrape.py

Scape a Heise News Article

Extract Article Header, Article Lead and Article itself:

import selenium.webdriver as webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from bs4 import BeautifulSoup

def scrape_website(website):
    print("Launching chrome browser...")
    service = Service()
    options = Options()
    options.headless = True  # Headless-Modus aktivieren, um den Browser unsichtbar zu machen
    driver = webdriver.Chrome(service=service, options=options)

    try:
        driver.get(website)
        print("Page loaded...")
        html = driver.page_source
        return html
    finally:
        driver.quit()


def split_dom_content(dom_content, max_length=6000):
    return [
        dom_content[i : i + max_length] for i in range(0, len(dom_content), max_length)
    ]


def scrape_heise_website(website):
    html  = scrape_website(website)

    # BeautifulSoup zum Parsen des HTML-Codes verwenden
    soup = BeautifulSoup(html, 'html.parser')

    # Artikel-Header und -Inhalt extrahieren
    # Der Header ist oft in einem <h1>-Tag zu finden
    header_title = soup.find('h1', {'class': 'a-article-header__title'}).get_text().strip()
    header_lead  = soup.find('p',  {'class': 'a-article-header__lead'}).get_text().strip()

    # Der eigentliche Artikelinhalt befindet sich oft in einem <div>-Tag mit der Klasse 'article-content'
    article_div = soup.find('div', {'class': 'article-content'})
    paragraphs = article_div.find_all('p') if article_div else []
    # 'redakteurskuerzel' entfernen
    for para in paragraphs:
        spans_to_remove = para.find_all('span', {'class': 'redakteurskuerzel'})
        for span in spans_to_remove:
            span.decompose()  # Entfernt den Tag vollständig aus dem Baum

    article_content = "\n".join([para.get_text().strip() for para in paragraphs])
    
    # Header und Artikelinhalt ausgeben
    result = "Header Title:" + header_title + "\nHeader Lead:" + header_lead + "\nContent:" + article_content
    return result

result = scrape_heise_website("https://www.heise.de/news/Streit-ueber-Kosten-Meta-kappt-Leitungen-zur-Telekom-9953162.html")
print(result)
Categories
Development Java

OpenPDF

Als Alternative zu iText und PDFBox habe ich mir OpenPDF angesehen.

Um mich in die Technik einzuarbeiten habe ich mir ein paar Bilder von Pixabay heruntergeladen, ein Projekt auf GitHub angelegt und dann schrittweise ein PDF mit Bildern erzeugt:

Wie man erkennen kann, passen die Bilder nicht in die Zelle sondern stehen oben über.

Ich vermute einen Bug und habe einen Issue eröffnet.

Categories
Development Java

Scan to PDF

Es muss das Formular mit der Bestell-Nr. 224 (CITES Bescheinigung; im Shop unter Verschiedene Vordrucke, Artenschutz) des Wilhelm Köhler Verlags bedruckt werden.

Wir benötigen zwei PDFs: Eines mit dem Formular und den Formularfeldern und eines nur mit den Formularfeldern, um es auf das Formular zu drucken.

Der Verlag bietet leider keine PDFs an, daher habe ich mir angesehen, ob ich mit vertretbarem Aufwand das Formular nachbauen kann.

Zur PDF Bearbeitung verwende ich Adobe Acrobat Pro.

Der perfekte Scan als Vorlage

Formular als PDF scannen. Anschließend auf einem zweiten Formular ausdrucken und neu scannen, bis Ausdruck und Formular übereinstimmen.

Dabei ist es wichtig, die "Tatsächliche Größe" beim Druck auszuwählen. Voreingestellt ist "Übergroße Seiten verkleinern", was das Druckergebnis verzerrt.

Formularfelder setzen

Das PDF mit dem Scan in Adobe Acrobat Pro öffnen.
Aus den Tools "Formular vorbereiten" auswählen:

Adobe konnte keine Formularfelder identifizieren:

Wir setzen die Felder per Hand, und richten sie anschließend aus, zB mehrere Textfelder am linken bzw. rechten Rand auswählen und in den Eigenschaften unter Position Links, bzw. Rechts, gleich setzen, Breite anpassen etc.

Einige Felder können mehrzeilig sein, das wird in den Eigenschaften unter Optionen aktiviert.

Bei den Kontrollkästchen wird die Randfarbe auf "Keine Farbe" gesetzt und der Kontrollkästchenstil auf "Kreuz".

Alle Formularelemente sind im Text Schriftgrad: "Auto"

Das so bearbeitete PDF wird mit Daten befüllt und von mehreren Mitarbeitern auf Formulare gedruckt, bis die Positionierung der Felder verifiziert ist. Dabei ist wichtig, die "Tatsächliche Größe" beim Druck auszuwählen.

Bei einigen Ausdrucken war grade im unteren Bereich eine leichte Verschiebung zu sehen. Vermutlich war das Formular nicht immer 100%ig grade eingelegt, aber bei ganz genauer Betrachtung stellte sich auch heraus, dass der Scan ganz leicht schief ist.

Scan to PDF

Dass im Hintergrund ein gescanntes Formular verwendet wird, gefällt mir nicht so gut, zB sieht es an einigen Stellen "fleckig" aus, was von dem besonderen Papier des Formulars herrührt. Daher habe ich den Versuch gestartet, aus dem Scan ein PDF zu generieren.

Der erste Versuch schlug grundlegend fehl, da das Programm nicht mit den hochkant stehenden Schriften (unten links, "Bestell-Nr. 224" etc.) zurecht kommt. Daher muss der Scan vorbereitet werden, indem diese Schrift entfernt wird, zumal wir sie auch nicht benötigen.

Scan vorbereiten

Mit dem Tool PDF24 Creator habe ich das Bild aus dem PDF extrahiert und eine TIFF-Datei erhalten.

Die Datei öffne ich mit Paint.net, füge eine weitere Ebene hinzu, die in den Hintergrund kommt, diese wird weiß aufgefüllt.

Die Schrift wird entfernt.

Mit dem Zauberstab-Tool wird so viel wie möglich entfernt, da einiges "Rauschen" im Hintergrund ist. Dazu stelle ich die Toleranz auf einen niedrigen Wert und aktiviere "Globale Auswahl" in der oberen Leiste. Dies ermöglicht es dem Zauberstab, alle passenden Pixel im gesamten Bild auszuwählen, nicht nur zusammenhängende Bereiche.

Als png speichern.

Scan to Docx

In Adobe Acrobat Pro ein PDF aus der png-Datei erstellen.

Aus dem Alle Tools Menu Scan & OCR auswählen. Gescannte Datei verbessern. Und dann Text erkennen -> In dieser Datei.

Speichern unter: Konvertieren in "Windows Word (*.docx)"

Docx to PDF

In Adobe Acrobat Pro ein PDF aus der docx-Datei erstellen.

Der erste Versuch sah bei mir schon ganz gut aus:

Fazit

Leider war es dann so, dass ich bei späteren Versuchen schlechtere Ergebnisse erzielt habe. Warum die Ergebnisse teilweise sehr voneinander abweichen konnte ich nicht herausfinden.

Das Vorgehen, um von einem analogen Formular einen digitalen Klon zu erzeugen, funktioniert grundsätzlich. Allerdings war es auch sehr zeitaufwändig und am Ende frustrierend, da ich nicht nachvollziehen konnte, warum auf einmal die PDFs immer schlechter wurden.

Jetzt ist erstmal Ende meiner Motivation und des Budgets.

Categories
Development Java

PDFBox

Für ein Projekt musste ich ein PDF erzeugen und habe das dann mit PDFBox umgesetzt.

Um mich in die Technik einzuarbeiten habe ich mir ein paar Bilder von Pixabay heruntergeladen, ein Projekt auf GitHub angelegt und dann schrittweise ein PDF mit Bildern erzeugt:

Categories
Development

Maven update versions

In my projects I use Maven as dependency managment system.

In the past I updated versions of libraries manually in pom.xml. But as projects grow, this becomes more and more annoying and time consuming. So I decided to give it a try to do this automatically.

Add Plugins

Add the Enforcer Maven Plugin and the Versions Maven Plugin:

<build>        
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-enforcer-plugin</artifactId>
      <version>3.5.0</version>
      <executions>
        <execution>
          <id>enforce-maven</id>
          <goals>
            <goal>enforce</goal>
          </goals>
          <configuration>
            <rules>
              <requireMavenVersion>
                <version>3.9</version>
              </requireMavenVersion>
            </rules>    
          </configuration>
        </execution>
      </executions>
    </plugin>
    <plugin>
      <groupId>org.codehaus.mojo</groupId>
      <artifactId>versions-maven-plugin</artifactId>
      <version>2.16.2</version>
      <configuration>
        <generateBackupPoms>false</generateBackupPoms>
      </configuration>
    </plugin>
  </plugins>
</build>

Preparation

I could not update the version of the plugins when the version information is hardcoded in the plugin section. So I used properties for the plugin versions.

<properties>
  <enforcer-plugin.version>3.5.0</enforcer-plugin.version>
  <versions-plugin.version>2.16.2</versions-plugin.version>
</properties>

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-enforcer-plugin</artifactId>
      <version>${enforcer-plugin.version}</version>
      <executions>
        <execution>
          <id>enforce-maven</id>
          <goals>
            <goal>enforce</goal>
          </goals>
          <configuration>
            <rules>
              <requireMavenVersion>
                <version>3.9</version>
              </requireMavenVersion>
            </rules>    
          </configuration>
        </execution>
      </executions>
    </plugin>
    <plugin>
      <groupId>org.codehaus.mojo</groupId>
      <artifactId>versions-maven-plugin</artifactId>
      <version>${versions-plugin.version}</version>
      <configuration>
        <generateBackupPoms>false</generateBackupPoms>
      </configuration>
    </plugin>
  </plugins>
</build>

Check updates

Check for newer versions in properties, dependencies and plugins:

mvn versions:display-property-updates
mvn versions:display-dependency-updates
mvn versions:display-plugin-updates

Update

Update everything:

mvn versions:update-properties
mvn versions:use-latest-releases
Categories
AI Development Java

GPTs with Quarkus

We will use LangChain within Quarkus to connect to some GPTs. Quarkus uses the LangChain4j library.

Quarkus LangChain Extensions

What extensions Quarkus provides?

./mvnw quarkus:list-extensions | grep langchain
[INFO]   quarkus-langchain4j-azure-openai                   LangChain4j Azure OpenAI
[INFO]   quarkus-langchain4j-chroma                         LangChain4j Chroma
[INFO]   quarkus-langchain4j-core                           LangChain4j
[INFO]   quarkus-langchain4j-easy-rag                       LangChain4j Easy RAG
[INFO]   quarkus-langchain4j-hugging-face                   LangChain4j Hugging Face
[INFO]   quarkus-langchain4j-milvus                         LangChain4j Milvus embedding store
[INFO]   quarkus-langchain4j-mistral-ai                     LangChain4j Mistral AI
[INFO]   quarkus-langchain4j-ollama                         LangChain4j Ollama
[INFO]   quarkus-langchain4j-openai                         LangChain4j OpenAI
[INFO]   quarkus-langchain4j-pgvector                       Quarkus LangChain4j pgvector embedding store
[INFO]   quarkus-langchain4j-pinecone                       LangChain4j Pinecone embedding store
[INFO]   quarkus-langchain4j-redis                          LangChain4j Redis embedding store

Chat window

We will reuse our chat window from the last post,

src/main/resources/META-INF/resources/chat.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket Chat Example</title>
    <style>
        #chat {
            resize: none;
            overflow: hidden;
            min-width: 70%;
            min-height: 300px;
            max-height: 300px;
            overflow-y: scroll;
        }
        #msg {
            min-width: 40%;
        }
    </style>
</head>
<body>
    <h1>WebSocket Chat Example</h1>
    <p id="message">Connecting...</p>
    <br/>
    <div class="container">
        <br/>
        <div class="row">
            <textarea id="chat"></textarea>
        </div>
        <div class="row">
            <input id="msg" type="text" placeholder="enter your message">
            <button id="send" type="button" disabled>send</button>
        </div>
    
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <script>
        var connected = false;
        var socket;

        $( document ).ready(function() {
            connect();
            $("#send").click(sendMessage);

            $("#name").keypress(function(event){
                if(event.keyCode == 13 || event.which == 13) {
                    connect();
                }
            });

            $("#msg").keypress(function(event) {
                if(event.keyCode == 13 || event.which == 13) {
                    sendMessage();
                }
            });

            $("#chat").change(function() {
                scrollToBottom();
            });

            $("#name").focus();
        });

        var connect = function() {
            if (! connected) {
                socket = new WebSocket('wss://' + location.host + '/chatsocket');
                socket.onopen = function(m) {
                    connected = true;
                    console.log("Connected to the web socket");
                    $("#send").attr("disabled", false);
                    $("#connect").attr("disabled", true);
                    $("#name").attr("disabled", true);
                    $("#chat").append("[Chatbot] Howdy, how may I help you? \n");
                    $("#msg").focus();
                    $("#message").text('Connected');
                };
                socket.onmessage = function(m) {
                    console.log("Got message: " + m.data);
                    $("#message").text('Received: ' + m.data);
                    $("#chat").append("[Chatbot] " + m.data + "\n");
                    scrollToBottom();
                };
                socket.onclose = function(event) {
                    console.log("Disconnected");
                    $("#message").text('Disconnected');
                    $("#chat").append("[Chatbot] Disconnected" + "\n");
                    scrollToBottom();
                };
                socket.onerror = function(error) {
                    console.log("Error: " + error.message);
                    $("#message").text('Error: ' + error.message);
                    $("#chat").append("[Chatbot] Error: " + error.message + "\n");
                    scrollToBottom();
                };
            }
        };

        var sendMessage = function() {
            if (connected) {
                var value = $("#msg").val();
                console.log("Sending " + value);
                $("#chat").append("[You] " + value + "\n")
                socket.send(value);
                $("#msg").val("");
            }
        };

        var scrollToBottom = function () {
            $('#chat').scrollTop($('#chat')[0].scrollHeight);
        };

    </script>
</body>
</html>
package org.acme;

import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
import jakarta.inject.Inject;

@WebSocket(path = "/chatsocket")
public class ChatSocket {
    @Inject
    ChatService chatService;

    @OnTextMessage
    public String onMessage(String userMessage){
        return chatService.chat(userMessage);
    }
}
package org.acme;

import io.quarkus.runtime.StartupEvent;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;

@ApplicationScoped
public class ChatService {
    protected void startup(@Observes StartupEvent event) { 
        System.out.println("Startuuuuuuuuuup event");
    }

    public String chat(String message) {
        return message + " you said.";
    }
}

ChatGPT

Extension

./mvnw quarkus:add-extension -Dextensions='quarkus-langchain4j-openai'

Configuration

quarkus.langchain4j.openai.api-key=<OPEN_API_KEY> 
quarkus.langchain4j.openai.chat-model.model-name=gpt-3.5-turbo

API-Key: You can get an API key from OpenAI. But you need at least to pay 5$, what I did. Alternativley you can use demo as API key for limited testing.

Model-Name: Here are the OpenAI Models. gpt-3.5-turbo is default.
Hint: It is not working, if there is a " "(space/blank) after the model-name.

I had stored my OpenAI-API-key as GitHub secret, so the key is available as environment variable in my Codespace. Therefore I changed the configuration:

quarkus.langchain4j.openai.api-key=${OPEN_API_KEY:demo} 
quarkus.langchain4j.openai.chat-model.model-name=gpt-4o

Code

package org.acme;

import io.quarkiverse.langchain4j.RegisterAiService; 

@RegisterAiService 
public interface Assistant { 
    String chat(String message); 
}

Use this Assistant instead of the ChatService:

package org.acme;

import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
import jakarta.inject.Inject;

@WebSocket(path = "/chatsocket")
public class ChatSocket {
    @Inject
    Assistant assistant;

    @OnTextMessage
    public String onMessage(String userMessage){
        return assistant.chat(userMessage);
    }
}

Hugging Face

Extension

./mvnw quarkus:add-extension -Dextensions='quarkus-langchain4j-hugging-face'

Configuration

quarkus.langchain4j.chat-model.provider=huggingface

quarkus.langchain4j.huggingface.api-key=${HUGGINGFACE_API_KEY:nokey}
quarkus.langchain4j.huggingface.chat-model.model-id=KingNish/OpenGPT-4o

Provider: Now we have two models configured, we need to specify which provider to use (huggingface)

API-Key: Get free API-Key from Hugging Face:
Login -> Settings -> Access Tokens -> Generate (Type: 'Read')

Model: Search on the Hugging Face website, I randomly took KingNish/OpenGPT-4o

Code

No code change needed, it works with the same code as for ChatGPT.

Everything is changed by configuration.

Antrophic Claude

Extension

./mvnw quarkus:add-extension -Dextensions='quarkus-langchain4j-anthropic'

[ERROR] ❗  Nothing installed because keyword(s) 'quarkus-langchain4j-anthropic' were not matched in the catalog.

It did not work with the maven executable. Need to add dependency manually to pom.xml, see documentation:

<dependency>
    <groupId>io.quarkiverse.langchain4j</groupId>
    <artifactId>quarkus-langchain4j-anthropic</artifactId>
    <version>0.15.1</version>
</dependency>

Configuration

quarkus.langchain4j.chat-model.provider=anthropic

quarkus.langchain4j.anthropic.api-key=${ANTHROPIC_API_KEY:no key}
quarkus.langchain4j.anthropic.chat-model.model-name=claude-3-haiku-20240307

API-Key: Login to Antropic Console and get an API key for free.

Model: Select one from documentation.

Code

No code change needed, it works with the same code as for ChatGPT.

But did not work:

org.jboss.resteasy.reactive.ClientWebApplicationException: Received: 'Bad Request, status code 400' when invoking: Rest Client method: 'io.quarkiverse.langchain4j.anthropic.AnthropicRestApi#createMessage'

Quarkus terminal logging

Without API-key I got a status code 401.

Ollama

Prerequisites

Ollama has to be installed. See this post or Ollama Homepage.

curl -fsSL https://ollama.com/install.sh | sh
export OLLAMA_HOST=0.0.0.0:11434
ollama serve
ollama pull moondream

ollama --version
ollama version is 0.1.41

Extension

./mvnw quarkus:add-extension -Dextensions='quarkus-langchain4j-ollama'

Configuration

quarkus.langchain4j.chat-model.provider=ollama

quarkus.langchain4j.ollama.chat-model.model-id=moondream
quarkus.langchain4j.ollama.timeout=120s

Model: I choose moondream, because it is the smallest one (829MB).

Models can be found on the GitHub page or on Ollama library.

However, Quarkus is ignoring my resourcefriendly choice, as I can see in the Logs: "Preloading model llama3" 🤷‍♂️
UPDATE: For Ollama it is model-id, not model-name!

Code

Also no change.

Mistral

Extension

./mvnw quarkus:add-extension -Dextensions='quarkus-langchain4j-mistral'

Configuration

quarkus.langchain4j.chat-model.provider=mistralai

quarkus.langchain4j.mistralai.api-key=${MISTRALAI_API_KEY:no key}
quarkus.langchain4j.mistralai.chat-model.model-name=mistral-tiny

API-key: You can generate an API-key in Mistral AI Console. But you are required to have a Abonnement, which I do not have. Therefore nor API-key for me.

Model: mistral-tiny is default one

Code

Also no change.

But could not test, because I do not have an API-key.

Groq

I like Groq but unfortunately there is no LangChain4j support yet.

The Python LangChain project has already implemented Groq.