Compare commits

1 Commits

132 changed files with 2049 additions and 4329 deletions
-22
View File
@@ -1,22 +0,0 @@
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,7 +8,6 @@ build/
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea
.idea/libraries/
*.iws
*.iml
Generated
+1 -1
View File
@@ -1 +1 @@
neurodock
AI-test
+2 -1
View File
@@ -5,6 +5,7 @@
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
@@ -13,7 +14,7 @@
<option value="$PROJECT_DIR$/Display" />
<option value="$PROJECT_DIR$/GeniusAPI" />
<option value="$PROJECT_DIR$/MALAPITool" />
<option value="$PROJECT_DIR$/Plugin-API" />
<option value="$PROJECT_DIR$/MovieSugest" />
<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_25" default="true" project-jdk-name="25" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+6 -11
View File
@@ -1,21 +1,22 @@
plugins {
id 'java'
id 'org.springframework.boot' version '4.1.0-M4'
id 'org.springframework.boot' version '3.2.2'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'me.zacharias'
version = '1.0-SNAPSHOT'
dependencies {
implementation project(":Core")
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'
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-actuator'
testImplementation 'org.springframework.boot:spring-boot-starter-test:4.1.0-M4'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//runtimeOnly('org.springframework.boot:spring-boot-starter-web')
}
@@ -29,9 +30,3 @@ jar{
attributes 'Main-Class': 'me.zacharias.chat.api.APIApplication'
}
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(javaVersion)) // Set Java version
}
}
@@ -1,11 +1,11 @@
package me.neurodock.api;
package me.zacharias.chat.api;
import me.neurodock.api.payload.request.NewQurryResponceHook;
import me.neurodock.api.payload.request.NewToolRequest;
import me.neurodock.core.Core;
import me.neurodock.core.GlobalObjects;
import me.neurodock.core.PrintMessageHandler;
import me.neurodock.ollama.OllamaObject;
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 org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@@ -1,15 +1,13 @@
package me.neurodock.api;
package me.zacharias.chat.api;
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 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 org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
@@ -47,7 +45,7 @@ public class APITool extends OllamaFunctionTool {
}
@Override
public @NonNull String name() {
public String name() {
return name;
}
@@ -57,7 +55,7 @@ public class APITool extends OllamaFunctionTool {
}
@Override
public @NotNull OllamaPerameter parameters() {
public OllamaPerameter parameters() {
OllamaPerameter.OllamaPerameterBuilder parameter = OllamaPerameter.builder();
for (ToolArgument argument : arguments) {
parameter.addProperty(argument.getName(), argument.getType(), argument.getDescription(), argument.isRequired());
@@ -66,8 +64,8 @@ public class APITool extends OllamaFunctionTool {
}
@Override
public @NonNull OllamaToolResponse function(OllamaFunctionArgument... args) {
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(requestUrl);
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(requestUrl);
for (OllamaFunctionArgument arg : args) {
builder.queryParam(arg.argument(), arg.value());
}
@@ -82,7 +80,7 @@ public class APITool extends OllamaFunctionTool {
if(response.getBody().getError() != null && !response.getBody().getError().isEmpty())
throw new OllamaToolErrorException(name, response.getBody().getError());
return new OllamaToolResponse(name, response.getBody().getResponse());
return new OllamaToolRespnce(name, response.getBody().getResponse());
}
else {
if(response.getBody() == null) {
@@ -1,7 +1,8 @@
package me.neurodock.api;
package me.zacharias.chat.api;
import me.neurodock.api.payload.webhook.WebhookError;
import me.zacharias.chat.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.neurodock.api.condations;
package me.zacharias.chat.api.condations;
import me.neurodock.core.LaunchOptions;
import me.zacharias.chat.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.neurodock.api.controllers;
package me.zacharias.chat.api.controllers;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
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 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 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().handleResponse(apiApplication.getCore().qurryOllama());
apiApplication.getCore().handleResponce(apiApplication.getCore().qurryOllama());
});
t.start();
return ResponseEntity.ok("Query received");
@@ -1,9 +1,10 @@
package me.neurodock.api.controllers;
package me.zacharias.chat.api.controllers;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import me.neurodock.api.APIApplication;
import me.neurodock.api.payload.request.NewToolRequest;
import me.neurodock.api.payload.response.NewToolResponse;
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 org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -33,7 +34,7 @@ public class Tool {
}
return ResponseEntity.ok(new NewToolResponse(request.getName(), request.getDescription(), arguments));
} else {
return ResponseEntity.badRequest().body("Tool already exists");
return new ResponseEntity<>("Tool already exists", null, 400);
}
}
}
@@ -1,4 +1,4 @@
package me.neurodock.api.payload;
package me.zacharias.chat.api.payload;
public class MessageResponce {
private final String message;
@@ -1,6 +1,6 @@
package me.neurodock.api.payload;
package me.zacharias.chat.api.payload;
import me.neurodock.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaPerameter;
public class ToolArgument {
String name;
@@ -1,4 +1,4 @@
package me.neurodock.api.payload;
package me.zacharias.chat.api.payload;
public class ToolRequest {
private final String name;
@@ -1,4 +1,4 @@
package me.neurodock.api.payload.request;
package me.zacharias.chat.api.payload.request;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -1,6 +1,6 @@
package me.neurodock.api.payload.request;
package me.zacharias.chat.api.payload.request;
import me.neurodock.api.payload.ToolArgument;
import me.zacharias.chat.api.payload.ToolArgument;
public class NewToolRequest {
/**
@@ -1,4 +1,4 @@
package me.neurodock.api.payload.response;
package me.zacharias.chat.api.payload.response;
public class NewToolResponse {
private String name;
@@ -1,4 +1,4 @@
package me.neurodock.api.payload.webhook;
package me.zacharias.chat.api.payload.webhook;
public class WebhookError {
private final String originalUrl;
@@ -1,4 +1,4 @@
package me.neurodock.api.payload.webhook.responce;
package me.zacharias.chat.api.payload.webhook.responce;
public class APIToolResponse {
private final String response;
+4 -17
View File
@@ -2,28 +2,15 @@ plugins {
id 'java-library'
}
version = '1.7'
group = 'me.zacharias'
version = '1.0-SNAPSHOT'
dependencies {
implementation project(":Plugin-API")
api "org.json:json:20250107"
}
java {
toolchain {
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)
}
languageVersion.set(JavaLanguageVersion.of(21)) // Set Java version
}
}
@@ -1,953 +0,0 @@
package me.neurodock.core;
import me.neurodock.core.memory.*;
import me.neurodock.ollama.*;
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
import me.neurodock.plugin.Data;
import me.neurodock.plugin.LoadedPlugin;
import me.neurodock.plugin.Plugin;
import me.neurodock.plugin.loader.Loader;
import me.neurodock.plugin.exceptions.PluginLoadingException;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.NotNull;
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.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import static me.neurodock.ollama.OllamaFunctionArgument.deconstructOllamaFunctionArguments;
import static me.neurodock.plugin.Plugin.UNKNOWN_PLUGIN;
/**
* 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 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;
/**
* 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 PrintAdvanceMessageHandler printMessageHandler;
public static String DATA;
public static File DATA_DIR;
public static File PLUGIN_DIRECTORY;
public static File CACHE_DIRECTORY;
/**
* 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, "localhost");
}
/**
* Creates a new instance of Core with the provided PrintMessageHandler
* and a specific Ollama backend address.
*
* @param printMessageHandler The PrintMessageHandler to use as the default output
* @param ollamaIP The IP or hostname of the Ollama backend
*/
public Core(@NotNull PrintAdvanceMessageHandler printMessageHandler, @NotNull String ollamaIP)
{
this.printMessageHandler = printMessageHandler;
this.ollamaIP = ollamaIP;
initDirectories();
initOllamaUrl();
confirmOllama();
initLogWriter();
initScheduler();
initShutdownHook();
}
static {
// This should not be enforced like this, instead this should read a data field, or wait for an init to be called....
// Unsure of how to properly do this at this time, however.
// After looking at things, just run {@link Core.setDataDirectory(String)} somewhere else before initilising the Core object and seems to be fine
setDataDirectory("AI-Chat", false);
}
public static void setLogDirectory(String logDirectory)
{
logDir = new File(logDirectory);
if (!logDir.exists())
{
logDir.mkdir();
}
logFile = new File(logDirectory, "latest.log");
}
/**
* Set the data directory in appropriate locations depending on the host OS, falling back to $WORKING_DIR/data
* @param dataDirectory the data directory to use
*/
public static void setDataDirectory(String dataDirectory, boolean fullDirectory) {
String data;
if(System.getenv("AI_CHAT_DEBUG") != null) {
data = "./data";
}
if(fullDirectory) {
data = dataDirectory;
}
else if(System.getProperty("os.name").toLowerCase().contains("windows")) {
String localappdata = System.getenv("LOCALAPPDATA");
if(localappdata == null) {
localappdata = System.getenv("APPDATA");
}
data = localappdata + "/"+ dataDirectory;
}
else if (System.getProperty("os.name").toLowerCase().contains("linux")) {
data = System.getenv("HOME") + "/.local/share/" + dataDirectory;
}
else if (System.getProperty("os.name").toLowerCase().contains("mac")) {
data = System.getProperty("user.home") + "/Library/Application Support/"+ dataDirectory;
}
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();
}
CACHE_DIRECTORY = new File(DATA + "/cache");
if(!CACHE_DIRECTORY.exists()) {
CACHE_DIRECTORY.mkdirs();
}
}
/**
* 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(DATA_DIR.getAbsolutePath() + "/messages");
}
/**
* 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();
}
/**
* Constructs the Ollama API {@link #url} from {@link #ollamaIP} and {@link #ollamaPort}.
*
* @throws RuntimeException If the resulting URL is malformed or the URI syntax is invalid
*/
private void initOllamaUrl() {
try {
url = new URI("http://" + ollamaIP + ":" + ollamaPort + "/api/chat").toURL();
} catch (MalformedURLException | URISyntaxException e) {
throw new RuntimeException("Failed to construct Ollama URL", e);
}
}
/**
* 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 OllamaObject} to two locations:
* a timestamped archive file under {@code ./messages/}, and a rolling {@code messages.json}
* under {@link #DATA_DIR} 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(DATA_DIR.getAbsolutePath()+"/messages/" + timestamp + ".json"), messages);
writeMessagesTo(new File(DATA_DIR, "messages.json"), messages);
}
/**
* Collects all messages from the {@link OllamaObject} and serializes them into a {@link JSONArray}.
*
* @return A {@link JSONArray} containing all current messages
*/
private JSONArray buildMessagesArray() {
JSONArray messages = new JSONArray();
for (OllamaMessage message : ollamaObject.getMessages()) {
messages.put(new JSONObject(message.toString()));
}
return messages;
}
/**
* 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);
}
}
/**
* This function is pending a better name, as this was introduced from being a generic block. This was ugly, and ideally things here should be refactored more.
*
* TODO: Fix a better name and refactor this method.
*/
@Deprecated(forRemoval = true)
private void constructCore(){
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_DIR,"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);
}
}));
}
/**
* A check to exit early if Ollama isn't reachable.
*/
private void confirmOllama()
{
try {
URL url = new URL("http://" + ollamaIP + ":" + ollamaPort + "/api/version");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Content-Type", "application/json");
connection.setDoOutput(true);
connection.setConnectTimeout(3 * 1000);
int responseCode = connection.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
new RuntimeException("Ollama is un-responsive on url: " + url.toString()).printStackTrace();
System.exit(1);
}
}catch (IOException ex)
{
ex.printStackTrace();
System.out.println("Can not reach Ollama!");
System.exit(1);
}
}
/**
* 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;
for(Pair<OllamaTool, String> tool : ollamaObject.getTools()) {
if(tool.getKey() instanceof OllamaFunctionTool functionTool)
{
funtionTools.add(new Pair<>(functionTool, tool.getValue()));
}
}
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;
for(Pair<OllamaTool, String> tool : ollamaObject.getTools()) {
if(tool.getKey() instanceof OllamaFunctionTool functionTool)
{
funtionTools.add(new Pair<>(functionTool, tool.getValue()));
}
}
}
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, source);
}
/**
* 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;
}
/**
* Removes a tool with a given name
* @param name The tool to remove
*/
public void removeTool(String name) {
Pair<OllamaFunctionTool, String> funtionTool = funtionTools.stream().filter(tool -> tool.getKey().name().equalsIgnoreCase(name)).findFirst().orElse(null);
funtionTools.stream()
.filter(tool -> tool.getKey().name().equalsIgnoreCase(name))
.findFirst()
.ifPresentOrElse(tool -> {
funtionTools.remove(tool);
ollamaObject.removeTool(tool.getKey());
}, () -> {
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) {}
}
/**
* Sends the OllamaObject to Ollama
* @return The response from Ollama
*/
public CompletableFuture<JSONObject> qurryOllama()
{
return CompletableFuture.supplyAsync(() -> {
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
InputStream errorStream = connection.getErrorStream();
if (errorStream != null) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream))) {
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
}
}
}
if (responseCode == HttpURLConnection.HTTP_OK) {
connection.disconnect();
return new JSONObject(response.toString());
} else {
connection.disconnect();
printMessageHandler.printErrorMessage(new OllamaMessage(OllamaMessageRole.SYSTEM,"Error: HTTP Response code - " + responseCode + "\n" + response.toString()));
throw new RuntimeException("HTTP Response code - " + responseCode);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
/**
* Handles the response from Ollama.
*
* 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(JSONObject response) {
if(response == null) return;
writeLog("Raw response: " + response.toString());
JSONObject message = response.getJSONObject("message");
if(!message.has("tool_calls")) {
checkIfResponceMessage(response);
return;
}
ollamaObject.addMessage(new OllamaMessageToolCall(
OllamaMessageRole.fromRole(message.optString("role")),
message.getString("content"),
message.getJSONArray("tool_calls")
));
List<CompletableFuture<Void>> futures = new ArrayList<>();
// Process each tool call
for(Object call : message.getJSONArray("tool_calls")) {
futures.add(processToolCall(call));
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenAccept(result -> {
checkIfResponceMessage(response);
qurryOllama().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;
if (!jsonObject.has("function")) return;
JSONObject function = jsonObject.getJSONObject("function");
OllamaFunctionTool func = findTool(function);
if (func == null) {
reportToolNotFound(function);
return;
}
JSONObject arguments = function.getJSONObject("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 OllamaFunctionTool findTool(JSONObject function) {
return funtionTools.stream()
.filter(f -> (f.getKey().name() + "_" + f.getValue())
.equalsIgnoreCase(function.getString("name")))
.map(Pair::getKey)
.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) {
OllamaToolError error = new OllamaToolError(
"Function '" + function.getString("name") + "' does not exist"
);
ollamaObject.addMessage(error);
printMessageHandler.printToolCalling(error.getError());
}
/**
* 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(OllamaFunctionTool func, JSONObject arguments) {
ArrayList<OllamaFunctionArgument> args = new ArrayList<>();
for(String key : arguments.keySet()) {
args.add(new OllamaFunctionArgument(key, arguments.get(key)));
}
try {
OllamaToolResponse response = func.function(args.toArray(new OllamaFunctionArgument[0]));
ollamaObject.addMessage(response);
printMessageHandler.printMessage(response);
writeLog("Successfully function call " + func.name() + " output: " + response.getResponse());
} catch(OllamaToolErrorException e) {
OllamaToolError error = new OllamaToolError(e.getMessage());
ollamaObject.addMessage(error);
printMessageHandler.printErrorMessage(error);
writeLog("ERROR: " + e.getMessage());
}
}
/**
* 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())
{
OllamaMessage ollamaMessage = new OllamaMessage(OllamaMessageRole.ASSISTANT, message);
printMessageHandler.printMessage(ollamaMessage);
writeLog("Response content: "+ message);
ollamaObject.addMessage(ollamaMessage);
}
}
/**
* 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(OllamaFunctionTool tool : loader.getTools(loadedPlugin.plugin()))
{
addTool(tool, Source.RPCP);
}
});
}
private static class LoaderPluginData extends Data {
ArrayList<LoadedPlugin> plugins = new ArrayList<>();
@Override
public File getDataDictionary() {
return DATA_DIR;
}
@Override
public LoadedPlugin[] getLoadedPlugins() {
return plugins.toArray(new LoadedPlugin[0]);
}
@Override
public File getCacheDirectory() {
return CACHE_DIRECTORY;
}
public void addPlugin(LoadedPlugin plugin)
{
plugins.add(plugin);
}
public void removePlugin(LoadedPlugin plugin)
{
plugins.remove(plugin);
}
}
/**
* Represents the source of a tool.
* <p>
* This is intended for use with {@link Core#addTool(OllamaFunctionTool, String)}
* to indicate the category from which a tool originates.
*/
public static class Source {
/**
* Tools boundeld with the Core runtime (memory, file access, etc.)
* DO NOT USE THIS unless you are poking at core stuff :).
*/
public static final String CORE = "Core";
/**
* Compile-Time Plugins: boundeld at build time as part of the application.
* Examples: MALAPITool, GeniusAPI, WikipediaTool.
*/
public static final String CTP = "CTP";
/**
* Runtime Pre-Compiled Plugins: external plugins loaded at runtime via Plugin-API.
*/
public static final String RPCP = "RPCP";
/**
* Tools defined via the RESt API interface dynamically.
*/
public static final String API = "API";
}
}
@@ -1,31 +0,0 @@
package me.neurodock.core;
import me.neurodock.ollama.OllamaMessage;
import me.neurodock.ollama.OllamaMessageRole;
/**
* Represents a {@link PrintAdvanceMessageHandler}.
* This is used by the {@link Core} to print messages for the user, LLM, Tools, or potentialy System, see {@link OllamaMessageRole}.
* For a simpler version see {@link PrintMessageHandler}
*/
public interface PrintAdvanceMessageHandler {
/**
* Expected to handle the printing of the provided {@link OllamaMessage}.
* @param message The {@link OllamaMessage} requested to be printed from a veriity of sources, see {@link OllamaMessageRole}
*/
void printMessage(OllamaMessage 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(OllamaMessage errorMessage);
/**
* Used when a tool is to have it's calling rendered
* @param representation the string to be printed
*/
void printToolCalling(String representation);
}
@@ -1,80 +0,0 @@
package me.neurodock.core;
import me.neurodock.ollama.OllamaMessage;
import me.neurodock.ollama.OllamaMessageRole;
import static me.neurodock.ollama.OllamaFunctionArgument.deconstructOllamaFunctionArguments;
/**
* 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 OllamaMessage} requested to be printed from a veriity of sources, see {@link OllamaMessageRole}
*/
default void printMessage(OllamaMessage message) {
printMessage(switch (message.getRole()) {
case ASSISTANT -> (color()?"\u001b[32m":"")+(LaunchOptions.getInstance().isShowFullMessage()? message.getContent() : message.getContent().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(OllamaMessage errorMessage) {
printError(errorMessage.getContent());
}
@Override
default void printToolCalling(String representation) {
if(color())
{
printMessage("> \u001B[34m"+representation+"\u001B[0m"); // Blue color for tool calling representations
}
else
{
printMessage("> "+representation);
}
}
}
@@ -1,7 +0,0 @@
package me.neurodock.core;
public sealed interface ToolCallingRender {
record Suppress() implements ToolCallingRender {}
record Default() implements ToolCallingRender {}
record Custom(String representation) implements ToolCallingRender {}
}
@@ -1,4 +0,0 @@
package me.neurodock.core.files;
public class ListFiles {
}
@@ -1,78 +0,0 @@
package me.neurodock.core.files.tools;
import me.neurodock.core.files.FileHandler;
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 java.io.*;
import java.nio.file.Path;
public class ReadFileTool extends OllamaFunctionTool {
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 OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("file_path", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The path to the file to be read", true)
.build();
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
String orgPath = null;
for (OllamaFunctionArgument arg : args) {
if(arg.argument().equals("file_path")) {
orgPath = (String) arg.value();
}
}
if(orgPath == null) {
throw new OllamaToolErrorException(this.name(), "Missing required argument 'file_path'");
}
Path filePath = null;
try{
filePath = fs.resolve(orgPath);
}
catch (IOException ex) {
throw new OllamaToolErrorException(this.name(), ex);
}
File file = filePath.toFile();
if(!file.exists()) {
throw new OllamaToolErrorException(this.name(), "File does not exist: " + fs.path(file));
}
BufferedReader br = null;
try{
br = new BufferedReader(new FileReader(file));
}catch (FileNotFoundException ex) {
throw new OllamaToolErrorException(this.name(), "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 OllamaToolErrorException(this.name(), "Error reading file: " + fs.path(file));
}
return new OllamaToolResponse(this.name(), sb.toString());
}
}
@@ -1,93 +0,0 @@
package me.neurodock.core.files.tools;
import me.neurodock.core.files.FileHandler;
import me.neurodock.ollama.OllamaFunctionArgument;
import me.neurodock.ollama.OllamaFunctionTool;
import me.neurodock.ollama.OllamaPerameter;
import me.neurodock.ollama.OllamaToolResponse;
import me.neurodock.ollama.OllamaPerameter.OllamaPerameterBuilder.Type;
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.nio.file.Path;
public class WriteFileTool extends OllamaFunctionTool {
FileHandler fs = FileHandler.getInstance();
@Override
public @NotNull String name() {
return "write_file";
}
@Override
public @NotNull OllamaPerameter parameters() {
return OllamaPerameter.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 OllamaToolResponse function(OllamaFunctionArgument... args) {
String path = null;
String content = null;
boolean overwrite = false;
for(OllamaFunctionArgument arg : args)
{
switch (arg.argument())
{
case "file_path" -> {
path = (String) arg.value();
}
case "file_content" -> {
content = (String) arg.value();
}
case "overwrite" -> {
overwrite = (boolean) arg.value();
}
}
}
if(path == null || content == null || path.isBlank() || content.isBlank())
{
throw new OllamaToolErrorException(name(), "file_content or file_path is empty or null");
}
Path filePath = null;
try{
filePath = fs.resolve(path);
}
catch (IOException ex) {
throw new OllamaToolErrorException(this.name(), ex);
}
File file = filePath.toFile();
if(file.exists() && !overwrite) {
throw new OllamaToolErrorException(this.name(), "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 OllamaToolErrorException(name(), e);
}
try(BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file))))
{
bw.write(content);
return new OllamaToolResponse(name(), "Successfully wrote data to "+fs.path(file));
}
catch (IOException ex)
{
throw new OllamaToolErrorException(name(), ex);
}
}
}
@@ -1,198 +0,0 @@
package me.neurodock.core.memory;
import me.neurodock.core.Core;
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(Core.DATA + "/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;
}
}
@@ -1,29 +0,0 @@
package me.neurodock.core.memory;
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;
public class GetMemoriesFunction extends OllamaFunctionTool {
@Override
public @NotNull String name() {
return "get_memories";
}
@Override
public String description() {
return "Retrieves all the memories.";
}
@Override
public @NotNull OllamaPerameter parameters() {
return null;
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
return new OllamaToolResponse(name(), CoreMemory.getInstance().getMappedMemories());
}
}
@@ -1,32 +0,0 @@
package me.neurodock.core.memory;
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.json.JSONArray;
public class GetMemoryIdentitiesFunction extends OllamaFunctionTool {
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 OllamaPerameter parameters() {
return null;
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
return new OllamaToolResponse(this.name(), new JSONArray(memory.getMemoriesIdentity()).toString());
}
}
@@ -1,55 +0,0 @@
package me.neurodock.core.memory.array;
import me.neurodock.core.memory.CoreMemory;
import me.neurodock.ollama.OllamaFunctionArgument;
import me.neurodock.ollama.OllamaFunctionTool;
import me.neurodock.ollama.OllamaPerameter;
import me.neurodock.ollama.OllamaPerameter.OllamaPerameterBuilder.Type;
import me.neurodock.ollama.OllamaToolResponse;
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
import org.jetbrains.annotations.NotNull;
public class AddArrayMemory extends OllamaFunctionTool {
private CoreMemory memory = CoreMemory.getInstance();
@Override
public @NotNull String name() {
return "add_arrayed_memory";
}
@Override
public @NotNull OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("memory", Type.STRING, "The memory to remember", true)
.addProperty("index", Type.INT, "The index to put it at. Should be avoided, unless overwriting", false)
.build();
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
if (args.length > 1) {
String memory = null;
int index = -1;
for (OllamaFunctionArgument arg : args) {
if(arg.argument().equals("memory")) {
memory = (String) arg.value();
}
else if(arg.argument().equals("index")) {
index = (Integer) arg.value();
}
}
if (memory == null || index < 0) {
throw new OllamaToolErrorException(name(), "no memory or index provided");
}
this.memory.addMemory(memory, index);
}
else {
if(!args[0].argument().equals("memory")) {
throw new OllamaToolErrorException(name(), "no memory provided");
}
this.memory.addMemory(name(), (String) args[0].value());
}
return new OllamaToolResponse(name(), "Added arrayed memory");
}
}
@@ -1,36 +0,0 @@
package me.neurodock.core.memory.array;
import me.neurodock.core.memory.CoreMemory;
import me.neurodock.ollama.OllamaFunctionArgument;
import me.neurodock.ollama.OllamaFunctionTool;
import me.neurodock.ollama.OllamaPerameter;
import me.neurodock.ollama.OllamaToolResponse;
import me.neurodock.ollama.OllamaPerameter.OllamaPerameterBuilder.Type;
import org.jetbrains.annotations.NotNull;
public class GetArrayMemory extends OllamaFunctionTool {
CoreMemory memory = CoreMemory.getInstance();
@Override
public @NotNull String name() {
return "get_array_memory";
}
@Override
public @NotNull OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("index", Type.STRING, "The index to retrieve memory from", false)
.build();
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
if (args.length != 1) {
return new OllamaToolResponse(name(), memory.getArrayMemories().toString());
}
return memory.getMemory((Integer) args[0].value())
.map(value -> new OllamaToolResponse(name(), value))
.orElse(OllamaToolResponse.empty("No memory found for key: " + args[0].value()));
}
}
@@ -1,26 +0,0 @@
package me.neurodock.core.memory.array;
import me.neurodock.core.memory.CoreMemory;
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;
public class GetArrayedMemories extends OllamaFunctionTool {
private CoreMemory memory = CoreMemory.getInstance();
@Override
public @NotNull String name() {
return "get_arrayed_memories";
}
@Override
public @NotNull OllamaPerameter parameters() {
return OllamaPerameter.empty();
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
return new OllamaToolResponse(name(), memory.getArrayMemories().toString());
}
}
@@ -1,5 +0,0 @@
/**
* 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;
@@ -1,69 +0,0 @@
package me.neurodock.ollama;
import java.util.ArrayList;
/**
* 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;
}
public <T extends Enum<T>> T getValue(Class<T> enumClass) {
if(value instanceof String str) {
return Enum.valueOf(enumClass, str);
}
throw new IllegalArgumentException(String.format("%s is not a valid enum value of type %s", value.toString(), enumClass.getName()));
}
public static String deconstructOllamaFunctionArgument(OllamaFunctionArgument argument) {
return argument.argument() + ": " + argument.value();
}
public static String deconstructOllamaFunctionArguments(OllamaFunctionArgument... arguments) {
StringBuilder sb = new StringBuilder();
for (OllamaFunctionArgument argument : arguments) {
sb.append(deconstructOllamaFunctionArgument(argument)).append(", ");
}
return sb.toString().replaceAll(", $", "");
}
public static String deconstructOllamaFunctionArguments(ArrayList<OllamaFunctionArgument> arguments) {
StringBuilder sb = new StringBuilder();
for (OllamaFunctionArgument argument : arguments) {
sb.append(deconstructOllamaFunctionArgument(argument)).append(", ");
}
return sb.toString().replaceAll(", $", "");
}
}
@@ -1,68 +0,0 @@
package me.neurodock.ollama;
import org.json.JSONObject;
/**
* Represents a response from a tool.
*/
public class OllamaToolResponse extends OllamaMessage {
/**
* Returns an empty tool response
* See {@link OllamaToolResponse#empty(String, String)} for a reasoned/described response
* @param tool The tool that responded
* @return an empty tool response
*/
public static OllamaToolResponse empty(String tool)
{
return empty(tool, "No reason provided");
}
/**
* Returns an empty tool response with a reason/description.
* See {@link OllamaToolResponse#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 OllamaToolResponse empty(String tool, String description)
{
return new OllamaToolResponse(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 OllamaToolResponse}.
* @param tool The tool that responded
* @param response The response from the tool
*/
public OllamaToolResponse(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,150 +0,0 @@
package me.neurodock.plugin.loader;
import jdk.jshell.spi.ExecutionControl;
import me.neurodock.core.Pair;
import me.neurodock.ollama.*;
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
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.ToolArguments;
import me.neurodock.plugin.tool.ToolResponse;
import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;
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 OllamaFunctionTool[] getTools(Plugin plugin) {
ArrayList<OllamaFunctionTool> tools = new ArrayList<>();
for(Tool tool : plugin.getTools()) {
tools.add(new OllamaFunctionTool() {
@Override
public @NotNull String name() {
return tool.name() + "_" + plugin.getMetadata().getName();
}
@Override
public String description() {
return tool.description();
}
@Override
public @NotNull OllamaPerameter parameters() {
return OllamaPerameter.builder().of(tool.parameters()).build();
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
// 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());
}
@Override
public String toString() {
// Replaces the toString which is used throughout the Core and OllamaObject to get the JSON representation of a Function Tool
// Ideally the toString method SHULD NOT be used like this, while yes a JSON representation is better than a Java Hash representation
// the toString should not be utilized or overwritten in this manner
return tool.getToolJSON();
}
});
}
return tools.toArray(tools.toArray(new OllamaFunctionTool[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*/);
}
}
@@ -0,0 +1,531 @@
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,4 +1,4 @@
package me.neurodock.core;
package me.zacharias.chat.core;
import java.util.HashMap;
import java.util.Map;
@@ -1,11 +1,11 @@
package me.neurodock.core;
package me.zacharias.chat.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,8 +15,6 @@ public class LaunchOptions {
return instance;
}
private LaunchOptions() {}
private boolean loadOld = true;
private boolean autoAccept;
private boolean serverMode;
@@ -1,4 +1,4 @@
package me.neurodock.core;
package me.zacharias.chat.core;
/**
* A simple Pair class.
@@ -0,0 +1,21 @@
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,18 +1,15 @@
package me.neurodock.core.files;
package me.zacharias.chat.core.files;
import me.neurodock.core.Core;
import me.neurodock.core.Pair;
import me.neurodock.core.files.tools.ReadFileTool;
import me.neurodock.core.files.tools.WriteFileTool;
import me.neurodock.ollama.OllamaTool;
import me.zacharias.chat.core.Core;
import org.intellij.lang.annotations.MagicConstant;
import java.io.*;
import java.nio.file.Path;
import java.util.ArrayList;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.util.Arrays;
/**
* Base class responsible for the {@link me.neurodock.core.files} related systems
* Base class responsible for the {@link me.zacharias.chat.core.files} related systems
*/
public class FileHandler {
/**
@@ -23,7 +20,7 @@ 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 Path/*System*/ root;
private final File/*System*/ directory;
/**
* Creates a new instance as well as setting the {@link #instance} to this new one
@@ -31,42 +28,19 @@ public class FileHandler {
*/
public FileHandler(@MagicConstant(valuesFromClass = FileHandlerLocation.class) String baseDirectory) {
try {
root = Path.of(baseDirectory).toAbsolutePath().normalize();
if (!root.toFile().exists()) {
root.toFile().mkdirs();
}
FileSystem fs = FileSystems.newFileSystem(new File(baseDirectory).toPath());
//fs.getPath()
directory = new File(baseDirectory);
if (!directory.exists())
directory.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 OllamaTool}'s this module adds
* @return
*/
public static ArrayList<Pair<? extends OllamaTool, String>> getTools() {
ArrayList<Pair<? extends OllamaTool, String>> fileTools = new ArrayList<>();
fileTools.add(new Pair<>(new ReadFileTool(), Core.Source.CORE));
fileTools.add(new Pair<>(new WriteFileTool(), Core.Source.CORE));
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
@@ -74,12 +48,11 @@ public class FileHandler {
* @throws FileHandlerException
*/
public String readFile(String filename) throws FileHandlerException {
File file = null;
try {
file = resolve(filename).toFile();
} catch (IOException e) {
throw new FileHandlerException("Illegal restricted path");
if(filename.contains(".."))
{
throw new FileHandlerException("File \"" + filename + "\" tries to retrace path");
}
File file = new File(directory, filename);
if(file.exists())
{
try{
@@ -109,13 +82,4 @@ 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.neurodock.core.files;
package me.zacharias.chat.core.files;
public class FileHandlerException extends RuntimeException {
public FileHandlerException(String message) {
@@ -1,6 +1,6 @@
package me.neurodock.core.files;
package me.zacharias.chat.core.files;
import me.neurodock.core.Core;
import me.zacharias.chat.core.Core;
public class FileHandlerLocation {
public static final String DATA_FILES = Core.DATA+"/files";
@@ -0,0 +1,4 @@
package me.zacharias.chat.core.files;
public class ListFiles {
}
@@ -1,11 +1,10 @@
package me.neurodock.core.memory;
package me.zacharias.chat.core.memory;
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 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>
@@ -18,7 +17,7 @@ public class AddMemoryFunction extends OllamaFunctionTool {
CoreMemory memory = CoreMemory.getInstance();
@Override
public @NotNull String name() {
public String name() {
return "add_memory";
}
@@ -28,7 +27,7 @@ public class AddMemoryFunction extends OllamaFunctionTool {
}
@Override
public @NotNull OllamaPerameter parameters() {
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)
@@ -36,7 +35,7 @@ public class AddMemoryFunction extends OllamaFunctionTool {
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
if (args.length == 0) {
throw new OllamaToolErrorException(name(), "Missing memory argument");
}
@@ -59,6 +58,6 @@ public class AddMemoryFunction extends OllamaFunctionTool {
}
this.memory.addMemory(identity, memory);
return new OllamaToolResponse(name(), "Added "+identity+" to the memory");
return new OllamaToolRespnce(name(), "Added "+identity+" to the memory");
}
}
@@ -0,0 +1,128 @@
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;
}
}
@@ -0,0 +1,28 @@
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,10 +1,11 @@
package me.neurodock.core.memory;
package me.zacharias.chat.core.memory;
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 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>
@@ -17,7 +18,7 @@ public class GetMemoryFunction extends OllamaFunctionTool {
CoreMemory memory = CoreMemory.getInstance();
@Override
public @NotNull String name() {
public String name() {
return "get_memory";
}
@@ -27,16 +28,14 @@ public class GetMemoryFunction extends OllamaFunctionTool {
}
@Override
public @NotNull OllamaPerameter parameters() {
public OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("identity", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The identity of the memory to retrieve", true)
.build();
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
return memory.getMemory((String) (args[0].value()))
.map(value -> new OllamaToolResponse(name(), value))
.orElse(OllamaToolResponse.empty("No memory found for key: " + args[0].value()));
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
return new OllamaToolRespnce(name(), memory.getMemory((String) (args[0].value())));
}
}
@@ -0,0 +1,31 @@
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,11 +1,10 @@
package me.neurodock.core.memory;
package me.zacharias.chat.core.memory;
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 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>
@@ -18,7 +17,7 @@ public class RemoveMemoryFunction extends OllamaFunctionTool {
CoreMemory memory = CoreMemory.getInstance();
@Override
public @NotNull String name() {
public String name() {
return "remove_memory";
}
@@ -28,19 +27,19 @@ public class RemoveMemoryFunction extends OllamaFunctionTool {
}
@Override
public @NotNull OllamaPerameter parameters() {
public OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("identity", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The identity of the memory to forget", true)
.build();
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
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 OllamaToolResponse(name(), "Removed "+value+" to the memory");
return new OllamaToolRespnce(name(), "Removed "+value+" to the memory");
}
}
@@ -0,0 +1,40 @@
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,34 +1,22 @@
package me.neurodock.ollama;
package me.zacharias.chat.ollama;
import me.neurodock.core.Core;
import me.neurodock.core.ToolCallingRender;
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
import org.jetbrains.annotations.NotNull;
import me.zacharias.chat.core.Core;
import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException;
import org.json.JSONObject;
import java.util.Optional;
/**
* Represents a tool that Ollama can call.
*/
public abstract class OllamaFunctionTool implements OllamaTool {
/**
* This field is set via Reflection injection from {@link OllamaObject#addTool(OllamaTool, String)}.
* As is, it will be overwritten. Do not set it yourself.
*/
protected String source = "";
@Override
public String toString() {
JSONObject ret = new JSONObject();
ret.put("type", "function");
ret.put("tool", "function");
JSONObject function = new JSONObject();
function.put("name", name()+"_"+(source != null ? source : ""));
if(description() != null) {
function.put("description", description());
}
function.put("name", name());
function.put("description", description());
function.put("parameters", (parameters() == null?
new JSONObject() : new JSONObject(parameters().toString())));
@@ -36,25 +24,12 @@ public abstract class OllamaFunctionTool implements OllamaTool {
return ret.toString();
}
/**
* Generates a string representation of this tool calling for rendering.
*
* @param calling the raw JSON sent from Ollama describing the complete function call
* @return an {@link Optional} containing the formatted representation if this calling
* should be rendered, or {@link Optional#empty()} otherwise
*/
public ToolCallingRender renderCalling(JSONObject calling)
{
return new ToolCallingRender.Suppress();
}
/**
* The name of the tool
* This is used by Ollama to know what the tool is
* @return The name of the tool
*/
@NotNull
abstract public String name();
/**
@@ -62,27 +37,23 @@ public abstract class OllamaFunctionTool implements OllamaTool {
* This is used by Ollama to know what the tool does
* @return The description of the tool
*/
public String description(){
return null;
}
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
*/
@NotNull
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#handleResponse(JSONObject)}
* 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, if null return {@link OllamaToolResponse}
* @return The response from the tool
* @throws OllamaToolErrorException If the tool encounters an error
*/
abstract public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args);
abstract public OllamaToolRespnce function(OllamaFunctionArgument... args);
}
@@ -1,7 +1,7 @@
package me.neurodock.ollama;
package me.zacharias.chat.ollama;
import me.neurodock.core.Core;
import me.neurodock.core.Pair;
import me.zacharias.chat.core.Core;
import me.zacharias.chat.core.Pair;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.NotNull;
@@ -53,7 +53,7 @@ public class OllamaFunctionTools implements Iterable<Pair<OllamaFunctionTool, St
}
for (String s : source) {
if (s.equals(Core.Source.CTP)) {
if (s.equals(Core.Source.INTERNAL)) {
this.source.add(s);
}
}
@@ -1,4 +1,4 @@
package me.neurodock.ollama;
package me.zacharias.chat.ollama;
import org.json.JSONObject;
@@ -25,20 +25,6 @@ public class OllamaMessage {
this.content = content;
}
/**
* @return The role of who sent this "message", represented as {@link OllamaMessageRole}
*/
public OllamaMessageRole getRole() {
return role;
}
/**
* @return The "message" or content sent by a source
*/
public String getContent() {
return content;
}
@Override
public String toString() {
JSONObject json = new JSONObject();
@@ -1,4 +1,4 @@
package me.neurodock.ollama;
package me.zacharias.chat.ollama;
/**
* Represents the role of a message.
@@ -1,4 +1,4 @@
package me.neurodock.ollama;
package me.zacharias.chat.ollama;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -1,10 +1,10 @@
package me.neurodock.ollama;
package me.zacharias.chat.ollama;
import me.neurodock.core.Core;
import me.neurodock.core.LaunchOptions;
import me.neurodock.core.Pair;
import me.neurodock.core.files.FileHandlerLocation;
import me.neurodock.core.files.FileHandler;
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;
@@ -13,7 +13,10 @@ import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.lang.reflect.Field;
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;
@@ -38,7 +41,7 @@ public class OllamaObject {
/**
* The tools of the Ollama Object.
*/
ArrayList<Pair<OllamaTool, String>> tools;
ArrayList<OllamaTool> tools;
/**
* The format of the Ollama Object.
*/
@@ -66,41 +69,15 @@ public class OllamaObject {
* @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<Pair<OllamaTool, String>> tools, JSONObject format, Map<String, Object> options, boolean stream, String 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;
// Reflecting the reflection injection for ALL tools that was added in the OllamaObjectBuilder, etc, etc comment.. its past midnignt and i'm tired.
for(Pair<OllamaTool, String> tool : tools)
{
Class<?> clazz = tool.getKey().getClass();
Field field = null;
while(!clazz.equals(Object.class)) {
try {
field = clazz.getDeclaredField("source");
if (field != null) {
break;
}
}
catch (NoSuchFieldException ignore){}
clazz = clazz.getSuperclass();
}
if (field != null) {
field.setAccessible(true);
try {
field.set(tool.getKey(), tool.getValue());
} catch (IllegalAccessException e) {
Core.writeLog("ERROR: "+e.getMessage());
}
}
}
this.tools = tools;
this.format = format;
this.options = options;
this.stream = stream;
this.keep_alive = keep_alive;
LaunchOptions launchOptions = LaunchOptions.getInstance();
LaunchOptions launchOptions = new LaunchOptions();
if(launchOptions.isLoadOld()) {
System.out.println("Loading old data...");
File f = new File(Core.DATA_DIR+"/messages.json");
@@ -128,10 +105,6 @@ public class OllamaObject {
e.printStackTrace();
}
}
else
{
System.out.println("No old data found, skipping loading old data.");
}
}
}
@@ -155,7 +128,7 @@ public class OllamaObject {
* Gets the tools
* @return The tools
*/
public ArrayList<Pair<OllamaTool, String>> getTools() {
public ArrayList<OllamaTool> getTools() {
return tools;
}
@@ -163,29 +136,8 @@ public class OllamaObject {
* Adds a tool to the Ollama Object
* @param tool The tool to add
*/
public void addTool(OllamaTool tool, @MagicConstant(valuesFromClass = Core.Source.class) String source) {
// We inject the source into the tool's source field if it exists, This is to not cause issues with duplicate tools
Class<?> clazz = tool.getClass();
Field field = null;
while(!clazz.equals(Object.class)) {
try {
field = clazz.getDeclaredField("source");
if (field != null) {
break;
}
}
catch (NoSuchFieldException ignore){}
clazz = clazz.getSuperclass();
}
if (field != null) {
field.setAccessible(true);
try {
field.set(tool, source);
} catch (IllegalAccessException e) {
Core.writeLog("ERROR: "+e.getMessage());
}
}
tools.add(new Pair<>(tool, source));
public void addTool(OllamaTool tool) {
tools.add(tool);
}
public void removeTool(OllamaTool tool) {
@@ -232,46 +184,13 @@ public class OllamaObject {
messages.add(message);
}
/**
* Returns the current list of messages, AND clears them.
* You will need to re-submit the System prompt if you had one
* @return an ArrayList containing all messages stored in this {@link OllamaObject}
*/
public List<OllamaMessage> dumpMessages()
{
ArrayList<OllamaMessage> messages = new ArrayList<>(this.messages);
this.messages.clear();
return messages;
}
/**
* Adds all messages from a povided list to this {@link OllamaObject}
* @param messages the message list to add
*/
public void addAllMessages(List<OllamaMessage> messages) {
this.messages.addAll(messages);
}
/**
* Adds a system message to the messages.
* <p>
* Equvalent to {@code OllamaObject#addMessage(new OllamaMessage(OllamaMessageRole.SYSTEM, <message>))}
* @param message
*/
public void addSystemMessage(String message) {
messages.add(new OllamaMessage(OllamaMessageRole.SYSTEM, message));
}
@Override
public String toString() {
JSONObject json = new JSONObject();
JSONArray tools = new JSONArray();
for (Pair<OllamaTool, String> tool : this.tools) {
if(tool.getKey().getClass().isInterface()) continue;
JSONObject obj = new JSONObject(tool.getKey().toString());
//obj.put("name", obj.getString("name") + tool.getValue()); // Injects the source of the tool into the name
tools.put(obj);
for (OllamaTool tool : this.tools) {
tools.put(new JSONObject(tool.toString()));
}
JSONArray messages = new JSONArray();
@@ -288,7 +207,7 @@ public class OllamaObject {
json.put("keep_alive", keep_alive);
return json.toString();
}
/**
* Creates a new instance of OllamaObjectBuilder.
* @return The {@link OllamaObjectBuilder}
@@ -313,7 +232,7 @@ public class OllamaObject {
/**
* The tools of the Ollama Object.
*/
ArrayList<Pair<OllamaTool, String>> tools = new ArrayList<>();
ArrayList<OllamaTool> tools = new ArrayList<>();
/**
* The format of the Ollama Object.
*/
@@ -402,73 +321,30 @@ public class OllamaObject {
/**
* Adds a tool to the Ollama Object
* Assumes the tool is from an external source, see {@link OllamaObject.OllamaObjectBuilder#addTool(OllamaTool, String)} to specify the source
* @param tool The tool to add
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder addTool(OllamaTool tool) {
this.tools.add(new Pair<>(tool, Core.Source.CTP));
return this;
}
/**
* Adds a tool to the Ollama Object
* This allows you to specify the source of the tool, see {@link OllamaObject.OllamaObjectBuilder#addTool(OllamaTool)} to add a tool from an external source
* @param tool The tool to add
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder addTool(OllamaTool tool, @MagicConstant(valuesFromClass = Core.Source.class) String source) {
this.tools.add(new Pair<>(tool, source));
this.tools.add(tool);
return this;
}
/**
* Adds tools to the Ollama Object
* Assumes the tools are from an external source, see {@link OllamaObject.OllamaObjectBuilder#addTools(ArrayList)}} to specify the source
* @param tools The tools to add
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder addToolsExternal(ArrayList<? extends OllamaTool> tools) {
for (OllamaTool tool : tools) {
this.tools.add(new Pair<>(tool, Core.Source.CTP));
}
return this;
}
/**
* Adds tools to the Ollama Object
* This allows you to specify the source of the tools, see {@link OllamaObject.OllamaObjectBuilder#addToolsExternal(ArrayList)}} to add tools from an external source
* @param tools The tools to add
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder addTools(ArrayList<Pair<? extends OllamaTool, String>> tools) {
for(Pair<? extends OllamaTool, String> tool : tools) {
this.tools.add(new Pair<>(tool.getKey(), tool.getValue()));
}
public OllamaObjectBuilder addTools(ArrayList<? extends OllamaTool> tools) {
this.tools.addAll(tools);
return this;
}
/**
* Adds tools to the Ollama Object
* Assumes the tools are from an external source, see {@link OllamaObject.OllamaObjectBuilder#addTools(Pair[])}} to specify the source
* @param tools The tools to add
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder addTools(OllamaTool... tools) {
for(OllamaTool tool : tools) {
this.tools.add(new Pair<>(tool, Core.Source.CTP));
}
return this;
}
/**
* Adds tools to the Ollama Object
* This allows you to specify the source of the tools, see {@link OllamaObject.OllamaObjectBuilder#addTools(OllamaTool[])} to add tools from an external source
* @param tools The tools to add
* @return The {@link OllamaObjectBuilder}
*/
@SafeVarargs
public final OllamaObjectBuilder addTools(Pair<OllamaTool, String>... tools) {
this.tools.addAll(List.of(tools));
return this;
}
@@ -503,23 +379,13 @@ public class OllamaObject {
return this;
}
/**
* Initializes the {@link FileHandler} singleton with the given base directory and adds its tools.
* <p>
* The {@link FileHandler} is intentionally constructed here rather than eagerly,
* as file access tooling should only be initialized if explicitly requested during
* object construction. Calling this after {@link #build()} is technically possible
* but not intended.
* </p>
* @param baseDirectory the base directory for file access, see {@link FileHandlerLocation}
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder addFileTools(@MagicConstant(valuesFromClass = FileHandlerLocation.class) String baseDirectory)
{
new FileHandler(baseDirectory);
FileHandler fileHandler = new FileHandler(baseDirectory);
//throw new IllegalArgumentException("FileHandler is not supported yet!");
return addTools(FileHandler.getTools());
if(false);
throw new IllegalArgumentException("FileHandler is not supported yet!");
}
/**
@@ -1,17 +1,17 @@
package me.neurodock.ollama;
package me.zacharias.chat.ollama;
import me.neurodock.plugin.tool.ToolParameters;
import org.json.JSONObject;
import java.util.*;
import java.util.stream.Stream;
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 OllamaPerameter {
/**
* Creates a new instance of {@link OllamaPerameter}.
* @param properties The properties of the parameters
@@ -21,7 +21,7 @@ public class OllamaPerameter {
this.properties = properties;
this.required = required;
};
@Override
public String toString() {
JSONObject json = new JSONObject();
@@ -32,16 +32,16 @@ public class OllamaPerameter {
return json.toString();
}
/**
* the properties of the parameters
*/
private final JSONObject properties;
JSONObject properties;
/**
* the required parameters
*/
private final String[] required;
String[] required;
/**
* Gets the properties of the {@link OllamaPerameter}
* @return The properties of the {@link OllamaPerameter}
@@ -49,7 +49,7 @@ public class OllamaPerameter {
public JSONObject getProperties() {
return properties;
}
/**
* Gets the required parameters of the {@link OllamaPerameter}
* @return The required parameters of the {@link OllamaPerameter}
@@ -57,7 +57,7 @@ public class OllamaPerameter {
public String[] getRequired() {
return required;
}
/**
* Creates a new instance of {@link OllamaPerameterBuilder}.
* @return The {@link OllamaPerameterBuilder}
@@ -65,19 +65,7 @@ public class OllamaPerameter {
public static OllamaPerameterBuilder builder() {
return new OllamaPerameterBuilder();
}
/**
* Creates an empty {@link OllamaPerameter}
* @return an empty {@link OllamaPerameter}
* @apiNote This is equvalent to
* <pre>{@code
* OllamaPerameter.builder().build();
* }</pre>
*/
public static OllamaPerameter empty() {
return builder().build();
}
/**
* Represents a builder for {@link OllamaPerameter}.
*/
@@ -90,21 +78,24 @@ public class OllamaPerameter {
* The required parameters.
*/
ArrayList<String> required = new ArrayList<>();
/**
* Add an optinal perameter to this {@link OllamaPerameterBuilder}
* Creates a new instance of {@link OllamaPerameterBuilder}.
* @param name The name of the parameter
* @param type The type of the parameter
* @param description The description of the parameter
* @return The {@link OllamaPerameterBuilder}
* @apiNote Prefer {@link #addProperty(String, Type, String, boolean)} to be explicit about required state
*/
public OllamaPerameterBuilder addProperty(String name, Type type, String description) {
return OllamaPerameterBuilder.this.addProperty(name, type, description, false);
if(name == null || type == null || description == null) {
return this;
}
propertyMap.put(name, new Property(type.getType(), description));
return this;
}
/**
* Add a potentialy required peremeter to this {@link OllamaPerameterBuilder}.
* Creates a new instance of {@link OllamaPerameterBuilder}.
* @param name The name of the parameter
* @param type The type of the parameter
* @param description The description of the parameter
@@ -116,43 +107,12 @@ public class OllamaPerameter {
return this;
}
propertyMap.put(name, new Property(type.getType(), description));
if(required) {
this.required.add(name);
}
this.required.add(name);
return this;
}
public <T extends Enum<T>> OllamaPerameterBuilder addEnumProperty(String name, Class<T> enumClass, String description, boolean required) {
StringBuilder newDescription = new StringBuilder(description);
if(newDescription.charAt(newDescription.length() - 1) != '.') {
newDescription.append(". ");
}
else
{
newDescription.append(' ');
}
newDescription.append("Enum values: ");
Iterator<T> it = Arrays.stream(enumClass.getEnumConstants()).iterator();
while(it.hasNext()) {
T item = it.next();
newDescription.append(item.name());
if(it.hasNext()) {
newDescription.append(", ");
}
}
return addProperty(name, Type.ENUM, newDescription.toString(), required);
}
public <T extends Enum<T>> OllamaPerameterBuilder addEnumProperty(Class<T> enumClass, String description) {
return addEnumProperty(enumClass.getSimpleName(), enumClass, description, false);
}
/**
* Makes a previusly optinal perameter required for this {@link OllamaPerameterBuilder}
* Creates a new instance of {@link OllamaPerameterBuilder}.
* @param name The name of the parameter
* @return The {@link OllamaPerameterBuilder}
*/
@@ -160,7 +120,7 @@ public class OllamaPerameter {
required.add(name);
return this;
}
/**
* Removes a property from the parameters.
* @param name The name of the property to remove
@@ -168,26 +128,9 @@ public class OllamaPerameter {
*/
public OllamaPerameterBuilder removeProperty(String name) {
propertyMap.remove(name);
required.remove(name);
return this;
}
/**
* Coverts a RPCP Tool Perameter to a CTP Ollama Peameter
* @param parameters
* @return
*/
public OllamaPerameterBuilder of(ToolParameters parameters) {
required.addAll(Arrays.asList(parameters.getRequired()));
for(String key : parameters.getProperties().keySet())
{
Property property = new Property(parameters.getProperties().getJSONObject(key).getString("type"),
parameters.getProperties().getJSONObject(key).getString("description"));
propertyMap.put(key, property);
}
return this;
}
/**
* Builds the {@link OllamaPerameter}
* @return The {@link OllamaPerameter}
@@ -199,7 +142,7 @@ public class OllamaPerameter {
}
return new OllamaPerameter(properties, required.toArray(new String[0]));
}
/**
* Represents a property of a parameter.
*/
@@ -212,7 +155,7 @@ public class OllamaPerameter {
* The description of the property.
*/
String description;
/**
* Creates a new instance of {@link Property}.
* @param type The type of the property
@@ -233,7 +176,7 @@ public class OllamaPerameter {
return json.toString();
}
}
/**
* Represents the type of parameter.
*/
@@ -257,17 +200,13 @@ public class OllamaPerameter {
/**
* Represents a array parameter.
*/
ARRAY("array"),
/**
* Represents a object parameter.
*/
OBJECT("object");
ARRAY("array");
/**
* The type of the parameter.
*/
private final String type;
private String type;
/**
* Gets the type of the parameter.
* @return The type of the parameter
@@ -275,7 +214,7 @@ public class OllamaPerameter {
public String getType() {
return type;
}
/**
* Creates a new instance of {@link Type}.
* @param type The type of the parameter
@@ -283,19 +222,6 @@ public class OllamaPerameter {
Type(String type) {
this.type = type;
}
Type of(String type)
{
return switch (type) {
case "string" -> STRING;
case "int" -> INT;
case "boolean" -> BOOLEAN;
case "enum" -> ENUM;
case "array" -> ARRAY;
case "object" -> OBJECT;
default -> throw new IllegalArgumentException("Unknown type " + type);
};
}
}
}
}
@@ -1,4 +1,4 @@
package me.neurodock.ollama;
package me.zacharias.chat.ollama;
/**
* Represents a tool.
@@ -1,4 +1,4 @@
package me.neurodock.ollama;
package me.zacharias.chat.ollama;
import org.json.JSONObject;
@@ -0,0 +1,44 @@
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,11 +1,11 @@
package me.neurodock.ollama.exceptions;
package me.zacharias.chat.ollama.exceptions;
import me.neurodock.core.Core;
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#handleResponse(JSONObject)}
* 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 {
/**
@@ -27,10 +27,6 @@ public class OllamaToolErrorException extends RuntimeException {
this.tool = tool;
this.error = error;
}
public OllamaToolErrorException(String tool, Exception ex) {
this(tool, ex.getMessage());
}
/**
* Gets the tool that caused the error.
@@ -1,7 +1,7 @@
package me.neurodock.ollama.utils;
package me.zacharias.chat.ollama.utils;
import me.neurodock.ollama.OllamaMessage;
import me.neurodock.ollama.OllamaMessageRole;
import me.zacharias.chat.ollama.OllamaMessage;
import me.zacharias.chat.ollama.OllamaMessageRole;
public class SystemMessage extends OllamaMessage {
/**
@@ -0,0 +1,5 @@
package me.zacharias.chat.plugin;
public class Plugin {
private final PluginMetadata metadata = null;
}
@@ -0,0 +1,62 @@
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.
}
}
@@ -0,0 +1,9 @@
package me.zacharias.chat.plugin;
public interface PluginMetadata {
public String getName();
public String getVersion();
public String getDescription();
public String[] getAuthor();
public String entryPoint();
}
@@ -1,4 +1,4 @@
package me.neurodock.plugin.annotations;
package me.zacharias.chat.plugin.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -6,6 +6,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectPluginMetadata {
@Target(ElementType.TYPE)
public @interface OllamaTool {
}
@@ -0,0 +1,14 @@
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;
}
@@ -0,0 +1,7 @@
package me.zacharias.chat.plugin.exceptions;
public class PluginLoadingException extends RuntimeException {
public PluginLoadingException(String message, String pluginName) {
super("Plugin: \""+pluginName+"\"> "+message);
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
import me.neurodock.core.files.FileHandler;
import me.neurodock.core.files.FileHandlerException;
import me.zacharias.chat.core.files.FileHandler;
import me.zacharias.chat.core.files.FileHandlerException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+1 -7
View File
@@ -1,12 +1,6 @@
package plugin;
import me.neurodock.plugin.Plugin;
import me.neurodock.plugin.PluginMetadata;
import org.jetbrains.annotations.NotNull;
import me.zacharias.chat.plugin.Plugin;
public class Test extends Plugin {
@Override
public @NotNull PluginMetadata getMetadata() {
return null;
}
}
+11 -14
View File
@@ -1,22 +1,19 @@
package plugin;
//// This class is broken due to the current refactoring on the RPCP part of this project.
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;
import me.neurodock.ollama.OllamaFunctionArgument;
import me.neurodock.ollama.OllamaFunctionTool;
import me.neurodock.ollama.OllamaPerameter;
import me.neurodock.ollama.OllamaToolResponse;
//import me.zacharias.chat.plugin.annotation.OllamaTool;
//import me.zacharias.chat.plugin.annotation.injectons.InjectPlugin;
import org.jetbrains.annotations.NotNull;
//@OllamaTool
@OllamaTool
public class Tool extends OllamaFunctionTool {
//@InjectPlugin(classType = Test.class)
@InjectPlugin(classType = Test.class)
Test core;
@Override
public @NotNull String name() {
public String name() {
return "";
}
@@ -26,12 +23,12 @@ public class Tool extends OllamaFunctionTool {
}
@Override
public @NotNull OllamaPerameter parameters() {
public OllamaPerameter parameters() {
return null;
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
return null;
}
}
+2 -9
View File
@@ -2,6 +2,7 @@ plugins {
id 'java'
}
group = 'me.zacharias'
version = '1.0-SNAPSHOT'
dependencies {
@@ -10,9 +11,7 @@ dependencies {
implementation project(":GeniusAPI")
implementation project(":API")
implementation project(":WikipediaTool")
implementation("com.github.docker-java:docker-java-core:3.7.1")
implementation("com.github.docker-java:docker-java-transport-httpclient5:3.7.1")
implementation project(":MovieSugest")
}
test {
@@ -23,10 +22,4 @@ jar{
manifest {
attributes 'Main-Class': 'me.zacharias.chat.display.Main'
}
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(javaVersion)) // Set Java version
}
}
@@ -1,557 +0,0 @@
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.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(Core.CACHE_DIRECTORY, "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();
}
}
@@ -1,38 +0,0 @@
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,18 +1,23 @@
package me.neurodock.display;
package me.zacharias.chat.display;
import me.neurodock.core.Core;
import me.neurodock.core.Pair;
import me.neurodock.core.PrintMessageHandler;
import me.neurodock.core.files.FileHandlerLocation;
import me.neurodock.core.memory.CoreMemory;
import me.neurodock.ollama.*;
import me.neurodock.ollama.utils.SystemMessage;
//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 org.json.JSONObject;
import java.io.*;
import java.util.*;
import static me.neurodock.core.Core.writeLog;
import static me.zacharias.chat.core.Core.writeLog;
/**
* The main class of the Display.<br>
@@ -43,24 +48,27 @@ public class Display {
public Display()
{
core.setOllamaObject/*NoMemory*/(OllamaObject.builder()
.setModel("llama3.1:8b")
core.setOllamaObjectNoMemory(OllamaObject.builder()
.setModel("llama3.2")
//.setModel("gemma3:12b")
// .setModel("qwen3:8b")
.keep_alive(10)
//.stream(false)
.addFileTools(FileHandlerLocation.DATA_FILES)
//.addFileTools(FileHandlerLocation.DATA_FILES)
.addMessages(new SystemMessage("You need to emulate feelings and emotions."))
.build());
core.enablePlugins(Core.PLUGIN_DIRECTORY);
core.addTool(new TimeTool(), Core.Source.CTP);
core.addTool(new TimeTool(), Core.Source.INTERNAL);
// TODO: Well Docker failes when luanched.... Fuck
core.addTool(new PythonRunner(core), Core.Source.CTP);
//core.addTools(new MALAPITool().getOllamaTools());
//core.addTools(new GeniusTools().getGeniusTools());
//core.addTools(new WikipediaTool().getWikipediaToolsInstance());
// 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());
//APIApplication.start();
// APIApplication.start();
//core.getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.SYSTEM, "Have a nice tone and use formal wording"));
@@ -103,7 +111,6 @@ 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.
""");
@@ -148,14 +155,6 @@ 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().toString());
writeLog("Function: " + funtion.getKey().toString());
}
break;
case "working":
System.out.println("Working directories:\n" +
" Data: " + Core.DATA_DIR.getAbsolutePath() + "\n" +
@@ -169,7 +168,7 @@ public class Display {
writeLog("User: " + message);
core.getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.USER, message.toString()));
//System.out.println(ollamaObject.toString());
core.qurryOllama().thenAccept(core::handleResponse);
core.handleResponce(core.qurryOllama());
}
}
} catch (Exception e) {
@@ -0,0 +1,447 @@
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;
}
}
}
@@ -0,0 +1,36 @@
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";
}
}
@@ -1,165 +0,0 @@
/*
* ====================================================================
* 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;
}
}
@@ -1,26 +0,0 @@
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
}
@@ -1,31 +0,0 @@
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;
}
@@ -1,26 +0,0 @@
import socket
import json
import builtins
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"))
response = s.recv(4096)
return response.decode("utf-8")
def print_output(text):
data = {"function": "print_output", "container_id": CONTAINER_ID, "text": text}
connect(json.dumps(data))
def _neurodock_print(*args, **kwargs):
text = " ".join(str(a) for a in args)
print_output(text)
builtins.print = _neurodock_print
## Following is the generated
-25
View File
@@ -1,25 +0,0 @@
# Genius API
## Important notes
### Legal Notice
This tool is provided for educational and experimental purposes only.
Please be aware that the Genius API Terms of Service **prohibit** web scraping of their website. This project contains code that performs scraping to retrieve song lyrics directly from Genius.com, which may violate those terms.
Since Genius does not provide official API endpoints for lyrics due to copyright restrictions, this tool includes scraping functionality as a workaround. However, scraping may result in legal or technical consequences such as IP bans, rate limiting, or other restrictions imposed by Genius.
**Use this tool responsibly and at your own risk.** The author does not encourage or endorse violating any third-party terms of service or applicable laws and disclaims any liability arising from misuse.
Whenever possible, prefer using official API endpoints and respect copyright laws.
## API key required
This tool requires a Genius API key to work since it fetches song metadata from the Genius API.
The API key shuld be stored in `${DATA_DIR}/geniusapi.json` using the following format:
```json
{
"client_id": "<your_client_id>",
"client_secret": "<your_client_secret>"
}
```
+2 -7
View File
@@ -2,6 +2,7 @@ plugins {
id 'java'
}
group = 'me.zacharias'
version = '1.0-SNAPSHOT'
repositories {
@@ -13,17 +14,11 @@ dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter'
implementation "org.jsoup:jsoup:1.20.1"
implementation 'io.github.classgraph:classgraph:4.8.184'
implementation 'io.github.classgraph:classgraph:4.8.158'
implementation project(":Core")
}
test {
useJUnitPlatform()
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(javaVersion)) // Set Java version
}
}
@@ -1,4 +1,4 @@
package me.neurodock.genius;
package me.zacharias.neuro.dock.genius;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -1,6 +1,6 @@
package me.neurodock.genius;
package me.zacharias.neuro.dock.genius;
import me.neurodock.ollama.OllamaFunctionTool;
import me.zacharias.chat.ollama.OllamaFunctionTool;
public abstract class GeniusEndpointTool extends OllamaFunctionTool {
protected GeniusTools geniusToolsInstance;
@@ -1,12 +1,13 @@
package me.neurodock.genius;
package me.zacharias.neuro.dock.genius;
import com.google.common.reflect.ClassPath;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ClassInfo;
import io.github.classgraph.ClassInfoList;
import io.github.classgraph.ScanResult;
import me.neurodock.core.Core;
import me.neurodock.ollama.OllamaFunctionTool;
import me.neurodock.ollama.OllamaFunctionTools;
import me.zacharias.chat.core.Core;
import me.zacharias.chat.ollama.OllamaFunctionTool;
import me.zacharias.chat.ollama.OllamaFunctionTools;
import org.json.JSONObject;
import java.io.BufferedReader;
@@ -73,7 +74,7 @@ public class GeniusTools {
if (OllamaFunctionTool.class.isAssignableFrom(clazz)) {
//System.out.println("Found endpoint: " + clazz.getName() + " With constructor: " + clazz.getDeclaredConstructors().f.getName() + " and arguments: " + Arrays.toString(clazz.getDeclaredConstructor().getParameterTypes()));
GeniusEndpointTool tool = (GeniusEndpointTool) clazz.getDeclaredConstructor(GeniusTools.class).newInstance(this);
builder.addTool(tool, Core.Source.CTP);
builder.addTool(tool, Core.Source.INTERNAL);
}
}
} catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) {
@@ -0,0 +1,27 @@
package me.zacharias.neuro.dock.genius;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;
public class ParameterStringBuilder {
public static String getParamsString(Map<String, String> params)
throws UnsupportedEncodingException {
if(params == null)
return "";
StringBuilder result = new StringBuilder();
for (Map.Entry<String, String> entry : params.entrySet()) {
result.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8));
result.append("=");
result.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8));
result.append("&");
}
String resultString = result.toString();
return !resultString.isEmpty()
? resultString.substring(0, resultString.length() - 1)
: resultString;
}
}
@@ -1,19 +1,18 @@
package me.neurodock.genius.endpoints;
package me.zacharias.neuro.dock.genius.endpoints;
import me.neurodock.ollama.OllamaFunctionArgument;
import me.neurodock.ollama.OllamaPerameter;
import me.neurodock.ollama.OllamaToolResponse;
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
import me.neurodock.genius.GeniusEndpoint;
import me.neurodock.genius.GeniusEndpointTool;
import me.neurodock.genius.GeniusTools;
import org.jetbrains.annotations.NotNull;
import me.zacharias.chat.ollama.OllamaFunctionArgument;
import me.zacharias.chat.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaToolRespnce;
import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException;
import me.zacharias.neuro.dock.genius.GeniusEndpoint;
import me.zacharias.neuro.dock.genius.GeniusEndpointTool;
import me.zacharias.neuro.dock.genius.GeniusTools;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.Map;
import static me.neurodock.ollama.OllamaPerameter.OllamaPerameterBuilder.Type.STRING;
import static me.zacharias.chat.ollama.OllamaPerameter.OllamaPerameterBuilder.Type.STRING;
@GeniusEndpoint
public class FindSong extends GeniusEndpointTool {
@@ -22,7 +21,7 @@ public class FindSong extends GeniusEndpointTool {
}
@Override
public @NotNull String name() {
public String name() {
return "findsong";
}
@@ -32,14 +31,14 @@ public class FindSong extends GeniusEndpointTool {
}
@Override
public @NotNull OllamaPerameter parameters() {
public OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("title", STRING, "The title, artitst, and song_id of the song to find.", true)
.build();
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
String title = (String) args[0].value();
if (title == null || title.isEmpty()) {
throw new OllamaToolErrorException(this.name(), "Title cannot be null or empty.");
@@ -64,6 +63,6 @@ public class FindSong extends GeniusEndpointTool {
throw new OllamaToolErrorException(this.name(), "No songs found for the given title.");
}
return new OllamaToolResponse(this.name(), responseData.toString());
return new OllamaToolRespnce(this.name(), responseData.toString());
}
}
@@ -1,13 +1,12 @@
package me.neurodock.genius.endpoints;
package me.zacharias.neuro.dock.genius.endpoints;
import me.neurodock.ollama.OllamaFunctionArgument;
import me.neurodock.ollama.OllamaPerameter;
import me.neurodock.ollama.OllamaToolResponse;
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
import me.neurodock.genius.GeniusEndpoint;
import me.neurodock.genius.GeniusEndpointTool;
import me.neurodock.genius.GeniusTools;
import org.jetbrains.annotations.NotNull;
import me.zacharias.chat.ollama.OllamaFunctionArgument;
import me.zacharias.chat.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaToolRespnce;
import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException;
import me.zacharias.neuro.dock.genius.GeniusEndpoint;
import me.zacharias.neuro.dock.genius.GeniusEndpointTool;
import me.zacharias.neuro.dock.genius.GeniusTools;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
@@ -16,22 +15,10 @@ import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.select.Elements;
import static me.neurodock.core.Core.writeLog;
import java.util.stream.Collectors;
import static me.zacharias.chat.core.Core.writeLog;
/**
* This class is an Ollama tool that fetches the lyrics of a song by its ID from the Genius API.
* It extends the GeniusEndpointTool to utilize the GeniusTools instance for API calls and caching.
* <p>
* <h1>Legal Notice</h1>
* <p>
* I (the developer) do NOT own the rights to the lyrics fetched by this tool, nor do I endorse scraping lyrics from Genius.com,
* which may violate their Terms of Service.
* </p>
* <p>
* I disclaim any responsibility or liability for how this tool is used.
* Use this tool at your own risk, and please respect all applicable copyright laws and third-party terms.
* </p>
*/
@GeniusEndpoint
public class GetLyrics extends GeniusEndpointTool {
public GetLyrics(GeniusTools geniusTools) {
@@ -39,7 +26,7 @@ public class GetLyrics extends GeniusEndpointTool {
}
@Override
public @NotNull String name() {
public String name() {
return "get_lyrics";
}
@@ -49,36 +36,28 @@ public class GetLyrics extends GeniusEndpointTool {
}
@Override
public @NotNull OllamaPerameter parameters() {
public OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("song_id", OllamaPerameter.OllamaPerameterBuilder.Type.INT, "The ID of the song to get lyrics for.", true)
.build();
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
if(!( args[0].value() instanceof Integer)) {
throw new OllamaToolErrorException(this.name(), "The song_id must be an integer.");
}
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
String lyricsStr = geniusToolsInstance.hasCache((int) args[0].value());
if(lyricsStr != null)
{
return new OllamaToolResponse(this.name(), lyricsStr.trim());
// If we have a cached response, return it
}
JSONObject obj = geniusToolsInstance.getGeniusEndpoint("/songs/" + args[0].value(), null);
String lyrics_path = obj.getJSONObject("response").getJSONObject("song").getString("url");
try {
// WARNING: This request scrapes the lyrics from the Genius website.
// Which is against their Terms of Service. Use responsibly
Document doc = Jsoup.connect(lyrics_path)
.userAgent("insomnia/11.1.0") // TODO: replace with somthing else then insomnia! since we in no way support what ever Insomnia's User-Agent says we do
.header("host", "genius.com")
.header("accept", "*/*")
.userAgent("Mozilla/5.0")
.get();
writeLog("Fetching lyrics from: " + lyrics_path);
@@ -116,7 +95,7 @@ public class GetLyrics extends GeniusEndpointTool {
geniusToolsInstance.cacheLyrics((int) args[0].value(), lyrics.toString());
return new OllamaToolResponse(this.name(), lyrics.toString().trim());
return new OllamaToolRespnce(this.name(), lyrics.toString().trim());
}catch (Exception ex)
{
ex.printStackTrace();
+3
View File
@@ -6,8 +6,11 @@ import org.jsoup.nodes.TextNode;
import org.jsoup.select.Elements;
import org.junit.jupiter.api.Test;
import javax.swing.text.html.HTML;
import java.util.regex.MatchResult;
import java.util.regex.Pattern;
import static me.zacharias.chat.core.Core.writeLog;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class LyricsFetch {
+2 -7
View File
@@ -2,6 +2,7 @@ plugins {
id 'java'
}
group = 'me.zacharias'
version = '1.0-SNAPSHOT'
repositories {
@@ -14,15 +15,9 @@ dependencies {
implementation project(":Core")
implementation 'io.github.classgraph:classgraph:4.8.184'
implementation 'io.github.classgraph:classgraph:4.8.158'
}
test {
useJUnitPlatform()
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(javaVersion)) // Set Java version
}
}
@@ -1,15 +1,15 @@
package me.neurodock.mal.api;
package me.zacharias.chat.mal.api;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ClassInfo;
import io.github.classgraph.ClassInfoList;
import io.github.classgraph.ScanResult;
import me.neurodock.core.Core;
import me.neurodock.ollama.OllamaFunctionTool;
import me.neurodock.ollama.OllamaFunctionTools;
import me.zacharias.chat.core.Core;
import me.zacharias.chat.ollama.*;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
@@ -18,6 +18,7 @@ import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
public class MALAPITool {
@@ -46,7 +47,7 @@ public class MALAPITool {
if (OllamaFunctionTool.class.isAssignableFrom(clazz)) {
//System.out.println("Found endpoint: " + clazz.getName() + " With constructor: " + clazz.getDeclaredConstructors().f.getName() + " and arguments: " + Arrays.toString(clazz.getDeclaredConstructor().getParameterTypes()));
MALEndpointTool tool = (MALEndpointTool) clazz.getDeclaredConstructor(MALAPITool.class).newInstance(MALAPITool.this);
builder.addTool(tool, Core.Source.CTP);
builder.addTool(tool, Core.Source.INTERNAL);
}
}
} catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) {
@@ -1,4 +1,4 @@
package me.neurodock.mal.api;
package me.zacharias.chat.mal.api;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -1,6 +1,6 @@
package me.neurodock.mal.api;
package me.zacharias.chat.mal.api;
import me.neurodock.ollama.OllamaFunctionTool;
import me.zacharias.chat.ollama.OllamaFunctionTool;
public abstract class MALEndpointTool extends OllamaFunctionTool {
protected MALAPITool MALAPIToolInstance;
@@ -1,4 +1,4 @@
package me.neurodock.genius;
package me.zacharias.chat.mal.api;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
@@ -1,16 +1,16 @@
package me.neurodock.mal.api.endpoints;
package me.zacharias.chat.mal.api.endpoints;
import me.neurodock.mal.api.MALAPITool;
import me.neurodock.mal.api.MALEndpoint;
import me.neurodock.mal.api.MALEndpointTool;
import me.neurodock.ollama.OllamaFunctionArgument;
import me.neurodock.ollama.OllamaPerameter;
import me.neurodock.ollama.OllamaToolResponse;
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
import org.jetbrains.annotations.NotNull;
import me.zacharias.chat.mal.api.MALAPITool;
import me.zacharias.chat.mal.api.MALEndpoint;
import me.zacharias.chat.mal.api.MALEndpointTool;
import me.zacharias.chat.ollama.OllamaFunctionArgument;
import me.zacharias.chat.ollama.OllamaPerameter;
import me.zacharias.chat.ollama.OllamaToolRespnce;
import me.zacharias.chat.ollama.exceptions.OllamaToolErrorException;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
@MALEndpoint
@@ -20,7 +20,7 @@ public class GetAnimeDetails extends MALEndpointTool {
}
@Override
public @NotNull String name() {
public String name() {
return "get_anime_details";
}
@@ -30,7 +30,7 @@ public class GetAnimeDetails extends MALEndpointTool {
}
@Override
public @NotNull OllamaPerameter parameters() {
public OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("id", OllamaPerameter.OllamaPerameterBuilder.Type.INT, "The id of the anime", true)
.addProperty("fields", OllamaPerameter.OllamaPerameterBuilder.Type.ARRAY, "The fields to return, defaults to [\"id\", \"title\", \"synopsis\", \"genres\"]")
@@ -38,7 +38,7 @@ public class GetAnimeDetails extends MALEndpointTool {
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
public OllamaToolRespnce function(OllamaFunctionArgument... args) {
int id = -1;
String[] fields = null;
@@ -81,6 +81,6 @@ public class GetAnimeDetails extends MALEndpointTool {
JSONObject obj = MALAPIToolInstance.APIRequest("/anime/"+id, map);
return new OllamaToolResponse(this.name(), obj.toString());
return new OllamaToolRespnce(this.name(), obj.toString());
}
}

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