52 Commits
Author SHA1 Message Date
Zacharias 2949a0d2fe Whonestly, i have forgoten what this contains, sooo ye :)
Removed Ollama integration
swaped it for llama.cpp integration
and made backend integration more generic/abstract.. soo ollama will return.. maybe.. probobly

some mopdules(GeniusAPI) dosent work due to not being ported yet.... and maybe will never be
2026-08-29 23:52:02 +02:00
Zacharias 209bde75e1 Added redirect to codeberg for issue tracking to README.md 2026-07-30 22:06:51 +02:00
Zacharias 5ae5920478 Moved versions of the "public facing" modules to the gradle.properties
Core:
- Fixed my promis of making Core work with multiple OllamaObjects
2026-07-30 21:24:06 +02:00
Zacharias b8280e6ec5 Refactored to Core 1.10.0
This includes chages to make less things static in the Core, instead using the Options Singelton instead
2026-07-30 20:49:06 +02:00
Zacharias 939fdb2211 Added support for Imagaes, Ollama model thinking, and streaming of the responce 2026-07-25 21:25:41 +02:00
Zacharias 663ab68172 !! PARTIAL COMMIT !!
This is a partial commit bc i felt like it...

This begain some implementations to support streaming from Ollama, and the ability to cancel a request.
2026-07-20 22:13:33 +02:00
Zacharias 330d7df389 Fixed some errors with the System prompt
Chaged java version to make this more compadible
2026-07-20 18:03:31 +02:00
Zacharias 4cd4367a19 Switching out toString returing a string JSON to toJSON that returns a JSONObject instead... and toString is not ment for this purpose 2026-07-19 23:05:48 +02:00
Zacharias 2defa32cb8 Making sure that Core.RPCP and plugin.tool.Tool.RPCP_SOURCE have the same
Updated Display to wait for Ollama to return before continuing
Updated GeniusTools.java to reflect correctly after "The Great Refector"
Temporarly comented out things from API after making Ollama be more threaded pending correction and expantion
2026-07-19 22:05:09 +02:00
Zacharias fc8a06bde5 Add LLMSystemPrompt as a unified system-prompt builder for OllamaObject
Replaces ad-hoc system-prompt string construction with a structured
builder (Identity, Context, Capabilities, Behavior, Output), plus
OllamaObject#setSystemMessage/setSystemPrompt for replacing the
system message post-construction instead of only at build time.
2026-07-19 21:47:18 +02:00
Zacharias 6e31e31718 Updated so you can dump all messages for an OllamaObject, add a system message, and add messages from a list 2026-07-05 21:19:55 +02:00
Zacharias 9c7ba9cb4f Made tool calling async as well, not yet tested 2026-06-28 17:58:44 +02:00
Zacharias caf60eddef Made Core#qurryOllama asynchronous instead to make so it doesn't lock up the calling thread, issue discovered in the (Ollama-chat)[https://git.server.4zellen.se/neurodock/Ollama-chat] Proof of Concept 2026-06-28 17:18:41 +02:00
Zacharias 3c6a75a587 Some small poking at things :)
dont midn the version bump, it's easier to bump versions then to get Maven to bahave
2026-06-28 15:46:57 +02:00
Zacharias 6d7088fd76 Renamed return type of Loader#getTools(Plugin) to OllamaFunctionTool from OllamaTool
Signed-off-by: zacharias <alienfromdia@proton.me>
2026-06-17 23:16:41 +02:00
Zacharias 0e31f15e03 Renamed the build.yml to build.yml.old as the backing gitea runner is not online.
Signed-off-by: zacharias <alienfromdia@proton.me>
2026-06-16 18:00:00 +02:00
Zacharias fcbb405a2e Updated Plugin-API version as it has recived updates since 0.1.0, but yet dine to be in 1.0 release
Updeaded the RPCP loader to use JarFile instead of a virtual FS

Signed-off-by: zacharias <alienfromdia@proton.me>
2026-06-16 17:58:33 +02:00
Zacharias b74c5e9e97 feat(plugin): scaffold RPCP plugin loading core
- Add JarFile-based plugin.json manifest reading and validation
- Implement reflection-based plugin class instantiation (Data or no-arg constructor)
- Wrap Tool objects into OllamaFunctionTool with basic parameter/response handling
- Implement ToolArguments storage and ToolResponse record
- Extract RPCP_SOURCE constant; add plugin name validation
- Update Data interface: getLoadedPlugins() returns LoadedPlugin[]
- Fix launcher package references (me.zacharias.chat → me.neurodock)

TODO: Extract FileSystem from JAR, populate PluginMetadata, complete
OllamaFunctionArgument↔ToolArguments wrapping, invoke plugin lifecycle hooks

Signed-off-by: zacharias <alienfromdia@proton.me>
2026-06-15 22:09:55 +02:00
Zacharias d016ad9a48 refactor(Core): decompose handleResponse and add ToolCallingRender system
- Extract handleResponse into focused helpers: processToolCall, findTool,
  reportToolNotFound, renderToolCalling, executeToolCall
- Introduce ToolCallingRender sealed interface (Suppress/Default/Custom)
  allowing tools to control how their invocations are rendered
- Add printToolCalling to PrintAdvanceMessageHandler contract
- Implement default tool calling rendering in PrintMessageHandler (blue text)
- Add WriteFileTool for file writing capabilities
- Fix "responce" → "response" typo across Java and Python files
- Improve PythonRunner docker error handling and output messages
- Update Gradle test configuration (maxHeapSize, test logging)

Signed-off-by: zacharias <alienfromdia@proton.me>
2026-06-15 14:55:09 +02:00
Zacharias ff0496eb61 Shit has happend, among the "shit" is working on the files stuff and refactoring Core.Source to reflect on the new names for the difrent types of tool sources 2026-06-15 00:20:07 +02:00
Zacharias b3cb4d86b6 Added early exit for unreachable Ollama 2026-05-28 13:06:37 +02:00
Zacharias ff45efbe67 Removed invalid import statment 2026-05-28 13:05:35 +02:00
Zacharias 9a43f8490e Removed invalid import statment 2026-05-28 12:52:22 +02:00
Zacharias 3395a591fb Update README.md
Updated the maven dependecies
Updated launcher dump example

Signed-off-by: Zacharias <1+zacharias@noreply.localhost>
2026-05-27 23:53:21 +02:00
Zacharias ab01ac80fb Update README.md
Fixed formating
2026-05-27 23:50:31 +02:00
Zacharias 4273f6c68c Update README.md
Noted the move of Maven dependecies
2026-05-27 23:50:13 +02:00
Zacharias 8b852bcf36 fixed some broken Javadoc after "The Great Refactor" 2026-05-27 23:40:10 +02:00
Zacharias f50c04c828 refactor: me.zacharias.chat → me.neurodock, org rename cleanup
- Renamed package from me.zacharias.chat to me.neurodock across all 8 modules
- Updated Gradle group from me.zacharias.neurodock to me.neurodock
- Updated README and other files to reflect new Gitea org URL (Chat_things → neurodock)

Dev note: 95 files touched. The Great Refactor is complete, long may it rest.
2026-05-27 23:34:22 +02:00
Zacharias 88c593c47d Added files to GIT that where missing
Dev note: Lowery, IDK why these where already added?
2026-05-27 23:06:31 +02:00
Zacharias 978cf8d7eb refactor(Core): decompose constructCore() into focused init methods
- Extracted Core#constructCore into: initDirectories(), ensureDir(String),
  initOllamaUrl(), initLogWriter(), rotateLogFile(), initScheduler(),
  initShutdownHook(), closeLogWriter(), saveMessages(),
  buildMessagesArray(), writeMessagesTo(File, JSONArray)
- Overloaded constructors for Core
- Cleaned up some Javadocs

Fixed error(AddArrayMemory)
- Line 51 used the wrong value

Updated minor version for(::Core)

Dev note: Long overdue — that generic init block was ugly and is finally
laid to rest.
2026-05-27 23:04:47 +02:00
Zacharias 931c274cd3 refactor: update CoreMemory to v2 and rename OllamaToolResponse
- Refactored `CoreMemory` to support versioning (v2) and dual-memory types: `MAPPED_MEMORY` (key-value) and `ARRAYED_MEMORY` (sequential).
- Added automatic migration logic for v1 memory files to v2.
- Introduced new memory tools: `AddArrayMemory`, `GetArrayMemory`, and `GetArrayedMemories`.
- Renamed `OllamaToolRespnce` to `OllamaToolResponse` and added `empty()` factory methods for better error handling.
- Updated multiple function tools (e.g., `GetMemoryFunction`, `GetMemoriesFunction`, `APITool`, `GetWikiPageText`) to use the new `OllamaToolResponse` and `Optional`-based memory retrieval.
- Improved null safety by adding `@NotNull` annotations to tool implementations.
2026-05-27 21:10:15 +02:00
Zacharias ac159c2498 Corrected the fact that
`OllamaPerameter.OllamaPerameterBuilder#addProperty(String, Type, String, boolean)` did not respect whether it's required or not
Corrected so `OllamaPerameter.OllamaPerameterBuilder#removeProperty(String)` also removes it from the required list
Corrected Javadocs
made OllamaPerameter.OllamaPerameterBuilder.Type#type a final
2026-05-27 20:20:27 +02:00
Zacharias 06cf1c766a Added a TODO for the memory package 2026-05-27 01:32:44 +02:00
Zacharias f6610777ae refactor: redesign plugin system and enhance message handling
- Move plugin and tool-related classes to a dedicated `Plugin-API` module.
- Introduce a new plugin lifecycle (`onInit`, `onEnable`, `onDisable`) and a more robust `Tool` API.
- Refactor `Core` to use `PrintAdvanceMessageHandler` for improved handling of assistant messages, tool responses, and errors.
- Rename `PluginLoader` to `Loader` and migrate it to `me.zacharias.chat.plugin.loader`.
- Update build configurations and Java toolchains across modules.
- Clean up obsolete plugin annotations and interfaces in the `Core` module.
2026-05-26 16:28:02 +02:00
Zacharias 339b176480 Updated README to add noting of maven dependency 2026-05-12 15:29:53 +02:00
Zacharias a33a37ec0a Updated OllamaFunctionTool.toString to reflect what the Ollama API actually wants
Added @NotNull annotation to OllamaFunctionTool.name and OllamaFunctionTool.parameters since the documentation reports as these being required

Made sure that OllamaFunctionTool.description is actually optional as the Ollama API docs define
2026-05-05 22:27:36 +02:00
Zacharias b1aa35e1b1 Updated depndencies for
- GradleAPI
- Launcher
- MALAPITool

Started finishing up the File handeling module in Core
- Added ReadFileTool.java which allowes to read files from this contained "file system"

modulized dependency for the API

Updated for Launcher.java to use class and method refrence to entry point to remove hard dependency

Added a @SafeVarargs to OllamaObject.java
made OllamaObject.OllamaObjectBuilder#addFileTools not throw IllegalArgumentException and instead add the tooling and return the builder

Added overloaded constructor OllamaToolErrorException#OllamaToolErrorException(String, Exception) to allow for forwarding of exceptions, although this is not recomended

PythonRunner
- Fixed bugs with it under Linux host(this MUST be refractord to support Windows and Mac hosts)
- Added a overwrite for default python print method to send prints over the external_tools socket to be put into output log for the runner
- Cleaned up some stuff
- Updated to properly refer to files
- Fixed cmd.sh generation
- Removed old "-c" addition to the command
- Removed old code related to how logs pulled from containers standerd output's
- Updated the generation of external_tools.py to respect pythons requirment of optinal arguments being after all required

Updated Tool#addTool to respect the standerd of spring boot's return model
2026-05-04 19:50:49 +02:00
Zacharias 7dfaffc862 Working on fixing the implementation of PythonRunner.java
added a global cache directory, not fully properly implemented, and dose not respect t.ex the FreeDesktop specifications for cache directories and files

OPS this is a partial commit! things might not add up on this commit.
2026-05-04 18:17:15 +02:00
Zacharias 96a6ed169e Extracted the external tool base for python runner
Fixed URL building in APITool.java.
Updated libraries for API, Launcher.
Started applying NotNull annotation to methods and arguments in Core.java.
Started handeling the big init block in Core.java.
Added overloaded constructor for Core.java and made Core.ollamaIP a variabled that's definible
Updated Tool.addTool return for 400 error.
2026-05-04 14:23:26 +02:00
Zacharias 943c8470a5 Honestly speaking... No clue what the updates are :/ 2026-01-17 14:30:26 +01:00
Zacharias 4262dd68c6 Enhanced tool management in OllamaObject and added Maven publishing configuration.
- Updated `OllamaObject` to support tool registration with sources.
- Improved error handling and debug logging in multiple classes.
- Added Maven plugin and publication setup in `build.gradle`.
- Updated version to `1.3`.

Signed-off-by: Zacharias <zacharias@4zellen.se>
2025-08-18 20:01:42 +02:00
Zacharias 2cf0428d2a Added legal disclaimers about scraping.. mostly to make sure i dont get into too mutch trouble about things 2025-08-05 01:33:48 +02:00
Zacharias bbf3c645b0 Added README.md to GeniusAPI 2025-08-05 00:51:35 +02:00
Zacharias 4015d37657 Merge remote-tracking branch 'origin/master' 2025-08-05 00:43:19 +02:00
Zacharias 5886d164b4 Core.java
- Added a safe handle for if Core#logWriter is not initialized to prevent craching

GetLyrics.java
- Slowly modifying the http headers to avoid package due to catching this as a Scrapper
- Added so we skip extra calls if the song is cached
2025-08-05 00:38:58 +02:00
Zacharias ab72e1ff78 Update README.md 2025-07-28 22:15:37 +02:00
Zacharias db53028d1e Fixed an issue in the config 2025-07-16 01:03:02 +02:00
Zacharias bf5ae6dd3c And again.... 2025-07-16 00:31:04 +02:00
Zacharias fbf7c504b1 Testing again... *sigh* 2025-07-16 00:29:46 +02:00
Zacharias 8426bcac37 CI is confusing 2025-07-16 00:28:37 +02:00
Zacharias 5724a2342d Merge remote-tracking branch 'origin/master' 2025-07-16 00:12:55 +02:00
Zacharias 95ac78b2d2 Testing gitea act_runner 2025-07-16 00:11:56 +02:00
149 changed files with 5533 additions and 3024 deletions
+22
View File
@@ -0,0 +1,22 @@
name: build
on:
push:
branches:
- master
jobs:
build:
runs-on: arch
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ gitea.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."
+1
View File
@@ -8,6 +8,7 @@ build/
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea
.idea/libraries/
*.iws
*.iml
Generated
+1 -1
View File
@@ -1 +1 @@
AI-test
neurodock
+1 -2
View File
@@ -5,7 +5,6 @@
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
@@ -14,7 +13,7 @@
<option value="$PROJECT_DIR$/Display" />
<option value="$PROJECT_DIR$/GeniusAPI" />
<option value="$PROJECT_DIR$/MALAPITool" />
<option value="$PROJECT_DIR$/MovieSugest" />
<option value="$PROJECT_DIR$/Plugin-API" />
<option value="$PROJECT_DIR$/WikipediaTool" />
<option value="$PROJECT_DIR$/launcher" />
</set>
+1 -1
View File
@@ -7,7 +7,7 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="graalvm-25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+13 -7
View File
@@ -1,22 +1,22 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.2'
id 'org.springframework.boot' version '4.1.0-M4'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'me.zacharias'
version = '1.0-SNAPSHOT'
version = APIVersion
dependencies {
implementation project(":Core")
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
implementation 'org.springframework.boot:spring-boot-starter-web:4.1.0-M4'
implementation 'org.springframework.boot:spring-boot-starter-webflux:4.1.0-M4'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.0-M1'
testImplementation 'org.springframework.boot:spring-boot-starter-test:4.1.0-M4'
//implementation 'org.springframework.boot:spring-boot-starter-actuator'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//runtimeOnly('org.springframework.boot:spring-boot-starter-web')
}
@@ -30,3 +30,9 @@ jar{
attributes 'Main-Class': 'me.zacharias.chat.api.APIApplication'
}
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(javaVersion)) // Set Java version
}
}
@@ -1,11 +1,10 @@
package me.zacharias.chat.api;
package me.neurodock.api;
import me.zacharias.chat.api.payload.request.NewQurryResponceHook;
import me.zacharias.chat.api.payload.request.NewToolRequest;
import me.zacharias.chat.core.Core;
import me.zacharias.chat.core.GlobalObjects;
import me.zacharias.chat.core.PrintMessageHandler;
import me.zacharias.chat.ollama.OllamaObject;
import me.neurodock.api.payload.request.NewQurryResponceHook;
import me.neurodock.api.payload.request.NewToolRequest;
import me.neurodock.core.Core;
import me.neurodock.core.PrintMessageHandler;
import me.neurodock.ollama.OllamaObject;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@@ -41,8 +40,9 @@ public class APIApplication {
instance = this;
if(GlobalObjects.getObject("core") instanceof Core coreInstance) {
this.core = coreInstance;
if(false) {
// TODO: This needs to be properly refactored.
//this.core = coreInstance;
} else {
this.core = new Core(new PrintMessageHandler() {
@Override
@@ -1,13 +1,15 @@
package me.zacharias.chat.api;
package me.neurodock.api;
import me.zacharias.chat.api.payload.ToolArgument;
import me.zacharias.chat.api.payload.request.NewToolRequest;
import me.zacharias.chat.api.payload.webhook.responce.APIToolResponse;
import me.zacharias.chat.ollama.OllamaFunctionArgument;
import me.zacharias.chat.ollama.OllamaFunctionTool;
import me.zacharias.chat.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaToolRespnce;
import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException;
import me.neurodock.api.payload.ToolArgument;
import me.neurodock.api.payload.request.NewToolRequest;
import me.neurodock.api.payload.webhook.responce.APIToolResponse;
import me.neurodock.ollama.OllamaFunctionArgument;
import me.neurodock.ollama.OllamaFunctionTool;
import me.neurodock.ollama.OllamaPerameter;
import me.neurodock.ollama.OllamaToolResponse;
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
import org.jetbrains.annotations.NotNull;
import org.jspecify.annotations.NonNull;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
@@ -45,7 +47,7 @@ public class APITool extends OllamaFunctionTool {
}
@Override
public String name() {
public @NonNull String name() {
return name;
}
@@ -55,7 +57,7 @@ public class APITool extends OllamaFunctionTool {
}
@Override
public OllamaPerameter parameters() {
public @NotNull OllamaPerameter parameters() {
OllamaPerameter.OllamaPerameterBuilder parameter = OllamaPerameter.builder();
for (ToolArgument argument : arguments) {
parameter.addProperty(argument.getName(), argument.getType(), argument.getDescription(), argument.isRequired());
@@ -64,8 +66,8 @@ public class APITool extends OllamaFunctionTool {
}
@Override
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(requestUrl);
public @NonNull OllamaToolResponse function(OllamaFunctionArgument... args) {
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(requestUrl);
for (OllamaFunctionArgument arg : args) {
builder.queryParam(arg.argument(), arg.value());
}
@@ -80,7 +82,7 @@ public class APITool extends OllamaFunctionTool {
if(response.getBody().getError() != null && !response.getBody().getError().isEmpty())
throw new OllamaToolErrorException(name, response.getBody().getError());
return new OllamaToolRespnce(name, response.getBody().getResponse());
return new OllamaToolResponse(name, response.getBody().getResponse());
}
else {
if(response.getBody() == null) {
@@ -1,8 +1,7 @@
package me.zacharias.chat.api;
package me.neurodock.api;
import me.zacharias.chat.api.payload.webhook.WebhookError;
import me.neurodock.api.payload.webhook.WebhookError;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
public class QuerryResponceEndpoint {
@@ -1,6 +1,6 @@
package me.zacharias.chat.api.condations;
package me.neurodock.api.condations;
import me.zacharias.chat.core.LaunchOptions;
import me.neurodock.core.LaunchOptions;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
@@ -1,11 +1,11 @@
package me.zacharias.chat.api.controllers;
package me.neurodock.api.controllers;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import me.zacharias.chat.api.APIApplication;
import me.zacharias.chat.api.condations.EnableIfNotDisplay;
import me.zacharias.chat.api.payload.request.NewQurryResponceHook;
import me.zacharias.chat.ollama.OllamaMessage;
import me.zacharias.chat.ollama.OllamaMessageRole;
import me.neurodock.api.APIApplication;
import me.neurodock.api.condations.EnableIfNotDisplay;
import me.neurodock.api.payload.request.NewQurryResponceHook;
import me.neurodock.ollama.OllamaMessage;
import me.neurodock.ollama.OllamaMessageRole;
import org.springframework.context.annotation.Conditional;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -28,7 +28,7 @@ public class Message {
}
Thread t = new Thread(() -> {
apiApplication.getCore().getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.USER, query));
apiApplication.getCore().handleResponce(apiApplication.getCore().qurryOllama());
//apiApplication.getCore().handleResponse(apiApplication.getCore().qurryOllama());
});
t.start();
return ResponseEntity.ok("Query received");
@@ -1,10 +1,9 @@
package me.zacharias.chat.api.controllers;
package me.neurodock.api.controllers;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import me.zacharias.chat.api.APIApplication;
import me.zacharias.chat.api.payload.request.NewToolRequest;
import me.zacharias.chat.api.payload.response.NewToolResponse;
import me.neurodock.api.APIApplication;
import me.neurodock.api.payload.request.NewToolRequest;
import me.neurodock.api.payload.response.NewToolResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -34,7 +33,7 @@ public class Tool {
}
return ResponseEntity.ok(new NewToolResponse(request.getName(), request.getDescription(), arguments));
} else {
return new ResponseEntity<>("Tool already exists", null, 400);
return ResponseEntity.badRequest().body("Tool already exists");
}
}
}
@@ -1,4 +1,4 @@
package me.zacharias.chat.api.payload;
package me.neurodock.api.payload;
public class MessageResponce {
private final String message;
@@ -1,6 +1,6 @@
package me.zacharias.chat.api.payload;
package me.neurodock.api.payload;
import me.zacharias.chat.ollama.OllamaPerameter;
import me.neurodock.ollama.OllamaPerameter;
public class ToolArgument {
String name;
@@ -1,4 +1,4 @@
package me.zacharias.chat.api.payload;
package me.neurodock.api.payload;
public class ToolRequest {
private final String name;
@@ -1,4 +1,4 @@
package me.zacharias.chat.api.payload.request;
package me.neurodock.api.payload.request;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -1,6 +1,6 @@
package me.zacharias.chat.api.payload.request;
package me.neurodock.api.payload.request;
import me.zacharias.chat.api.payload.ToolArgument;
import me.neurodock.api.payload.ToolArgument;
public class NewToolRequest {
/**
@@ -1,4 +1,4 @@
package me.zacharias.chat.api.payload.response;
package me.neurodock.api.payload.response;
public class NewToolResponse {
private String name;
@@ -1,4 +1,4 @@
package me.zacharias.chat.api.payload.webhook;
package me.neurodock.api.payload.webhook;
public class WebhookError {
private final String originalUrl;
@@ -1,4 +1,4 @@
package me.zacharias.chat.api.payload.webhook.responce;
package me.neurodock.api.payload.webhook.responce;
public class APIToolResponse {
private final String response;
+20 -4
View File
@@ -2,15 +2,31 @@ plugins {
id 'java-library'
}
group = 'me.zacharias'
version = '1.0-SNAPSHOT'
version = coreVersion
dependencies {
implementation project(":Plugin-API")
api "org.json:json:20250107"
implementation 'org.graalvm.polyglot:polyglot:25.1.3'
implementation 'org.graalvm.polyglot:js:25.1.3'
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21)) // Set Java version
languageVersion.set(JavaLanguageVersion.of(javaVersion)) // Set Java version
}
}
// Tell shadow not to interfere with the main publication
shadowJar {
archiveClassifier = 'all' // keeps shadow as -all.jar, not the main artifact
}
configurations {
[apiElements, runtimeElements].each {
it.outgoing.artifacts.removeIf {
it.buildDependencies.getDependencies(null).contains(shadowJar)
}
}
}
@@ -0,0 +1,63 @@
package me.neurodock.backend.open.ai;
import me.neurodock.llm.Message;
import org.json.JSONArray;
public class OpenAIAssistentMessage extends Message {
private Reason endReason;
public OpenAIAssistentMessage(Role role, Object content, Reason endReason) {
super(role, content);
this.endReason = endReason;
}
public OpenAIAssistentMessage(Role role, Object content, JSONArray toolCalls, Reason endReason) {
super(role, content, toolCalls);
this.endReason = endReason;
}
public OpenAIAssistentMessage(Role role, String toolID, Object content, Reason endReason) {
super(role, toolID, content);
this.endReason = endReason;
}
public Reason getEndReason() {
return endReason;
}
enum Reason {
STOP,
LENGTH,
TOOL_CALLS,
CONTENT_FILTER,
FUNCTION_CALL,
/**
* Still streaming, or null
*/
NULL;
public static Reason fromString(String reason) {
if(reason == null) {
return NULL;
}
else if(reason.equalsIgnoreCase("STOP")) {
return STOP;
}
else if(reason.equalsIgnoreCase("LENGTH")) {
return LENGTH;
}
else if(reason.equalsIgnoreCase("TOOL_CALLS")) {
return TOOL_CALLS;
}
else if(reason.equalsIgnoreCase("CONTENT_FILTER")) {
return CONTENT_FILTER;
}
else if(reason.equalsIgnoreCase("FUNCTION_CALL")) {
return FUNCTION_CALL;
}
else {
return NULL;
}
}
}
}
@@ -0,0 +1,629 @@
package me.neurodock.backend.open.ai;
import me.neurodock.llm.*;
import me.neurodock.llm.exceptions.ModelNotFoundException;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.Tool;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.serializer.ToolSerializer;
import me.neurodock.llm.tools.serializer.ToolToJSON;
import me.xdrop.fuzzywuzzy.FuzzySearch;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
public class OpenAIModel implements StreamingModel {
/** Minimum fuzzy-match ratio (0-100) required for a model name to be considered a match. */
private static final int MIN_MODEL_MATCH_RATIO = 40;
private static final ToolSerializer DEFAULT_SERIALIZER = new ToolSerializer();
private URL backendURL;
private String model;
private HttpURLConnection activeConnection;
private AtomicBoolean cancelled = new AtomicBoolean(false);
static {
DEFAULT_SERIALIZER.addSerializer(FunctionTool.class, (ToolToJSON<FunctionTool>) tool -> {
JSONObject result = new JSONObject();
result.put("type", "function");
JSONObject function = new JSONObject();
ToolParameters parms = tool.parameters();
JSONObject parameters = new JSONObject();
parameters.put("type", "object");
parameters.put("required", parms.getRequired());
JSONObject properties = new JSONObject();
parms.getProperties().forEach((str,prop) -> {
JSONObject obj = new JSONObject();
obj.put("description", prop.description());
obj.put("type", prop.type().name().toLowerCase(Locale.ROOT));
properties.put(str, obj);
});
parameters.put("properties", properties);
function.put("parameters", parameters);
function.put("name", tool.name());
function.put("description", tool.description());
result.put("function", function);
return result;
});
}
/**
* Constructs an {@code me.neurodock.backend.open.ai.OpenAIModel} bound to a specific backend and model.
* <p>
* This performs a blocking request against the backend to fetch the list of
* available models, then fuzzy-matches {@code modelName} against the returned
* model IDs, selecting the closest match above a minimum confidence threshold.
*
* @param backendURI the base URL of the OpenAI-compatible backend
* (e.g. {@code http://localhost:11434})
* @param modelName the model name, or partial name, to fuzzy-match against
* the backend's available models
*
* @apiNote This {@code me.neurodock.backend.open.ai.OpenAIModel} does NOT support OpenAI's ChatGPT or
* OpenAI's hosted API — it sends no API key. Pointing {@code backendURI} at
* {@code api.openai.com} will not be rejected here; the request will instead
* fail server-side (typically with a 401 Unauthorized), which {@link #queryJSON}
* surfaces as a {@code null} response, causing this constructor to throw
* {@link ModelNotFoundException} — even though no model is actually missing.
* This class is intended for locally hosted, non-keyed OpenAI-compatible
* APIs only.
*
* @throws ModelNotFoundException if no models could be retrieved from the
* backend (e.g. the server is unreachable, misconfigured, or — see
* {@code @apiNote} — rejects the request due to a missing API key),
* or if none of the returned models sufficiently match {@code modelName}
* @throws RuntimeException if {@code backendURI} is malformed
*/
public OpenAIModel(@NotNull String backendURI, @NotNull String modelName) throws ModelNotFoundException {
try {
modelName = Objects.requireNonNull(modelName, "modelName cannot be null");
this.backendURL = new URI(Objects.requireNonNull(backendURI, "backendURI cannot be null")).toURL();
JSONObject models = queryJSON(null, "v1/models", "GET");
if(models == null || models.isEmpty()) {
throw new ModelNotFoundException("No models found");
}
int highestMatch = 0;
String highestMatchString = "";
for(Object obj : models.optJSONArray("data", new JSONArray()))
{
if(!(obj instanceof JSONObject)) continue;
JSONObject model = (JSONObject)obj;
int matchRatio = FuzzySearch.partialRatio(modelName, model.getString("id"));
if(matchRatio > highestMatch) {
highestMatch = matchRatio;
highestMatchString = model.getString("id");
}
// 100% match... might as well return?
if(matchRatio == 100) {
break;
}
}
// Too low of a match, failing
if(highestMatch < MIN_MODEL_MATCH_RATIO) {
throw new ModelNotFoundException(modelName, backendURI);
}
this.model = highestMatchString;
// Creating a shutdown hook to unload the model after the program shutdowns, this is to converse RAM!
Runtime.getRuntime().addShutdownHook(new Thread(this::unload));
} catch (URISyntaxException e) {
throw new RuntimeException(e);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
/**
* Unloads the current model.<br>
* And ignoring any errors
*/
public void unload()
{
try {
queryJSON(new JSONObject().put("model", model), "/models/unload", "POST");
}
catch (Exception _)
{
// We catch the exception, but since expected exception here is SOLY a 400 Bad Request for when a model was already unloaded, we throw it out.
}
}
/**
* Sends the conversation history in {@code obj} to the backend and requests
* the next assistant response.
* <p>
* This performs the request asynchronously; the returned future completes
* once the backend has responded (or completes exceptionally if the request
* fails at the transport level — see {@link #queryJSON}).
*
* @param obj the chat context, containing the full conversation history
* to send to the model
* @param tools the tools available to the model for this request (currently
* unused — tool support is not yet implemented)
* @return a future resolving to the assistant's reply as a {@link Message}.
* If the backend returns no choices, or the response is otherwise
* malformed/missing, resolves to a {@link Message} with empty content
* rather than throwing.
*/
@Override
public CompletableFuture<Message> qurryModel(ChatObject obj, List<Tool> tools) {
return CompletableFuture.supplyAsync(() -> {
JSONObject request = new JSONObject();
request.put("model", model);
JSONArray messages = new JSONArray();
for (Message msg : obj.getConversation()) {
JSONObject message = new JSONObject();
String role = msg.getRole().name().toLowerCase(Locale.ROOT);
message.put("role", role);
message.put("content", msg.getContent());
if (msg.getRole() == Message.Role.TOOL) {
message.put("tool_call_id", msg.getToolID());
}
messages.put(message);
}
request.put("messages", messages);
JSONArray toolArray = new JSONArray();
if(tools == null || tools.isEmpty()) {
for(Tool tool : tools) {
toolArray.put(DEFAULT_SERIALIZER.serialize(tool));
}
}
JSONObject response = queryJSON(request, "v1/chat/completions", "POST");
if(response == null) {
response = new JSONObject();
}
JSONObject message = response
.optJSONArray("choices", new JSONArray())
.optJSONObject(0, new JSONObject())
.optJSONObject("message", new JSONObject());
String content = message.optString("content", "");
String roleStr = message.optString("role", "assistant");
OpenAIAssistentMessage.Reason endReason = OpenAIAssistentMessage.Reason.fromString(response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).optString("endReason", null));
Message msg;
if(response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).has("tool_calls")) {
msg = new OpenAIAssistentMessage(
Message.Role.valueOf(roleStr.toUpperCase(Locale.ROOT)),
(Object) content,
response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).getJSONArray("tool_calls"),
endReason
);
}
else
{
msg = new OpenAIAssistentMessage(Message.Role.valueOf(roleStr.toUpperCase(Locale.ROOT)), content, endReason);
}
return msg;
});
}
@Override
public CompletableFuture<Message> singleFire(Message msg) {
return CompletableFuture.supplyAsync(() -> {
JSONObject request = new JSONObject();
request.put("model", model);
JSONArray messages = new JSONArray();
JSONObject message = new JSONObject();
String role = msg.getRole().name().toLowerCase(Locale.ROOT);
message.put("role", role);
message.put("content", msg.getContent());
if (msg.getRole() == Message.Role.TOOL) {
message.put("tool_call_id", msg.getToolID());
}
messages.put(message);
request.put("messages", messages);
JSONArray toolArray = new JSONArray();
JSONObject response = queryJSON(request, "v1/chat/completions", "POST");
if(response == null) {
response = new JSONObject();
}
JSONObject resMessage = response
.optJSONArray("choices", new JSONArray())
.optJSONObject(0, new JSONObject())
.optJSONObject("message", new JSONObject());
String content = resMessage.optString("content", "");
String roleStr = resMessage.optString("role", "assistant");
OpenAIAssistentMessage.Reason endReason = OpenAIAssistentMessage.Reason.fromString(response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).optString("endReason", null));
return new OpenAIAssistentMessage(Message.Role.valueOf(roleStr.toUpperCase(Locale.ROOT)), content, endReason);
});
}
@Override
public CompletableFuture<Message> singleFire(Message msg, List<Tool> tools) {
return CompletableFuture.supplyAsync(() -> {
JSONObject request = new JSONObject();
request.put("model", model);
JSONArray messages = new JSONArray();
JSONObject message = new JSONObject();
String role = msg.getRole().name().toLowerCase(Locale.ROOT);
message.put("role", role);
message.put("content", msg.getContent());
if (msg.getRole() == Message.Role.TOOL) {
message.put("tool_call_id", msg.getToolID());
}
messages.put(message);
request.put("messages", messages);
JSONArray toolArray = new JSONArray();
if(tools == null || tools.isEmpty()) {
for(Tool tool : tools) {
toolArray.put(DEFAULT_SERIALIZER.serialize(tool));
}
}
JSONObject response = queryJSON(request, "v1/chat/completions", "POST");
if(response == null) {
response = new JSONObject();
}
JSONObject resMessage = response
.optJSONArray("choices", new JSONArray())
.optJSONObject(0, new JSONObject())
.optJSONObject("message", new JSONObject());
String content = resMessage.optString("content", "");
String roleStr = resMessage.optString("role", "assistant");
OpenAIAssistentMessage.Reason endReason = OpenAIAssistentMessage.Reason.fromString(response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).optString("endReason", null));
return new OpenAIAssistentMessage(Message.Role.valueOf(roleStr.toUpperCase(Locale.ROOT)), content, endReason);
});
}
@Override
public ToolSerializer getToolSerializer() {
return DEFAULT_SERIALIZER;
}
public static ToolSerializer getDefaultToolSerializer() {
return DEFAULT_SERIALIZER;
}
public void cancel()
{
cancelled.set(true);
HttpURLConnection conn = activeConnection;
if (conn != null) {
conn.disconnect();
}
}
private JSONObject queryJSON(JSONObject payload, String endpoint, String method) {
return Objects.requireNonNull(queryJSON(payload, endpoint, method, null)).join();
}
/**
* Sends a blocking HTTP request to the given endpoint on the configured backend
* and parses the response body as JSON.
*
* @param payload the JSON request body to send, or {@code null} to send no body
* @param endpoint the endpoint to request, resolved against {@link #backendURL}
* (e.g. {@code "v1/models"})
* @param method the HTTP method to use (e.g. {@code "GET"}, {@code "POST"})
* @return the parsed {@link JSONObject} response body if the request succeeds
* with a 200 status, or {@code null} if a non-200 status is received
*
* @throws RuntimeException if an I/O error occurs while communicating with
* the backend, or if {@code endpoint} cannot be resolved into a valid URL
*/
@Nullable
public CompletableFuture<JSONObject> queryJSON(JSONObject payload, String endpoint, String method,
Consumer<JSONObject> onChunk) {
return CompletableFuture.supplyAsync(() -> {
HttpURLConnection connection = null;
try {
URL url = backendURL.toURI().resolve(endpoint).toURL();
connection = (HttpURLConnection) url.openConnection();
activeConnection = connection;
if (cancelled.get()) {
throw new CancellationException("Query cancelled before request was sent");
}
boolean isStreaming = onChunk != null;
connection.setRequestMethod(method);
connection.setRequestProperty("Content-Type", "application/json");
if (isStreaming) {
connection.setRequestProperty("Accept", "text/event-stream");
}
connection.setDoOutput(true);
connection.setConnectTimeout(80 * 1000);
if (payload != null) {
if (isStreaming) {
payload.put("stream", true);
}
String payloadString = payload.toString().replace("\n", "\\n");
try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
wr.write(payloadString.getBytes(StandardCharsets.UTF_8));
wr.flush();
}
}
int responseCode = connection.getResponseCode();
StringBuilder rawErrorOrDump = new StringBuilder();
JSONObject result;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
if (isStreaming) {
result = readStream(reader, onChunk);
} else {
// Non-streaming: OpenAI-compatible endpoints return one JSON object body
StringBuilder messageContent = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
if (cancelled.get()) throw new CancellationException("Query cancelled mid-response");
messageContent.append(line);
}
result = new JSONObject(messageContent.toString());
}
} catch (IOException ex) {
if (cancelled.get()) throw new CancellationException("Query cancelled mid-response");
InputStream errorStream = connection.getErrorStream();
if (errorStream != null) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream))) {
String line;
while ((line = reader.readLine()) != null) rawErrorOrDump.append(line);
}
}
result = null;
}
if (responseCode == HttpURLConnection.HTTP_OK) {
return result;
} else {
throw new RuntimeException("HTTP Response code - " + responseCode);
}
} catch (IOException | URISyntaxException e) {
if (cancelled.get()) throw new CancellationException("Query cancelled");
throw new RuntimeException(e);
} finally {
if (connection != null) connection.disconnect();
activeConnection = null;
}
});
}
/**
* Consumes an OpenAI-style SSE stream, dispatching each parsed chunk to
* {@code onChunk} and accumulating the deltas into a single final
* {@link JSONObject} shaped like a non-streaming chat completion response
* ({@code choices[0].message.content} holding the full assembled text).
*/
private JSONObject readStream(BufferedReader reader, Consumer<JSONObject> onChunk) throws IOException {
StringBuilder messageContent = new StringBuilder();
JSONObject last = null;
String role = "assistant";
String line;
while ((line = reader.readLine()) != null) {
if (cancelled.get()) throw new CancellationException("Query cancelled mid-response");
if (line.isBlank() || !line.startsWith("data:")) continue;
String data = line.substring(5).trim();
if (data.equals("[DONE]")) break;
JSONObject chunk = new JSONObject(data);
onChunk.accept(chunk);
last = chunk;
JSONObject choice = chunk.getJSONArray("choices").getJSONObject(0);
JSONObject delta = choice.getJSONObject("delta");
if (delta.has("role") && delta.get("role") != null)
{
role = delta.optString("role");
}
if (delta.has("content"))
{
messageContent.append(delta.optString("content", ""));
}
}
if (last == null) return null;
// Re-shape the final chunk into a single OpenAI-style response object,
// so callers get the same shape whether they streamed or not.
JSONObject finalObj = new JSONObject(last.toString());
JSONObject message = new JSONObject();
message.put("role", role);
message.put("content", messageContent.toString());
finalObj.getJSONArray("choices").getJSONObject(0).put("message", message);
return finalObj;
}
@Override
public CompletableFuture<Message> qurryModel(ChatObject obj, List<Tool> tools, Consumer<JSONObject> chunkConsumer) {
return CompletableFuture.supplyAsync(() -> {
JSONObject request = new JSONObject();
request.put("model", model);
JSONArray messages = new JSONArray();
for (Message msg : obj.getConversation()) {
JSONObject message = new JSONObject();
String role = msg.getRole().name().toLowerCase(Locale.ROOT);
message.put("role", role);
message.put("content", msg.getContent());
if (msg.getRole() == Message.Role.TOOL) {
message.put("tool_call_id", msg.getToolID());
}
messages.put(message);
}
request.put("messages", messages);
JSONArray toolArray = new JSONArray();
if(tools == null || tools.isEmpty()) {
for(Tool tool : tools) {
toolArray.put(DEFAULT_SERIALIZER.serialize(tool));
}
}
JSONObject response = queryJSON(request, "v1/chat/completions", "POST", chunkConsumer).join();
if(response == null) {
response = new JSONObject();
}
JSONObject message = response
.optJSONArray("choices", new JSONArray())
.optJSONObject(0, new JSONObject())
.optJSONObject("message", new JSONObject());
String content = message.optString("content", "");
String roleStr = message.optString("role", "assistant");
OpenAIAssistentMessage.Reason endReason = OpenAIAssistentMessage.Reason.fromString(response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).optString("endReason", null));
Message msg;
if(response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).has("tool_calls")) {
msg = new OpenAIAssistentMessage(
Message.Role.valueOf(roleStr.toUpperCase(Locale.ROOT)),
(Object) content,
response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).getJSONArray("tool_calls"),
endReason
);
}
else
{
msg = new OpenAIAssistentMessage(Message.Role.valueOf(roleStr.toUpperCase(Locale.ROOT)), content, endReason);
}
return msg;
});
}
@Override
public CompletableFuture<Message> singleFire(Message msg, Consumer<JSONObject> chunkConsumer) {
return CompletableFuture.supplyAsync(() -> {
JSONObject request = new JSONObject();
request.put("model", model);
JSONArray messages = new JSONArray();
JSONObject message = new JSONObject();
String role = msg.getRole().name().toLowerCase(Locale.ROOT);
message.put("role", role);
message.put("content", msg.getContent());
if (msg.getRole() == Message.Role.TOOL) {
message.put("tool_call_id", msg.getToolID());
}
messages.put(message);
request.put("messages", messages);
JSONArray toolArray = new JSONArray();
JSONObject response = queryJSON(request, "v1/chat/completions", "POST", chunkConsumer).join();
if(response == null) {
response = new JSONObject();
}
JSONObject resMessage = response
.optJSONArray("choices", new JSONArray())
.optJSONObject(0, new JSONObject())
.optJSONObject("message", new JSONObject());
String content = resMessage.optString("content", "");
String roleStr = resMessage.optString("role", "assistant");
OpenAIAssistentMessage.Reason endReason = OpenAIAssistentMessage.Reason.fromString(response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).optString("endReason", null));
return new OpenAIAssistentMessage(Message.Role.valueOf(roleStr.toUpperCase(Locale.ROOT)), content, endReason);
});
}
@Override
public CompletableFuture<Message> singleFire(Message msg, List<Tool> tools, Consumer<JSONObject> chunkConsumer) {
return CompletableFuture.supplyAsync(() -> {
JSONObject request = new JSONObject();
request.put("model", model);
JSONArray messages = new JSONArray();
JSONObject message = new JSONObject();
String role = msg.getRole().name().toLowerCase(Locale.ROOT);
message.put("role", role);
message.put("content", msg.getContent());
if (msg.getRole() == Message.Role.TOOL) {
message.put("tool_call_id", msg.getToolID());
}
messages.put(message);
request.put("messages", messages);
JSONArray toolArray = new JSONArray();
if(tools == null || tools.isEmpty()) {
for(Tool tool : tools) {
toolArray.put(DEFAULT_SERIALIZER.serialize(tool));
}
}
JSONObject response = queryJSON(request, "v1/chat/completions", "POST", chunkConsumer).join();
if(response == null) {
response = new JSONObject();
}
JSONObject resMessage = response
.optJSONArray("choices", new JSONArray())
.optJSONObject(0, new JSONObject())
.optJSONObject("message", new JSONObject());
String content = resMessage.optString("content", "");
String roleStr = resMessage.optString("role", "assistant");
OpenAIAssistentMessage.Reason endReason = OpenAIAssistentMessage.Reason.fromString(response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).optString("endReason", null));
return new OpenAIAssistentMessage(Message.Role.valueOf(roleStr.toUpperCase(Locale.ROOT)), content, endReason);
});
}
}
@@ -0,0 +1,634 @@
package me.neurodock.core;
import me.neurodock.core.memory.*;
import me.neurodock.llm.ChatObject;
import me.neurodock.llm.Message;
import me.neurodock.llm.Model;
import me.neurodock.llm.StreamingModel;
import me.neurodock.llm.exceptions.ToolException;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.Tool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolResponse;
import me.neurodock.plugin.Data;
import me.neurodock.plugin.LoadedPlugin;
import me.neurodock.plugin.loader.Loader;
import me.neurodock.plugin.exceptions.PluginLoadingException;
import org.jetbrains.annotations.NotNull;
import org.json.JSONArray;
import org.json.JSONObject;
import javax.naming.OperationNotSupportedException;
import java.io.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* The Main class for the System, responsible for managing the OllamaObject, tools, and the Ollama API.
*/
public class Core {
/**
* The file to write the logs to.
*/
private static File logFile = new File("./logs/latest.log");
private static File logDir = new File("./logs");
/**
* The writer to write the logs to.
*/
private static BufferedWriter logWriter;
/**
* The scheduler to schedule the log flushing.
*/
private ScheduledExecutorService scheduler;
/**
* The OllamaObject to use.
*/
private Model model;
/**
* The list of tools to use.
*/
private final ArrayList<Tool> tools = new ArrayList<>();
private ChatObject chatObject = new ChatObject();
/**
* Used to cancel/stop a responce, only usefull if
*/
private AtomicBoolean cancelled = new AtomicBoolean(false);
/**
* The PrintMessageHandler to use.
*/
private final PrintAdvanceMessageHandler printMessageHandler;
/**
* Creates a new instance of Core with the provided PrintMessageHandler,
* defaulting the Ollama backend to {@code localhost}.
*
* @param printMessageHandler The PrintMessageHandler to use as the default output
*/
public Core(@NotNull PrintAdvanceMessageHandler printMessageHandler)
{
this.printMessageHandler = printMessageHandler;
initDirectories();
initLogWriter();
initScheduler();
initShutdownHook();
}
public static void setLogDirectory(String logDirectory)
{
logDir = new File(logDirectory);
if (!logDir.exists())
{
logDir.mkdir();
}
logFile = new File(logDirectory, "latest.log");
}
/**
* Creates the base directories required by the application.
* <p>
* TODO: These paths should be resolved to OS-appropriate locations
* rather than being relative to the working directory.
*/
private void initDirectories() {
ensureDir(logDir.getAbsolutePath());
ensureDir(Options.getInstance().getDataDir() + "/messages");
Options.getInstance().initiateDirectories();
}
/**
* Ensures that a directory exists at the given path, creating it if it does not.
*
* @param path The path of the directory to create
*/
public static void ensureDir(String path) {
File dir = new File(path);
if (!dir.exists()) dir.mkdir();
}
/**
* Initializes the {@link #logWriter}, rotating any pre-existing log file beforehand.
*
* @throws RuntimeException If the log file cannot be created or opened for writing
* @see #rotateLogFile()
*/
private void initLogWriter() {
try {
if(!logDir.exists()) {
logDir.mkdir();
}
if (logFile.exists()) {
rotateLogFile();
}
logWriter = new BufferedWriter(new FileWriter(logFile));
} catch (IOException e) {
throw new RuntimeException("Failed to initialize log writer", e);
}
}
/**
* Rotates the existing log file by renaming it to a timestamped filename derived from
* its first line, then recreates {@link #logFile} as a fresh {@code latest.log}.
* <p>
* If the existing log file is empty, it is simply deleted and recreated.
*
* @throws IOException If the log file cannot be read, renamed, deleted, or recreated
*/
private void rotateLogFile() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(logFile))) {
String line = br.readLine();
if (line != null) {
String date = line.substring(0, line.indexOf(">")).replaceAll("[/:]", "-");
logFile.renameTo(new File(logDir, date + ".log"));
logFile = new File(logDir,"/latest.log");
} else {
System.out.println("Existing log file is empty, overwriting it!");
logFile.delete();
}
}
logFile.createNewFile();
}
/**
* Initializes the {@link #scheduler} and schedules periodic flushing of {@link #logWriter}
* to disk every 3 minutes.
*/
private void initScheduler() {
this.scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
try {
logWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
}, 0, 3, TimeUnit.MINUTES);
}
/**
* Registers a JVM shutdown hook that shuts down the {@link #scheduler},
* flushes and closes the {@link #logWriter}, and persists the current
* session's messages to disk.
*
* @see #closeLogWriter()
* @see #saveMessages()
*/
private void initShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
scheduler.shutdownNow();
closeLogWriter();
saveMessages();
}));
}
/**
* Flushes and closes the {@link #logWriter}.
* <p>
* Any {@link IOException} thrown during this process is suppressed, as the writer
* may already be closed by the time the shutdown hook runs.
*/
private void closeLogWriter() {
try {
logWriter.flush();
logWriter.close();
} catch (IOException ignore) {
System.out.println("Failed to flush log file, but that is not a problem.");
}
}
/**
* Persists the current session's messages from the {@link Model} to two locations:
* a timestamped archive file under {@code ./messages/}, and a rolling {@code messages.json}
* under {@link Options#getDataDir()} for resuming the session later.
*
* @see #buildMessagesArray()
* @see #writeMessagesTo(File, JSONArray)
*/
private void saveMessages() {
JSONArray messages = buildMessagesArray();
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd_HH-mm-ss"));
writeMessagesTo(new File(Options.getInstance().getDataDir().getAbsolutePath()+"/messages/" + timestamp + ".json"), messages);
writeMessagesTo(new File(Options.getInstance().getDataDir(), "messages.json"), messages);
}
/**
* Collects all messages from the {@link Model} and serializes them into a {@link JSONArray}.
*
* @return A {@link JSONArray} containing all current messages
*/
private JSONArray buildMessagesArray() {
JSONArray messages = new JSONArray();
for (Message msg : this.chatObject.getConversation()) {
JSONObject message = new JSONObject();
String role = msg.getRole().name().toLowerCase(Locale.ROOT);
message.put("role", role);
message.put("content", msg.getContent());
if (msg.getRole() == Message.Role.TOOL) {
message.put("tool_call_id", msg.getToolID());
}
messages.put(message);
}
return messages;
}
public ChatObject getChatObject() {
return chatObject;
}
/**
* Writes a {@link JSONArray} of messages to the given file, overwriting it if it already exists.
*
* @param file The target file to write to
* @param messages The message content to write
* @throws RuntimeException If the file cannot be created or written to
*/
private void writeMessagesTo(File file, JSONArray messages) {
try {
if (file.exists()) file.delete();
file.createNewFile();
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
writer.write(messages.toString());
}
} catch (IOException e) {
throw new RuntimeException("Failed to write messages to " + file.getPath(), e);
}
}
/**
* Sets the {@link #model} object to the provided argument,
* Also adds the memory base system. See {@link Core#setModelNoMemory(Model)} if you don't want to add memory functions
* @param model The Model to use
*/
public void setModel(Model model) {
this.model = model;
addTool(new AddMemoryFunction());
addTool(new RemoveMemoryFunction());
addTool(new GetMemoryFunction());
addTool(new GetMemoriesFunction());
addTool(new GetMemoryIdentitiesFunction());
}
/**
* Sets the {@link #model} object to the provided argument,
* Does not add the base system for memory. see {@link #setModel(Model)} if you want to add memory function
* @param model The Model to use
*/
public void setModelNoMemory(Model model) {
this.model = model;
}
/**
* Adds a new tool to the System
* @param functionTool The tool to add
*/
public void addTool(Tool functionTool) {
tools.add(functionTool);
}
public void addMessage(Message message) {
chatObject.addMessage(message);
}
/*
/**
* Adds a list of tools to the System
* @param tools The tools to add
*/
/*@SuppressWarnings("MagicConstant")
public void addTools(Tools tools)
{
for(Pair<Tool, String> tool : tools)
{
addTool(tool.getKey(), tool.getValue());
}
}*/
/**
* Gets the list of tools added to the System
* @return The list of tools added to the System compressed as Pairs of the tool and the source
*/
public ArrayList<Tool> getTools() {
return tools;
}
/**
* Gets the Ollama Object
* @return The Ollama Object
*/
public Model getModel() {
return model;
}
/**
* Removes a tool with a given name
* @param name The tool to remove
*/
public void removeTool(String name) {
tools.stream()
.filter(tool -> tool.name().equalsIgnoreCase(name))
.findFirst()
.ifPresentOrElse(tools::remove, () -> {
new IllegalArgumentException("Function tool with name '"+name+"' does not exist")
.printStackTrace();
System.exit(1);
});
}
/**
* Flushes the log file
*/
public static void flushLog() {
try {
logWriter.flush();
}catch (IOException e) {}
}
public CompletableFuture<Message> queryModel()
{
return model.qurryModel(chatObject, tools);
}
public CompletableFuture<Message> queryModel(Consumer<JSONObject> consumer) throws OperationNotSupportedException {
if(model instanceof StreamingModel streamingModel) {
return streamingModel.qurryModel(chatObject, tools, consumer);
}
throw new OperationNotSupportedException("The chosen backend of type \""+model.getClass().getSimpleName()+"\" dose not support streaming");
}
/**
* Handles the response from Ollama.
* <p>
* Processes tool calls, logs information, appends messages to the OllamaObject,
* and prints output to the user.
*
* @param response The response from Ollama
*/
public void handleResponse(Message response) {
if(response == null) return;
//chatObject.addMessage(response);
List<CompletableFuture<Void>> futures = new ArrayList<>();
// Process each tool call
JSONArray toolCalls = response.getToolCalls();
if(toolCalls == null) toolCalls = new JSONArray();
if(toolCalls.length() == 0)
{
checkIfResponceMessage(response);
return;
}
for(Object call : toolCalls) {
futures.add(processToolCall(call));
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenAccept(result -> {
checkIfResponceMessage(response);
model.qurryModel(chatObject, tools).thenAccept(this::handleResponse);
});
}
/**
* Processes a single tool call from the Ollama response.
*
* Validates the call, finds the corresponding tool, renders the invocation,
* and executes it.
*
* @param call a JSONObject representing the tool call
*/
private CompletableFuture<Void> processToolCall(Object call) {
return CompletableFuture.runAsync(() -> {
if (!(call instanceof JSONObject jsonObject)) return;
JSONObject function = jsonObject.getJSONObject("function");
Tool func = findTool(function);
if (func == null) {
reportToolNotFound(function);
return;
}
JSONObject arguments = new JSONObject(function.getString("arguments"));
renderToolCalling(func.renderCalling(function), function, arguments);
executeToolCall(func, arguments);
});
}
/**
* Finds a registered tool by name.
*
* @param function the function JSON containing the tool name
* @return the OllamaFunctionTool if found, {@code null} otherwise
*/
private Tool findTool(JSONObject function) {
return tools.stream()
.filter(f -> (f.name())
.equalsIgnoreCase(function.getString("name")))
.findFirst()
.orElse(null);
}
/**
* Reports when a tool call references a function that doesn't exist.
*
* @param function the function JSON representing a hallucinated or removed function
*/
private void reportToolNotFound(JSONObject function) {
Message error = new Message(
Message.Role.TOOL,
function.getString("name"),
"Function '" + function.getString("name") + "' does not exist"
);
chatObject.addMessage(error);
printMessageHandler.printToolCalling(error.getContent().toString());
}
/**
* Renders a tool calling based on the tool's rendering preference.
*
* <p>Tools may suppress rendering entirely, use a default JSON representation,
* or provide a custom string representation via {@link ToolCallingRender}.
*
* @param render the {@link ToolCallingRender} returned by the tool
* @param function the raw JSON of the function being called
* @param arguments the raw JSON arguments for the function
*/
private void renderToolCalling(ToolCallingRender render, JSONObject function, JSONObject arguments) {
switch(render) {
case ToolCallingRender.Suppress() -> {}
case ToolCallingRender.Default() -> {
JSONObject obj = new JSONObject();
obj.put("name", function.getString("name"));
obj.put("args", arguments);
printMessageHandler.printToolCalling(obj.toString());
}
case ToolCallingRender.Custom(String representation) ->
printMessageHandler.printToolCalling(representation);
}
}
/**
* Executes a tool call and handles the response.
*
* <p><strong>TODO:</strong> Refactor to support
* <a href="https://docs.ollama.com/capabilities/tool-calling#parallel-tool-calling">
* Ollama parallel tool calling</a>.
*
* @param func the {@link OllamaFunctionTool} to execute
* @param arguments the raw JSON arguments for the tool
*/
private void executeToolCall(Tool tool, JSONObject arguments) {
if(tool instanceof FunctionTool func) {
ToolArguments args = new ToolArguments();
for (String key : arguments.keySet()) {
args.addArgument(key, arguments.get(key));
}
try {
ToolResponse response = func.function(args);
if(response.getToolID() == null) {
response.setToolID(tool.name());
}
chatObject.addMessage(response);
printMessageHandler.printMessage(response);
writeLog("Successfully function call " + func.name() + " output: " + response.getResponse());
} catch (ToolException e) {
Message error = new Message(
Message.Role.TOOL,
tool.name(),
e.getMessage()
);
chatObject.addMessage(error);
printMessageHandler.printErrorMessage(error);
writeLog("ERROR: " + e.getMessage());
}
}
// TODO: Add support for misc tools
}
/**
* Checks if the response contains a message and if so, prints it to the user
* @param responce the Ollama response
*/
private void checkIfResponceMessage(Message responce) {
if(responce.getContent() instanceof String str)
{
printMessageHandler.printMessage(responce);
writeLog("Response content: "+ str);
chatObject.addMessage(responce);
}
}
/**
* Writes a message to the log file
* @param message The message to write
*/
public static void writeLog(String message)
{
if(logWriter == null) {
System.err.println("!! Log writer is not initialized !!");
return;
}
try {
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd%EEEE HH:mm:ss'#'SSS");
logWriter.write(now.format(formatter) + "> " + message + "\n");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void enablePlugins(File pluginDirectory) {
if(!pluginDirectory.exists()) {
throw new IllegalArgumentException("Plugin directory does not exist");
}
if(!pluginDirectory.isDirectory()) {
throw new IllegalArgumentException("Plugin directory is not a directory");
}
File[] files = pluginDirectory.listFiles((dir, name) -> name.endsWith(".jar"));
if(files == null) {
return;
}
LoaderPluginData data = new LoaderPluginData();
Loader loader = new Loader();
for(File file : files) {
try(JarFile jar = new JarFile(file)){
JarEntry pluginJsonFile = jar.getJarEntry("plugin.json");
if(pluginJsonFile == null)
{
throw new PluginLoadingException("Plugin does not contain a plugin.json file", file.getName());
}
if(jar.getJarEntry("me/neurodock/plugin/Plugin") != null)
throw new PluginLoadingException("Plugin bundles NeuroDock API classes. Consider using compileOnly", file.getName());
StringBuilder pluginJsonData = new StringBuilder();
String tmp = null;
BufferedReader inReader = new BufferedReader(new InputStreamReader(jar.getInputStream(pluginJsonFile)));
while((tmp = inReader.readLine()) != null)
{
pluginJsonData.append(tmp);
}
JSONObject pluginJson = new JSONObject(pluginJsonData.toString());
LoadedPlugin plugin = loader.loadPlugin(pluginJson, jar, file.toPath(), data);
data.addPlugin(plugin);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
data.plugins.forEach(loadedPlugin -> {
for(Tool tool : loader.getTools(loadedPlugin.plugin()))
{
addTool(tool);
}
});
}
private static class LoaderPluginData extends Data {
ArrayList<LoadedPlugin> plugins = new ArrayList<>();
@Override
public File getDataDictionary() {
return Options.getInstance().getDataDir();
}
@Override
public LoadedPlugin[] getLoadedPlugins() {
return plugins.toArray(new LoadedPlugin[0]);
}
@Override
public File getCacheDirectory() {
return Options.getInstance().getCacheDirectory();
}
public void addPlugin(LoadedPlugin plugin)
{
plugins.add(plugin);
}
public void removePlugin(LoadedPlugin plugin)
{
plugins.remove(plugin);
}
}
}
@@ -0,0 +1,593 @@
package me.neurodock.core;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.Tool;
import me.neurodock.llm.Message;
import me.neurodock.llm.Model;
import org.json.JSONObject;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;
//# Identity
//You are [name], [role/purpose].
//
//# Context
//[Static facts it needs — what NeuroDock is, what world/game state means, etc.]
//
//# Capabilities / Tools
//[What tools it has access to and when to use them]
//
//# Behavior rules
//- [Constraints: tone, length, what not to do]
//- [Formatting requirements]
//
//# Output format
//[Exact schema if you need structured output — JSON shape, etc.]
/**
* Compiles a structured system prompt for an LLM out of discrete, reusable pieces —
* identity, static context, tool-usage capabilities, behavior rules, and output format —
* instead of hand-writing and concatenating prompt strings inline.
* <p>
* Each populated section is wrapped in its own XML-style tag
* ({@code <Identity>}, {@code <Context>}, {@code <Capabilities>}, {@code <Behavior>},
* {@code <Output>}) and only included in the final prompt if it was actually set — see
* {@link #generateSystemPrompt()}.
* <p>
* Instances are assembled via {@link #builder()}. Callers who need capability routing
* hints compiled from a live set of registered tools must call
* {@link #generateCapabilities(ArrayList)} before {@link #generateSystemPrompt()};
* see that method's docs for why this is a separate step rather than being folded into
* construction.
*
* @see Builder
* @see Capabilities
*/
public class LLMSystemPrompt {
/**
* The persona/role block for this prompt, or {@code null} if not set. See {@link Identity}.
*/
private Identity identity;
/**
* The raw, pre-built {@code <Context>} body for this prompt (static facts the model
* needs), or {@code null} if not set. Typically produced via {@link Context#build()}.
*/
private String context;
/**
* Usage-hint definitions for registered tools, or {@code null} if this prompt doesn't
* compile a capabilities section. This holds the *rules* for what to say about each
* tool; the actual compiled text lives in {@link #compiledCapabilities} and is only
* populated once {@link #generateCapabilities(ArrayList)} has been called.
*/
private Capabilities capabilities;
/**
* The compiled, ready-to-render {@code <Capabilities>} body, built by
* {@link #generateCapabilities(ArrayList)} from {@link #capabilities} and a live
* set of registered tools. {@code null} until that method has been called at least
* once, or if {@link #capabilities} was never set.
*/
private String compiledCapabilities;
/**
* The raw, pre-built {@code <Behavior>} body for this prompt (tone, constraints,
* formatting rules), or {@code null} if not set. Usually populated via
* {@link Builder#loadBehaviorFromFile(String)}.
*/
private String behavior;
/**
* The output-format specification for this prompt (e.g. a JSON schema the model
* should reply in), or {@code null} if the model should respond in free text.
*/
private OutputFormat outputFormat;
/**
* Constructs a system prompt from its component parts. Prefer {@link #builder()}
* over calling this directly.
*
* @param identity the persona/role block, or {@code null} to omit it
* @param context the pre-built static-context body, or {@code null} to omit it
* @param capabilities the tool usage-hint definitions, or {@code null} to omit the
* capabilities section entirely
* @param behavior the pre-built behavior/constraints body, or {@code null} to omit it
* @param outputFormat the output-format specification, or {@code null} for free-text output
*/
public LLMSystemPrompt(Identity identity, String context, Capabilities capabilities, String behavior, OutputFormat outputFormat) {
this.identity = identity;
this.context = context;
this.capabilities = capabilities;
this.behavior = behavior;
this.outputFormat = outputFormat;
}
/**
* Compiles the {@code <Capabilities>} section from the tools currently registered on
* the given {@link ArrayList<Tool>}, using the usage hints defined in {@link #capabilities}.
* <p>
* This is intentionally a separate step from construction rather than something the
* constructor does automatically: which tools are registered on an {@link ArrayList<Tool>}
* can change after this prompt is built (tools added/removed at runtime), so the
* compiled capabilities text needs to be regenerated on demand against whatever the
* current tool set actually is, not frozen at construction time.
* <p>
* For each registered {@link FunctionTool}, this looks up a usage hint by the
* tool's {@link Class} via {@link Capabilities#getUsageHints()}. If no hint was
* registered for that class, the tool is silently omitted from the compiled output —
* there is no fallback to {@link FunctionTool#description()}, since that text
* is written to help the model choose a tool mid-conversation, not to explain routing
* up front in a system prompt; conflating the two would blur what each is for.
* <p>
* If {@link #capabilities} is {@code null} (this prompt doesn't use a capabilities
* section at all), this clears {@link #compiledCapabilities} to {@code null} and
* returns without inspecting {@code ollamaObject}.
*
* @param tools the object whose currently-registered tools should be inspected
*/
public void generateCapabilities(ArrayList<Tool> tools) {
if(capabilities == null) {
compiledCapabilities = null;
return;
}
StringBuilder builder = new StringBuilder();
for (Tool tool : tools) {
if(tool instanceof FunctionTool funcTool) {
String llmName = funcTool.name();
String hint = capabilities.getUsageHints().get(funcTool.getClass());
// fall back to description() if no explicit routing hint was set
String line = hint != null ? hint : funcTool.description();
if (line == null) continue; // nothing usable to say about this tool, skip it
builder.append(llmName).append(": ").append(line).append(System.lineSeparator());
}
}
compiledCapabilities = builder.toString();
}
/**
* Renders this prompt's currently-set sections into a single system-role
* {@link Message}, ready to be sent to Ollama.
* <p>
* Each section — {@link #identity}, {@link #context}, {@link #compiledCapabilities},
* {@link #behavior}, {@link #outputFormat} — is wrapped in its own XML-style tag and
* appended in that fixed order. A section is included only if it is non-null (and, for
* capabilities, non-blank); sections that were never set or never compiled are simply
* skipped rather than emitted as empty tags.
* <p>
* Note that {@link #compiledCapabilities} reflects whatever tool set was live the last
* time {@link #generateCapabilities(ArrayList)} was called — call that first if the
* registered tools may have changed since.
*
* @return a new {@link Message} with role {@link Message.Role#SYSTEM}
* containing the compiled prompt text
*/
public Message generateSystemPrompt() {
StringBuilder builder = new StringBuilder();
if(identity != null) {
builder.append("<Identity>").append(System.lineSeparator()).append(identity.build()).append(System.lineSeparator()).append("</Identity>").append(System.lineSeparator());
}
if(context != null) {
builder.append("<Context>").append(System.lineSeparator()).append(context).append(System.lineSeparator()).append("</Context>").append(System.lineSeparator());
}
if (compiledCapabilities != null && !compiledCapabilities.isBlank()) {
builder.append("<Capabilities>").append(System.lineSeparator())
.append(compiledCapabilities)
.append(System.lineSeparator()).append("</Capabilities>").append(System.lineSeparator());
}
if(behavior != null) {
builder.append("<Behavior>").append(System.lineSeparator()).append(behavior).append(System.lineSeparator()).append("</Behavior>").append(System.lineSeparator());
}
if(outputFormat != null) {
builder.append("<Output>").append(System.lineSeparator()).append(outputFormat.build()).append(System.lineSeparator()).append("</Output>").append(System.lineSeparator());
}
return new Message(Message.Role.SYSTEM, builder.toString());
}
/**
* Creates a new {@link Builder} for assembling an {@link LLMSystemPrompt}.
*
* @return a fresh, empty {@link Builder}
*/
public static Builder builder()
{
return new Builder();
}
/**
* The persona/role block of a system prompt — who the model is and what it's for.
* <p>
* At least one of {@code name}, {@code role}, or {@code purpose} must be non-blank;
* {@link #build()} throws if all three are absent, since an empty identity block is
* almost certainly a mistake rather than an intentional choice.
*
* @param name the model's given name (e.g. {@code "Alice"}), or {@code null}/blank to omit
* @param role the model's role (e.g. {@code "Personal assistant"}), or {@code null}/blank to omit
* @param purpose the model's purpose/goal, or {@code null}/blank to omit
*/
public record Identity(String name, String role, String purpose) {
/**
* Builds the identity sentence from whichever of {@link #name}, {@link #role}, and
* {@link #purpose} are present, joining them with commas.
*
* @return the composed identity sentence, e.g.
* {@code "You are Alice, Your role is Personal assistant"}
* @throws IllegalArgumentException if {@link #name}, {@link #role}, and
* {@link #purpose} are all {@code null} or blank
*/
public String build()
{
StringBuilder builder = new StringBuilder();
if(name != null && !name.isBlank())
{
builder.append("You are ").append(name);
}
if(role != null && !role.isBlank())
{
if(!builder.isEmpty())
{
builder.append(", ");
}
builder.append("Your role is ").append(role);
}
if(purpose != null && !purpose.isBlank())
{
if(!builder.isEmpty())
{
builder.append(", ");
}
builder.append("Your purpose is ").append(purpose);
}
if(!builder.isEmpty())
return builder.toString();
throw new IllegalArgumentException("Invalid identity");
}
}
/**
* A builder for the static-facts {@code <Context>} section of a system prompt —
* things the model should always know (e.g. "Always answer in English").
* <p>
* Facts are appended in the order they're added and joined with newlines by
* {@link #build()}. The resulting string is what gets passed to
* {@link Builder#context(String)}.
*/
public static class Context {
/**
* The ordered list of context facts added so far.
*/
ArrayList<String> facts = new ArrayList<>();
/**
* Appends a single fact to this context block.
*
* @param fact the fact text to add
* @return this {@link Context}, for chaining
*/
public Context addFact(String fact) {
facts.add(fact);
return this;
}
/**
* Returns the facts added to this context block so far, in insertion order.
*
* @return the live list of facts backing this {@link Context}
*/
public ArrayList<String> getFacts() {
return facts;
}
/**
* Joins all added facts into a single newline-separated string.
*
* @return the compiled context body, or an empty string if no facts were added
*/
public String build() {
StringBuilder builder = new StringBuilder();
for(String fact: facts) {
if(!builder.isEmpty()) builder.append("\n");
builder.append(fact);
}
return builder.toString();
}
}
/**
* Defines per-tool usage-routing hints for the {@code <Capabilities>} section of a
* system prompt — i.e. "use this tool only in these cases" guidance, distinct from a
* tool's own {@link FunctionTool#description()}.
* <p>
* Hints are keyed by the tool's {@link Class} rather than by a registered instance or
* its LLM-facing name, since NeuroDock's convention is one instance per tool class —
* see {@link #use(Class, String)}. This class only holds the hint definitions; the
* actual compiled text is produced by
* {@link LLMSystemPrompt#generateCapabilities(ArrayList)}.
*/
public static class Capabilities {
/**
* Usage hints keyed by tool class, in the order they were registered.
*/
private final LinkedHashMap<Class<? extends FunctionTool>, String> usageHints = new LinkedHashMap<>();
/**
* Registers a usage-routing hint for a tool class — guidance on when the model
* should reach for this tool, e.g. {@code "This should only be used when needing
* to run CLI tools"}.
* <p>
* Keyed by class rather than instance because a tool's LLM-facing name (which
* depends on its registration source) isn't known until it's actually registered
* on an {@link Model}; class identity is stable and known up front.
*
* @param toolClass the tool class this hint applies to
* @param usageHint the routing guidance text for this tool
* @return this {@link Capabilities}, for chaining
*/
public Capabilities use(Class<? extends FunctionTool> toolClass, String usageHint) {
usageHints.put(toolClass, usageHint);
return this;
}
/**
* Returns the usage hints registered so far, keyed by tool class.
*
* @return the live map backing this {@link Capabilities}
*/
Map<Class<? extends FunctionTool>, String> getUsageHints() {
return usageHints;
}
}
/**
* The behavior/constraints section of a system prompt — tone, formatting rules, and
* other "how to behave" instructions.
* <p>
* <b>Not yet implemented</b> — currently unused; {@link LLMSystemPrompt#behavior} is
* populated directly as a {@link String}, typically via
* {@link Builder#loadBehaviorFromFile(String)}, rather than through this class.
*/
public static class Behavior {
/**
* Structured behavioral constraints. Not yet implemented.
*/
Constraints constraints;
/**
* Output formatting rules. Not yet implemented.
*/
String formating;
// TODO: Implement this
/**
* Structured constraints for a {@link Behavior} block (tone, length limits, things
* to avoid, etc.).
* <p>
* <b>Not yet implemented.</b>
*/
public static class Constraints
{
// TODO: Implement this
}
}
/**
* Specifies the expected output format for a system prompt's {@code <Output>} section
* — e.g. a JSON schema the model should structure its replies as.
*/
public static class OutputFormat {
/**
* The raw format specification, rendered verbatim in the {@code <Output>} section.
*/
String format;
/**
* Creates an output format from a raw, pre-formatted string.
*
* @param format the format specification text
*/
public OutputFormat(String format) {
this.format = format;
}
/**
* Creates an output format from a JSON schema.
*
* @param format the JSON schema to use as the format specification
*/
public OutputFormat(JSONObject format) {
this.format = format.toString();
}
/**
* Returns the format specification text for rendering into the prompt.
*
* @return the raw format string
*/
public String build () {
return format;
}
}
/**
* Builder for assembling an {@link LLMSystemPrompt} from its component sections.
* Obtain an instance via {@link LLMSystemPrompt#builder()}.
*/
public static class Builder {
/**
* The identity block to use, or {@code null} to omit it. Set via {@link #identity(Identity)}.
*/
private Identity identity;
/**
* The pre-built context body to use, or {@code null} to omit it. Set via {@link #context(String)}.
*/
private String context;
/**
* The capabilities/usage-hint definitions to use, or {@code null} to omit the
* capabilities section. Set via {@link #capabilities(Capabilities)}.
*/
private Capabilities capabilities;
/**
* The pre-built behavior body to use, or {@code null} to omit it. Set via
* {@link #behavior(String)} or loaded from disk via {@link #loadBehaviorFromFile(String)}.
*/
private String behavior;
/**
* The output format specification to use, or {@code null} for free-text output.
* Set via {@link #outputFormat(OutputFormat)}.
*/
private OutputFormat outputFormat;
/**
* Loads {@link #behavior} from a text file, resolved in order against:
* <ol>
* <li>the classpath</li>
* <li>{@link Options#getDataDir()}</li>
* <li>the current runtime/working directory</li>
* <li>an exact file path match</li>
* </ol>
*
* @param path the file to look for, resolved as described above
* @return this {@link Builder}, for chaining
* @throws IllegalStateException if no file could be found at any of the candidate
* locations, or if it exists but cannot be read
*/
public Builder loadBehaviorFromFile(String path) {
behavior = BehaviorLoader.load(path);
return this;
}
/**
* Sets the identity/persona block for the prompt being built.
*
* @param identity the identity to use
* @return this {@link Builder}, for chaining
*/
public Builder identity(Identity identity) {
this.identity = identity;
return this;
}
/**
* Sets the pre-built static-context body for the prompt being built.
*
* @param context the context body, typically produced via {@link Context#build()}
* @return this {@link Builder}, for chaining
*/
public Builder context(String context) {
this.context = context;
return this;
}
/**
* Sets the capability usage-hint definitions for the prompt being built. These are
* only compiled into prompt text once
* {@link LLMSystemPrompt#generateCapabilities(ArrayList)} is called on the built
* instance.
*
* @param capabilities the usage-hint definitions to use
* @return this {@link Builder}, for chaining
*/
public Builder capabilities(Capabilities capabilities) {
this.capabilities = capabilities;
return this;
}
/**
* Sets the pre-built behavior body for the prompt being built directly, bypassing
* {@link #loadBehaviorFromFile(String)}.
*
* @param behavior the behavior/constraints text to use
* @return this {@link Builder}, for chaining
*/
public Builder behavior(String behavior) {
this.behavior = behavior;
return this;
}
/**
* Sets the output format specification for the prompt being built.
*
* @param outputFormat the output format to use
* @return this {@link Builder}, for chaining
*/
public Builder outputFormat(OutputFormat outputFormat) {
this.outputFormat = outputFormat;
return this;
}
/**
* Builds the {@link LLMSystemPrompt} from the sections configured on this builder.
*
* @return a new {@link LLMSystemPrompt}
*/
public LLMSystemPrompt build() {
return new LLMSystemPrompt(identity, context, capabilities, behavior, outputFormat);
}
}
/**
* Internal helper for resolving and reading a behavior file from one of several
* candidate locations. See {@link Builder#loadBehaviorFromFile(String)}.
*/
private static class BehaviorLoader {
/**
* Checks whether a classpath resource exists at the given path without leaving it open.
*
* @param path the classpath-relative resource path to check
* @return {@code true} if a resource stream could be opened at that path
*/
private static boolean classpathResourceExists(String path) {
try (InputStream is = BehaviorLoader.class.getResourceAsStream(path)) {
return is != null;
} catch (IOException e) {
return false;
}
}
/**
* Resolves and reads a behavior file as UTF-8 text, checking the classpath,
* {@link Options#getDataDir()}, the working directory, and an exact path match, in that order.
*
* @param path the file to look for
* @return the full file contents, with each line terminated by {@code \n}
* @throws IllegalStateException if the resource cannot be found at any candidate
* location, or if it exists but cannot be read
*/
public static String load(String path) {
try {
InputStream in;
if (classpathResourceExists("/"+path)) {
in = BehaviorLoader.class.getResourceAsStream("/"+path);
} else {
Path dataDirPath = Path.of(Options.getInstance().getDataDir().getPath(), path);
Path relativePath = Path.of(path);
if (Files.exists(dataDirPath)) {
in = new FileInputStream(dataDirPath.toFile());
} else if (Files.exists(relativePath)) {
in = new FileInputStream(relativePath.toFile());
} else {
throw new IllegalStateException("Could not find resource " + path);
}
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append('\n'); // see note below
}
return sb.toString();
}
} catch (IOException e) {
throw new IllegalStateException("Could not load resource " + path, e);
}
}
}
}
@@ -1,11 +1,11 @@
package me.zacharias.chat.core;
package me.neurodock.core;
/**
* The Configuration class, where arguments are stored and retrieved. This is used to configure the running of the program from the Launcher.
*/
public class LaunchOptions {
private static LaunchOptions instance = new LaunchOptions();
/**
* Gets the singleton instance of the LaunchOptions class.
* Meant to be used to get or set ant option.
@@ -15,6 +15,8 @@ public class LaunchOptions {
return instance;
}
private LaunchOptions() {}
private boolean loadOld = true;
private boolean autoAccept;
private boolean serverMode;
@@ -0,0 +1,123 @@
package me.neurodock.core;
import java.io.File;
import java.nio.file.Path;
public class Options {
/**
* The singleton options object
*/
private static Options instance = new Options();
/**
* Provides the singleton options object for modification to options
* @return the current {@link Options} singleton object
*/
public static Options getInstance()
{
return instance;
}
private Path data;
private File dataDir;
private File pluginDirectory;
private File cacheDirectory;
/**
* Sets a new singleton options object
* @param options the new singleton object
*/
public static void setInstance(Options options)
{
instance = options;
}
public Options setDataDir(Path dataDir, boolean fullDirectory)
{
Path data = Path.of("./data");
String os = System.getProperty("os.name").toLowerCase();
Path cache;
if(System.getenv("AI_CHAT_DEBUG") == null) {
if (fullDirectory) {
data = dataDir;
} else {
if (os.contains("windows")) {
String localappdata = System.getenv("LOCALAPPDATA");
if (localappdata == null) {
localappdata = System.getenv("APPDATA");
}
data = Path.of(localappdata, dataDir.toFile().getPath());
} else if (os.contains("linux")) {
data = Path.of(System.getenv("HOME"), ".local/share", dataDir.toFile().getPath());
} else if (os.contains("mac")) {
data = Path.of(System.getProperty("user.home"), "Library/Application Support", dataDir.toFile().getPath());
}
}
}
if (os.contains("win")) {
String localAppData = System.getenv("LOCALAPPDATA");
cache = Path.of(localAppData != null ? localAppData
: System.getProperty("user.home") + "\\AppData\\Local");
} else if (os.contains("mac")) {
cache = Path.of(System.getProperty("user.home"), "Library", "Caches");
} else {
// Linux / other unix
String xdgCache = System.getenv("XDG_CACHE_HOME");
cache = Path.of(xdgCache != null && !xdgCache.isBlank()
? xdgCache
: System.getProperty("user.home") + "/.cache");
}
this.data = data;
this.cacheDirectory = new File(cache.toFile(), data.getFileName().toFile().toString());
return this;
}
public Options initiateDirectories()
{
this.dataDir = this.data.toFile();
if(!this.dataDir.exists()) {
this.dataDir.mkdirs();
}
String pluginDir = this.data + "/plugins";
pluginDirectory = new File(pluginDir);
if(!pluginDirectory.exists()) {
pluginDirectory.mkdirs();
}
if(!cacheDirectory.exists()) {
cacheDirectory.mkdirs();
}
return this;
}
public Path getData() {
return data;
}
public File getDataDir() {
return dataDir;
}
public File getPluginDirectory() {
return pluginDirectory;
}
public File getCacheDirectory() {
return cacheDirectory;
}
public Path getFileHandlerDataLocation()
{
return Path.of(dataDir.toString(), "files");
}
}
@@ -1,4 +1,4 @@
package me.zacharias.chat.core;
package me.neurodock.core;
/**
* A simple Pair class.
@@ -0,0 +1,30 @@
package me.neurodock.core;
import me.neurodock.llm.Message;
/**
* Represents a {@link PrintAdvanceMessageHandler}.
* This is used by the {@link Core} to print messages for the user, LLM, Tools, or potentialy System, see {@link Message.Role}.
* For a simpler version see {@link PrintMessageHandler}
*/
public interface PrintAdvanceMessageHandler {
/**
* Expected to handle the printing of the provided {@link Message}.
* @param message The {@link Message} requested to be printed from a veriity of sources, see {@link Message}
*/
void printMessage(Message message);
/**
* Default method to print error messages to the user or API Client.
* This uses ANSI escape codes to color the output red if color is supported.
* @param errorMessage The error message to be printed.
* If color is not supported, it will print the message without color.
*/
void printErrorMessage(Message errorMessage);
/**
* Used when a tool is to have it's calling rendered
* @param representation the string to be printed
*/
void printToolCalling(String representation);
}
@@ -0,0 +1,78 @@
package me.neurodock.core;
import me.neurodock.llm.Message;
/**
* Represents a PrintMessageHandler.
* The Core uses this to print messages to the user or API Clients.
*/
public interface PrintMessageHandler extends PrintAdvanceMessageHandler {
/**
* Handles the printing of a message.
* This is meant to output the message to the user or API Client.
* @param message The message to be printed.
*/
void printMessage(String message);
/**
* This is a default implementation when wrapping {@link PrintAdvanceMessageHandler} to this "simpler" {@link PrintMessageHandler}
*
*
* @param message The {@link Message} requested to be printed from a veriity of sources, see {@link Message.Role}
*/
default void printMessage(Message message) {
printMessage(switch (message.getRole()) {
case ASSISTANT -> (color()?"\u001b[32m":"")+(LaunchOptions.getInstance().isShowFullMessage()? message.getContent() : message.getContent().toString().replaceAll("(?s)<think>.*?</think>", "")) +(color()?"\u001b[0m":"");
case TOOL -> (color() ?"\u001b[31m":"")+message.getContent()+(color()?"\u001b[0m":"");
case USER, SYSTEM -> (color() ?"\u001b[37m":"")+message.getContent()+(color()?"\u001b[0m":"");
default -> throw new IllegalArgumentException("Invalid message role");
});
}
/**
* Gets if color is supported by the PrintMessageHandler.
* This uses ANSI escape codes to color the output.
* @return a boolean indicating if color is supported by the PrintMessageHandler.
*/
boolean color();
/**
* Default method to print error messages to the user or API Client.
* This uses ANSI escape codes to color the output red if color is supported.
* @param message The error message to be printed.
* If color is not supported, it will print the message without color.
*/
default void printError(String message) {
if (color()) {
printMessage("\u001B[31m" + message + "\u001B[0m"); // Red color for errors
} else {
printMessage(message);
}
}
/**
* This is a default implementation when wrapping {@link PrintAdvanceMessageHandler} to this "simpler" {@link PrintMessageHandler}
*
*
* Default method to print error messages to the user or API Client.
* This uses ANSI escape codes to color the output red if color is supported.
* @param errorMessage The error message to be printed.
* If color is not supported, it will print the message without color.
*/
default void printErrorMessage(Message errorMessage) {
printError(errorMessage.getContent().toString());
}
@Override
default void printToolCalling(String representation) {
if(color())
{
printMessage("> \u001B[34m"+representation+"\u001B[0m"); // Blue color for tool calling representations
}
else
{
printMessage("> "+representation);
}
}
}
@@ -0,0 +1,7 @@
package me.neurodock.core;
public sealed interface ToolCallingRender {
record Suppress() implements ToolCallingRender {}
record Default() implements ToolCallingRender {}
record Custom(String representation) implements ToolCallingRender {}
}
@@ -1,15 +1,18 @@
package me.zacharias.chat.core.files;
package me.neurodock.core.files;
import me.zacharias.chat.core.Core;
import org.intellij.lang.annotations.MagicConstant;
import me.neurodock.core.Core;
import me.neurodock.core.Options;
import me.neurodock.core.Pair;
import me.neurodock.core.files.tools.ReadFileTool;
import me.neurodock.core.files.tools.WriteFileTool;
import me.neurodock.llm.tools.Tool;
import java.io.*;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.util.Arrays;
import java.nio.file.Path;
import java.util.ArrayList;
/**
* Base class responsible for the {@link me.zacharias.chat.core.files} related systems
* Base class responsible for the {@link me.neurodock.core.files} related systems
*/
public class FileHandler {
/**
@@ -20,27 +23,51 @@ public class FileHandler {
/**
* The directory used as base for this instance of {@link FileHandler}. This is where all files that can be read or writen will be located
*/
private final File/*System*/ directory;
private final Path/*System*/ root;
/**
* Creates a new instance as well as setting the {@link #instance} to this new one
* A good start is to use {@link Options#getFileHandlerDataLocation()} as it's located along all other files
* @param baseDirectory the directory to be used as base directory
*/
public FileHandler(@MagicConstant(valuesFromClass = FileHandlerLocation.class) String baseDirectory) {
public FileHandler(Path baseDirectory) {
try {
FileSystem fs = FileSystems.newFileSystem(new File(baseDirectory).toPath());
//fs.getPath()
directory = new File(baseDirectory);
if (!directory.exists())
directory.mkdirs();
root = baseDirectory.toAbsolutePath().normalize();
if (!root.toFile().exists()) {
root.toFile().mkdirs();
}
instance = this;
}catch (Exception ex)
{
ex.printStackTrace();
throw new FileHandlerException("Failed to create FileHandler instance with base directory \"" + baseDirectory + "\"");
}
}
public static FileHandler getInstance() {
return instance;
}
/**
* Returns a list of all {@link Tool}'s this module adds
* @return
*/
public static ArrayList<Tool> getTools() {
ArrayList<Tool> fileTools = new ArrayList<>();
fileTools.add(new ReadFileTool());
fileTools.add(new WriteFileTool());
return fileTools;
}
public Path resolve(String relative) throws IOException{
Path p = root.resolve(relative).normalize();
if (!p.startsWith(root)) throw new IOException("Access denied");
return p;
}
/**
*
* @param filename
@@ -48,11 +75,12 @@ public class FileHandler {
* @throws FileHandlerException
*/
public String readFile(String filename) throws FileHandlerException {
if(filename.contains(".."))
{
throw new FileHandlerException("File \"" + filename + "\" tries to retrace path");
File file = null;
try {
file = resolve(filename).toFile();
} catch (IOException e) {
throw new FileHandlerException("Illegal restricted path");
}
File file = new File(directory, filename);
if(file.exists())
{
try{
@@ -82,4 +110,13 @@ public class FileHandler {
}
throw new FileHandlerException("Cant find file \""+filename+"\"");
}
/**
* Provides a relative path from {@link FileHandler#root} to file
* @param file the file to be relativised
* @return the relative path from {@link FileHandler#root} to file
*/
public String path(File file) {
return root.relativize(file.toPath()).toString();
}
}
@@ -1,4 +1,4 @@
package me.zacharias.chat.core.files;
package me.neurodock.core.files;
public class FileHandlerException extends RuntimeException {
public FileHandlerException(String message) {
@@ -0,0 +1,4 @@
package me.neurodock.core.files;
public class ListFiles {
}
@@ -0,0 +1,73 @@
package me.neurodock.core.files.tools;
import me.neurodock.core.files.FileHandler;
import me.neurodock.llm.exceptions.ToolException;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.nio.file.Path;
public class ReadFileTool extends FunctionTool {
FileHandler fs = FileHandler.getInstance();
@Override
public @NotNull String name() {
return "read_file";
}
@Override
public String description() {
return "Reads the file of path or null if that file dosent exists";
}
@Override
public @NotNull ToolParameters parameters() {
return ToolParameters.builder()
.addProperty("file_path", ToolParameters.ToolParametersBuilder.Type.STRING, "The path to the file to be read", true)
.build();
}
@Override
public @NotNull ToolResponse function(ToolArguments args) {
String orgPath = args.optArgument("file_path", String.class);
if(orgPath == null) {
throw new ToolException(this, "Missing required argument 'file_path'");
}
Path filePath = null;
try{
filePath = fs.resolve(orgPath);
}
catch (IOException ex) {
throw new ToolException(this, ex);
}
File file = filePath.toFile();
if(!file.exists()) {
throw new ToolException(this, "File does not exist: " + fs.path(file));
}
BufferedReader br = null;
try{
br = new BufferedReader(new FileReader(file));
}catch (FileNotFoundException ex) {
throw new ToolException(this, "File not found: " + fs.path(file));
}
StringBuilder sb = new StringBuilder();
String tmp = null;
try {
while ((tmp = br.readLine()) != null) {
sb.append(tmp).append("\n");
}
}
catch (IOException e) {
throw new ToolException(this, "Error reading file: " + fs.path(file));
}
return new ToolResponse(this.name(), sb.toString());
}
}
@@ -0,0 +1,77 @@
package me.neurodock.core.files.tools;
import me.neurodock.core.files.FileHandler;
import me.neurodock.llm.exceptions.ToolException;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull;
import static me.neurodock.llm.tools.ToolParameters.ToolParametersBuilder.*;
import java.io.*;
import java.nio.file.Path;
public class WriteFileTool extends FunctionTool {
FileHandler fs = FileHandler.getInstance();
@Override
public @NotNull String name() {
return "write_file";
}
@Override
public @NotNull ToolParameters parameters() {
return ToolParameters.builder()
.addProperty("file_path", Type.STRING, "The path to the file to write to", true)
.addProperty("file_content", Type.STRING, "The content to write to the file", true)
.addProperty("overwrite", Type.BOOLEAN, "Overwrite the file, defaults to false", false)
.build();
}
@Override
public @NotNull ToolResponse function(ToolArguments args) {
String path = args.getArgument("file_path", String.class);
String content = args.getArgument("file_content", String.class);
boolean overwrite = args.getArgument("overwrite", Boolean.class);
if(path == null || content == null || path.isBlank() || content.isBlank())
{
throw new ToolException(this, "file_content or file_path is empty or null");
}
Path filePath = null;
try{
filePath = fs.resolve(path);
}
catch (IOException ex) {
throw new ToolException(this, ex);
}
File file = filePath.toFile();
if(file.exists() && !overwrite) {
throw new ToolException(this, "File already exists, and instructed to not be overwritten: " + fs.path(file));
}
else if(file.exists() && overwrite)
{
file.delete();
}
try {
file.createNewFile();
} catch (IOException e) {
throw new ToolException(this, e);
}
try(BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file))))
{
bw.write(content);
return new ToolResponse(name(), "Successfully wrote data to "+fs.path(file));
}
catch (IOException ex)
{
throw new ToolException(this, ex);
}
}
}
@@ -0,0 +1,50 @@
package me.neurodock.core.memory;
import me.neurodock.llm.exceptions.ToolException;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull;
/**
* Provides the add_memory function.<br>
* This function adds a string to the memory.
*/
public class AddMemoryFunction extends FunctionTool {
/**
* The CoreMemory instance.
*/
CoreMemory memory = CoreMemory.getInstance();
@Override
public @NotNull String name() {
return "add_memory";
}
@Override
public String description() {
return "Remember somthing";
}
@Override
public @NotNull ToolParameters parameters() {
return ToolParameters.builder()
.addProperty("memory", ToolParameters.ToolParametersBuilder.Type.STRING, "The memory to remember", true)
.addProperty("identity", ToolParameters.ToolParametersBuilder.Type.STRING, "The identity of the memory to remember", true)
.build();
}
@Override
public @NotNull ToolResponse function(ToolArguments args) {
String memory = args.optArgument("memory", String.class);
String identity = args.optArgument("identity", String.class);
if (memory == null || identity == null) {
throw new ToolException(this, "Missing memory or identity argument");
}
this.memory.addMemory(identity, memory);
return new ToolResponse(name(), "Added "+identity+" to the memory");
}
}
@@ -0,0 +1,198 @@
package me.neurodock.core.memory;
import me.neurodock.core.Options;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.*;
import java.util.ArrayList;
import java.util.Optional;
/**
* CoreMemory is a class that provides a way to store and retrieve strings from a file.<br>
* This is meant to be used as a way to store and retrieve strings from a file.
*/
public class CoreMemory {
public static final int VERSION = 2;
/**
* The singleton instance of CoreMemory.
*/
private static final CoreMemory instance = new CoreMemory(Options.getInstance().getDataDir() + "/CoreMemory.json");
/**
* Memory type identifier for key-value mapped memory storage.
*/
public static final String MAPPED_MEMORY = "mapped_memory";
/**
* Memory type identifier for sequential/array-based memory storage.
*/
public static final String ARRAYED_MEMORY = "arrayed_memory";
/**
* Gets the singleton instance of CoreMemory.
* @return The singleton instance of CoreMemory
*/
public static CoreMemory getInstance() {
return instance;
}
/**
* Creates a new instance of CoreMemory.
* @param memoryFile The file to store the memory in
*/
public CoreMemory(String memoryFile) {
// Since version 2, version 1(un versiond) memory files are incompadible, and will be refectored
File f = new File(memoryFile);
if (f.exists()) {
try {
BufferedReader br = new BufferedReader(new FileReader(f));
StringBuilder data = new StringBuilder();
String buffer = null;
while ((buffer = br.readLine()) != null) {
data.append(buffer);
}
JSONObject tmpMemory = new JSONObject(data.toString());
if(tmpMemory.optInt("VERSION", 0) == 0)
{
memory = new JSONObject();
memory.put("VERSION", VERSION);
memory.put(MAPPED_MEMORY, tmpMemory);
memory.put(ARRAYED_MEMORY, new JSONArray());
}
else
{
memory = tmpMemory;
}
}catch (Exception e) {
e.printStackTrace();
}
}
else {
memory.put(MAPPED_MEMORY, new JSONObject());
memory.put(ARRAYED_MEMORY, new JSONArray());
}
this.memoryFile = memoryFile;
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
try{
File f = new File(memoryFile);
if(f.exists()) {
f.delete();
}
f.createNewFile();
BufferedWriter bw = new BufferedWriter(new FileWriter(f));
bw.write(memory.toString());
bw.close();
}catch (Exception e) {
e.printStackTrace();
}
}
});
}
/**
* The memory.
*/
private JSONObject memory = new JSONObject();
/**
* The file to store the memory in.
*/
private final String memoryFile;
/**
* Gets the memory.
* @return A list of memory identifies/names
*/
public String[] getMemoriesIdentity() {
return memory.getJSONObject(MAPPED_MEMORY).keySet().toArray(new String[0]);
}
public Optional<String> getMemory(String name) {
return Optional.ofNullable(memory.getJSONObject(MAPPED_MEMORY).optString(name, null));
}
/**
* Sets the memory.
* @param name The name/identity of the memory
* @param memory The memory
*/
public void addMemory(String name, String memory) {
this.memory.getJSONObject(MAPPED_MEMORY).put(name, memory);
}
/**
* Removes the memory.
* @param name The memory to remove
*/
public void removeMemory(String name) {
this.memory.getJSONObject(MAPPED_MEMORY).remove(name);
}
/**
* Add an array-based memory
* @param memory The memory to remember
*/
public void addMemory(String memory)
{
this.memory.getJSONArray(ARRAYED_MEMORY).put(memory);
}
/**
* Add an array-based memory
* @param memory The memory to remember
* @param index The index to put it at
* @apiNote Prefer {@link #addMemory(String)} unless there is a reason to use this one.
*/
public void addMemory(String memory, int index)
{
this.memory.getJSONArray(ARRAYED_MEMORY).put(index, memory);
}
/**
* Get all array-stored memories
* @return An array of all array-based memories
*/
public JSONArray getArrayMemories()
{
return memory.getJSONArray(ARRAYED_MEMORY);
}
/**
* Get the array-based memory at index
* @param index The memory to retrieve
* @return The memory stored at index
*/
public Optional<String> getMemory(int index)
{
return Optional.ofNullable(memory.getJSONArray(ARRAYED_MEMORY).optString(index, null));
}
/**
* Gets all memories as a JSON string.
* @return A JSON string of all memories
*/
public String getMappedMemories() {
ArrayList<String> memories = new ArrayList<>();
for (String key : memory.getJSONObject(MAPPED_MEMORY).keySet()) {
memories.add(key + ": " + memory.getJSONObject(MAPPED_MEMORY).getString(key));
}
return new JSONArray(memories).toString();
}
public ArrayList<String> getMemoriesArray() {
ArrayList<String> memories = new ArrayList<>();
int length = memory.getJSONArray(ARRAYED_MEMORY).length();
for (int i = 0; i < length; i++) {
memories.add(i + ": " + memory.getJSONArray(ARRAYED_MEMORY).getString(i));
}
return memories;
}
}
@@ -0,0 +1,29 @@
package me.neurodock.core.memory;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull;
public class GetMemoriesFunction extends FunctionTool {
@Override
public @NotNull String name() {
return "get_memories";
}
@Override
public String description() {
return "Retrieves all the memories.";
}
@Override
public @NotNull ToolParameters parameters() {
return ToolParameters.empty();
}
@Override
public @NotNull ToolResponse function(ToolArguments args) {
return new ToolResponse(name(), CoreMemory.getInstance().getMappedMemories());
}
}
@@ -0,0 +1,42 @@
package me.neurodock.core.memory;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull;
/**
* Provides the get_memory function.<br>
* This function retrives all the memory.
*/
public class GetMemoryFunction extends FunctionTool {
/**
* The CoreMemory instance.
*/
CoreMemory memory = CoreMemory.getInstance();
@Override
public @NotNull String name() {
return "get_memory";
}
@Override
public String description() {
return "Retrieves the memory for a given identity.";
}
@Override
public @NotNull ToolParameters parameters() {
return ToolParameters.builder()
.addProperty("identity", ToolParameters.ToolParametersBuilder.Type.STRING, "The identity of the memory to retrieve", true)
.build();
}
@Override
public @NotNull ToolResponse function(ToolArguments args) {
return memory.getMemory(args.optArgument("identity", String.class))
.map(value -> new ToolResponse(name(), value))
.orElse(ToolResponse.empty(name(), "No memory found for key: " + args.optArgument("identity", String.class)));
}
}
@@ -0,0 +1,32 @@
package me.neurodock.core.memory;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull;
import org.json.JSONArray;
public class GetMemoryIdentitiesFunction extends FunctionTool {
CoreMemory memory = CoreMemory.getInstance();
@Override
public @NotNull String name() {
return "get_memory_identities";
}
@Override
public String description() {
return "Retrieves all the memory identities.";
}
@Override
public @NotNull ToolParameters parameters() {
return ToolParameters.empty();
}
@Override
public @NotNull ToolResponse function(ToolArguments args) {
return new ToolResponse(this.name(), new JSONArray(memory.getMemoriesIdentity()).toString());
}
}
@@ -0,0 +1,46 @@
package me.neurodock.core.memory;
import me.neurodock.llm.exceptions.ToolException;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull;
/**
* Provides the remove_memory function.<br>
* This function removes a value from the memory.
*/
public class RemoveMemoryFunction extends FunctionTool {
/**
* The CoreMemory instance.
*/
CoreMemory memory = CoreMemory.getInstance();
@Override
public @NotNull String name() {
return "remove_memory";
}
@Override
public String description() {
return "Forget a memory";
}
@Override
public @NotNull ToolParameters parameters() {
return ToolParameters.builder()
.addProperty("identity", ToolParameters.ToolParametersBuilder.Type.STRING, "The identity of the memory to forget", true)
.build();
}
@Override
public @NotNull ToolResponse function(ToolArguments args) {
String value = args.optArgument("identity", String.class);
if(value == null || value.isEmpty()) {
throw new ToolException(this, "Missing identity argument");
}
memory.removeMemory(value);
return new ToolResponse(name(), "Removed "+value+" to the memory");
}
}
@@ -0,0 +1,43 @@
package me.neurodock.core.memory.array;
import me.neurodock.core.memory.CoreMemory;
import me.neurodock.llm.exceptions.ToolException;
import me.neurodock.llm.tools.*;
import org.jetbrains.annotations.NotNull;
public class AddArrayMemory extends FunctionTool {
private CoreMemory memory = CoreMemory.getInstance();
@Override
public @NotNull String name() {
return "add_arrayed_memory";
}
@Override
public @NotNull ToolParameters parameters() {
return ToolParameters.builder()
.addProperty("memory", ToolParameters.ToolParametersBuilder.Type.STRING, "The memory to remember", true)
.addProperty("index", ToolParameters.ToolParametersBuilder.Type.INT, "The index to put it at. Should be avoided, unless overwriting", false)
.build();
}
@Override
public @NotNull ToolResponse function(ToolArguments args) {
if (args.hasArgument("index")) {
String memory = args.optArgument("memory", String.class);
int index = args.optArgument("index", Integer.class);
if (memory == null || index < 0) {
throw new ToolException(this, "no memory or index provided");
}
this.memory.addMemory(memory, index);
}
else {
if(!args.hasArgument("memory")) {
throw new ToolException(this, "no memory provided");
}
this.memory.addMemory(name(), args.optArgument("memory", String.class));
}
return new ToolResponse(name(), "Added arrayed memory");
}
}
@@ -0,0 +1,34 @@
package me.neurodock.core.memory.array;
import me.neurodock.core.memory.CoreMemory;
import me.neurodock.llm.exceptions.ToolException;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull;
public class GetArrayMemory extends FunctionTool {
CoreMemory memory = CoreMemory.getInstance();
@Override
public @NotNull String name() {
return "get_array_memory";
}
@Override
public @NotNull ToolParameters parameters() {
return ToolParameters.builder()
.addProperty("index", ToolParameters.ToolParametersBuilder.Type.STRING, "The index to retrieve memory from", false)
.build();
}
@Override
public @NotNull ToolResponse function(ToolArguments args) {
if(!args.hasArgument("index")) throw new ToolException(this, "Missing index");
return memory.getMemory(args.optArgument("index", Integer.class, -1))
.map(value -> new ToolResponse(name(), value))
.orElse(ToolResponse.empty("No memory found for key: " + args.optArgument("index", -1)));
}
}
@@ -0,0 +1,26 @@
package me.neurodock.core.memory.array;
import me.neurodock.core.memory.CoreMemory;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull;
public class GetArrayedMemories extends FunctionTool {
private CoreMemory memory = CoreMemory.getInstance();
@Override
public @NotNull String name() {
return "get_arrayed_memories";
}
@Override
public @NotNull ToolParameters parameters() {
return ToolParameters.empty();
}
@Override
public @NotNull ToolResponse function(ToolArguments args) {
return new ToolResponse(name(), memory.getArrayMemories().toString());
}
}
@@ -0,0 +1,5 @@
/**
* A memory module for the AI Assistent to have the abilit to store memories between conversations, and over longer time
* TODO: Key-Value map memory bank alongside of the array based.
*/
package me.neurodock.core.memory;
@@ -0,0 +1,18 @@
package me.neurodock.llm;
import java.util.ArrayList;
import java.util.List;
public class ChatObject {
private ArrayList<Message> messages = new ArrayList<>();
public List<Message> getConversation()
{
return messages;
}
public void addMessage(Message msg)
{
messages.add(msg);
}
}
@@ -0,0 +1,70 @@
package me.neurodock.llm;
import org.json.JSONArray;
public class Message {
protected Role role;
protected Object content;
protected String toolID;
protected JSONArray toolCalls;
/**
*
* @param role
* @param content
* @throws IllegalArgumentException If the backend does not support the content or role provided
* @implSpec Implementers of a backend is expected to sanitize the content to be valid for your backend.
* Throw {@link IllegalArgumentException} for cases where your backend does not support the arguments provided.
* <b>DO NOT</b> default to somthing else that is for the user of your backend to handle.
* You may however provide your own builder/contstuctor that can make such assumptions or defaults but this contractor <b>SHULD NEVER</b> do that
*/
public Message(Role role, Object content)
{
this.role = role;
this.content = content;
}
public Message(Role role, Object content, JSONArray toolCalls)
{
this(role, content);
this.toolCalls = toolCalls;
}
public Message(Role role, String toolID, Object content)
{
this(role, content);
this.toolID = toolID;
}
public Role getRole() {
return role;
}
public Object getContent() {
return content;
}
public String getToolID() {
return toolID;
}
public JSONArray getToolCalls() {
return toolCalls;
}
public static enum Role {
USER,
ASSISTANT,
TOOL,
SYSTEM;
}
@Override
public String toString() {
return "Message{" +
"role=" + role +
", content=" + content +
", toolID='" + toolID + '\'' +
", toolCalls=" + toolCalls +
'}';
}
}
@@ -0,0 +1,60 @@
package me.neurodock.llm;
import me.neurodock.llm.tools.Tool;
import me.neurodock.llm.tools.serializer.ToolSerializer;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* This interface is used for when a backend dose not supports streaming responses, otherwise use {@link StreamingModel}
*/
public interface Model {
/**
* Sends the given chat context to the model and requests the next response.
*
* @param obj the chat context, containing the conversation history to
* send to the model
* @param tools the tools available to the model for this request, or an
* empty list if none
* @return a future resolving to the model's reply as a {@link Message}
*
* @apiNote This method does not append the resulting {@link Message} to
* {@code obj} itself. The caller is expected to append it to
* {@code obj}'s conversation once the future completes, if the reply
* should persist as part of the chat history.
*/
CompletableFuture<Message> qurryModel(ChatObject obj, List<Tool> tools);
/**
* Sends a single, standalone message to the model, without any prior
* conversation context.
*
* @param msg the message to send
* @return a future resolving to the model's reply as a {@link Message}
*/
CompletableFuture<Message> singleFire(Message msg);
/**
* Sends a single, standalone message to the model with tool support,
* without any prior conversation context.
*
* @apiNote If no tools are needed for this request, prefer
* {@link #singleFire(Message)} instead of passing an empty or null
* {@code tools} list here.
* @param msg the message to send
* @param tools the tools available to the model for this request
* @return a future resolving to the model's reply as a {@link Message}
*/
CompletableFuture<Message> singleFire(Message msg, List<Tool> tools);
/**
* @return this backend's {@link ToolSerializer}, used to convert
* {@link Tool}s into the JSON shape this backend expects.
* Each backend instance owns its own — this is not shared
* across the JVM.
*/
ToolSerializer getToolSerializer();
}
@@ -0,0 +1,17 @@
package me.neurodock.llm;
import me.neurodock.llm.tools.Tool;
import org.json.JSONObject;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
/**
* This interface is used for when a backend supports streaming responses, otherwise use {@link Model}
*/
public interface StreamingModel extends Model {
CompletableFuture<Message> qurryModel(ChatObject obj, List<Tool> tools, Consumer<JSONObject> chunkConsumer);
CompletableFuture<Message> singleFire(Message msg, Consumer<JSONObject> chunkConsumer);
CompletableFuture<Message> singleFire(Message msg, List<Tool> tools, Consumer<JSONObject> chunkConsumer);
}
@@ -0,0 +1,11 @@
package me.neurodock.llm.exceptions;
public class ModelNotFoundException extends RuntimeException {
public ModelNotFoundException(String modelName) {
super("Model not found: " + modelName);
}
public ModelNotFoundException(String modelName, String backend) {
super("Model not found: \"" + modelName + "\" at backend: \"" + backend + "\"");
}
}
@@ -0,0 +1,21 @@
package me.neurodock.llm.exceptions;
import me.neurodock.llm.Message;
import me.neurodock.llm.tools.Tool;
public class ToolException extends RuntimeException {
protected Tool exceptingTool;
public ToolException(Tool tool, String message) {
super(message);
exceptingTool = tool;
}
public ToolException(Tool tool, Throwable cause) {
this(tool, cause.getMessage());
}
public Message getErrorMessage()
{
return new Message(Message.Role.TOOL, getMessage());
}
}
@@ -0,0 +1,41 @@
package me.neurodock.llm.tools;
import me.neurodock.core.ToolCallingRender;
import me.neurodock.llm.exceptions.ToolException;
import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;
public abstract class FunctionTool implements Tool {
/**
* The name of the tool. Used by the model to identify what it's calling.
* @return the tool's name
*/
@NotNull
@Override
public abstract String name();
/**
* The description of the tool. Used by the model to understand what the
* tool does. May be {@code null} to omit it from the serialized JSON.
* @return the tool's description, or {@code null}
*/
public String description() {
return null;
}
/**
* The parameters this tool accepts.
* @return the tool's parameter schema
*/
@NotNull
public abstract ToolParameters parameters();
/**
* Invokes the tool.
* @param args the arguments passed by the model
* @return the tool's response
* @throws ToolException if the tool encounters an error
*/
@NotNull
public abstract ToolResponse function(ToolArguments args) throws ToolException;
}
@@ -0,0 +1,27 @@
package me.neurodock.llm.tools;
import me.neurodock.core.ToolCallingRender;
import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
public interface Tool {
static List<Tool> emptyTools() {
return new ArrayList<>();
}
/**
* The name of the tool, as sent to the model. Used to route an
* incoming tool call back to this tool.
*
* @return the tool's name
*/
@NotNull
String name();
default ToolCallingRender renderCalling(JSONObject calling) {
return new ToolCallingRender.Default();
}
}
@@ -0,0 +1,66 @@
package me.neurodock.llm.tools;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
public class ToolArguments {
Map<String, Object> arguments = new HashMap<>();
public void addArguments(Map<String, Object> args)
{
arguments.putAll(args);
}
public void addArgument(String name, Object value)
{
arguments.put(name, value);
}
public Object getArgument(String name)
{
if(!arguments.containsKey(name)) throw new NoSuchElementException("Missing required argument: " + name);;
return arguments.get(name);
}
public <T> T getArgument(String name, Class<T> type)
{
if(!arguments.containsKey(name)) throw new NoSuchElementException("Missing required argument: " + name);;
Object o = arguments.get(name);
if(type.isAssignableFrom(o.getClass())) return type.cast(o);
throw new ClassCastException("Argument '" + name + "' is " + o.getClass().getSimpleName() + ", expected " + type.getSimpleName());
}
public Object optArgument(String name)
{
if(!arguments.containsKey(name)) return null;
return arguments.get(name);
}
public Object optArgument(String name, Object defaultValue)
{
if(!arguments.containsKey(name)) return defaultValue;
return arguments.get(name);
}
public <T> T optArgument(String name, Class<T> type)
{
if(!arguments.containsKey(name)) return null;
Object o = arguments.get(name);
if(type.isAssignableFrom(o.getClass())) return type.cast(o);
return null;
}
public <T> T optArgument(String name, Class<T> type, T defaultValue)
{
if(!arguments.containsKey(name)) return defaultValue;
Object o = arguments.get(name);
if(type.isAssignableFrom(o.getClass())) return type.cast(o);
return defaultValue;
}
public boolean hasArgument(String name)
{
return arguments.containsKey(name);
}
}
@@ -0,0 +1,212 @@
package me.neurodock.llm.tools;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* Represents the parameters of a tool.
* This is used by Ollama to determine the parameters of a tool.
*/
public class ToolParameters {
/**
* Creates a new instance of {@link ToolParameters}.
* @param properties The properties of the parameters
* @param required The required parameters
*/
private ToolParameters(Map<String, ToolParametersBuilder.Property> propertyMap, ArrayList<String> required) {
this.propertyMap = propertyMap;
this.required = required;
};
/**
* The properties of the parameters.
*/
private Map<String, ToolParametersBuilder.Property> propertyMap;
/**
* The required parameters.
*/
private ArrayList<String> required;
/**
* Gets the properties of the {@link ToolParameters}
* @return The properties of the {@link ToolParameters}
*/
public Map<String, ToolParametersBuilder.Property> getProperties() {
return propertyMap;
}
/**
* Gets the required parameters of the {@link ToolParameters}
* @return The required parameters of the {@link ToolParameters}
*/
public ArrayList<String> getRequired() {
return required;
}
/**
* Creates a new instance of {@link ToolParametersBuilder}.
* @return The {@link ToolParametersBuilder}
*/
public static ToolParametersBuilder builder() {
return new ToolParametersBuilder();
}
/**
* Creates an empty {@link ToolParameters}
* @return an empty {@link ToolParameters}
* @apiNote This is equvalent to
* <pre>{@code
* ToolParameters.builder().build();
* }</pre>
*/
public static ToolParameters empty() {
return builder().build();
}
/**
* Represents a builder for {@link ToolParameters}.
*/
public static class ToolParametersBuilder {
/**
* The properties of the parameters.
*/
private Map<String, Property> propertyMap = new HashMap<>();
/**
* The required parameters.
*/
private ArrayList<String> required = new ArrayList<>();
/**
* Add an optinal perameter to this {@link ToolParametersBuilder}
* @param name The name of the parameter
* @param type The type of the parameter
* @param description The description of the parameter
* @return The {@link ToolParametersBuilder}
* @apiNote Prefer {@link #addProperty(String, Type, String, boolean)} to be explicit about required state
*/
public ToolParametersBuilder addProperty(String name, Type type, String description) {
return addProperty(name, type, description, false);
}
/**
* Add a potentialy required peremeter to this {@link ToolParametersBuilder}.
* @param name The name of the parameter
* @param type The type of the parameter
* @param description The description of the parameter
* @param required The required state of the parameter
* @return The {@link ToolParametersBuilder}
*/
public ToolParametersBuilder addProperty(String name, Type type, String description, boolean required) {
if(name == null || type == null || description == null) {
return this;
}
propertyMap.put(name, new Property(type, description));
if(required) {
this.required.add(name);
}
return this;
}
/**
* Makes a previusly optinal perameter required for this {@link ToolParametersBuilder}
* @param name The name of the parameter
* @return The {@link ToolParametersBuilder}
*/
public ToolParametersBuilder required(String name) {
if (!propertyMap.containsKey(name)) {
throw new IllegalArgumentException("Cannot require unknown property: " + name);
}
required.add(name);
return this;
}
/**
* Removes a property from the parameters.
* @param name The name of the property to remove
* @return The {@link ToolParametersBuilder}
*/
public ToolParametersBuilder removeProperty(String name) {
propertyMap.remove(name);
required.remove(name);
return this;
}
/**
* Builds the {@link ToolParameters}
* @return The {@link ToolParameters}
*/
public ToolParameters build() {
return new ToolParameters(propertyMap, required);
}
/**
* Represents a property of a parameter.
*
* @param type The type of the property.
* @param description The description of the property.
*/
public record Property(Type type, String description) {
/**
* Creates a new instance of {@link Property}.
*
* @param type The type of the property
* @param description The description of the property
*/
public Property {
}
}
/**
* Represents the type of parameter.
*/
public enum Type {
/**
* Represents a string parameter.
*/
STRING("string"),
/**
* Represents an integer parameter.
*/
INT("integer"),
/**
* Represents a boolean parameter.
*/
BOOLEAN("boolean"),
/**
* Represents a enum parameter.
*/
ENUM("enum"),
/**
* Represents a array parameter.
*/
ARRAY("array"),
/**
* Represents a object parameter.
*/
OBJECT("object");
/**
* The type of the parameter.
*/
private final String type;
/**
* Gets the type of the parameter.
* @return The type of the parameter
*/
public String getType() {
return type;
}
/**
* Creates a new instance of {@link Type}.
* @param type The type of the parameter
*/
Type(String type) {
this.type = type;
}
}
}
}
@@ -0,0 +1,74 @@
package me.neurodock.llm.tools;
import me.neurodock.llm.Message;
import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;
/**
* Represents a response from a tool.
*/
public class ToolResponse extends Message {
/**
* Returns an empty tool response
* See {@link ToolResponse#empty(String, String)} for a reasoned/described response
* @param tool The tool that responded
* @return an empty tool response
*/
public static ToolResponse empty(String tool)
{
return empty(tool, "No reason provided");
}
/**
* Returns an empty tool response with a reason/description.
* See {@link ToolResponse#empty(String)} for reason/description less response
* @param tool The tool that responded
* @param description A description for why this is empty
* @return an empty tool response with a reason
*/
public static ToolResponse empty(String tool, String description)
{
return new ToolResponse(tool, "Empty! reason: " + description);
}
/**
* The tool that responded.
*/
private final String tool;
/**
* The response from the tool.
*/
private final String response;
/**
* Creates a new instance of {@link ToolResponse}.
* @param tool The tool that responded
* @param response The response from the tool
*/
public ToolResponse(String tool, String response) {
super(Message.Role.TOOL, new JSONObject().put("tool", tool).put("result", response).toString());
this.tool = tool;
this.response = response;
}
/**
* Gets the tool that responded.
* @return The tool that responded
*/
public String getTool() {
return tool;
}
/**
* Gets the response from the tool.
* @return The response from the tool
*/
public String getResponse() {
return response;
}
public void setToolID(@NotNull String name) {
this.toolID = name;
}
}
@@ -0,0 +1,36 @@
package me.neurodock.llm.tools.serializer;
import me.neurodock.llm.tools.Tool;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
public class ToolSerializer {
private final Map<Class<? extends Tool>, ToolToJSON<? extends Tool>> serializers = new HashMap<>();
public <T extends Tool> boolean addSerializer(Class<T> clazz, ToolToJSON<T> serializer) {
if(!serializers.containsKey(clazz)) {
serializers.put(clazz, serializer);
return true;
}
return false;
}
@SuppressWarnings({"unchecked"})
public <T extends Tool> ToolToJSON<T> getSerializer(Class<T> clazz) {
return (ToolToJSON<T>) serializers.get(clazz);
}
public <T extends Tool> JSONObject serialize(T tool)
{
if(!serializers.containsKey(tool.getClass())) throw new IllegalArgumentException("Tool " + tool.getClass() + " not found!");
@SuppressWarnings({"unchecked"})
ToolToJSON<T> serializer = (ToolToJSON<T>) serializers.get(tool.getClass());
return serializer.toJSON(tool);
}
public boolean canSerialize(Class<? extends Tool> clazz) {
return serializers.containsKey(clazz);
}
}
@@ -0,0 +1,14 @@
package me.neurodock.llm.tools.serializer;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.Tool;
import org.json.JSONObject;
public interface ToolToJSON<T extends Tool> {
/**
* @param tool the tool to serialize
* @return the JSON representation of {@code tool}, in the shape this
* backend expects
*/
JSONObject toJSON(T tool);
}
@@ -0,0 +1,145 @@
package me.neurodock.plugin.loader;
import jdk.jshell.spi.ExecutionControl;
import me.neurodock.core.Pair;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.plugin.Data;
import me.neurodock.plugin.LoadedPlugin;
import me.neurodock.plugin.Plugin;
import me.neurodock.plugin.PluginMetadata;
import me.neurodock.plugin.exceptions.PluginLoadingException;
import me.neurodock.plugin.exceptions.ToolRuntimeException;
import me.neurodock.plugin.tool.Tool;
import me.neurodock.plugin.tool.ToolResponse;
import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;
import javax.naming.OperationNotSupportedException;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Parameter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import static me.neurodock.plugin.Plugin.UNKNOWN_PLUGIN;
public class Loader {
private final ArrayList<Pair<Plugin, PluginMetadata>> plugins = new ArrayList<>();
/**
* Mock example!
* @return
*/
public me.neurodock.llm.tools.Tool[] getTools(Plugin plugin) {
ArrayList<me.neurodock.llm.tools.Tool> tools = new ArrayList<>();
for(Tool tool : plugin.getTools()) {
tools.add(new FunctionTool() {
@Override
public @NotNull String name() {
return tool.name() + "_" + plugin.getMetadata().getName();
}
@Override
public String description() {
return tool.description();
}
@Override
public @NotNull ToolParameters parameters() {
throw new RuntimeException(new OperationNotSupportedException("No"));
//return ToolParameters.builder().of(tool.parameters()).build();
}
@Override
public @NotNull me.neurodock.llm.tools.ToolResponse function(ToolArguments args) {
throw new UnsupportedOperationException("no");
// Wrap OllamaFunctionArguments[] to ether ToolArgument[] or ToolArguments(current implementation)
/*ToolArguments toolArgs = new ToolArguments();
for (OllamaFunctionArgument arg : args) {
toolArgs.addArgument(arg.argument(), arg.value());
}
ToolResponse toolResponse;
try {
toolResponse = tool.callTool(toolArgs);
} catch (ToolRuntimeException e) {
throw new OllamaToolErrorException(name(), e);
}
// Wrap ToolResponce to an OllamaToolRespnce(Well I see a typo here now)
return new OllamaToolResponse(toolResponse.name(), toolResponse.response());*/
}
});
}
return tools.toArray(tools.toArray(new me.neurodock.llm.tools.Tool[0]));
}
public LoadedPlugin loadPlugin(JSONObject pluginJson, JarFile jar, Path pluginJar, Data data) {
if(!pluginJson.has("name") || !(pluginJson.get("name") instanceof String pluginName))
throw new PluginLoadingException("Malformed plugin json", UNKNOWN_PLUGIN);
if(!pluginJson.has("entryPoint") || !(pluginJson.get("entryPoint") instanceof String pluginEntryPoint))
throw new PluginLoadingException("Malformed plugin json", pluginName);
JarEntry entry = jar.getJarEntry(pluginEntryPoint.replaceAll("\\.", "/")+".class");
if(entry == null) throw new PluginLoadingException("Missing plugin entrypoint", pluginName);
Plugin plugin = null;
URLClassLoader classLoader;
try {
classLoader = new URLClassLoader(
new URL[]{pluginJar.toUri().toURL()},
getClass().getClassLoader()
);
Class<?> pluginClass = classLoader.loadClass(pluginEntryPoint);
if(!Plugin.class.isAssignableFrom(pluginClass)) throw new PluginLoadingException(
"Entrypoint does not implement Plugin",
pluginName
);
Class<? extends Plugin> typedClass =
pluginClass.asSubclass(Plugin.class);
for(Constructor<?> constructor : typedClass.getDeclaredConstructors())
{
Parameter[] parameters = constructor.getParameters();
if(parameters.length == 1 &&
parameters[0].getType() == Data.class)
{
plugin = (Plugin) constructor.newInstance(data);
}
else if(parameters.length == 0)
{
plugin = (Plugin) constructor.newInstance();
}
}
}catch (ClassNotFoundException | InvocationTargetException | InstantiationException | IllegalAccessException | IOException e)
{
throw new PluginLoadingException("Failed to load plugin jar", e, pluginName);
}
if(plugin == null)
{
throw new PluginLoadingException(
"Missing proper constructor",
pluginName
);
}
return new LoadedPlugin(plugin, classLoader, plugin.getMetadata()/*TODO: MUST BE REPLACED WITH ACTUAL PLUGIN METADATA! this is read from the PluginEntyPoint*/);
}
}
@@ -1,531 +0,0 @@
package me.zacharias.chat.core;
import me.zacharias.chat.core.memory.*;
import me.zacharias.chat.ollama.*;
import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException;
import me.zacharias.chat.plugin.Plugin;
import me.zacharias.chat.plugin.PluginLoader;
import me.zacharias.chat.plugin.exceptions.PluginLoadingException;
import org.intellij.lang.annotations.MagicConstant;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* The Main class for the System, responsible for managing the OllamaObject, tools, and the Ollama API.
*/
public class Core {
/**
* The file to write the logs to.
*/
private static File logFile = new File("./logs/latest.log");
/**
* The writer to write the logs to.
*/
private static BufferedWriter logWriter;
/**
* The scheduler to schedule the log flushing.
*/
private ScheduledExecutorService scheduler;
/**
* The OllamaObject to use.
*/
private OllamaObject ollamaObject;
/**
* The list of tools to use.
*/
private ArrayList<Pair<OllamaFunctionTool, String>> funtionTools = new ArrayList<>();
/**
* The IP of the Ollama API.
*/
private String ollamaIP = "localhost";//"192.168.5.184";
/**
* The port of the Ollama API.
*/
private int ollamaPort = 11434;
/**
* The URL of the Ollama API.
*/
private URL url;
/**
* The PrintMessageHandler to use.
*/
private final PrintMessageHandler printMessageHandler;
/**
* If color is supported by the PrintMessageHandler.
*/
private boolean supportColor;
public static final String DATA;
public static final File DATA_DIR;
public static final File PLUGIN_DIRECTORY;
static {
String data;
if(System.getenv("AI_CHAT_DEBUG") != null) {
data = "./data";
}
else if(System.getProperty("os.name").toLowerCase().contains("windows")) {
String localappdata = System.getenv("LOCALAPPDATA");
if(localappdata == null) {
localappdata = System.getenv("APPDATA");
}
data = localappdata + "/AI-CHAT";
}
else if (System.getProperty("os.name").toLowerCase().contains("linux")) {
data = System.getenv("HOME") + "/.local/share/AI-CHAT";
}
else if (System.getProperty("os.name").toLowerCase().contains("mac")) {
data = System.getProperty("user.home") + "/Library/Application Support/AI-CHAT";
}
else {
data = "./data";
}
DATA = data;
DATA_DIR = new File(DATA);
if(!DATA_DIR.exists()) {
DATA_DIR.mkdirs();
}
String pluginDir = DATA + "/plugins";
PLUGIN_DIRECTORY = new File(pluginDir);
if(!PLUGIN_DIRECTORY.exists()) {
PLUGIN_DIRECTORY.mkdirs();
}
}
{
File dir = new File("./logs/");
if (!dir.exists()) {
dir.mkdir();
}
dir = new File("./pythonFiles/");
if (!dir.exists()) {
dir.mkdir();
}
dir = new File("./messages");
if (!dir.exists()) {
dir.mkdir();
}
try {
url = new URI("http://"+ollamaIP+":"+ollamaPort+"/api/chat").toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
try {
if (logFile.exists()) {
BufferedReader br = new BufferedReader(new FileReader(logFile));
String line = br.readLine();
br.close();
if (line != null) {
String date = line.substring(0, line.indexOf(">")).replaceAll("[/:]", "-");
logFile.renameTo(new File(logFile.getParentFile(), date + ".log"));
logFile = new File("./logs/latest.log");
}
else {
System.out.println("Exisitng log file is empty, overwriting it!");
logFile.delete();
}
logFile.createNewFile();
}
logWriter = new BufferedWriter(new FileWriter(logFile));
}catch (IOException e) {
throw new RuntimeException(e);
}
this.scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
try {
logWriter.flush();
//System.out.println("Buffer flushed to file.");
} catch (IOException e) {
e.printStackTrace();
}
}, 0, 3, TimeUnit.MINUTES);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
scheduler.shutdownNow();
try {
try {
logWriter.flush();
logWriter.close();
}catch (IOException ignore)
{
// This exception is kinda expected. Since it can often occur that the logWriter is already closed
System.out.println("Failed to flush log file, but that is not a problem.");
}
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd_HH-mm-ss");
File messagesFile = new File("./messages/"+now.format(formatter)+".json");
BufferedWriter messagesWriter = new BufferedWriter(new FileWriter(messagesFile));
JSONArray messages = new JSONArray();
for(OllamaMessage message : ollamaObject.getMessages()) {
messages.put(new JSONObject(message.toString()));
}
messagesWriter.write(messages.toString());
messagesWriter.close();
File f = new File("./data/messages.json");
if(f.exists())
{
f.delete();
}
f.createNewFile();
messagesWriter = new BufferedWriter(new FileWriter(f));
messagesWriter.write(messages.toString());
messagesWriter.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}));
}
/**
* Creates a new instance of Core with the provided PrintMessageHandler
* @param printMessageHandler The PrintMessageHandler to use as the default Output
*/
public Core(PrintMessageHandler printMessageHandler) {
this.printMessageHandler = printMessageHandler;
supportColor = printMessageHandler.color();
}
/**
* Sets the {@link #ollamaObject} object to the provided argument,
* Also adds the memory base system. see {@link Core#setOllamaObjectNoMemory} if you don't want to add memory functions
* @param ollamaObject The OllamaObject to use
*/
public void setOllamaObject(OllamaObject ollamaObject) {
if(this.ollamaObject == null) {
this.ollamaObject = ollamaObject;
addTool(new AddMemoryFunction(), Source.CORE);
addTool(new RemoveMemoryFunction(), Source.CORE);
addTool(new GetMemoryFunction(), Source.CORE);
addTool(new GetMemoriesFunction(), Source.CORE);
addTool(new GetMemoryIdentitiesFunction(), Source.CORE);
}
else {
throw new IllegalArgumentException("Ollama object is already set");
}
}
/**
* Sets the {@link #ollamaObject} object to the provided argument,
* Dose not add the base system for memory. see {@link #setOllamaObject} if you want to add memory function
* @param ollamaObject The OllamaObject to use
*/
public void setOllamaObjectNoMemory(OllamaObject ollamaObject) {
if(this.ollamaObject == null) {
this.ollamaObject = ollamaObject;
}
else {
throw new IllegalArgumentException("Ollama object is already set");
}
}
/**
* Adds a new tool to the System
* @param functionTool The tool to add
* @param source The source of the tool
*/
public void addTool(OllamaFunctionTool functionTool, @MagicConstant(valuesFromClass = Source.class) String source) {
funtionTools.add(new Pair<>(functionTool, source));
ollamaObject.addTool(functionTool);
}
/**
* Adds a list of tools to the System
* @param tools The tools to add
*/
@SuppressWarnings("MagicConstant")
public void addTools(OllamaFunctionTools tools)
{
for(Pair<OllamaFunctionTool, String> tool : tools)
{
addTool(tool.getKey(), tool.getValue());
}
}
/**
* Gets the list of tools added to the System
* @return The list of tools added to the System compressed as Pairs of the tool and the source
*/
public ArrayList<Pair<OllamaFunctionTool, String>> getFuntionTools() {
return funtionTools;
}
/**
* Gets the Ollama Object
* @return The Ollama Object
*/
public OllamaObject getOllamaObject() {
return ollamaObject;
}
public void removeTool(String name) {
Pair<OllamaFunctionTool, String> funtionTool = funtionTools.stream().filter(tool -> tool.getKey().name().equalsIgnoreCase(name)).findFirst().orElse(null);
funtionTools.remove(funtionTool);
if(funtionTool.getKey() == null) {
// This should never happens... So if it does, Shit hit the fan
Exception e = new IllegalArgumentException("Function tool with name '"+name+"' does not exist");
e.printStackTrace();
System.exit(1);
}
ollamaObject.removeTool(funtionTool.getKey());
}
/**
* Flushes the log file
*/
public static void flushLog() {
try {
logWriter.flush();
}catch (IOException e) {}
}
/**
* Sends the OllamaObject to Ollama
* @return The response from Ollama
*/
public JSONObject qurryOllama()
{
try {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json");
connection.setDoOutput(true);
connection.setConnectTimeout(80*1000);
String ollamaObjectString = ollamaObject.toString();
ollamaObjectString = ollamaObjectString.replace("\n", "\\n");
try(DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
wr.write(ollamaObjectString.getBytes(StandardCharsets.UTF_8));
wr.flush();
}
int responseCode = connection.getResponseCode();
// HTTP_OK or 200 response code generally means that the server ran successfully without any errors
StringBuilder response = new StringBuilder();
// Read response content
// connection.getInputStream() purpose is to obtain an input stream for reading the server's response.
try (
BufferedReader reader = new BufferedReader( new InputStreamReader( connection.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
response.append(line); // Adds every line to response till the end of file.
}
}catch (Exception ex)
{
// If the server returns an error, we read the error stream instead
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
} catch (Exception e) {
System.err.println("Error reading error stream: " + e.getMessage());
}
}
if (responseCode == HttpURLConnection.HTTP_OK) {
connection.disconnect();
return new JSONObject(response.toString());
}
else {
connection.disconnect();
System.err.println("Error: HTTP Response code - " + responseCode + "\n"+response.toString());
throw new RuntimeException("HTTP Response code - " + responseCode);
}
} catch (IOException e) {
// System.err.println("Error: JSON: "+);
throw new RuntimeException(e);
}
}
/**
* Handles the response from Ollama
* By Processing the response, handles Function Calls, Logs relevant information, Appends information to the OllamaObject, Prints messages to the User.
* @param responce The response from Ollama
*/
public void handleResponce(JSONObject responce)
{
//System.out.println("Responce: "+responce);
if(responce != null) {
writeLog("Raw responce: "+responce.toString());
JSONObject message = responce.getJSONObject("message");
if(message.has("tool_calls"))
{
ollamaObject.addMessage(new OllamaMessageToolCall(OllamaMessageRole.fromRole(message.optString("role")), message.getString("content"), message.getJSONArray("tool_calls")));
JSONArray calls = message.getJSONArray("tool_calls");
for(Object call : calls)
{
if(call instanceof JSONObject jsonObject)
{
if(jsonObject.has("function"))
{
JSONObject function = jsonObject.getJSONObject("function");
List<Pair<OllamaFunctionTool, String>> functions = funtionTools.stream().filter(func -> (func.getKey().name()).equalsIgnoreCase(function.getString("name"))).toList();
if(functions.isEmpty()) {
ollamaObject.addMessage(new OllamaToolError("Function '"+function.getString("name")+"' does not exist"));
printMessageHandler.printMessage((supportColor ?"\u001b[31m":"")+"Tried funtion call "+function.getString("name")+" but failed to find it."+(printMessageHandler.color()?"\u001b[0m":""));
writeLog("Failed function call to "+function.getString("name"));
}
else {
OllamaFunctionTool func = functions.getFirst().getKey();
ArrayList<OllamaFunctionArgument> argumentArrayList = new ArrayList<>();
JSONObject arguments = function.getJSONObject("arguments");
for (String key : arguments.keySet()) {
argumentArrayList.add(new OllamaFunctionArgument(key, arguments.get(key)));
}
try {
OllamaToolRespnce function1 = func.function(argumentArrayList.toArray(new OllamaFunctionArgument[0]));
ollamaObject.addMessage(function1);
printMessageHandler.printMessage((supportColor?"\u001b[34m":"")+"Call "+func.name() + (supportColor?"\u001b[0m":""));
writeLog("Successfully function call " + func.name() + " output: " + function1.getResponse());
} catch (OllamaToolErrorException e) {
ollamaObject.addMessage(new OllamaToolError(e.getMessage()));
printMessageHandler.printMessage((supportColor?"\u001b[31m":"")+"Tried funtion call " + func.name() + " but failed due to " + e.getError() + (supportColor?"\u001b[0m":""));
writeLog(e.getMessage());
}
}
}
}
}
checkIfResponceMessage(responce);
handleResponce(qurryOllama());
}
else checkIfResponceMessage(responce);
}
}
/**
* Checks if the response contains a message and if so, prints it to the user
* @param responce the Ollama response
*/
private void checkIfResponceMessage(JSONObject responce) {
String message = responce.getJSONObject("message").getString("content");
if(responce.getJSONObject("message").has("content") && !message.isBlank())
{
printMessageHandler.printMessage((supportColor?"\u001b[32m":"")+(LaunchOptions.getInstance().isShowFullMessage()? message : message.replaceAll("(?s)<think>.*?</think>", "")) +(supportColor?"\u001b[0m":""));
writeLog("Response content: "+ message);
ollamaObject.addMessage(new OllamaMessage(OllamaMessageRole.ASSISTANT, message));
}
}
/**
* Writes a message to the log file
* @param message The message to write
*/
public static void writeLog(String message)
{
try {
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd%EEEE HH:mm:ss'#'SSS");
logWriter.write(now.format(formatter) + "> " + message + "\n");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void enablePlugins(File pluginDirectory) {
if(!pluginDirectory.exists()) {
throw new IllegalArgumentException("Plugin directory does not exist");
}
if(!pluginDirectory.isDirectory()) {
throw new IllegalArgumentException("Plugin directory is not a directory");
}
File[] files = pluginDirectory.listFiles((dir, name) -> name.endsWith(".jar"));
if(files == null) {
return;
}
PluginLoader loader = new PluginLoader();
for(File file : files) {
try(FileSystem fs = FileSystems.newFileSystem(file.toPath())){
if(!fs.getPath("/plugin.json").toFile().exists())
{
throw new PluginLoadingException("Plugin does not contain a plugin.json file", file.getName());
}
//JSONObject pluginJson = new JSONObject(new String(fs.getPath("/plugin.json").toFile().readAllBytes()));
//Plugin plugin = loader.loadPlugin(pluginJson, fs);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
/**
* Represents the source of a tool.
* <p>
* This is intended for use with {@link Core#addTool(OllamaFunctionTool, String)}
* to indicate the module from which a tool originates.
*/
public static class Source {
/**
* Represents an external tool that is not derived from the Core.
* Instead, it belongs to an internally defined system or module within the project/program.
*/
public static final String EXTERNAL = "External";
/**
* Represents an internally defined tool that is part of the Core system.
* This is meant for tools that are strictly part of the Core and should not be used for definitions outside of it.
*/
public static final String CORE = "Core";
/**
* Represents a tool defined through an API system.
* These tools are more dynamic, as they originate from fully external sources using the API.
*/
public static final String API = "Api";
/**
* Represents an internally defined tool that is derived from Core Components but not the Core itself.
* This is used for tools that are part of the Core Components, such as internal modules, but do not belong directly to the Core.
*/
public static final String INTERNAL = "Internal";
}
}
@@ -1,29 +0,0 @@
package me.zacharias.chat.core;
import java.util.HashMap;
import java.util.Map;
public class GlobalObjects {
private static final Map<String, Object> objects = new HashMap<>();
public static void addObject(String name, Object object) {
if (name == null || object == null) {
throw new IllegalArgumentException("Name and object cannot be null");
}
objects.put(name, object);
}
public static Object getObject(String name) {
if (name == null) {
throw new IllegalArgumentException("Name cannot be null");
}
return objects.get(name);
}
public static boolean removeObject(String name) {
if (name == null) {
throw new IllegalArgumentException("Name cannot be null");
}
return objects.remove(name) != null;
}
}
@@ -1,21 +0,0 @@
package me.zacharias.chat.core;
/**
* Represents a PrintMessageHandler.
* This is used by the Core to print messages to the user or API Clients.
*/
public interface PrintMessageHandler {
/**
* Handles the printing of a message.
* This is meant to output the message to the user or API Client.
* @param message The message to be printed.
*/
void printMessage(String message);
/**
* Gets if color is supported by the PrintMessageHandler.
* This uses ANSI escape codes to color the output.
* @return a boolean indicating if color is supported by the PrintMessageHandler.
*/
boolean color();
}
@@ -1,7 +0,0 @@
package me.zacharias.chat.core.files;
import me.zacharias.chat.core.Core;
public class FileHandlerLocation {
public static final String DATA_FILES = Core.DATA+"/files";
}
@@ -1,4 +0,0 @@
package me.zacharias.chat.core.files;
public class ListFiles {
}
@@ -1,63 +0,0 @@
package me.zacharias.chat.core.memory;
import me.zacharias.chat.ollama.OllamaFunctionArgument;
import me.zacharias.chat.ollama.OllamaFunctionTool;
import me.zacharias.chat.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaToolRespnce;
import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException;
/**
* Provides the add_memory function.<br>
* This function adds a string to the memory.
*/
public class AddMemoryFunction extends OllamaFunctionTool {
/**
* The CoreMemory instance.
*/
CoreMemory memory = CoreMemory.getInstance();
@Override
public String name() {
return "add_memory";
}
@Override
public String description() {
return "Remember somthing";
}
@Override
public OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("memory", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The memory to remember", true)
.addProperty("identity", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The identity of the memory to remember", true)
.build();
}
@Override
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
if (args.length == 0) {
throw new OllamaToolErrorException(name(), "Missing memory argument");
}
String memory = null;
String identity = null;
for(OllamaFunctionArgument arg : args) {
if (arg.argument().equals("memory")) {
memory = (String) arg.value();
} else if (arg.argument().equals("identity")) {
identity = (String) arg.value();
} else {
throw new OllamaToolErrorException(name(), "Unknown argument: " + arg.argument());
}
}
if (memory == null || identity == null) {
throw new OllamaToolErrorException(name(), "Missing memory or identity argument");
}
this.memory.addMemory(identity, memory);
return new OllamaToolRespnce(name(), "Added "+identity+" to the memory");
}
}
@@ -1,128 +0,0 @@
package me.zacharias.chat.core.memory;
import me.zacharias.chat.core.Core;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.*;
import java.util.ArrayList;
/**
* CoreMemory is a class that provides a way to store and retrieve strings from a file.<br>
* This is meant to be used as a way to store and retrieve strings from a file.
*/
public class CoreMemory {
/**
* The singleton instance of CoreMemory.
*/
private static final CoreMemory instance = new CoreMemory(Core.DATA + "/CoreMemory.json");
/**
* Gets the singleton instance of CoreMemory.
* @return The singleton instance of CoreMemory
*/
public static CoreMemory getInstance() {
return instance;
}
/**
* Creates a new instance of CoreMemory.
* @param memoryFile The file to store the memory in
*/
public CoreMemory(String memoryFile) {
File f = new File(memoryFile);
if (f.exists()) {
try {
BufferedReader br = new BufferedReader(new FileReader(f));
StringBuilder data = new StringBuilder();
String buffer = null;
while ((buffer = br.readLine()) != null) {
data.append(buffer);
}
memory = new JSONObject(data.toString());
}catch (Exception e) {
e.printStackTrace();
}
}
this.memoryFile = memoryFile;
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
try{
File f = new File(memoryFile);
if(f.exists()) {
f.delete();
}
f.createNewFile();
BufferedWriter bw = new BufferedWriter(new FileWriter(f));
bw.write(memory.toString());
bw.close();
}catch (Exception e) {
e.printStackTrace();
}
}
});
}
/**
* The memory.
*/
private JSONObject memory = new JSONObject();
/**
* The file to store the memory in.
*/
private final String memoryFile;
/**
* Gets the memory.
* @return A list of memory identifies/names
*/
public String[] getMemoriesIdentity() {
return memory.keySet().toArray(new String[0]);
}
public String getMemory(String name) {
return memory.optString(name, null);
}
/**
* Sets the memory.
* @param name The name/identity of the memory
* @param memory The memory
*/
public void addMemory(String name, String memory) {
this.memory.put(name, memory);
}
/**
* Removes the memory.
* @param name The memory to remove
*/
public void removeMemory(String name) {
this.memory.remove(name);
}
/**
* Gets all memories as a JSON string.
* @return A JSON string of all memories
*/
public String getMemories() {
ArrayList<String> memories = new ArrayList<>();
for (String key : memory.keySet()) {
memories.add(key + ": " + memory.getString(key));
}
return new JSONArray(memories).toString();
}
public ArrayList<String> getMemoriesArray() {
ArrayList<String> memories = new ArrayList<>();
for (String key : memory.keySet()) {
memories.add(key + ": " + memory.getString(key));
}
return memories;
}
}
@@ -1,28 +0,0 @@
package me.zacharias.chat.core.memory;
import me.zacharias.chat.ollama.OllamaFunctionArgument;
import me.zacharias.chat.ollama.OllamaFunctionTool;
import me.zacharias.chat.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaToolRespnce;
public class GetMemoriesFunction extends OllamaFunctionTool {
@Override
public String name() {
return "get_memories";
}
@Override
public String description() {
return "Retrieves all the memories.";
}
@Override
public OllamaPerameter parameters() {
return null;
}
@Override
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
return new OllamaToolRespnce(name(), CoreMemory.getInstance().getMemories());
}
}
@@ -1,41 +0,0 @@
package me.zacharias.chat.core.memory;
import me.zacharias.chat.ollama.OllamaFunctionArgument;
import me.zacharias.chat.ollama.OllamaFunctionTool;
import me.zacharias.chat.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaToolRespnce;
import org.json.JSONArray;
import org.json.JSONObject;
/**
* Provides the get_memory function.<br>
* This function retrives all the memory.
*/
public class GetMemoryFunction extends OllamaFunctionTool {
/**
* The CoreMemory instance.
*/
CoreMemory memory = CoreMemory.getInstance();
@Override
public String name() {
return "get_memory";
}
@Override
public String description() {
return "Retrieves the memory for a given identity.";
}
@Override
public OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("identity", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The identity of the memory to retrieve", true)
.build();
}
@Override
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
return new OllamaToolRespnce(name(), memory.getMemory((String) (args[0].value())));
}
}
@@ -1,31 +0,0 @@
package me.zacharias.chat.core.memory;
import me.zacharias.chat.ollama.OllamaFunctionArgument;
import me.zacharias.chat.ollama.OllamaFunctionTool;
import me.zacharias.chat.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaToolRespnce;
import org.json.JSONArray;
public class GetMemoryIdentitiesFunction extends OllamaFunctionTool {
CoreMemory memory = CoreMemory.getInstance();
@Override
public String name() {
return "get_memory_identities";
}
@Override
public String description() {
return "Retrieves all the memory identities.";
}
@Override
public OllamaPerameter parameters() {
return null;
}
@Override
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
return new OllamaToolRespnce(this.name(), new JSONArray(memory.getMemoriesIdentity()).toString());
}
}
@@ -1,45 +0,0 @@
package me.zacharias.chat.core.memory;
import me.zacharias.chat.ollama.OllamaFunctionArgument;
import me.zacharias.chat.ollama.OllamaFunctionTool;
import me.zacharias.chat.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaToolRespnce;
import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException;
/**
* Provides the remove_memory function.<br>
* This function removes a value from the memory.
*/
public class RemoveMemoryFunction extends OllamaFunctionTool {
/**
* The CoreMemory instance.
*/
CoreMemory memory = CoreMemory.getInstance();
@Override
public String name() {
return "remove_memory";
}
@Override
public String description() {
return "Forget a memory";
}
@Override
public OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("identity", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The identity of the memory to forget", true)
.build();
}
@Override
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
if (args.length == 0) {
throw new OllamaToolErrorException(name(), "Missing memory argument");
}
String value = (String) args[0].value();
memory.removeMemory(value);
return new OllamaToolRespnce(name(), "Removed "+value+" to the memory");
}
}
@@ -1,40 +0,0 @@
package me.zacharias.chat.ollama;
/**
* Represents an argument passed to a tool.
*
* @param argument The argument name
* @param value The argument value
*/
public record OllamaFunctionArgument(String argument, Object value) {
/**
* Creates a new instance of OllamaFunctionArgument.<br>
* This is used by Ollama to pass arguments to a tool.
*
* @param argument The argument name
* @param value The argument value
*/
public OllamaFunctionArgument {
}
/**
* Gets the argument name
*
* @return The argument name
*/
@Override
public String argument() {
return argument;
}
/**
* Gets the argument value.<br>
* This needs to be cast to the correct type by the tool itself
*
* @return The argument value
*/
@Override
public Object value() {
return value;
}
}
@@ -1,59 +0,0 @@
package me.zacharias.chat.ollama;
import me.zacharias.chat.core.Core;
import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException;
import org.json.JSONObject;
/**
* Represents a tool that Ollama can call.
*/
public abstract class OllamaFunctionTool implements OllamaTool {
@Override
public String toString() {
JSONObject ret = new JSONObject();
ret.put("tool", "function");
JSONObject function = new JSONObject();
function.put("name", name());
function.put("description", description());
function.put("parameters", (parameters() == null?
new JSONObject() : new JSONObject(parameters().toString())));
ret.put("function", function);
return ret.toString();
}
/**
* The name of the tool
* This is used by Ollama to know what the tool is
* @return The name of the tool
*/
abstract public String name();
/**
* The description of the tool
* This is used by Ollama to know what the tool does
* @return The description of the tool
*/
abstract public String description();
/**
* The parameters of the tool
* This is used by Ollama to know what parameters the tool takes
* If null, the tool does not take any parameters
* @return The parameters of the tool or null if the tool does not take any parameters
*/
abstract public OllamaPerameter parameters();
/**
* The function of the tool.<br>
* This is used by Ollama to call the tool.<br>
* Throw {@link OllamaToolErrorException} if the tool encounters an error instead of normal exceptions. The {@link OllamaToolErrorException} gets handled more gracefully by {@link Core#handleResponce(JSONObject)}
* @param args The arguments to pass to the tool, if any
* @return The response from the tool
* @throws OllamaToolErrorException If the tool encounters an error
*/
abstract public OllamaToolRespnce function(OllamaFunctionArgument... args);
}
@@ -1,111 +0,0 @@
package me.zacharias.chat.ollama;
import me.zacharias.chat.core.Core;
import me.zacharias.chat.core.Pair;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Spliterator;
import java.util.function.Consumer;
public class OllamaFunctionTools implements Iterable<Pair<OllamaFunctionTool, String>> {
/**
* A list of tools for the OllamaObject.
* OPS! Shuld only be ussed to add a set of tools to the OllamaObject, not to be used for storage of tools internaly or externaly
*/
private ArrayList<OllamaFunctionTool> tools;
/**
* A list of source for the tools.
* OPS! Shuld only be ussed to add a set of tools to the OllamaObject, not to be used for storage of tools internaly or externaly
* OPS! most be the same size as the tools list! since each tool matches to a source!
*/
private ArrayList<String> source;
/**
* Gets the tools of the {@link OllamaFunctionTools}
* @param tools A list of {@link OllamaFunctionTool}
* @param source The source of the tools
*/
private OllamaFunctionTools(ArrayList<OllamaFunctionTool> tools, ArrayList<String> source) {
if(source == null || tools == null || source.size() != tools.size() || source.isEmpty())
throw new IllegalArgumentException("The source and tools must be the same size! and not empty!");
this.tools = tools;
this.source = source;
}
/**
* Gets the tools of the {@link OllamaFunctionTools}
* @param tools A list of {@link OllamaFunctionTool}
* @param source The source of the tools
*/
private OllamaFunctionTools(OllamaFunctionTool[] tools, @MagicConstant(valuesFromClass = Core.Source.class) String[] source) {
if(source == null || tools == null || source.length != tools.length || source.length == 0)
throw new IllegalArgumentException("The source and tools must be the same size! and not empty!");
this.tools = new ArrayList<>();
this.source = new ArrayList<>();
for (OllamaFunctionTool tool : tools) {
this.tools.add(tool);
this.source.add(tool.name());
}
for (String s : source) {
if (s.equals(Core.Source.INTERNAL)) {
this.source.add(s);
}
}
}
public static OllamaFunctionToolsBuilder builder() {
return new OllamaFunctionToolsBuilder();
}
@Override
public @NotNull Iterator<Pair<OllamaFunctionTool, String>> iterator() {
ArrayList<Pair<OllamaFunctionTool, String>> pairs = new ArrayList<>();
for (int i = 0; i < tools.size(); i++) {
pairs.add(new Pair<>(tools.get(i), source.get(i)));
}
return pairs.iterator();
}
@Override
public void forEach(Consumer<? super Pair<OllamaFunctionTool, String>> action) {
for (Pair<OllamaFunctionTool, String> pair : this) {
action.accept(pair);
}
}
@Override
public Spliterator<Pair<OllamaFunctionTool, String>> spliterator() {
// TODO: Implement this method
throw new UnsupportedOperationException("Not implemented yet");
//return Iterable.super.spliterator();
}
public static class OllamaFunctionToolsBuilder {
private ArrayList<OllamaFunctionTool> tools = new ArrayList<>();
private ArrayList<String> source = new ArrayList<>();
public OllamaFunctionToolsBuilder addTool(OllamaFunctionTool tool, @MagicConstant(valuesFromClass = Core.Source.class) String source) {
this.tools.add(tool);
this.source.add(source);
return this;
}
public OllamaFunctionToolsBuilder addTools(HashMap<OllamaFunctionTool, String> tools) {
for (OllamaFunctionTool tool : tools.keySet()) {
this.tools.add(tool);
this.source.add(tools.get(tool));
}
return this;
}
public OllamaFunctionTools build() {
return new OllamaFunctionTools(tools, source);
}
}
}
@@ -1,35 +0,0 @@
package me.zacharias.chat.ollama;
import org.json.JSONObject;
/**
* Represents a message sent by a Tool, Assistant(Ollama), or User.
*/
public class OllamaMessage {
/**
* The role of the message.
*/
OllamaMessageRole role;
/**
* The content of the message.
*/
String content;
/**
* Creates a new instance of OllamaMessage.
* @param role The role of the message
* @param content The content of the message
*/
public OllamaMessage(OllamaMessageRole role, String content) {
this.role = role;
this.content = content;
}
@Override
public String toString() {
JSONObject json = new JSONObject();
json.put("role", role.getRole());
json.put("content", content.replace("\n", "\\n"));
return json.toString();
}
}
@@ -1,58 +0,0 @@
package me.zacharias.chat.ollama;
/**
* Represents the role of a message.
* This is used by Ollama to determine the role of a message.
*/
public enum OllamaMessageRole {
/**
* Represents a user message.
*/
USER("user"),
/**
* Represents an assistant message.
*/
ASSISTANT("assistant"),
/**
* Represents a tool message
*/
TOOL("tool"),
/**
* Represents a system message.
*/
SYSTEM("system");
/**
* The role of the message.
*/
private String role;
/**
* Creates a new instance of OllamaMessageRole.
* @param role The role of the message
*/
OllamaMessageRole(String role) {
this.role = role;
}
/**
* Gets the role of the message.
* @return The role of the message
*/
public String getRole() {
return role;
}
/**
* Gets the role of the message from a string.
* @param role The role of the message as a string
* @return The role of the message
*/
public static OllamaMessageRole fromRole(String role) {
for(OllamaMessageRole roleRole : values()) {
if(roleRole.role.equals(role.toLowerCase()))
return roleRole;
}
throw new IllegalArgumentException("Invalid role: " + role);
}
}
@@ -1,37 +0,0 @@
package me.zacharias.chat.ollama;
import org.json.JSONArray;
import org.json.JSONObject;
/**
* Represents a message sent by a Tool.
*/
public class OllamaMessageToolCall extends OllamaMessage{
/**
* The tool calls in the message
*/
private JSONArray tool_calls;
/**
* Creates a new instance of OllamaMessage
* @param role The role of the message
* @param content The content of the message
* @param tool_calls The tool calls in the message
*/
public OllamaMessageToolCall(OllamaMessageRole role, String content, JSONArray tool_calls) {
super(role, content);
this.tool_calls = tool_calls;
}
@Override
public String toString() {
JSONObject json = new JSONObject();
json.put("role", role);
json.put("content", content);
json.put("tool_calls", tool_calls);
return json.toString();
}
}
@@ -1,399 +0,0 @@
package me.zacharias.chat.ollama;
import com.sun.source.util.Plugin;
import me.zacharias.chat.core.Core;
import me.zacharias.chat.core.LaunchOptions;
import me.zacharias.chat.core.files.FileHandlerLocation;
import me.zacharias.chat.core.files.FileHandler;
import org.intellij.lang.annotations.MagicConstant;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Represents an Ollama Object.
* This is used to represent the state of the Ollama Object.
* This is used by the Core to store the state of the Ollama Object.
* This is used by the API to send the state of the Ollama Object to the client.
* @see Core#setOllamaObject(OllamaObject)
*/
public class OllamaObject {
/**
* The model of the Ollama Object.
*/
String model;
/**
* The messages of the Ollama Object.
*/
ArrayList<OllamaMessage> messages;
/**
* The tools of the Ollama Object.
*/
ArrayList<OllamaTool> tools;
/**
* The format of the Ollama Object.
*/
JSONObject format;
/**
* The options of the Ollama Object.
*/
Map<String, Object> options;
/**
* If the Ollama Object is streamed.
*/
boolean stream;
/**
* The keep alive of the Ollama Object.
*/
String keep_alive;
/**
* Creates a new instance of OllamaObject.
* @param model The model of the Ollama Object. see {@link OllamaObject#model}
* @param messages The messages of the Ollama Object. see {@link OllamaObject#messages}
* @param tools The tools of the Ollama Object. see {@link OllamaObject#tools}
* @param format The format of the Ollama Object. see {@link OllamaObject#format}
* @param options The options of the Ollama Object. see {@link OllamaObject#options}
* @param stream If the Ollama Object is streamed. see {@link OllamaObject#stream}
* @param keep_alive The keep alive of the Ollama Object. see {@link OllamaObject#keep_alive}
*/
private OllamaObject(String model, ArrayList<OllamaMessage> messages, ArrayList<OllamaTool> tools, JSONObject format, Map<String, Object> options, boolean stream, String keep_alive) {
this.model = model;
this.messages = messages;
this.tools = tools;
this.format = format;
this.options = options;
this.stream = stream;
this.keep_alive = keep_alive;
LaunchOptions launchOptions = new LaunchOptions();
if(launchOptions.isLoadOld()) {
System.out.println("Loading old data...");
File f = new File(Core.DATA_DIR+"/messages.json");
if(f.exists()) {
try {
BufferedReader br = new BufferedReader(new FileReader(f));
StringBuilder data = new StringBuilder();
String buffer = null;
while ((buffer = br.readLine()) != null) {
data.append(buffer).append("\n");
}
JSONArray jsonArray = new JSONArray(data.toString());
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject obj = jsonArray.getJSONObject(i);
OllamaMessage message;
if (!obj.has("tool_calls")) {
message = new OllamaMessage(OllamaMessageRole.fromRole(obj.getString("role")), obj.getString("content"));
} else {
message = new OllamaMessageToolCall(OllamaMessageRole.fromRole(obj.getString("role")), obj.getString("content"), obj.getJSONArray("tool_calls"));
}
messages.add(message);
}
}catch (Exception e) {
System.out.println("Error loading old data");
e.printStackTrace();
}
}
}
}
/**
* Gets the model of the Ollama Object.
* @return The model of the Ollama Object
*/
public String getModel() {
return model;
}
/**
* Gets the messages
* @return The messages
*/
public ArrayList<OllamaMessage> getMessages() {
return messages;
}
/**
* Gets the tools
* @return The tools
*/
public ArrayList<OllamaTool> getTools() {
return tools;
}
/**
* Adds a tool to the Ollama Object
* @param tool The tool to add
*/
public void addTool(OllamaTool tool) {
tools.add(tool);
}
public void removeTool(OllamaTool tool) {
tools.remove(tool);
}
/**
* Gets the format of the Ollama Object.
* @return The format of the Ollama Object
*/
public JSONObject getFormat() {
return format;
}
/**
* Gets the options of the Ollama Object.
* @return The options of the Ollama Object
*/
public Map<String, Object> getOptions() {
return options;
}
/**
* Gets if the Ollama Object is streamed.
* @return If the Ollama Object is streamed
*/
public boolean isStream() {
return stream;
}
/**
* Gets the keep alive of the Ollama Object.
* @return The keep alive of the Ollama Object
*/
public String getKeep_alive() {
return keep_alive;
}
/**
* Adds a message to the Ollama Object
* @param message The message to add
*/
public void addMessage(OllamaMessage message) {
messages.add(message);
}
@Override
public String toString() {
JSONObject json = new JSONObject();
JSONArray tools = new JSONArray();
for (OllamaTool tool : this.tools) {
tools.put(new JSONObject(tool.toString()));
}
JSONArray messages = new JSONArray();
for (OllamaMessage message : this.messages) {
messages.put(new JSONObject(message.toString()));
}
json.put("model", model);
json.put("messages", messages);
json.put("tools", tools);
json.put("format", format);
json.put("options", options);
json.put("stream", stream);
json.put("keep_alive", keep_alive);
return json.toString();
}
/**
* Creates a new instance of OllamaObjectBuilder.
* @return The {@link OllamaObjectBuilder}
*/
public static OllamaObjectBuilder builder()
{
return new OllamaObjectBuilder();
}
/**
* Represents a builder for OllamaObject.
*/
public static class OllamaObjectBuilder {
/**
* The model of the Ollama Object.
*/
String model;
/**
* The messages of the Ollama Object.
*/
ArrayList<OllamaMessage> messages = new ArrayList<>();
/**
* The tools of the Ollama Object.
*/
ArrayList<OllamaTool> tools = new ArrayList<>();
/**
* The format of the Ollama Object.
*/
JSONObject format;
/**
* The options of the Ollama Object.
*/
Map<String, Object> options = new HashMap<>();
/**
* If the Ollama Object is streamed.
*/
boolean stream = false;
/**
* The keep alive of the Ollama Object.
*/
String keep_alive;
/**
* Creates a new instance of {@link OllamaObjectBuilder}.
*/
public OllamaObjectBuilder() {}
/**
* Sets the format of the Ollama Object as a JSON schema.
* @param format The format of the Ollama Object as a JSON schema
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder format(String format) {
this.format = new JSONObject(format);
return this;
}
/**
* Sets the options of the Ollama Object.
* @param options The options of the Ollama Object
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder options(Map<String, Object> options) {
this.options.putAll(options);
return this;
}
/**
* Sets an option of the Ollama Object.
* @param key The key of the option
* @param value The value of the option
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder option(String key, String value) {
this.options.put(key, value);
return this;
}
/**
* Sets if the Ollama Object is streamed.
* @param stream If the Ollama Object is streamed
* @deprecated This should be false due to being broken in the current version of this system
* @return The {@link OllamaObjectBuilder}
*/
@Deprecated
public OllamaObjectBuilder stream(boolean stream) {
this.stream = stream;
return this;
}
/**
* Sets the keep alive of the Ollama Object.<br>
* This is a string formated as "minutes"m or "hours"h or "days"d
* @param keep_alive The keep alive of the Ollama Object
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder keep_alive(String keep_alive) {
this.keep_alive = keep_alive;
return this;
}
/**
* Sets the keep alive of the Ollama Object.<br>
* @param minutes The keep alive of the Ollama Object in minutes
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder keep_alive(int minutes) {
this.keep_alive = minutes+"m";
return this;
}
/**
* Adds a tool to the Ollama Object
* @param tool The tool to add
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder addTool(OllamaTool tool) {
this.tools.add(tool);
return this;
}
/**
* Adds tools to the Ollama Object
* @param tools The tools to add
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder addTools(ArrayList<? extends OllamaTool> tools) {
this.tools.addAll(tools);
return this;
}
/**
* Adds tools to the Ollama Object
* @param tools The tools to add
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder addTools(OllamaTool... tools) {
this.tools.addAll(List.of(tools));
return this;
}
/**
* Adds messages to the Ollama Object
* @param messages The messages to add
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder addMessages(OllamaMessage... messages) {
this.messages.addAll(List.of(messages));
return this;
}
/**
* Adds a message to the Ollama Object
* @param messages The message to add
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder addMessage(OllamaMessage messages) {
this.messages.add(messages);
return this;
}
/**
* Sets the model of the Ollama Object
* @param model The model of the Ollama Object
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder setModel(String model) {
this.model = model;
return this;
}
public OllamaObjectBuilder addFileTools(@MagicConstant(valuesFromClass = FileHandlerLocation.class) String baseDirectory)
{
FileHandler fileHandler = new FileHandler(baseDirectory);
if(false);
throw new IllegalArgumentException("FileHandler is not supported yet!");
}
/**
* Builds the {@link OllamaObject}
* @return The {@link OllamaObject}
*/
public OllamaObject build() {
return new OllamaObject(model, messages, tools, format, options, stream, keep_alive);
}
}
}
@@ -1,7 +0,0 @@
package me.zacharias.chat.ollama;
/**
* Represents a tool.
*/
public interface OllamaTool {
}
@@ -1,31 +0,0 @@
package me.zacharias.chat.ollama;
import org.json.JSONObject;
/**
* Represents an error from a tool.<br>
* This is used by a tool to indicate to Ollama that an error occurred.
*/
public class OllamaToolError extends OllamaMessage {
/**
* The error from the tool.
*/
String error;
/**
* Creates a new instance of OllamaToolError.
* @param error The error from the tool
*/
public OllamaToolError(String error) {
super(OllamaMessageRole.TOOL, new JSONObject().put("error", error).toString());
this.error = error;
}
/**
* Gets the error from the tool.
* @return The error from the tool
*/
public String getError() {
return error;
}
}
@@ -1,44 +0,0 @@
package me.zacharias.chat.ollama;
import org.json.JSONObject;
/**
* Represents a response from a tool.
*/
public class OllamaToolRespnce extends OllamaMessage {
/**
* The tool that responded.
*/
private final String tool;
/**
* The response from the tool.
*/
private final String response;
/**
* Creates a new instance of {@link OllamaToolRespnce}.
* @param tool The tool that responded
* @param response The response from the tool
*/
public OllamaToolRespnce(String tool, String response) {
super(OllamaMessageRole.TOOL, new JSONObject().put("tool", tool).put("result", response).toString());
this.tool = tool;
this.response = response;
}
/**
* Gets the tool that responded.
* @return The tool that responded
*/
public String getTool() {
return tool;
}
/**
* Gets the response from the tool.
* @return The response from the tool
*/
public String getResponse() {
return response;
}
}
@@ -1,46 +0,0 @@
package me.zacharias.chat.ollama.exceptions;
import me.zacharias.chat.core.Core;
import org.json.JSONObject;
/**
* Represents an error from a tool.<br>
* This is used internally by tools instead of {@link Exception}, to then be handled gracefully by {@link Core#handleResponce(JSONObject)}
*/
public class OllamaToolErrorException extends RuntimeException {
/**
* The tool that caused the error.
*/
private final String tool;
/**
* The error from the tool.
*/
private final String error;
/**
* Creates a new instance of OllamaToolErrorException.
* @param tool The tool that caused the error
* @param error The error from the tool
*/
public OllamaToolErrorException(String tool, String error) {
super(tool + ": " + error);
this.tool = tool;
this.error = error;
}
/**
* Gets the tool that caused the error.
* @return The tool that caused the error
*/
public String getTool() {
return tool;
}
/**
* Gets the error from the tool.
* @return The error from the tool
*/
public String getError() {
return error;
}
}
@@ -1,15 +0,0 @@
package me.zacharias.chat.ollama.utils;
import me.zacharias.chat.ollama.OllamaMessage;
import me.zacharias.chat.ollama.OllamaMessageRole;
public class SystemMessage extends OllamaMessage {
/**
* Creates a new instance of OllamaMessage.
*
* @param systemMessage The content of the message
*/
public SystemMessage(String systemMessage) {
super(OllamaMessageRole.SYSTEM, systemMessage);
}
}
@@ -1,5 +0,0 @@
package me.zacharias.chat.plugin;
public class Plugin {
private final PluginMetadata metadata = null;
}
@@ -1,62 +0,0 @@
package me.zacharias.chat.plugin;
import org.json.JSONObject;
import java.lang.reflect.Field;
import java.nio.file.FileSystem;
import java.nio.file.Path;
public class PluginLoader {
public PluginLoader() {
}
public Plugin loadPlugin(FileSystem pluginJar, JSONObject pluginMeta) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
String pluginName = pluginMeta.getString("name");
String pluginEntryPoint = pluginMeta.getString("entryPoint");
String pluginVersion = pluginMeta.getString("version");
String pluginDescription = pluginMeta.optString("description", "No description provided");
String[] pluginAuthors = pluginMeta.optJSONArray("author") != null ?
pluginMeta.getJSONArray("author").toList().toArray(new String[0]) : new String[0];
PluginMetadata pluginMetadata = new PluginMetadata() {
@Override
public String getName() {
return pluginName;
}
@Override
public String getVersion() {
return pluginVersion;
}
@Override
public String getDescription() {
return pluginDescription;
}
@Override
public String[] getAuthor() {
return pluginAuthors;
}
@Override
public String entryPoint() {
return pluginEntryPoint;
}
};
try {
Plugin plugin = (Plugin) Class.forName(pluginEntryPoint).newInstance();
Field metadataField = plugin.getClass().getDeclaredField("metadata");
metadataField.set(plugin, pluginMetadata);
if (metadataField.get(plugin) == null)
throw new IllegalStateException("Plugin metadata field is null for plugin: " + pluginName);
}catch (NoSuchFieldException e)
{
throw new IllegalStateException("Plugin class " + pluginEntryPoint + " does not have a 'metadata' field", e);
}
return null;
//ClassLoader classLoader = pluginJar.
}
}
@@ -1,9 +0,0 @@
package me.zacharias.chat.plugin;
public interface PluginMetadata {
public String getName();
public String getVersion();
public String getDescription();
public String[] getAuthor();
public String entryPoint();
}
@@ -1,14 +0,0 @@
package me.zacharias.chat.plugin.annotation.injectons;
import me.zacharias.chat.plugin.Plugin;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
public @interface InjectPlugin {
Class<? extends Plugin> classType() default Plugin.class;
}
@@ -1,7 +0,0 @@
package me.zacharias.chat.plugin.exceptions;
public class PluginLoadingException extends RuntimeException {
public PluginLoadingException(String message, String pluginName) {
super("Plugin: \""+pluginName+"\"> "+message);
}
}
+5 -3
View File
@@ -1,13 +1,15 @@
import me.zacharias.chat.core.files.FileHandler;
import me.zacharias.chat.core.files.FileHandlerException;
import me.neurodock.core.files.FileHandler;
import me.neurodock.core.files.FileHandlerException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.nio.file.Path;
public class FileTest {
@Test
void TestError()
{
FileHandler fileHandler = new FileHandler("./test/files");
FileHandler fileHandler = new FileHandler(Path.of("./test/files"));
Assertions.assertThrowsExactly(FileHandlerException.class, () -> fileHandler.readFile("../build.gradle"));
}
}
-6
View File
@@ -1,6 +0,0 @@
package plugin;
import me.zacharias.chat.plugin.Plugin;
public class Test extends Plugin {
}
-34
View File
@@ -1,34 +0,0 @@
package plugin;
import me.zacharias.chat.ollama.OllamaFunctionArgument;
import me.zacharias.chat.ollama.OllamaFunctionTool;
import me.zacharias.chat.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaToolRespnce;
import me.zacharias.chat.plugin.annotation.OllamaTool;
import me.zacharias.chat.plugin.annotation.injectons.InjectPlugin;
@OllamaTool
public class Tool extends OllamaFunctionTool {
@InjectPlugin(classType = Test.class)
Test core;
@Override
public String name() {
return "";
}
@Override
public String description() {
return "";
}
@Override
public OllamaPerameter parameters() {
return null;
}
@Override
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
return null;
}
}
+9 -2
View File
@@ -2,7 +2,6 @@ plugins {
id 'java'
}
group = 'me.zacharias'
version = '1.0-SNAPSHOT'
dependencies {
@@ -11,7 +10,9 @@ dependencies {
implementation project(":GeniusAPI")
implementation project(":API")
implementation project(":WikipediaTool")
implementation project(":MovieSugest")
implementation("com.github.docker-java:docker-java-core:3.7.1")
implementation("com.github.docker-java:docker-java-transport-httpclient5:3.7.1")
}
test {
@@ -22,4 +23,10 @@ jar{
manifest {
attributes 'Main-Class': 'me.zacharias.chat.display.Main'
}
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(javaVersion)) // Set Java version
}
}
@@ -1,23 +1,25 @@
package me.zacharias.chat.display;
package me.neurodock.display;
//import me.zacharias.chat.api.APIApplication;
import me.noah.movie.sugest.MovieSugestTool;
import me.zacharias.chat.core.Core;
import me.zacharias.chat.core.Pair;
import me.zacharias.chat.core.PrintMessageHandler;
import me.zacharias.chat.core.files.FileHandlerLocation;
import me.zacharias.chat.core.memory.CoreMemory;
import me.zacharias.chat.mal.api.MALAPITool;
import me.zacharias.chat.ollama.*;
import me.zacharias.chat.ollama.utils.SystemMessage;
import me.zacharias.neuro.dock.genius.GeniusTools;
import me.zacharias.neuro.dock.wikipedia.WikipediaTool;
import me.neurodock.backend.open.ai.OpenAIModel;
import me.neurodock.core.Core;
import me.neurodock.core.Options;
import me.neurodock.core.Pair;
import me.neurodock.core.PrintMessageHandler;
import me.neurodock.core.memory.CoreMemory;
import me.neurodock.llm.ChatObject;
import me.neurodock.llm.Message;
import me.neurodock.llm.exceptions.ToolException;
import me.neurodock.llm.tools.*;
import me.neurodock.ollama.*;
import me.neurodock.ollama.utils.SystemMessage;
import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;
import java.io.*;
import java.nio.file.Path;
import java.util.*;
import static me.zacharias.chat.core.Core.writeLog;
import static me.neurodock.core.Core.writeLog;
/**
* The main class of the Display.<br>
@@ -25,7 +27,14 @@ import static me.zacharias.chat.core.Core.writeLog;
* Somewhat meant to be used as a Debug tool for testing your API.
*/
public class Display {
/*
* Inizilizer
*/
{
Options.getInstance().setDataDir(Path.of("AI-Chat"), false);
}
/**
* The Core instance.
*/
@@ -47,28 +56,28 @@ public class Display {
*/
public Display()
{
OpenAIModel model = new OpenAIModel("http://localhost:8080", "MiniCPM5");
core.setOllamaObjectNoMemory(OllamaObject.builder()
.setModel("llama3.2")
//.setModel("gemma3:12b")
// .setModel("qwen3:8b")
/*core.setOllamaObject/*NoMemory*//*(OllamaObject.builder()
.setModel("llama3.1:8b")
.keep_alive(10)
//.stream(false)
//.addFileTools(FileHandlerLocation.DATA_FILES)
.addFileTools(Options.getInstance().getFileHandlerDataLocation())
.addMessages(new SystemMessage("You need to emulate feelings and emotions."))
.build());
.build());*/
core.enablePlugins(Core.PLUGIN_DIRECTORY);
core.setModel(model);
core.addTool(new TimeTool(), Core.Source.INTERNAL);
core.enablePlugins(Options.getInstance().getPluginDirectory());
core.addTool(new TimeTool(), Core.Source.CTP);
// TODO: Well Docker failes when luanched.... Fuck
// core.addTool(new PythonRunner(core), Core.Source.INTERNAL);
// core.addTools(new MALAPITool().getOllamaTools());
// core.addTools(new GeniusTools().getGeniusTools());
core.addTools(new WikipediaTool().getWikipediaToolsInstance());
core.addTools(new MovieSugestTool().getMovieSugestTools());
core.addTool(new PythonRunner(core), Core.Source.CTP);
//core.addTools(new MALAPITool().getOllamaTools());
//core.addTools(new GeniusTools().getGeniusTools());
//core.addTools(new WikipediaTool().getWikipediaToolsInstance());
// APIApplication.start();
//APIApplication.start();
//core.getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.SYSTEM, "Have a nice tone and use formal wording"));
@@ -111,6 +120,7 @@ public class Display {
/bye Exits the program.
/write Flushes the current log stream to file.
/list Lists all available tools.
/corelist Lists all tools according to the OllamaObject.
/working Prints the current working directories.
/peek Peeks the current memory.
""");
@@ -155,11 +165,19 @@ public class Display {
writeLog("Function: " + funtion.getKey().name() + "(" + args + ") [" + funtion.getValue() + "]");
}
break;
case "corelist":
writeLog("Tools installed in this instance acording to the coire OllamaObject");
for(Pair<OllamaTool, String> funtion : core.getOllamaObject().getTools()) {
System.out.println("> Function: " + funtion.getKey().toJSON());
writeLog("Function: " + funtion.getKey().toJSON());
}
break;
case "working":
System.out.println("Working directories:\n" +
" Data: " + Core.DATA_DIR.getAbsolutePath() + "\n" +
" DateFiles: " + FileHandlerLocation.DATA_FILES + "\n" +
" Plugins: " + Core.PLUGIN_DIRECTORY.getAbsolutePath());
" Data: " + Options.getInstance().getDataDir().getAbsolutePath() + "\n" +
" DateFiles: " + Options.getInstance().getFileHandlerDataLocation() + "\n" +
" Plugins: " + Options.getInstance().getPluginDirectory().getAbsolutePath());
break;
default:
System.out.println("Unknown command: " + message);
@@ -168,7 +186,7 @@ public class Display {
writeLog("User: " + message);
core.getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.USER, message.toString()));
//System.out.println(ollamaObject.toString());
core.handleResponce(core.qurryOllama());
core.qurryOllama().thenAccept(core::handleResponse).join();
}
}
} catch (Exception e) {
@@ -0,0 +1,558 @@
package me.neurodock.display;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.model.HostConfig;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientImpl;
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
import com.github.dockerjava.transport.DockerHttpClient;
import me.neurodock.core.Core;
import me.neurodock.core.Options;
import me.neurodock.core.Pair;
import me.neurodock.ollama.OllamaFunctionArgument;
import me.neurodock.ollama.OllamaFunctionTool;
import me.neurodock.ollama.OllamaPerameter;
import me.neurodock.ollama.OllamaToolResponse;
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
import org.apache.commons.io.IOUtils;
import org.jetbrains.annotations.NotNull;
import org.json.JSONArray;
import org.json.JSONObject;
import org.jspecify.annotations.NonNull;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import static me.neurodock.core.Core.ensureDir;
import static me.neurodock.core.Core.writeLog;
/**
* A tool that runs python code.
* This is a wrapper around a docker container.
* This is partly meant as a proof of concept, but also as a way to run python code while keeping the executed code in a secure environment.
*/
public class PythonRunner extends OllamaFunctionTool {
/**
* The DockerClient instance.
*/
private DockerClient dockerClient;
/**
* The Core instance.
*/
private Core core;
/**
* The ServerSocket instance.
*/
private ServerSocket serverSocket;
/**
* A hash map of all python runners, this is to make it thread safe in a theoretical case of multiple python runners running at once
*/
private ConcurrentHashMap<String, StringBuilder> outputBuffers = new ConcurrentHashMap<>();
/**
* The docker client to be used throughout this tool
*/
DockerHttpClient dockerHttpClient;
/**
* Creates a new instance of PythonRunner.
* @param core The Core instance
*/
public PythonRunner(Core core) {
this.core = core;
ensureDir("./pythonFiles/");
try {
serverSocket = new ServerSocket(6050);
Thread thread = new Thread(() -> {
while (true) {
try {
Socket socket = serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String inputLine = in.readLine();
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
try {
JSONObject data = new JSONObject(inputLine);
String toolName = data.optString("function", "");
if(toolName.equals("print_output")) {
// This is a "special" "tool" that is used to hijack the output of the python program so logs can be put else where since the LLM dose'nt need the entirety of the log from pacman and what not.
String containerId = data.optString("container_id", "");
if(containerId.isEmpty()) {
out.write(new JSONObject().put("error", "Missing container id").toString());
out.newLine();
out.flush();
out.close();
in.close();
socket.close();
continue;
}
outputBuffers.compute(containerId, (k, buf) -> {
if(buf == null) {
buf = new StringBuilder();
}
buf.append(data.optString("text", ""));
buf.append(System.lineSeparator());
return buf;
});
out.write(new JSONObject().put("container_id", containerId).toString());
out.newLine();
out.flush();
out.close();
in.close();
socket.close();
continue;
}
List<Pair<OllamaFunctionTool, String>> list = core.getFuntionTools().stream().filter(funtionTool -> funtionTool.getKey().name().equalsIgnoreCase(toolName)).toList();
if (list.isEmpty()) {
out.write(new JSONObject().put("error", "Function don't exist").toString());
out.newLine();
out.flush();
out.close();
in.close();
socket.close();
continue;
}
ArrayList<OllamaFunctionArgument> args = new ArrayList<>();
for (Object o : data.optJSONArray("arguments", new JSONArray())) {
if (o instanceof JSONObject obj) {
if(obj.has("value") && !obj.isNull("value")) {
OllamaFunctionArgument arg = new OllamaFunctionArgument(obj.getString("name"), obj.getString("value"));
args.add(arg);
}
}
}
out.write(list.getFirst().getKey().function(args.toArray(new OllamaFunctionArgument[0])).getResponse());
out.newLine();
out.flush();
out.close();
in.close();
socket.close();
} catch (Exception e) {
}
} catch (Exception e) {
}
}
});
thread.start();
}catch (Exception e) {
e.printStackTrace();
}
DefaultDockerClientConfig config
= DefaultDockerClientConfig.createDefaultConfigBuilder()
.build();
dockerHttpClient = new ApacheDockerHttpClient.Builder()
.dockerHost(config.getDockerHost())
.sslConfig(config.getSSLConfig())
.maxConnections(10)
.connectionTimeout(Duration.ofSeconds(100))
.responseTimeout(Duration.ofSeconds(100))
.build();
DockerHttpClient.Request ping = DockerHttpClient.Request.builder()
.method(DockerHttpClient.Request.Method.GET)
.path("/_ping")
.build();
try(DockerHttpClient.Response response = dockerHttpClient.execute(ping))
{
if(!(response.getStatusCode() == 200))
{
writeLog("Failed to ping docker: " + response.getStatusCode());
System.out.println("Failed to ping docker (" + response.getStatusCode() + "). Docker components is disabled.");
dockerClient = null;
return;
}
if(!(IOUtils.toString(response.getBody(), Charset.defaultCharset()).equals("OK")))
{
writeLog("Failed to ping docker: body not OK");
System.out.println("Failed to ping docker. Docker components is disabled.");
dockerClient = null;
return;
}
}
catch (RuntimeException re)
{
if(re.getCause() instanceof SocketException) {
writeLog("Failed to ping docker: " + re.getMessage());
System.out.println("Failed to ping docker. Docker component is disabled.");
dockerClient = null;
return;
}
else{
writeLog("Failed to ping docker: " + re.getMessage());
re.printStackTrace();
System.out.println("Failed to ping docker. Docker component is disabled.");
dockerClient = null;
return;
}
}
catch (Exception e)
{
writeLog("Failed to ping docker: " + e.getMessage());
e.printStackTrace();
System.out.println("Failed to ping docker. Docker component is disabled.");
dockerClient = null;
return;
}
dockerClient = DockerClientImpl.getInstance(config, dockerHttpClient);
initializingImage();
}
/**
* A small function to pull the archlinux:latest image from Docker Hub. See <a href="https://hub.docker.com/_/archlinux/">ArchLinux Docker Hub</a>
* <br>
* I want to note here that the reason for using an ArchLinux image and not somthing like python or Ubuntu. Is because I'm more
* comfortable with how to do this on an Arch host, and I could not manage to pull libraries for the python image. So those who despise Arch you're welcome to re-write the arch specific parts.
*/
private void initializingImage() {
// Check if archlinux:latest is pulled already
DockerHttpClient.Request checkArchlinux_latest = DockerHttpClient.Request.builder()
.method(DockerHttpClient.Request.Method.GET)
.path("/images/archlinux:latest/json")
.build();
boolean archlinux_latestAvailable = false;
try(DockerHttpClient.Response response = dockerHttpClient.execute(checkArchlinux_latest))
{
if(response.getStatusCode() == 200)
{
archlinux_latestAvailable = true;
}
}
catch (Exception e)
{
writeLog("Failed to fetch archlinux:latest: " + e.getMessage());
e.printStackTrace();
System.out.println("Failed to fetch archlinux:latest.");
return;
}
if(!archlinux_latestAvailable) {
// If it's not already downloaded, we will download it.
DockerHttpClient.Request downloadArchlinux_latest = DockerHttpClient.Request.builder()
.method(DockerHttpClient.Request.Method.POST)
.path("/images/create?fromImage=archlinux&tag=latest")
.build();
try (DockerHttpClient.Response response = dockerHttpClient.execute(downloadArchlinux_latest)) {
if (response.getStatusCode() == 200) {
writeLog("Successfully downloaded archlinux:latest");
}
else {
writeLog("Failed to download archlinux:latest: " + response.getStatusCode() + "\n" + response.getBody());
}
} catch (Exception e) {
writeLog("Failed to fetch archlinux:latest: " + e.getMessage());
e.printStackTrace();
System.out.println("Failed to fetch archlinux:latest.");
return;
}
}
else
{
writeLog("archlinux:latest already exists");
}
}
@Override
public @NonNull String name() {
return "python_runner";
}
@Override
public String description() {
return "Runs python code.\nuse python's print method for output";
}
@Override
public @NotNull OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("code", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The code to be executed, optional if a file with the same name exists.")
.addProperty("name", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The name of the python code")
.addProperty("libs", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "A space separated list of pip packages needed")
.build();
}
@Override
public @NonNull OllamaToolResponse function(OllamaFunctionArgument... args) {
if(dockerClient == null)
{
return new OllamaToolResponse(name(), "Docker is disabled");
}
if(args.length == 0)
{
throw new OllamaToolErrorException(name(), "Missing code argument");
}
String name = null;
String code = null;
String libs = null;
for(OllamaFunctionArgument arg : args)
{
if(arg.argument().equals("name") && !arg.value().equals(""))
{
name = (String) arg.value();
if(!name.endsWith(".py"))
{
name += ".py";
}
} else if (arg.argument().equals("code") && !arg.value().equals("")) {
code = (String) arg.value();
}
else if(arg.argument().equals("libs") && !arg.value().equals(""))
{
libs = (String) arg.value();
}
}
if(name == null && code == null)
{
throw new OllamaToolErrorException(name(), "Missing code or name");
}
if(name == null)
{
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] encodedhash = digest.digest(String.valueOf(args[0].value()).getBytes(StandardCharsets.UTF_8));
StringBuffer hexString = new StringBuffer();
for(byte b : encodedhash)
{
hexString.append(String.format("%02x", b));
}
name = hexString.toString()+".py";
}catch (Exception e) {}
}
name = name.replace(" ", "_");
writeLog("Running python code `" + name + "`");
File pythonFile = new File("./pythonFiles", name);
code = "from external_tools import *\n\n"+code;
if(!pythonFile.exists())
{
try {
BufferedWriter writer = new BufferedWriter(new FileWriter(pythonFile));
writer.write(code);
writer.close();
}catch(IOException e) {}
}
File f = new File("./pythonFiles", "external_tools.py");
String containerId = dockerClient.createContainerCmd("archlinux")
.withCmd("/bin/bash","/cmd.sh")
.withHostConfig(HostConfig.newHostConfig()
.withExtraHosts("host.docker.internal:host-gateway"))
.exec().getId();
outputBuffers.put(containerId, new StringBuilder());
try {
String external_tools = generateExternalTools(containerId);
BufferedWriter bw = new BufferedWriter(new FileWriter(f));
bw.write(external_tools/*.replace("\\n", "\n").replace("\\!n", "\\n")*/);
bw.flush();
bw.close();
}catch(IOException e) {}
try {
ArrayList<String> pythonArgs = new ArrayList<>();
pythonArgs.add("#!/bin/env bash\n");
StringBuilder cmd = new StringBuilder();
cmd.append("""
set -e
pacman --noconfirm -Sy > /dev/null
pacman --noconfirm -S python python-pip > /dev/null
mkdir pythonRun > /dev/null
python -m venv ./pythonRun > /dev/null
cp /external_tools.py ./pythonRun > /dev/null
cp\s/""")
.append(name).append("""
\s./pythonRun > /dev/null
cd pythonRun > /dev/null
source ./bin/activate > /dev/null
""");
if(libs != null && !libs.isEmpty())
{
cmd.append("pip install ");
for(String lib : libs.split(" "))
{
cmd.append(lib).append(" ");
}
cmd.append(" > /dev/null\n");
}
cmd.append("python ").append(name);//.append(" exit");
pythonArgs.add(cmd.toString());
StringBuilder fullCmd = new StringBuilder();
for(String arg : pythonArgs)
{
fullCmd.append(arg).append(" ");
}
File program = new File(Options.getInstance().getCacheDirectory(), "cmd.sh");
if(program.exists()){
program.delete();
}
program.createNewFile();
BufferedWriter bw = new BufferedWriter(new FileWriter(program));
bw.write(fullCmd.toString());
bw.flush();
bw.close();
dockerClient.copyArchiveToContainerCmd(containerId)
.withHostResource(pythonFile.getPath())
.exec();
dockerClient.copyArchiveToContainerCmd(containerId)
.withHostResource(f.getPath())
.exec();
dockerClient.copyArchiveToContainerCmd(containerId)
.withHostResource(program.getPath())
.exec();
dockerClient.startContainerCmd(containerId).exec();
dockerClient.waitContainerCmd(containerId).start().awaitCompletion();
String output = outputBuffers.remove(containerId).toString();
dockerClient.removeContainerCmd(containerId).exec();
return new OllamaToolResponse(name(), output.isEmpty() ? "Python script names "+name+" ran successfully with no output" : "Python script names "+name+" output: "+output);
}
catch (Exception e) {
throw new OllamaToolErrorException(name(), "Docker unavailable");
}
}
/**
* Generates the external_tools.py file.<br>
* This is meant to provide the python code with all ExternalTools defined in the OllamaObject.
* @return The generated external_tools.py file
* @throws IllegalStateException This is thrown when the external_tool_base.py can not be read, this should be handled by disabling the python runner.
*/
private String generateExternalTools(String containerId) throws IllegalStateException{
StringBuilder code = new StringBuilder();
code.append("CONTAINER_ID = \"").append(containerId).append("\"\n\n");
try (InputStream in = getClass().getResourceAsStream("/external_tool_base.py");
BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
String tmp = null;
while((tmp = reader.readLine()) != null)
{
code.append(tmp).append('\n');
}
}catch (Exception ex)
{
throw new IllegalStateException("Can not read the base external tools file, this is a fetal error for the python runner!");
}
for(Pair<OllamaFunctionTool, String> funtionTool : core.getFuntionTools())
{
OllamaFunctionTool tool = funtionTool.getKey();
String name = tool.name();
code.append("def ").append(name).append("(");
ArrayList<String> args = new ArrayList<>();
boolean first = true;
try {
for(String argName : tool.parameters().getRequired())
{
args.add(argName);
if (!first) {
code.append(", ");
}
code.append(argName);
first = false;
}
for (String argName : tool.parameters().getProperties().keySet()) {
if(Arrays.asList(tool.parameters().getRequired()).contains(argName)) continue;
args.add(argName);
if (!first) {
code.append(", ");
}
code.append(argName).append("=None");
first = false;
}
}catch (Exception e) {}
code.append("):\n");
code.append(" data = {\"function\":\"").append(tool.name()).append("\"");
if(args.size() > 0)
{
code.append(",\"arguments\":[");
}
first = true;
for(String str : args)
{
if(!first)
{
code.append(", ");
}
code.append("{\"name\": \"").append(str).append("\", \"value\": ").append(str).append("}");
first = false;
}
if(args.size() > 0)
{
code.append("]");
}
code.append("}\n");
code.append(" return json.loads(connect(json.dumps(data)))\n\n");
}
return code.toString();
}
}
@@ -0,0 +1,38 @@
package me.neurodock.display;
import me.neurodock.ollama.OllamaFunctionArgument;
import me.neurodock.ollama.OllamaFunctionTool;
import me.neurodock.ollama.OllamaPerameter;
import me.neurodock.ollama.OllamaToolResponse;
import org.jetbrains.annotations.NotNull;
import org.jspecify.annotations.NonNull;
import java.util.Date;
/**
* A tool that returns the current date.
* This gives Ollama the ability to get the current date.
*/
public class TimeTool extends OllamaFunctionTool {
@Override
public @NonNull OllamaToolResponse function(OllamaFunctionArgument... arguments) {
Date date = new Date();
return new OllamaToolResponse(name(), date.toString());
}
@Override
public @NonNull String name() {
return "get_current_date";
}
@Override
public @NotNull OllamaPerameter parameters() {
return null;
}
@Override
public String description() {
return "Get the current date";
}
}
@@ -1,447 +0,0 @@
package me.zacharias.chat.display;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.async.ResultCallback;
import com.github.dockerjava.api.async.ResultCallbackTemplate;
import com.github.dockerjava.api.command.*;
import com.github.dockerjava.api.model.BuildResponseItem;
import com.github.dockerjava.api.model.Frame;
import com.github.dockerjava.api.model.Statistics;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientBuilder;
import me.zacharias.chat.core.Core;
import me.zacharias.chat.core.Pair;
import me.zacharias.chat.ollama.*;
import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
import static me.zacharias.chat.core.Core.writeLog;
/**
* A tool that runs python code.
* This is a wrapper around a docker container.
* This is partly meant as a proof of concept, but also as a way to run python code while keeping the executed code in a secure environment.
*/
public class PythonRunner extends OllamaFunctionTool {
/**
* The DockerClient instance.
*/
private DockerClient dockerClient;
/**
* The Core instance.
*/
private Core core;
/**
* The ServerSocket instance.
*/
private ServerSocket serverSocket;
/**
* Creates a new instance of PythonRunner.
* @param core The Core instance
*/
public PythonRunner(Core core) {
this.core = core;
try {
serverSocket = new ServerSocket(6050);
Thread thread = new Thread(() -> {
while (true) {
try {
Socket socket = serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String inputLine = in.readLine();
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
try {
JSONObject data = new JSONObject(inputLine);
List<Pair<OllamaFunctionTool, String>> list = core.getFuntionTools().stream().filter(funtionTool -> funtionTool.getKey().name().equalsIgnoreCase(data.optString("function", ""))).toList();
if (list.isEmpty()) {
out.write(new JSONObject().put("error", "Function don't exist").toString());
out.newLine();
out.flush();
out.close();
in.close();
socket.close();
continue;
}
ArrayList<OllamaFunctionArgument> args = new ArrayList<>();
for (Object o : data.optJSONArray("arguments", new JSONArray())) {
if (o instanceof JSONObject obj) {
if(obj.has("value") && !obj.isNull("value")) {
OllamaFunctionArgument arg = new OllamaFunctionArgument(obj.getString("name"), obj.getString("value"));
args.add(arg);
}
}
}
out.write(list.getFirst().getKey().function(args.toArray(new OllamaFunctionArgument[0])).getResponse());
out.newLine();
out.flush();
out.close();
in.close();
socket.close();
} catch (Exception e) {
}
} catch (Exception e) {
}
}
});
thread.start();
}catch (Exception e) {
e.printStackTrace();
}
DefaultDockerClientConfig.Builder config
= DefaultDockerClientConfig.createDefaultConfigBuilder()
.withDockerHost("tcp://localhost:2375")
.withDockerTlsVerify(false);
dockerClient = DockerClientBuilder
.getInstance(config)
.build();
}
@Override
public String name() {
return "python_runner";
}
@Override
public String description() {
return "Runs python code";
}
@Override
public OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("code", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The code to be executed", true)
.addProperty("name", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The name of the python code")
.addProperty("libs", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "A space separated list of pip packages needed")
.build();
}
@Override
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
if(args.length == 0)
{
throw new OllamaToolErrorException(name(), "Missing code argument");
}
String name = null;
String code = null;
String libs = null;
for(OllamaFunctionArgument arg : args)
{
if(arg.argument().equals("name") && !arg.value().equals(""))
{
name = (String) arg.value();
if(!name.endsWith(".py"))
{
name += ".py";
}
} else if (arg.argument().equals("code") && !arg.value().equals("")) {
code = (String) arg.value();
}
else if(arg.argument().equals("libs") && !arg.value().equals(""))
{
libs = (String) arg.value();
}
}
if(name == null)
{
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] encodedhash = digest.digest(String.valueOf(args[0].value()).getBytes(StandardCharsets.UTF_8));
StringBuffer hexString = new StringBuffer();
for(byte b : encodedhash)
{
hexString.append(String.format("%02x", b));
}
name = hexString.toString()+".py";
}catch (Exception e) {}
}
name = name.replace(" ", "_");
writeLog("Running python code `" + name + "`");
File pythonFile = new File("./pythonFiles", name);
code = "from external_tools import *\n\n"+code;
if(!pythonFile.exists())
{
try {
BufferedWriter writer = new BufferedWriter(new FileWriter(pythonFile));
writer.write(code);
writer.close();
}catch(IOException e) {}
}
File f = new File("./pythonFiles", "external_tools.py");
try {
String external_tools = generateExternalTools();
BufferedWriter bw = new BufferedWriter(new FileWriter(f));
bw.write(external_tools.replace("\\n", "\n"));
bw.flush();
bw.close();
}catch(IOException e) {}
try {
ArrayList<String> pythonArgs = new ArrayList<>();
pythonArgs.add("/bin/bash");
StringBuilder cmd = new StringBuilder();
cmd.append("set -e\n\npacman --noconfirm -Sy > /dev/null\npacman --noconfirm -S python python-pip > /dev/null\nmkdir pythonRun > /dev/null\npython -m venv ./pythonRun > /dev/null\ncp external_tools.py ./pythonRun > /dev/null\ncp ").append(name).append(" ./pythonRun > /dev/null\ncd pythonRun > /dev/null\nsource ./bin/activate > /dev/null\n");
if(libs != null && !libs.isEmpty())
{
cmd.append("pip install ");
for(String lib : libs.split(" "))
{
cmd.append(lib).append(" ");
}
cmd.append(" > /dev/null\n");
}
cmd.append("python ").append(name);//.append(" exit");
pythonArgs.add("-c");
pythonArgs.add(cmd.toString());
StringBuilder fullCmd = new StringBuilder();
for(String arg : pythonArgs)
{
fullCmd.append(arg).append(" ");
}
File program = new File("./cache", "cmd.sh");
if(program.exists()){
program.delete();
}
program.createNewFile();
BufferedWriter bw = new BufferedWriter(new FileWriter(program));
bw.write(cmd.toString());
bw.flush();
bw.close();
String containerId = dockerClient.createContainerCmd("archlinux").withCmd("/bin/bash","./cmd.sh").exec().getId();
dockerClient.copyArchiveToContainerCmd(containerId)
.withHostResource(pythonFile.getPath())
.exec();
dockerClient.copyArchiveToContainerCmd(containerId)
.withHostResource(f.getPath())
.exec();
dockerClient.copyArchiveToContainerCmd(containerId)
.withHostResource(program.getPath())
.exec();
//InputStream stdin = new ByteArrayInputStream(fullCmd.toString().getBytes(StandardCharsets.UTF_8));
dockerClient.startContainerCmd(containerId).exec();
//dockerClient.attachContainerCmd(containerId).withStdIn(stdin).exec(null);
//dockerClient.execCreateCmd(containerId).withCmd(fullCmd.toString()).exec();
GetContainerLog log = new GetContainerLog(dockerClient, containerId);
List<String> logs = new ArrayList<>();
do {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
logs.addAll(log.getDockerLogs());
}
while (isRunning(containerId));
StringBuilder output = new StringBuilder();
for (String s : logs) {
output.append(s).append("\n");
}
//writeLog("Result from python: " + output.toString());
return new OllamaToolRespnce(name(), output.toString());
}
catch (Exception e) {
throw new OllamaToolErrorException(name(), "Docker unavalible");
}
}
/**
* Generates the external_tools.py file.<br>
* This is meant to provide the python code with all ExternalTools defined in the OllamaObject.
* @return The generated external_tools.py file
*/
private String generateExternalTools() {
StringBuilder code = new StringBuilder();
code.append("""
import socket
import json
HOST = "host.docker.internal"
PORT = 6050
def connect(data):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
data = data + "\\n"
s.sendall(data.encode("utf-8"))
responce = s.recv(4096)
return responce.decode("utf-8")
""");
for(Pair<OllamaFunctionTool, String> funtionTool : core.getFuntionTools())
{
OllamaFunctionTool tool = funtionTool.getKey();
String name = tool.name();
code.append("def ").append(name).append("(");
ArrayList<String> args = new ArrayList<>();
boolean first = true;
try {
for (String argName : tool.parameters().getProperties().keySet()) {
args.add(argName);
if (!first) {
code.append(", ");
}
code.append(argName);
if (Arrays.stream(tool.parameters().getRequired()).noneMatch(required -> required.equals(argName))) {
code.append("=None");
}
first = false;
}
}catch (Exception e) {}
code.append("):\n");
code.append(" data = {\"function\":\"").append(tool.name()).append("\"");
if(args.size() > 0)
{
code.append(",\"arguments\":[");
}
first = true;
for(String str : args)
{
if(!first)
{
code.append(", ");
}
code.append("{\"name\": \"").append(str).append("\", \"value\": ").append(str).append("}");
first = false;
}
if(args.size() > 0)
{
code.append("]");
}
code.append("}\n");
code.append(" return json.loads(connect(json.dumps(data)))\n\n");
}
return code.toString();
}
/**
* Checks if a Docker Container is running
* @param containerId The ID of the Container
* @return a boolean weather it's running or not
*/
public boolean isRunning(String containerId)
{
InspectContainerResponse cmd = dockerClient.inspectContainerCmd(containerId).exec();
return Boolean.TRUE.equals(cmd.getState().getRunning());
}
/**
* A Helper class to get the logs from a docker container.
*/
public class GetContainerLog {
private DockerClient dockerClient;
private String containerId;
private int lastLogTime;
private static String nameOfLogger = "dockertest.PrintContainerLog";
private static Logger myLogger = Logger.getLogger(nameOfLogger);
/**
* Creates a new instance of {@link GetContainerLog}
* @param dockerClient The DockerClient instance
* @param containerId The container id
*/
public GetContainerLog(DockerClient dockerClient, String containerId) {
this.dockerClient = dockerClient;
this.containerId = containerId;
this.lastLogTime = (int) (System.currentTimeMillis() / 1000);
}
/**
* Gets the logs of the container.
* @return The logs of the container
*/
public List<String> getDockerLogs() {
final List<String> logs = new ArrayList<>();
LogContainerCmd logContainerCmd = dockerClient.logContainerCmd(containerId);
logContainerCmd.withStdOut(true).withStdErr(true);
logContainerCmd.withSince(lastLogTime); // UNIX timestamp (integer) to filter logs. Specifying a timestamp will only output log-entries since that timestamp.
// logContainerCmd.withTail(4); // get only the last 4 log entries
logContainerCmd.withTimestamps(true);
try {
logContainerCmd.exec(new ResultCallback.Adapter<Frame>() {
@Override
public void onNext(Frame item) {
logs.add(new String(item.getPayload()).trim());
}
}).awaitCompletion();
} catch (InterruptedException e) {
myLogger.severe("Interrupted Exception!" + e.getMessage());
}
lastLogTime = (int) (System.currentTimeMillis() / 1000) + 5; // assumes at least a 5 second wait between calls to getDockerLogs
return logs;
}
}
}
@@ -1,36 +0,0 @@
package me.zacharias.chat.display;
import me.zacharias.chat.ollama.OllamaFunctionArgument;
import me.zacharias.chat.ollama.OllamaFunctionTool;
import me.zacharias.chat.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaToolRespnce;
import java.util.Date;
/**
* A tool that returns the current date.
* This gives Ollama the ability to get the current date.
*/
public class TimeTool extends OllamaFunctionTool {
@Override
public OllamaToolRespnce function(OllamaFunctionArgument... arguments) {
Date date = new Date();
return new OllamaToolRespnce(name(), date.toString());
}
@Override
public String name() {
return "get_current_date";
}
@Override
public OllamaPerameter parameters() {
return null;
}
@Override
public String description() {
return "Get the current date";
}
}
@@ -0,0 +1,165 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.ssl;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.function.Factory;
import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
import org.apache.hc.core5.reactor.ssl.SSLBufferMode;
import org.apache.hc.core5.reactor.ssl.TlsDetails;
import org.apache.hc.core5.ssl.SSLContexts;
/**
* TLS upgrade strategy for non-blocking client connections.
*
* @since 5.0
*/
@Contract(threading = ThreadingBehavior.STATELESS)
public class DefaultClientTlsStrategy extends AbstractClientTlsStrategy {
/**
* @since 5.4
*/
public static DefaultClientTlsStrategy createDefault() {
return new DefaultClientTlsStrategy(
SSLContexts.createDefault(),
HostnameVerificationPolicy.BOTH,
HttpsSupport.getDefaultHostnameVerifier());
}
/**
* @since 5.4
*/
public static DefaultClientTlsStrategy createSystemDefault() {
return new DefaultClientTlsStrategy(
SSLContexts.createSystemDefault(),
HttpsSupport.getSystemProtocols(),
HttpsSupport.getSystemCipherSuits(),
SSLBufferMode.STATIC,
HostnameVerificationPolicy.BOTH,
HttpsSupport.getDefaultHostnameVerifier());
}
/**
* @deprecated Use {@link #createDefault()}.
*/
@Deprecated
public static TlsStrategy getDefault() {
return createDefault();
}
/**
* @deprecated Use {@link #createSystemDefault()}.
*/
@Deprecated
public static TlsStrategy getSystemDefault() {
return createSystemDefault();
}
/**
* @deprecated To be removed.
*/
@Deprecated
private Factory<SSLEngine, TlsDetails> tlsDetailsFactory;
/**
* @deprecated Use {@link DefaultClientTlsStrategy#DefaultClientTlsStrategy(SSLContext, String[], String[], SSLBufferMode, HostnameVerifier)}
*/
@Deprecated
public DefaultClientTlsStrategy(
final SSLContext sslContext,
final String[] supportedProtocols,
final String[] supportedCipherSuites,
final SSLBufferMode sslBufferManagement,
final HostnameVerifier hostnameVerifier,
final Factory<SSLEngine, TlsDetails> tlsDetailsFactory) {
super(sslContext, supportedProtocols, supportedCipherSuites, sslBufferManagement, HostnameVerificationPolicy.CLIENT, hostnameVerifier);
this.tlsDetailsFactory = tlsDetailsFactory;
}
/**
* @since 5.4
*/
public DefaultClientTlsStrategy(
final SSLContext sslContext,
final String[] supportedProtocols,
final String[] supportedCipherSuites,
final SSLBufferMode sslBufferManagement,
final HostnameVerificationPolicy hostnameVerificationPolicy,
final HostnameVerifier hostnameVerifier) {
super(sslContext, supportedProtocols, supportedCipherSuites, sslBufferManagement, hostnameVerificationPolicy, hostnameVerifier);
}
public DefaultClientTlsStrategy(
final SSLContext sslContext,
final String[] supportedProtocols,
final String[] supportedCipherSuites,
final SSLBufferMode sslBufferManagement,
final HostnameVerifier hostnameVerifier) {
this(sslContext, supportedProtocols, supportedCipherSuites, sslBufferManagement, HostnameVerificationPolicy.CLIENT, hostnameVerifier);
}
public DefaultClientTlsStrategy(
final SSLContext sslContext,
final HostnameVerifier hostnameVerifier) {
this(sslContext, null, null, SSLBufferMode.STATIC, hostnameVerifier);
}
/**
* @since 5.4
*/
public DefaultClientTlsStrategy(
final SSLContext sslContext,
final HostnameVerificationPolicy hostnameVerificationPolicy,
final HostnameVerifier hostnameVerifier) {
this(sslContext, null, null, SSLBufferMode.STATIC, hostnameVerificationPolicy, hostnameVerifier);
}
public DefaultClientTlsStrategy(final SSLContext sslContext) {
this(sslContext, HttpsSupport.getDefaultHostnameVerifier());
}
@Override
void applyParameters(final SSLEngine sslEngine, final SSLParameters sslParameters, final String[] appProtocols) {
sslParameters.setApplicationProtocols(appProtocols);
sslEngine.setSSLParameters(sslParameters);
}
@Override
@SuppressWarnings("deprecated")
TlsDetails createTlsDetails(final SSLEngine sslEngine) {
return tlsDetailsFactory != null ? tlsDetailsFactory.create(sslEngine) : null;
}
}
@@ -0,0 +1,26 @@
package org.apache.hc.client5.http.ssl;
/**
* Hostname verification policy.
*
* @see javax.net.ssl.HostnameVerifier
* @see DefaultHostnameVerifier
*
* @since 5.4
*/
public enum HostnameVerificationPolicy {
/**
* Hostname verification is delegated to the JSSE provider, usually executed during the TLS handshake.
*/
BUILTIN,
/**
* Hostname verification is executed by HttpClient post TLS handshake.
*/
CLIENT,
/**
* Hostname verification is executed by the JSSE provider and by HttpClient post TLS handshake.
*/
BOTH
}
@@ -0,0 +1,31 @@
package org.apache.hc.client5.http.ssl;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.protocol.HttpContext;
import javax.net.ssl.SSLSocket;
import java.io.IOException;
import java.net.Socket;
@Contract(threading = ThreadingBehavior.STATELESS)
public interface TlsSocketStrategy {
/**
* Upgrades the given plain socket and executes the TLS handshake over it.
*
* @param socket the existing plain socket
* @param target the name of the target host.
* @param port the port to connect to on the target host.
* @param context the actual HTTP context.
* @param attachment connect request attachment.
* @return socket upgraded to TLS.
*/
SSLSocket upgrade(
Socket socket,
String target,
int port,
Object attachment,
HttpContext context) throws IOException;
}

Some files were not shown because too many files have changed in this diff Show More