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.

Categories
Development Java

Quarkus in Codespace

Set up Quarkus in Codespace environment

Setup

Create a repository “workshop”:

Open codespace and setup Linux in Terminal:

sudo apt update && \
sudo apt upgrade -y && \
sudo apt install httpie -y

Setup Quarkus Project

Quarkus Homepage

Getting started → 4. Bootstrapping the project → Maven

copy and change ArtifactId to workshop

cd ..
# pwd -> /workspaces
mvn io.quarkus.platform:quarkus-maven-plugin:3.10.2:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=workshop

We can not just delete the workshop folder, because GIT information are inside this folder.

Workaround:

cd /workspaces/workshop/

mvn io.quarkus.platform:quarkus-maven-plugin:3.10.2:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=workshop

mv workshop/* .
ls -lisah workshop/
# hidden files/directory still there
mv workshop/.* .
ls -lisah workshop/
# now everthing has moved
rmdir workshop

Stage, commit and push to repository:

Open GreetingResource, this will force Visual Code to ask to install Extension Pack for Java:

Go to Explorer → Java Projects → Import Projects

It takes a minute or two to show our workshop project:

Start Quarkus

./mvnw quarkus:dev

In Ports Port 5005 is automatically added, but not Port 8080.

We need to add Port 8080 manually.

Click on the Globus Icon and Quarkus welcome page opens:

Go to Visit the dev UI → Endpoints

Click on /hello:

Test from terminal

Open new Terminal

## "&& echo" for additional linebreak
curl localhost:8080/hello && echo
http localhost:8080/hello

WebSockets with Quarkus

Quarkus Guide for using WebSockets

Setup ChatSocket

First we need to install extensions:

./mvnw quarkus:add-extension -Dextensions='websockets'
./mvnw quarkus:add-extension -Dextensions='websockets-next'

Create simple ChatSocket:

package org.acme;

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

@WebSocket(path = "/chatsocket")
public class ChatSocket {
    @OnTextMessage
    public String onMessage(String userMessage){
        return "You said: " + userMessage;
    }
}

Test from terminal

npm install -g wscat
wscat -c ws://localhost:8080/chatsocket

wscat -c ws://localhost:8080/chatsocket
> Hello World!
< You said: Hello World!

Build html client

mkdir -p src/main/resources/META-INF/resources
touch 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>

CONTEXTS AND DEPENDENCY INJECTION Example

Add a ChatService for the 'logic' and inject it into the Chat Socket:

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.";
    }
}
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);
    }
}

Build and run native Image

Building a Native Executable - Quarkus

Build:

# Ich muss vermutlich nur den zweiten Befehl ausführen?
# ./mvnw install -Dnative 
./mvnw package -Dnative -Dquarkus.native.container-build=true -Dquarkus.container-image.build=true

Open another terminal and see how much codespace machine is sweating:

htop

Run:

target/workshop-1.0.0-SNAPSHOT-runner

The project generation has provided a Dockerfile.native-micro in the src/main/docker directory.

# build
docker build -f src/main/docker/Dockerfile.native-micro -t deringo/workshop .

# run
docker run -i --rm -p 8080:8080 deringo/workshop
Categories
Development

WebSockets with Node.js

WebSockets are a protocol that provides full-duplex communication channels over a single, long-lived connection. They are designed for real-time, event-driven web applications and allow for low-latency communication between a client (typically a web browser) and a server. Here are some key points about WebSockets:

  1. Full-Duplex Communication: Unlike HTTP, which is request-response based, WebSockets allow for two-way communication where both client and server can send and receive messages independently of each other.
  2. Persistent Connection: WebSocket connections are persistent, meaning they remain open as long as both the client and server agree to keep the connection alive. This reduces the overhead associated with establishing new connections.
  3. Low Latency: WebSockets are ideal for scenarios requiring real-time updates because they reduce the latency associated with polling or long-polling techniques.
  4. Protocol: WebSockets are established by upgrading an HTTP/HTTPS connection using a WebSocket handshake, switching the protocol from HTTP to WebSocket.
  5. Use Cases: Common use cases include live chat applications, real-time notifications, collaborative editing, online gaming, and any application requiring real-time data updates.

Node.js in Codespace

I want to test WebSockets with Node.js in GitHub Codespace.

Node Version Manager (nvm)

# test installation
command -v nvm
nvm


# check version
nvm ls
       v18.20.1
       v20.12.1
->       system
default -> 20 (-> v20.12.1)
iojs -> N/A (default)
unstable -> N/A (default)
node -> stable (-> v20.12.1) (default)
stable -> 20.12 (-> v20.12.1) (default)
lts/* -> lts/iron (-> v20.12.1)
lts/argon -> v4.9.1 (-> N/A)
lts/boron -> v6.17.1 (-> N/A)
lts/carbon -> v8.17.0 (-> N/A)
lts/dubnium -> v10.24.1 (-> N/A)
lts/erbium -> v12.22.12 (-> N/A)
lts/fermium -> v14.21.3 (-> N/A)
lts/gallium -> v16.20.2 (-> N/A)
lts/hydrogen -> v18.20.1
lts/iron -> v20.12.1


# check node version
node --version
v20.12.1

Node.js

# install LTS
nvm install --lts
Installing latest LTS version.
Now using node v20.14.0 (npm v10.7.0)


# check version
nvm ls


# check node version
node --version
v20.14.0

Some npm commands

# Show installed Nodes
nvm ls
# Show available versions
nvm ls-remote
# Install latest version
nvm install node
# Install LTS version
nvm install --lts
# Install a specific version (list available -> example 16.20.2)
nvm install 16.20.2
# Use a specific version
nvm use 16.20.2
# Show npm version
npm --version

Simple Website with Node.js

To serve an HTML page using Node.js, we can use the built-in http module.

Create an HTML file

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Node.js HTML Server</title>
</head>
<body>
    <h1>Hello, World!</h1>
    <p>This is a simple HTML page served by Node.js.</p>
</body>
</html>

Create a Node.js script to serve the HTML file

const http = require('http');
const fs = require('fs');
const path = require('path');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
    if (req.method === 'GET' && req.url === '/') {
        const filePath = path.join(__dirname, 'index.html');
        fs.readFile(filePath, (err, data) => {
            if (err) {
                res.statusCode = 500;
                res.setHeader('Content-Type', 'text/plain');
                res.end('Internal Server Error');
            } else {
                res.statusCode = 200;
                res.setHeader('Content-Type', 'text/html');
                res.end(data);
            }
        });
    } else {
        res.statusCode = 404;
        res.setHeader('Content-Type', 'text/plain');
        res.end('Not Found');
    }
});

server.listen(port, hostname, () => {
    console.log(`Server running at http://${hostname}:${port}/`);
});

Run the Node.js server

node server.js

Test Node.js server in terminal

curl localhost:3000
http localhost:3000

Test Node.js server in browser

WebSocket in Codespace

To create a Node.js server that provides both an HTTP server for serving an HTML page and a WebSocket server for real-time communication, we can use the ws library for WebSockets.

Install the ws library

npm install ws

Create the HTML file

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket Example</title>
</head>
<body>
    <h1>WebSocket Example</h1>
    <p id="message">Connecting...</p>
    <script>
        const socket = new WebSocket('ws://localhost:3000');

        socket.onopen = function(event) {
            document.getElementById('message').innerText = 'Connected';
            socket.send('Hello Server');
        };

        socket.onmessage = function(event) {
            document.getElementById('message').innerText = 'Received: ' + event.data;
        };

        socket.onclose = function(event) {
            document.getElementById('message').innerText = 'Disconnected';
        };

        socket.onerror = function(error) {
            document.getElementById('message').innerText = 'Error: ' + error.message;
        };
    </script>
</body>
</html>

Create the Node.js server

const http = require('http');
const fs = require('fs');
const path = require('path');
const WebSocket = require('ws');

const hostname = '127.0.0.1';
const port = 3000;

// Create HTTP server
const server = http.createServer((req, res) => {
    if (req.method === 'GET' && req.url === '/') {
        const filePath = path.join(__dirname, 'index.html');
        fs.readFile(filePath, (err, data) => {
            if (err) {
                res.statusCode = 500;
                res.setHeader('Content-Type', 'text/plain');
                res.end('Internal Server Error');
            } else {
                res.statusCode = 200;
                res.setHeader('Content-Type', 'text/html');
                res.end(data);
            }
        });
    } else {
        res.statusCode = 404;
        res.setHeader('Content-Type', 'text/plain');
        res.end('Not Found');
    }
});

// Create WebSocket server
const wss = new WebSocket.Server({ server });

wss.on('connection', ws => {
    console.log('Client connected');

    ws.on('message', message => {
        console.log(`Received: ${message}`);
        ws.send('Hello Client');
    });

    ws.on('close', () => {
        console.log('Client disconnected');
    });
});

server.listen(port, hostname, () => {
    console.log(`Server running at http://${hostname}:${port}/`);
});

Run the Node.js server

node server.js

Test Node.js server in terminal

curl localhost:3000
http localhost:3000

Test Node.js server in browser

Brower Fix

We need to adjust the address of the WebSocket.

When setting up a WebSocket connection from the client-side script within the HTML file, the WebSocket URL must match the address and port where the WebSocket server is running. This URL should include the WebSocket protocol (ws:// or wss:// for secure connections)

Adjustment:

const socket = new WebSocket('wss://symmetrical-disco-g454xqrq9pqvfw6pr-3000.app.github.dev/:3000');

OK, this works, so make it a little more dynamic:

const socket = new WebSocket('wss://' + location.host + '/:3000');

Test WebSocket from terminal

Unfortunately, curl and httpie do not natively support WebSocket protocols. To test WebSocket connections using command-line we can use wscat, which is specifically designed for WebSocket communication.

npm install -g wscat

Using wscat to Test WebSocket Connections:

wscat -c ws://localhost:3000

Connected (press CTRL+C to quit)
> hello
< Hello Client

Build a Chat App

Let's build a "Chat App"

Server

We enhance the server to dynamically answer to a message:

[...]
     ws.on('message', message => {
        console.log(`Received: ${message}`);
        ws.send('Hello ' + message);
    });
[...]

Amazing!

Client

<!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 + '/:3000');
                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>

Awesome!

Also 'Chatbot' is working from terminal:

Categories
Development Java Linux

GitHub Codespace

I was on JCON 2024 and beside other interesting talks I heard one talk about cloud-based IDEs, and I wanted to try out, if GitHub Codespaces could work for me.

Explore the evolving landscape of cloud-based integrated development environments (IDEs), focusing on Gitpod, GitHub codespaces and Devpod. Compare and contrast these cloud IDEs with traditional counterparts, emphasizing the role of container technology, specifically the devcontainer specification. The discussion includes advances, existing limitations, and the potential for developing polyglot, container-based distributed applications. A live demo illustrates the rapid setup and coding process across various languages and frameworks, showcasing testing capabilities and seamless deployment to Kubernetes. Discover how custom additions enhance flexibility. Additionally, uncover the impact of cloud IDEs on teaching and team projects, ensuring consistent development setups for enhanced efficiency and streamlined processes.

[EN] Codespaces, Gitpod, Devpod ... what cloud and container-based IDEs can do for you
by Matthias Haeussler (Novatec Consulting GmbH)

Create GitHub Account

Go to GitHub and create an account. Free plan is suitable.

Create Repository

Create a new repository with name “workshop”. Add a README file.

Create Codespace

TODO: funktioniert das GIF?

Change Keyboard Layout to German: In the lower right corner click on “Layout: US” and enter “German” in the upcoming window.

TODO: Ich hätte gerne die Sprache von Visual Code auf Englisch umgestellt. Wie?

Work in the Terminal

Copy & Paste

Type something into the terminal.
Mark it with your mouse.
One Right Click to copy into Clipboard.
Another Right Click to paste from Clipboard.

Timezone

Set Timzone to Europe -> Berlin

sudo dpkg-reconfigure tzdata

Internet

Do we have access to the Internet? Let’s try with curl:

curl google.com

HTTPie

A modern alternative to curl is HTTPie:

Install httpie:

sudo apt update && \
sudo apt upgrade -y && \
sudo apt install httpie -y

This will take a few minutes. Meanwhile we can work in another Terminal window. Later we come back and test HTTPie:

http google.com

Additional Terminal window

Open a second Terminal with bash:

VIM

ls -lisah
touch test.sh
ls -lisah
vim test.sh
chmod +x test.sh
./test.sh
name=Ingo
echo "My name is $name"
echo "But here I am: $(whoami)"

Python

Do we have Python in our Codespace? Which version(s)?

python3 --version
python --version
vim hello_world.py
python hello_world.py
# Print "Hello World" to the console 
print("Hello World") 

Docker

docker --version
docker-compose --version
docker run hello-world 

Apache HTTPD

docker run -p 8888:80 httpd

Open in Browser:

Find all open Ports in the Ports-Tab:

Normally Port 8888 should be listed here.
We need to add Port, just enter 8888:

Open Website just with a click on the Globus-Icon.

When we try to open the address in another browser, we will see a GitHub-Login.
When we login with another GitHub-Account, we will get a 404-error. Because the page is Private.
Switch to Public:

Now we can access the page in another brower.

At the end we can shutdown HTTPD with <STRG>+<C> in Terminal window. It should automatically disapear in the Ports-Tab. If not, you can remove it manually.

Microsoft Edge - Caching problem

Open the Public page in MS Edge.
Make the page Private again. Try to open in a new browser, won’t work.
Reload (the Public loaded) page in MS Edge: You can still see the site!
This is a cached version and we need to force MS Edge to reload from server.

Open Developer Tools (F12 or <STRG>+<SHIFT>+<I>), then you can Right Click on the reload button to have additional options:

Java

java --version
vim HelloWorld.java
javac HelloWorld.java
java HelloWorld
rm -f HelloWorld*
class HelloWorld { 
  public static void main(String args[]) { 
      System.out.println("Hello World"); 
  } 
}

Run Java Source as Shell Scripts

type -a java
# java is /home/codespace/java/current/bin/java
# java is /usr/local/sdkman/candidates/java/current/bin/java

vim HelloWorld.sh
chmod +x HelloWorld.sh
./HelloWorld.sh
rm HelloWorld.sh
#!/home/codespace/java/current/bin/java --source 21 

class HelloWorld { 
  public static void main(String args[]) { 
      System.out.println("Hello World"); 
  } 
}

Maven

Start

We create a new pom.xml from scratch.
We need a template. We will take “The Basics”-one from the Apache Maven POM Reference page.

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
     
      <groupId>org.codehaus.mojo</groupId>
      <artifactId>my-project</artifactId>
      <version>1.0</version>
    </project>
mvn --version
vim pom.xml
mvn clean verify 

Sample Project

Open pom.xml in Explorer (GUI) and change:

  • org.codehaus.mojo to org.acme
  • my-project to workshop

No need to save: Changes are automatically saved

To doublecheck that everything is still ok run mvn clean verify  again.

mkdir -p src/main/java/org/acme
touch src/main/java/org/acme/HelloWorld.java 

Open HelloWorld.java with <STRG>+<MOUSECLICK> in GUI-Editor.

Install Extension Pack for Java as suggested:

And also the next two suggestions:

package org.acme;

class HelloWorld { 
  public static void main(String args[]) { 
      System.out.println("Hello World"); 
  } 
}
mvn package
java -classpath ./target/workshop-1.0.jar org.acme.HelloWorld

Maven - different version

In our Codespace we have Maven 3.9.6 and Java 21.
Let’s test with a different version. We will use Docker.

Official Maven Image on DockerHub.

We want to re-use the local Maven Cache. Let’s find out where it is:

sudo apt install locate -y
sudo updatedb
locate .m2
# /home/codespace/.m2

Adjust the “How to use this image” command:

docker run -it --rm \
--name workshop-maven-project \
-v /home/codespace/.m2:/root/.m2 \
-v "$(pwd)":/usr/src/workshop \
-w /usr/src/workshop \
maven:3.3-jdk-8 \
mvn clean package
java -classpath ./target/workshop-1.0.jar org.acme.HelloWorld

Sourcecode management

We have 7 uncommited changes, but only 2 files should go into the repository:

What we need is a .gitignore file.

touch .gitignore

There are two template files we will copy:

Now there are only 3 files we can commit:

Now we can see these files in our repository:

Secrets

Use GitHub Secrets for API-keys etc.

In the upper-right corner of any page, click your profile photo, then click Settings. Under Codespaces we can set our secrets:

In our Codespace we can access the secret as environment variable:

A running codespace has to be restarted!

Cleanup

Delete all files:

rm -rf target && \
rm -rf src && \
rm pom.xml && \
rm README.md && \
rm .gitignore

Stage & commit changes:

Now we have a clean repository:

Close browser window with codespace and delete the codespace:

Delete the repository:

Go to Settings → General → Danger Zone → Delete this repository

Categories
Linux

SSH Key-Based Authentication on Linux Servers

I want to login from one Linux server to another Linux server without the need to enter a password.

Create SSH keys

# Login to server 1
ssh-keygen

Output:

Generating public/private rsa key pair.
Enter file in which to save the key (/home/'USERNAME'/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/'USERNAME'/.ssh/id_rsa
Your public key has been saved in /home/'USERNAME'/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:27U2nApZQLSwS1UVh2Lw4aDM/B9/gR0Uum1ppRrqjpg 'USERNAME'@server1
The key\'s randomart image is:
+---[RSA 3072]----+
|    +o+ .o       |
| + . Bo+o .      |
|  = o.*...       |
|   o + o .       |
|    + o S   .    |
|     . X * +     |
| .    * @ O      |
|E . .. B * .     |
| ..+..o o        |
+----[SHA256]-----+

The utility will prompt to select a location for the keys that will be generated. By default, the keys will be stored in the ~/.ssh directory within user’s home directory. The private key will be called id_rsa and the associated public key will be called id_rsa.pub.

Now we have a private and public key that we can use to authenticate.

Copy keys to server 2

We will use the same keys on server 2:

scp ~/.ssh/id_rsa     USERNAME@server2ip:~/.ssh
scp ~/.ssh/id_rsa.pub USERNAME@server2ip:~/.ssh

Create authorized_keys on both servers:

ssh-copy-id USERNAME@localhost
ssh-copy-id USERNAME@server2ip

Connect to server 2

ssh USERNAME@server2ip
# or just
ssh server2ip

Now we SHOULD connect to server2 without the need to enter a password.

Failing - But why?

Unluckily in my case I still have to enter a password. But why?

# Login to server 2
ssh server2ip
Password:

# check auth.log
less /var/log/auth.log
...
rexec line 15: Deprecated option RSAAuthentication
...

# check sshd_config
less /etc/ssh/sshd_config
...
RSAAuthentication no
...

So in my case the RSA-Authentication has been disabled.
As the default is enabled by purpose, this has been done by purpose.
Unfortunatly I am not the admin or manager of the server, so I can not change this settings.

Categories
AI

Ollama

For my last post about PrivateGPT I need to install Ollama on my machine.

The Ollama page itself is very simple and so is the instruction to install in Linux (WSL):

curl -fsSL https://ollama.com/install.sh | sh
ollama serve

Couldn't find '/home/ingo/.ollama/id_ed25519'. Generating new private key.
Your new public key is:

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGgHcpiQqs4qOUu1f2tyjs9hfiseDnPfujpFj9nV3RVt
ollama run llama2 "why is the sky blue"
ollama list
curl 127.0.0.1:11434

Ollama is running

OK, now pull the files we need for the PrivateGPT installation:

ollama pull mistral
ollama pull nomic-embed-text

Information about Ollamas Model Library is here.

IP Problem

Ollama is bound to localhost:11434.
So Ollama is only available from localhost or 127.0.0.1, but not from other IPs, like from inside a docker container.

There is already a feature request for this issue.

Meanwhile we have to do a workaround:

export OLLAMA_HOST=0.0.0.0:11434
ollama serve

Test with local IP:

export DOCKER_GATEWAY_HOST="`/sbin/ip route|awk '/dev eth0 proto kernel/{print $9}'|xargs`"
curl $DOCKER_GATEWAY_HOST:11434
Ollama is running
Categories
AI

PrivateGPT

I want to try PrivateGPT to chat with my documents privatly.

As always I put the files in a GitHub repository.

OpenAI

To start with an easier example, I will use PrivateGPT with OpenAI/ChatGPT as AI. Of course therefore the chat will not be private, what is the main reason to use PrivateGPT, but it is a good start to bring things up and running and in a next step add a local AI.

OpenAI API key

To use ChatGPT we need an OpenAI API key.
The key itself is free, but I needed to charge my account with 5$ to get it working.

For testing a Playground is available.

Before funding my account:

After funding my account with the minimum of 5$:

Docker

The OpenAI API key is stored in a file .env, that provides its content to docker compose as environment variables.

In docker-compose we set the API key and profile: openai as environment for our Docker container:

    environment:
      - PGPT_PROFILES=openai
      - OPENAI_API_KEY=${OPENAI_API_KEY}

In Docker image we configure installation for openai:

RUN poetry install --extras "ui llms-openai vector-stores-qdrant embeddings-openai"

PrivateGPT will download Language Model files during its setup, so we provide a mounted volume for this model files and execute the setup at the start of the container and not at image build:

volumes:
	- ../models/cache:/app/privateGPT/models/cache
command: /bin/bash -c "poetry run python scripts/setup && make run" 

Here are the complete files, you can also find them on my GitHub:

# Use the specified Python base image
FROM python:3.11-slim

# Set the working directory in the container
WORKDIR /app

# Install necessary packages
RUN apt-get update && apt-get install -y \
    git \
    build-essential

# Clone the private repository
RUN git clone https://github.com/imartinez/privateGPT

WORKDIR /app/privateGPT

# Install poetry
RUN pip install poetry

# Lock and install dependencies using poetry
RUN poetry lock
RUN poetry install --extras "ui llms-openai vector-stores-qdrant embeddings-openai"
version: '3'

services:
  privategpt:
    image: privategptopenai
    container_name: privategptopenai
    ports:
      - "8001:8001"
    volumes:
      - privategpt:/app
      - ../models/cache:/app/privateGPT/models/cache
    environment:
      - PGPT_PROFILES=openai
      - OPENAI_API_KEY=${OPENAI_API_KEY}
    command: /bin/bash -c "poetry run python scripts/setup && make run" 
    
volumes:
  privategpt:
OPENAI_API_KEY=YOUR_SECRET_OPENAI_API_KEY

Now we can build the image, start the container and watch the logs:

docker build -t privategptopenai .
docker-compose up -d
docker logs --follow privategptopenai

Private GPT

Open http://localhost:8001 in your browser to open Private GPT and run a simple test:

Have a look at the logs to see that there is communication with OpenAI servers:

Chat with document

To "chat" with a document we first need a public available one, because right now we are using ChatGPT where we must not upload internal project documents.

So first ask PrivateGPT/ChatGPT to help us to find a document:

Working fine, we could easily find and download a PDF:

The upload of the PDF (The Go to Guide for Healthy Meals and Snacks.pdf) with 160 pages in 24 MB into PrivateGPT took nearly two minutes.
In the logs we can see, that the file was uploaded to ChatGPT:

Let's chat with the book:

Uh, was that question too hard? Give it another try:

OK, sounds better.
In the logs we can see the traffic to OpenAI:

Local, Ollama-powered setup

Now we want to go private, baby.

Copy configuration to a new folder, can be found in GitHub.

In docker-compose we change the profile to ollama:

    environment:
      - PGPT_PROFILES=ollama

In Docker image we configure installation for ollama:

RUN poetry install --extras "ui llms-ollama embeddings-ollama vector-stores-qdrant"

As before we can build the image, start the container and watch the logs:

docker build -t privategptollama .
docker-compose up -d
docker logs --follow privategptollama

Open http://localhost:8001 in your browser to open Private GPT and run a simple test:

PrivateGPT is running, but we receive no answer to our question.
Not really surprising, because we have to install and start Ollama first.

Installation of Ollama is written in next Post.

Test Ollama reachable from PrivateGPT:

docker exec -it privategptollama bash

curl 172.17.0.1:11434
Ollama is running

curl 172.29.184.58:11434
Ollama is running

We will make this configurable within environment variables, add:

sed -i 's@llm_model: mistral@llm_model: ${OLLAMA_LLM_MODEL:mistral}@g' settings-ollama.yaml
sed -i 's@api_base: http://localhost:11434@api_base: ${OLLAMA_API_BASE:http\://172.17.0.1\:11434}@g' settings-ollama.yaml
    environment:
      - OLLAMA_LLM_MODEL=mistral
      - OLLAMA_API_BASE=${DOCKER_GATEWAY_HOST}:11434

Re-Build, set DOCKER_GATEWAY_HOST and start PrivateGPT:

docker build -t privategptollama .

export DOCKER_GATEWAY_HOST="`/sbin/ip route|awk '/dev eth0 proto kernel/ { print  $9}'`"
docker-compose up -d

# watch logs:
docker logs --follow privategptollama

Finally working 🥳

The chat is working:

File upload and query file is working:

I did not use the large ~24MB file I tried with ChatGPT, but a much smaller one ~297 KB I randomly found in the internet. It is written in german, but it seems, like Ollama understands german.

Well, then I tried the 24 MB file and ... it worked pretty well, the result of the first question was even better than the result from ChatGPT!

Categories
Uncategorized

PostgreSQL Excel Data Imports Using DBeaver

There are many ways to work with data, here is a quick walkthrough how to transfer data from an Excel file into a database.

Excel-File -> CSV-File -> DBeaver -> PostgreSQL

Excel file

We have a simple Excel file:

with a simple sample data structure:

Excel file conversion

We need to convert the Excel file into a .csv (Comma Seperated Values) file.

Just open the file -> Datei -> Exportieren > Dateityp ändern -> CSV
and save as sample.csv

Import from CSV

In DBeaver:

Open Database connection -> database -> schema -> Rightclick -> Data import:

Import from CSV:

Change delimiter (Spaltentrennzeichen) from , to ;

click "Auto assign", change target to "sample"

TADA! We have a new table "sample" in our database:

Create CSV connection

Let's try another way. Delete table "sample" and create a connection to the CSV file:

Click Browse… and select the folder where your csv file is that you saved from Excel. You’re selecting a folder here, not a file. The connection will load ALL csv files in that folder. Click Finish.

Download the driver, if using for the very first time:

You will now see a new connection; this connection is set up exactly like a regular connection. Each csv file in your folder will be set up as a table with columns.

But there is a problem with the columns, it's just one, not three:

Doublecheck delimiter, but it is already set to ;

Ah, in driver details we have to set the separator from , to ;

Looks better now:

Export CSV data into database:

Once again we have our sample table in our database: