Create And Revamp Your Own Offline ChatGPT On Local PC With GPT4All LLM In Java

Author:Murphy  |  View: 27196  |  Time: 2025-03-22 22:17:16

Following the advent of ChatGPT by OpenAI, the uptake rate and popularity of this technology has surged and continues to rise exponentially to this date. The resulting paradigm shifts have already been reflected in multiple facets of our lives; ranging from the ways we work to switching our default go-to source of information to answer day-to-day questions. While the masses are fawning over the convenience ChatGPT has brought about, there is also the other side of the coin which the mainstream media is seemingly under-reporting – (1) "What happens during a power/network outage or server malfunction on user/provider's end?", (2) "What if there is no internet connection at the workplace?" or (3) "Would data privacy be compromised?" etc.

Applying (2) to the real-world context, this is extremely relevant since it is a known fact that workers in high-security settings such as healthcare and military defence have to carry out much of their work with little to no network access.

This is simply because the risks of any unwanted data leakage (e.g. patient health data) are risks which cannot be afforded in these high-stakes settings. In turn, this then naturally begs the question of "Is ChatGPT or other technologies alike off-limits to members of this group?"

In short, the answer is no since fortunately, the open-source community has created alternatives with comparable performance. More specifically, a category known as "Offline ChatGPT" has emerged amidst ongoing competition among commercial and open-source rivals.

Illustration by Author

Project Motivation

Running ChatGPT Offline On Local PC

Increased reliability leads to greater potential liability.

Apart from the aforementioned target audiences, it is also worth noting that similar to Google Maps, ChatGPT is at its core an API endpoint made available by a 3rd-party service provider (i.e. OpenAI). As such, it shares common single points of failure such as inability to operate due to lack of/limited internet connection or breach in data confidentiality when exchange of highly sensitive data occurs, technically making all end-users susceptible to any liability posed by their reliance on the usage of any online GPT service providers.

Thankfully, some of these drawbacks can be mitigated by turning to "Offline ChatGPT"s, which are locally run and keep input/output information private and contained.

Choice of localised ChatGPT: GPT4All

There were 2 key points of consideration which led me to this choice.

  1. Straightforward setup

The official GPT4All site provides concise and precise instructions to set up its interactive chat interface for both developers and end-users alike. Under the segment "Installation Instructions", there are 3 types of selection available for download –

Screenshot by Author | The 3 different downloads are each OS-specific – Windows, Mac and Linux respectively. | Note: Windows users should select leftmost button as shown above (outlined in yellow).

Upon installation, chat application interface can be launched from the start menu –

Screenshot by Author | On Windows OS search "GPT4All" and select to start conversing with the AI bot assistant via the GPT4All interface.

2. Ease of customisation (for developers)

While the chat application itself is fairly user-friendly, its convenience does come at the expense of rigid user experience. It is not platform-independent since it requires different installer builds for each OS.To improve user experience, the application is configured at source code level using GPT4All Java bindings, eliminating the need for custom builds.

As such, to improve user-experience, I embarked on this pet project to configure the application on a source code level using GPT4All Java bindings (version 2.4.11) to **** eliminate the need for custom builds and re-package it as a portable and standalone tool.

Important note on GPT4All version

Only GPT4All v2.5.0 and newer supports models in GGUF format (.gguf). At the time of this post, the latest available version of the Java bindings is v2.4.11 – which are compatible with solely GGML formatted models. (Source: Official GPT4All GitHub repo)

Steps To Set Up GPT4All Java Project

Pre-requisites

  • Both JDK 11 and JDK 8 installed on the PC. Installers can be retrieved from Oracle (by default project runs on JDK 11 platform)
  • (Optional but preferred) A Java IDE such as NetBeans or Eclipse (note that in this demo NetBeans shall be used)
  • Download a sample model such as ggml-gpt4all-j-v1.3-groovy.bin (~ 3.5GB) to test generated responses

gpt4all/gpt4all-bindings/java at main · nomic-ai/gpt4all

Based on the above GitHub documentation, download the Java bindings which are packaged into a single jar file – gpt4all-java-binding-1.1.5.jar and include it in the project classpath:


    com.hexadevlabs
    gpt4all-java-binding
    1.1.5
Imagine by Author | Note that within gpt4all-java-binding-1.1.5.jar, the native bindings for each of the respective OS – Linux (Ubuntu), MacOS and Windows are embedded.

Other mandatory JAR dependencies to be included are:

ASM

  • asm-9.2.jar
  • asm-analysis-9.2.jar
  • asm-commons-9.2.jar
  • asm-tree-9.2.jar
  • asm-util-9.2.jar

jnr/jffi

  • jffi-1.3.0.100-native.jar
  • jffi-1.3.0.jar
  • jnr-a64asm-1.0.0.jar
  • jnr-ffi-2.2.13.jar
  • jnr-x86asm-1.0.2.jar

SLF4J

  • slf4j-api-1.7.36.jar
  • **slf4j-simple-1.7.35.jar***

***** Rationale for JAR file –

Image by Author | slf4j-simple-1.7.35.jar is included to prevent the above warning message from showing up on runtime.

Important. Do note that the JDK platform used to run this source code is JDK 11.

Sample application code

The application code to test run its functionalities can be found at the same GPT4All GitHub repo link.

Sample application output

Illustration by Author | Upon running the application code sample, the above output is generated. Notice that the lines which are highlighted in yellow are displayed specifically because of the additional "System.out.println" lines added at the end of the source code.
  • Certain core features shown from the above test run include model loading, user prompt input (hardcoded) and output of generated response by the GGML model (For a list of other sample ggml models, feel free to check out model.json.)

Additional Steps Required For Project Configuration

In alignment with the aim of this project to make the GPT chatbot platform independent and personalise user experiences, specific issues I have decided to tackle here are –

  • JDK 8 incompatibility
  • Chatbot response formatting
  • Native binaries extracted from JAR file on runtime (more details below in step 2)

Step 1. Import source code of gpt4all-java-binding-1.1.5.jar

In order to tweak its source code, the non-compiled artifacts including its original source code in gpt4all-java-binding-1.1.5.jar must first be retrieved. This can be done by either running the below command to clone the GitHub repo (assuming GIT is installed) –

git clone https://github.com/nomic-ai/gpt4all.git

or selecting "Download ZIP":

Image by Author | A screenshot of github repo of GPT4All where source code is downloadable from selecting "Download ZIP" outlined in red as shown above.

Navigate to ./gpt4all-bindings/java where src directory is present and paste its contents into the Java project IDE.

Image by Author | The above shows how the project looks like after pasting the contents of the GPT4All binding source code.

Step 2. Extract the binaries from gpt4all-java-binding-1.1.5.jar to the project folder

To access binary files packaged in a JAR file, use a file unarchiver such as 7zip and proceed to retrieve the folder native as shown.

Illustration by Author | "native" folder containing native bindings (e.g. the files with .dll extension for Windows OS platform) are being dragged out from the JAR file | Since the source code component of the JAR file has been imported into the project in step 1, this step serves to remove all dependencies on gpt4all-java-binding-1.1.5.jar by placing the binary files at a place accessible by the application

Step 3. Remove gpt4all-java-binding-1.1.5.jar and switch to JDK 8 project platform

Finally, exclude Java bindings JAR from classpath and configure project to run on JDK 8 instead of JDK 11. The project setup shall now resemble the following:

Image by Author | Errors are flagged out by NetBeans IDE in the source code files upon switching project JDK platform from 11 to 8 due to compatibility issues

Technical Implementation – 5 Steps In Total

Step 1. Tweak the source code in each respective file to resolve compatibility issues with JDK 8

File: Application.java

Screenshot by Author | Left: File before edit | Right: File after edit

Before: try (LLModel model = new LLModel(Path.of(modelFilePath))) {

After: try (LLModel model = new LLModel(Paths.get(modelFilePath))) {

File: LLModel.java

Screenshot by Author | Left: File before edit | Right: File after edit

Before:

return bufferingForWholeGeneration.toString(StandardCharsets.UTF_8);

After:

//        return bufferingForWholeGeneration.toString(StandardCharsets.UTF_8);
StringBuilder textBuilder = new StringBuilder();
InputStream is = new ByteArrayInputStream(bufferingForWholeGeneration.toByteArray());
try (Reader reader = new BufferedReader(new InputStreamReader(is, 
Charset.forName(StandardCharsets.UTF_8.name())))) {
    int c = 0;
    while ((c = reader.read()) != -1) {
        textBuilder.append((char) c);
    }
} catch (IOException ex) {
    System.err.println(ex.getLocalizedMessage());
}
String str = textBuilder.toString();
return str;
Screenshot by Author | Left: File before edit | Right: File after edit

Before:

Map message = new HashMap<>();
message.put("role", "assistant");
message.put("content", generatedText);
response.choices = List.of(message);
return response;

After:

Map message = new HashMap<>();
message.put("role", "assistant");
message.put("content", generatedText);
response.choices = Arrays.asList(message);
return response;

File: Util.java

Screenshot by Author | Left: File before edit | Right: File after edit

Before: searchPaths.put(libraryName, List.of(librarySearchPath));

After: searchPaths.put(libraryName, Arrays.asList(librarySearchPath));

Note that any attempts to run the application at this point would be unsuccessful as the required native binaries (previously extracted) have yet to be loaded by the application.

Step 2. Ensure that native binaries are loaded from external folder

In the LLModel(Path modelPath) constructor in file LLModel.java, notice that in the if-else code block, the following lines of code are present in the else condition:

// Copy system libraries to Temp folder
Path tempLibraryDirectory = Util.copySharedLibraries();
library = Util.loadSharedLibrary(tempLibraryDirectory.toString());
library.llmodel_set_implementation_search_path(tempLibraryDirectory.toString() );

These are responsible for extracting all required native bindings (file extensions .dll and .so for Windows and MacOS respectively) contained within gpt4all-java-binding-1.1.5.jar when it was included in the project's classpath. Now that this JAR library has been removed, the native bindings would have to be loaded from another source instead. For this to occur,

  1. Create a new folder named "external" in the project folder.
  2. Place the extracted folder native and the model ggml-gpt4all-j-v1.3-groovy.bin into the newly created directory (external).
  3. In Application.java file, replace modelFilePath with:
String modelFilePath = System.getProperty("user.dir") + File.separator + "external" + File.separator + "ggml-gpt4all-j-v1.3-groovy.bin";

This updates the specified location of the model read by the application as well. At this point, the project directory structure should resemble this:

Illustration by Author | Depicts project directory structure. Note the respective folder levels relative to the main class in Application.java

Thereafter, modify the constructor class in LLModel.java –

  • Comment out the if-else block
  • Insert code snippet to check which OS application is running on
  • Create new variable libPath to refer to the exact folder path where the native bindings are located (folders should still be named based on system OS)
public LLModel(Path modelPath) {
    logger.info("Java bindings for gpt4all version: " + GPT4ALL_VERSION);
    if(library==null) {
        String osName = System.getProperty("os.name").toLowerCase();
        boolean isWindows = osName.startsWith("windows");
        boolean isMac = osName.startsWith("mac os x");
        boolean isLinux = osName.startsWith("linux");
        if(isWindows) osName = "windows";
        if(isMac) osName = "macos";
        if(isLinux) osName = "linux";
        String libPath=System.getProperty("user.dir") + File.separator + "external" + File.separator + "native" + File.separator +osName;    
        // if (LIBRARY_SEARCH_PATH != null){
        library = Util.loadSharedLibrary(libPath);
        library.llmodel_set_implementation_search_path(libPath);
        //   }  
        //   else {
        //   // Copy system libraries to Temp folder
        //   Path tempLibraryDirectory = Util.copySharedLibraries();
        //   library = Util.loadSharedLibrary(tempLibraryDirectory.toString());
        //
        //   library.llmodel_set_implementation_search_path(tempLibraryDirectory.toString() );
        // }
    }
    // ... (code continues below) ...

Step 3. Interim clean-up of source code

Apart from code changes made in steps 1 & 2, other code tweaks to be made in Application.java are:

  • Remove the console printout behaviour by commenting out System.out.print(validString); in LLModel.java
  • To remove the default break line before the bot response, include the following code snippet in Application.java –
char c = 10;
fullGeneration=fullGeneration.substring(fullGeneration.indexOf(c)+1);

Specifically, certain functionalities such as copySharedLibraries in Util.java and chunks of commented code can be now removed (since binaries are now loaded from an external folder). Note: For cleaned up versions of these 3 files (LLModel.java, Util.java and Application.java), feel free to retrieve them from here.

Assuming the above directory structure, proceed to launch application –

Image by Author | Note that ggml-gpt4all-j-v1.3-groovy.bin is now being loaded from the "external" folder instead.

If response is successfully generated and returned without errors, then application has now essentially become both portable and platform independent.

Step 4. Construct look and feel of chatbot response and GUI

Presently, the default prompt is hardcoded as "What is the meaning of life?" To input custom prompts, a simple chat interface shall be implemented with built-in Java Swing components. Proceed to create a new class "ChatPanel.java" with the below boilerplate –

Image by Author | Preview of simple chat interface rendered by the boilerplate

Since the application requires only one entry point, retain the main method in ChatPanel.java and remove the other in Application.java. Convert Application.java into an entity class instead (renamed to ChatApplication.java)—

  • Create an instance of ChatApplication in the main class ChatPanel
  • Call function getResponse() to parse in prompt text and return complete response generated
Diagram illustration by Author | Numbers in diagram showcase sequence of workflow and application logic

Demo Prompt: ❝What are some ways to become more resilient?❞

Screencapture by Author | Upon selecting [Send >>], the new prompt gets read and progress bar status becomes indeterminate until the full response is generated and output for display.

Step 5. Add a Splash Screen

Finally, you may have noticed that there is at least an interval of ~10 seconds before model is loaded in the first step. As such, it could be beneficial to have a splash screen to signal that application is still loading before the GUI displays itself.

While there are several ways to create a splash screen using Java Swing, I shall be using the component JWindow for this demo where the splash screen displays the following loading.gif

Image by Author | A loading GIF created from 9 frames merged into a GIF file

Include the following code snippet in ChatPanel.java:

private static final JWindow splashScreen;
private static final String dataURI=""; // replace value with the actual dataURI
static {
    GraphicsEnvironment grEnv = GraphicsEnvironment.getLocalGraphicsEnvironment();
    double w1=grEnv.getMaximumWindowBounds().getWidth();
    double h1=grEnv.getMaximumWindowBounds().getHeight();
    int w0=400; // width of gif file
    int h0=194;// height of gif file
    int leftW = (int) ((w1-w0)/2);
    int bottomH = (int) ((h1-h0)/2);

    byte[] fileBytes = Base64.getDecoder().decode(dataURI);
    Icon loadingIcon=new ImageIcon(fileBytes);

    splashScreen=new JWindow();
    splashScreen.setLocation(leftW, bottomH);
    splashScreen.setAlwaysOnTop(true);
    splashScreen.setSize(w0, h0);

    Container displayInfoWindowPane=splashScreen.getContentPane();
    displayInfoWindowPane.setLayout(new GridLayout());
    JLabel displayIconLabel=new JLabel();
    displayIconLabel.setIcon(loadingIcon);
    displayIconLabel.setVerticalAlignment(SwingConstants.CENTER);
    displayIconLabel.setHorizontalAlignment(SwingConstants.CENTER);
    displayIconLabel.setVerticalTextPosition(SwingConstants.BOTTOM);
    displayIconLabel.setHorizontalTextPosition(SwingConstants.CENTER);
    displayInfoWindowPane.add(displayIconLabel);

    splashScreen.setVisible(true);
}

To minimise the no. of external file dependencies, the GIF file graphics would need to be encoded into Base-64 Data URI format. For this task, I shall be re-using a web utility tool at encode_base64 I had developed in a previous post:

Screenshot by Author | Upload loading.gif file and proceed to copy the encoded data URL.

Assign the copied String to the variable dataURI (currently an empty String). Thereafter, remove the prefix data:image/gif;base64,***** from dataURI for encoded data to be read correctly.

*_The prefix is used solely to indicate the mime type of the data and should be excluded in this setting._

In the main method of ChatPanel.java, right after the line frame.setVisible(true);, add in the following line to remove the splash screen after chat interface appears:

splashScreen.dispose();

Run Application

Export the application (gpt4all.jar) as a runnable JAR file* and double-click it to launch the chatbot:

Screencapture by Author | Application file i.e. gpt4all.jar is launched and splash screen is displayed | Note that "external" folder containing both binaries and the model must be present.

*Runnable JAR can be generated depending on the IDE used. Else, there is an open-source utility OneJAR at SourceForge which performs the same function.

Tags: Artificial Intelligence Gpt Large Language Models Programming UX

Comment