6 Commits
Author SHA1 Message Date
Zacharias 209bde75e1 Added redirect to codeberg for issue tracking to README.md 2026-07-30 22:06:51 +02:00
Zacharias 5ae5920478 Moved versions of the "public facing" modules to the gradle.properties
Core:
- Fixed my promis of making Core work with multiple OllamaObjects
2026-07-30 21:24:06 +02:00
Zacharias b8280e6ec5 Refactored to Core 1.10.0
This includes chages to make less things static in the Core, instead using the Options Singelton instead
2026-07-30 20:49:06 +02:00
Zacharias 939fdb2211 Added support for Imagaes, Ollama model thinking, and streaming of the responce 2026-07-25 21:25:41 +02:00
Zacharias 663ab68172 !! PARTIAL COMMIT !!
This is a partial commit bc i felt like it...

This begain some implementations to support streaming from Ollama, and the ability to cancel a request.
2026-07-20 22:13:33 +02:00
Zacharias 330d7df389 Fixed some errors with the System prompt
Chaged java version to make this more compadible
2026-07-20 18:03:31 +02:00
22 changed files with 582 additions and 338 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
<component name="FrameworkDetectionExcludesConfiguration"> <component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" /> <file type="web" url="file://$PROJECT_DIR$" />
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_26" default="true" project-jdk-name="26" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="graalvm-25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>
+3 -2
View File
@@ -5,7 +5,7 @@ plugins {
id 'io.spring.dependency-management' version '1.1.4' id 'io.spring.dependency-management' version '1.1.4'
} }
version = '1.0-SNAPSHOT' version = APIVersion
dependencies { dependencies {
implementation project(":Core") implementation project(":Core")
@@ -14,9 +14,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:4.1.0-M4' 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.springframework.boot:spring-boot-starter-webflux:4.1.0-M4'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.0-M1' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.0-M1'
//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:4.1.0-M4'
//implementation 'org.springframework.boot:spring-boot-starter-actuator'
//runtimeOnly('org.springframework.boot:spring-boot-starter-web') //runtimeOnly('org.springframework.boot:spring-boot-starter-web')
} }
@@ -3,7 +3,6 @@ package me.neurodock.api;
import me.neurodock.api.payload.request.NewQurryResponceHook; import me.neurodock.api.payload.request.NewQurryResponceHook;
import me.neurodock.api.payload.request.NewToolRequest; import me.neurodock.api.payload.request.NewToolRequest;
import me.neurodock.core.Core; import me.neurodock.core.Core;
import me.neurodock.core.GlobalObjects;
import me.neurodock.core.PrintMessageHandler; import me.neurodock.core.PrintMessageHandler;
import me.neurodock.ollama.OllamaObject; import me.neurodock.ollama.OllamaObject;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
@@ -41,8 +40,9 @@ public class APIApplication {
instance = this; instance = this;
if(GlobalObjects.getObject("core") instanceof Core coreInstance) { if(false) {
this.core = coreInstance; // TODO: This needs to be properly refactored.
//this.core = coreInstance;
} else { } else {
this.core = new Core(new PrintMessageHandler() { this.core = new Core(new PrintMessageHandler() {
@Override @Override
+3 -1
View File
@@ -2,11 +2,13 @@ plugins {
id 'java-library' id 'java-library'
} }
version = '1.9.0' version = coreVersion
dependencies { dependencies {
implementation project(":Plugin-API") implementation project(":Plugin-API")
api "org.json:json:20250107" api "org.json:json:20250107"
implementation 'org.graalvm.polyglot:polyglot:25.1.3'
implementation 'org.graalvm.polyglot:js:25.1.3'
} }
java { java {
+139 -232
View File
@@ -19,10 +19,9 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.*;
import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.ScheduledExecutorService; import java.util.function.Consumer;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarFile; import java.util.jar.JarFile;
@@ -68,17 +67,20 @@ public class Core {
* The URL of the Ollama API. * The URL of the Ollama API.
*/ */
private URL url; private URL url;
/**
* Used to cancel/stop a responce, only usefull if
*/
private AtomicBoolean cancelled = new AtomicBoolean(false);
/**
* The current connection to Ollama
*/
private HttpURLConnection activeConnection;
/** /**
* The PrintMessageHandler to use. * The PrintMessageHandler to use.
*/ */
private final PrintAdvanceMessageHandler printMessageHandler; 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, * Creates a new instance of Core with the provided PrintMessageHandler,
* defaulting the Ollama backend to {@code localhost}. * defaulting the Ollama backend to {@code localhost}.
@@ -109,13 +111,6 @@ public class Core {
initShutdownHook(); 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) public static void setLogDirectory(String logDirectory)
{ {
logDir = new File(logDirectory); logDir = new File(logDirectory);
@@ -126,55 +121,6 @@ public class Core {
logFile = new File(logDirectory, "latest.log"); 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. * Creates the base directories required by the application.
* <p> * <p>
@@ -183,7 +129,8 @@ public class Core {
*/ */
private void initDirectories() { private void initDirectories() {
ensureDir(logDir.getAbsolutePath()); ensureDir(logDir.getAbsolutePath());
ensureDir(DATA_DIR.getAbsolutePath() + "/messages"); ensureDir(Options.getInstance().getDataDir() + "/messages");
Options.getInstance().initiateDirectories();
} }
/** /**
@@ -301,7 +248,7 @@ public class Core {
/** /**
* Persists the current session's messages from the {@link OllamaObject} to two locations: * 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} * a timestamped archive file under {@code ./messages/}, and a rolling {@code messages.json}
* under {@link #DATA_DIR} for resuming the session later. * under {@link Options#getDataDir()} for resuming the session later.
* *
* @see #buildMessagesArray() * @see #buildMessagesArray()
* @see #writeMessagesTo(File, JSONArray) * @see #writeMessagesTo(File, JSONArray)
@@ -310,8 +257,8 @@ public class Core {
JSONArray messages = buildMessagesArray(); JSONArray messages = buildMessagesArray();
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd_HH-mm-ss")); String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd_HH-mm-ss"));
writeMessagesTo(new File(DATA_DIR.getAbsolutePath()+"/messages/" + timestamp + ".json"), messages); writeMessagesTo(new File(Options.getInstance().getDataDir().getAbsolutePath()+"/messages/" + timestamp + ".json"), messages);
writeMessagesTo(new File(DATA_DIR, "messages.json"), messages); writeMessagesTo(new File(Options.getInstance().getDataDir(), "messages.json"), messages);
} }
/** /**
@@ -346,119 +293,13 @@ public class Core {
} }
} }
/**
* 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(message.toJSON());
}
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. * A check to exit early if Ollama isn't reachable.
*/ */
private void confirmOllama() private boolean confirmOllama()
{ {
try { try {
URL url = new URL("http://" + ollamaIP + ":" + ollamaPort + "/api/version"); URL url = URI.create("http://" + ollamaIP + ":" + ollamaPort + "/api/version").toURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET"); connection.setRequestMethod("GET");
connection.setRequestProperty("Content-Type", "application/json"); connection.setRequestProperty("Content-Type", "application/json");
@@ -469,14 +310,14 @@ public class Core {
if (responseCode != HttpURLConnection.HTTP_OK) { if (responseCode != HttpURLConnection.HTTP_OK) {
new RuntimeException("Ollama is un-responsive on url: " + url.toString()).printStackTrace(); new RuntimeException("Ollama is un-responsive on url: " + url.toString()).printStackTrace();
System.exit(1); return false;
} }
}catch (IOException ex) }catch (IOException ex)
{ {
ex.printStackTrace(); ex.printStackTrace();
System.out.println("Can not reach Ollama!"); return false;
System.exit(1);
} }
return true;
} }
/** /**
@@ -485,25 +326,20 @@ public class Core {
* @param ollamaObject The OllamaObject to use * @param ollamaObject The OllamaObject to use
*/ */
public void setOllamaObject(OllamaObject ollamaObject) { public void setOllamaObject(OllamaObject ollamaObject) {
if(this.ollamaObject == null) { this.ollamaObject = ollamaObject;
this.ollamaObject = ollamaObject;
for(Pair<OllamaTool, String> tool : ollamaObject.getTools()) { for(Pair<OllamaTool, String> tool : ollamaObject.getTools()) {
if(tool.getKey() instanceof OllamaFunctionTool functionTool) if(tool.getKey() instanceof OllamaFunctionTool functionTool)
{ {
funtionTools.add(new Pair<>(functionTool, tool.getValue())); funtionTools.add(new Pair<>(functionTool, tool.getValue()));
}
} }
}
addTool(new AddMemoryFunction(), Source.CORE); addTool(new AddMemoryFunction(), Source.CORE);
addTool(new RemoveMemoryFunction(), Source.CORE); addTool(new RemoveMemoryFunction(), Source.CORE);
addTool(new GetMemoryFunction(), Source.CORE); addTool(new GetMemoryFunction(), Source.CORE);
addTool(new GetMemoriesFunction(), Source.CORE); addTool(new GetMemoriesFunction(), Source.CORE);
addTool(new GetMemoryIdentitiesFunction(), Source.CORE); addTool(new GetMemoryIdentitiesFunction(), Source.CORE);
}
else {
throw new IllegalArgumentException("Ollama object is already set");
}
} }
/** /**
@@ -512,18 +348,13 @@ public class Core {
* @param ollamaObject The OllamaObject to use * @param ollamaObject The OllamaObject to use
*/ */
public void setOllamaObjectNoMemory(OllamaObject ollamaObject) { public void setOllamaObjectNoMemory(OllamaObject ollamaObject) {
if(this.ollamaObject == null) { this.ollamaObject = ollamaObject;
this.ollamaObject = ollamaObject; for(Pair<OllamaTool, String> tool : ollamaObject.getTools()) {
for(Pair<OllamaTool, String> tool : ollamaObject.getTools()) { if(tool.getKey() instanceof OllamaFunctionTool functionTool)
if(tool.getKey() instanceof OllamaFunctionTool functionTool) {
{ funtionTools.add(new Pair<>(functionTool, tool.getValue()));
funtionTools.add(new Pair<>(functionTool, tool.getValue()));
}
} }
} }
else {
throw new IllegalArgumentException("Ollama object is already set");
}
} }
/** /**
@@ -593,23 +424,78 @@ public class Core {
logWriter.flush(); logWriter.flush();
}catch (IOException e) {} }catch (IOException e) {}
} }
public void cancel()
{
cancelled.set(true);
HttpURLConnection conn = activeConnection;
if (conn != null) {
conn.disconnect();
}
}
/** /**
* Sends the OllamaObject to Ollama * Queries Ollama and resolves once the full response is available, discarding any
* @return The response from Ollama * intermediate streaming chunks along the way.
* <p>
* This is a convenience overload of {@link #qurryOllama(Consumer)} with a no-op chunk
* listener. Whether the underlying request is actually sent as a streaming or
* non-streaming HTTP request is determined entirely by this query's {@link OllamaObject}'s
* {@code stream} setting — this method does not itself impose a mode. If the
* {@link OllamaObject} is configured to stream, each intermediate chunk is still received
* and parsed internally, just not exposed to the caller; use
* {@link #qurryOllama(Consumer)} if intermediate chunks are needed.
*
* @return a future that resolves to the final response object once generation completes
*/ */
public CompletableFuture<JSONObject> qurryOllama() public CompletableFuture<JSONObject> qurryOllama()
{ {
return qurryOllama(_ -> {});
}
/**
* Queries Ollama, invoking {@code onChunk} for each JSON object received from the server,
* and resolves once the final response is available.
* <p>
* Whether this is a streaming or non-streaming exchange is determined by this query's
* {@link OllamaObject}'s {@code stream} setting, not by which overload was called:
* <ul>
* <li>If {@code stream} is {@code true}, Ollama emits one JSON object per line as
* generation progresses; {@code onChunk} fires once per line, in order, as each
* arrives.</li>
* <li>If {@code stream} is {@code false}, Ollama emits the entire response as a single
* object once generation is complete; {@code onChunk} fires exactly once with that
* full object.</li>
* </ul>
* In both cases, the returned future resolves once, to the final ({@code done: true})
* response object — {@code onChunk} is for observing progress, the returned future is for
* "the response is complete."
* <p>
* If {@link #cancel()} is called while this query is in flight, the underlying connection
* is closed and the returned future completes exceptionally with a
* {@link java.util.concurrent.CancellationException}.
*
* @param onChunk callback invoked for each response object received from Ollama, in
* arrival order; never invoked with {@code null}
* @return a future that resolves to the final response object once generation completes
*/
public CompletableFuture<JSONObject> qurryOllama(Consumer<JSONObject> onChunk) {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
HttpURLConnection connection = null;
try { try {
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection = (HttpURLConnection) url.openConnection();
activeConnection = connection;
if (cancelled.get()) {
throw new CancellationException("Query cancelled before request was sent");
}
connection.setRequestMethod("POST"); connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json"); connection.setRequestProperty("Content-Type", "application/json");
connection.setDoOutput(true); connection.setDoOutput(true);
connection.setConnectTimeout(80 * 1000); connection.setConnectTimeout(80 * 1000);
String ollamaObjectString = ollamaObject.toJSON().toString(); String ollamaObjectString = ollamaObject.toJSON().toString();
ollamaObjectString = ollamaObjectString.replace("\n", "\\n"); ollamaObjectString = ollamaObjectString.replace("\n", "\\n");
try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) { try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
@@ -618,43 +504,64 @@ public class Core {
} }
int responseCode = connection.getResponseCode(); int responseCode = connection.getResponseCode();
boolean isStreaming = ollamaObject.isStream(); // whatever the real accessor is
JSONObject last = null;
StringBuilder rawErrorOrDump = new StringBuilder();
StringBuilder messageContent = new StringBuilder();
// HTTP_OK or 200 response code generally means that the server ran successfully without any errors try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
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; String line;
while ((line = reader.readLine()) != null) { while ((line = reader.readLine()) != null) {
response.append(line); // Adds every line to response till the end of file. if (cancelled.get()) throw new CancellationException("Query cancelled mid-response");
if (line.isBlank()) continue;
rawErrorOrDump.append(line);
if (isStreaming) {
// Ollama emits one JSON object per line when streaming — parse and
// dispatch each one as it arrives.
JSONObject chunkObj = new JSONObject(line);
last = chunkObj;
if(chunkObj.has("message")) {
messageContent.append(chunkObj.getJSONObject("message").optString("content", ""));
}
onChunk.accept(chunkObj);
}
else
{
last = new JSONObject(line);
}
} }
} catch (Exception ex) { } catch (IOException ex) {
// If the server returns an error, we read the error stream instead if (cancelled.get()) throw new CancellationException("Query cancelled mid-response");
InputStream errorStream = connection.getErrorStream(); InputStream errorStream = connection.getErrorStream();
if (errorStream != null) { if (errorStream != null) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream))) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream))) {
String line; String line;
while ((line = reader.readLine()) != null) { while ((line = reader.readLine()) != null) rawErrorOrDump.append(line);
response.append(line);
}
} }
} }
} }
if (isStreaming && last != null && last.has("message")) {
JSONObject finalObj = new JSONObject(last.toString()); // deep-ish copy via re-parse
finalObj.getJSONObject("message").put("content", messageContent.toString());
last = finalObj;
}
if (responseCode == HttpURLConnection.HTTP_OK) { if (responseCode == HttpURLConnection.HTTP_OK) {
return last;
connection.disconnect();
return new JSONObject(response.toString());
} else { } else {
connection.disconnect(); printMessageHandler.printErrorMessage(new OllamaMessage(OllamaMessageRole.SYSTEM,
printMessageHandler.printErrorMessage(new OllamaMessage(OllamaMessageRole.SYSTEM,"Error: HTTP Response code - " + responseCode + "\n" + response.toString())); "Error: HTTP Response code - " + responseCode + "\n" + rawErrorOrDump));
throw new RuntimeException("HTTP Response code - " + responseCode); throw new RuntimeException("HTTP Response code - " + responseCode);
} }
} catch (IOException e) { } catch (IOException e) {
if (cancelled.get()) throw new CancellationException("Query cancelled");
throw new RuntimeException(e); throw new RuntimeException(e);
} finally {
if (connection != null) connection.disconnect();
activeConnection = null;
} }
}); });
} }
@@ -892,7 +799,7 @@ public class Core {
@Override @Override
public File getDataDictionary() { public File getDataDictionary() {
return DATA_DIR; return Options.getInstance().getDataDir();
} }
@Override @Override
@@ -902,7 +809,7 @@ public class Core {
@Override @Override
public File getCacheDirectory() { public File getCacheDirectory() {
return CACHE_DIRECTORY; return Options.getInstance().getCacheDirectory();
} }
public void addPlugin(LoadedPlugin plugin) public void addPlugin(LoadedPlugin plugin)
@@ -1,29 +0,0 @@
package me.neurodock.core;
import java.util.HashMap;
import java.util.Map;
public class GlobalObjects {
private static final Map<String, Object> objects = new HashMap<>();
public static void addObject(String name, Object object) {
if (name == null || object == null) {
throw new IllegalArgumentException("Name and object cannot be null");
}
objects.put(name, object);
}
public static Object getObject(String name) {
if (name == null) {
throw new IllegalArgumentException("Name cannot be null");
}
return objects.get(name);
}
public static boolean removeObject(String name) {
if (name == null) {
throw new IllegalArgumentException("Name cannot be null");
}
return objects.remove(name) != null;
}
}
@@ -1,15 +1,12 @@
package me.neurodock.core; package me.neurodock.core;
import com.sun.jdi.connect.spi.TransportService;
import me.neurodock.ollama.*; import me.neurodock.ollama.*;
import org.json.JSONObject; import org.json.JSONObject;
import java.io.*; import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
@@ -137,7 +134,7 @@ public class LLMSystemPrompt {
for (Pair<OllamaTool, String> tool : ollamaObject.getTools()) { for (Pair<OllamaTool, String> tool : ollamaObject.getTools()) {
if(tool.getKey() instanceof OllamaFunctionTool funcTool) { if(tool.getKey() instanceof OllamaFunctionTool funcTool) {
String llmName = funcTool.name() + "_" + (funcTool.getSource() != null ? funcTool.getSource() : ""); String llmName = funcTool.name() + "_" + (funcTool.getSource() != null ? funcTool.getSource() : "");
String hint = capabilities.getUsageHints().get(tool.getClass()); String hint = capabilities.getUsageHints().get(funcTool.getClass());
// fall back to description() if no explicit routing hint was set // fall back to description() if no explicit routing hint was set
String line = hint != null ? hint : funcTool.description(); String line = hint != null ? hint : funcTool.description();
@@ -448,7 +445,7 @@ public class LLMSystemPrompt {
* Loads {@link #behavior} from a text file, resolved in order against: * Loads {@link #behavior} from a text file, resolved in order against:
* <ol> * <ol>
* <li>the classpath</li> * <li>the classpath</li>
* <li>{@link Core#DATA_DIR}</li> * <li>{@link Options#getDataDir()}</li>
* <li>the current runtime/working directory</li> * <li>the current runtime/working directory</li>
* <li>an exact file path match</li> * <li>an exact file path match</li>
* </ol> * </ol>
@@ -553,7 +550,7 @@ public class LLMSystemPrompt {
/** /**
* Resolves and reads a behavior file as UTF-8 text, checking the classpath, * Resolves and reads a behavior file as UTF-8 text, checking the classpath,
* {@link Core#DATA_DIR}, the working directory, and an exact path match, in that order. * {@link Options#getDataDir()}, the working directory, and an exact path match, in that order.
* *
* @param path the file to look for * @param path the file to look for
* @return the full file contents, with each line terminated by {@code \n} * @return the full file contents, with each line terminated by {@code \n}
@@ -566,7 +563,7 @@ public class LLMSystemPrompt {
if (classpathResourceExists("/"+path)) { if (classpathResourceExists("/"+path)) {
in = BehaviorLoader.class.getResourceAsStream("/"+path); in = BehaviorLoader.class.getResourceAsStream("/"+path);
} else { } else {
Path dataDirPath = Path.of(Core.DATA_DIR.getPath(), path); Path dataDirPath = Path.of(Options.getInstance().getDataDir().getPath(), path);
Path relativePath = Path.of(path); Path relativePath = Path.of(path);
if (Files.exists(dataDirPath)) { if (Files.exists(dataDirPath)) {
in = new FileInputStream(dataDirPath.toFile()); in = new FileInputStream(dataDirPath.toFile());
@@ -0,0 +1,123 @@
package me.neurodock.core;
import java.io.File;
import java.nio.file.Path;
public class Options {
/**
* The singleton options object
*/
private static Options instance = new Options();
/**
* Provides the singleton options object for modification to options
* @return the current {@link Options} singleton object
*/
public static Options getInstance()
{
return instance;
}
private Path data;
private File dataDir;
private File pluginDirectory;
private File cacheDirectory;
/**
* Sets a new singleton options object
* @param options the new singleton object
*/
public static void setInstance(Options options)
{
instance = options;
}
public Options setDataDir(Path dataDir, boolean fullDirectory)
{
Path data = Path.of("./data");
String os = System.getProperty("os.name").toLowerCase();
Path cache;
if(System.getenv("AI_CHAT_DEBUG") == null) {
if (fullDirectory) {
data = dataDir;
} else {
if (os.contains("windows")) {
String localappdata = System.getenv("LOCALAPPDATA");
if (localappdata == null) {
localappdata = System.getenv("APPDATA");
}
data = Path.of(localappdata, dataDir.toFile().getPath());
} else if (os.contains("linux")) {
data = Path.of(System.getenv("HOME"), ".local/share", dataDir.toFile().getPath());
} else if (os.contains("mac")) {
data = Path.of(System.getProperty("user.home"), "Library/Application Support", dataDir.toFile().getPath());
}
}
}
if (os.contains("win")) {
String localAppData = System.getenv("LOCALAPPDATA");
cache = Path.of(localAppData != null ? localAppData
: System.getProperty("user.home") + "\\AppData\\Local");
} else if (os.contains("mac")) {
cache = Path.of(System.getProperty("user.home"), "Library", "Caches");
} else {
// Linux / other unix
String xdgCache = System.getenv("XDG_CACHE_HOME");
cache = Path.of(xdgCache != null && !xdgCache.isBlank()
? xdgCache
: System.getProperty("user.home") + "/.cache");
}
this.data = data;
this.cacheDirectory = new File(cache.toFile(), data.getFileName().toFile().toString());
return this;
}
public Options initiateDirectories()
{
this.dataDir = this.data.toFile();
if(!this.dataDir.exists()) {
this.dataDir.mkdirs();
}
String pluginDir = this.data + "/plugins";
pluginDirectory = new File(pluginDir);
if(!pluginDirectory.exists()) {
pluginDirectory.mkdirs();
}
if(!cacheDirectory.exists()) {
cacheDirectory.mkdirs();
}
return this;
}
public Path getData() {
return data;
}
public File getDataDir() {
return dataDir;
}
public File getPluginDirectory() {
return pluginDirectory;
}
public File getCacheDirectory() {
return cacheDirectory;
}
public Path getFileHandlerDataLocation()
{
return Path.of(dataDir.toString(), "files");
}
}
@@ -1,11 +1,11 @@
package me.neurodock.core.files; package me.neurodock.core.files;
import me.neurodock.core.Core; import me.neurodock.core.Core;
import me.neurodock.core.Options;
import me.neurodock.core.Pair; import me.neurodock.core.Pair;
import me.neurodock.core.files.tools.ReadFileTool; import me.neurodock.core.files.tools.ReadFileTool;
import me.neurodock.core.files.tools.WriteFileTool; import me.neurodock.core.files.tools.WriteFileTool;
import me.neurodock.ollama.OllamaTool; import me.neurodock.ollama.OllamaTool;
import org.intellij.lang.annotations.MagicConstant;
import java.io.*; import java.io.*;
import java.nio.file.Path; import java.nio.file.Path;
@@ -27,11 +27,12 @@ public class FileHandler {
/** /**
* Creates a new instance as well as setting the {@link #instance} to this new one * Creates a new instance as well as setting the {@link #instance} to this new one
* A good start is to use {@link Options#getFileHandlerDataLocation()} as it's located along all other files
* @param baseDirectory the directory to be used as base directory * @param baseDirectory the directory to be used as base directory
*/ */
public FileHandler(@MagicConstant(valuesFromClass = FileHandlerLocation.class) String baseDirectory) { public FileHandler(Path baseDirectory) {
try { try {
root = Path.of(baseDirectory).toAbsolutePath().normalize(); root = baseDirectory.toAbsolutePath().normalize();
if (!root.toFile().exists()) { if (!root.toFile().exists()) {
root.toFile().mkdirs(); root.toFile().mkdirs();
} }
@@ -1,7 +0,0 @@
package me.neurodock.core.files;
import me.neurodock.core.Core;
public class FileHandlerLocation {
public static final String DATA_FILES = Core.DATA+"/files";
}
@@ -1,6 +1,6 @@
package me.neurodock.core.memory; package me.neurodock.core.memory;
import me.neurodock.core.Core; import me.neurodock.core.Options;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
@@ -17,7 +17,7 @@ public class CoreMemory {
/** /**
* The singleton instance of CoreMemory. * The singleton instance of CoreMemory.
*/ */
private static final CoreMemory instance = new CoreMemory(Core.DATA + "/CoreMemory.json"); private static final CoreMemory instance = new CoreMemory(Options.getInstance().getDataDir() + "/CoreMemory.json");
/** /**
* Memory type identifier for key-value mapped memory storage. * Memory type identifier for key-value mapped memory storage.
@@ -1,7 +1,17 @@
package me.neurodock.ollama; package me.neurodock.ollama;
import org.jetbrains.annotations.NotNull;
import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.RenderedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
/** /**
* Represents a message sent by a Tool, Assistant(Ollama), or User. * Represents a message sent by a Tool, Assistant(Ollama), or User.
*/ */
@@ -14,6 +24,11 @@ public class OllamaMessage {
* The content of the message. * The content of the message.
*/ */
String content; String content;
/**
* The image content of a message.
* This is a base64 encoded string of either an Image or an Audio file (this is how Ollama dose this, ask them, not me)
*/
ArrayList<String> images;
/** /**
* Creates a new instance of OllamaMessage. * Creates a new instance of OllamaMessage.
@@ -25,6 +40,25 @@ public class OllamaMessage {
this.content = content; this.content = content;
} }
/**
* Creates a new instance of OllamaMessage.
* @param role The role of the message
* @param content The content of the message
* @param images The image of the message
* @throws IOException if an error occurs during writing or when not able to create the required ImageOutputStream.
*/
public OllamaMessage(OllamaMessageRole role, String content, @NotNull Image... images) throws IOException
{
this(role, content);
ArrayList<String> base65Images = new ArrayList<>();
for(Image i : images) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
ImageIO.write((RenderedImage) i, "PNG", stream);
base65Images.add(Base64.getEncoder().encodeToString(stream.toByteArray()));
}
this.images = base65Images;
}
/** /**
* @return The role of who sent this "message", represented as {@link OllamaMessageRole} * @return The role of who sent this "message", represented as {@link OllamaMessageRole}
*/ */
@@ -43,6 +77,10 @@ public class OllamaMessage {
JSONObject json = new JSONObject(); JSONObject json = new JSONObject();
json.put("role", role.getRole()); json.put("role", role.getRole());
json.put("content", content.replace("\n", "\\n")); json.put("content", content.replace("\n", "\\n"));
if(images != null && !images.isEmpty())
{
json.put("images", new JSONArray(images));
}
return json; return json;
} }
} }
@@ -1,10 +1,6 @@
package me.neurodock.ollama; package me.neurodock.ollama;
import me.neurodock.core.Core; import me.neurodock.core.*;
import me.neurodock.core.LLMSystemPrompt;
import me.neurodock.core.LaunchOptions;
import me.neurodock.core.Pair;
import me.neurodock.core.files.FileHandlerLocation;
import me.neurodock.core.files.FileHandler; import me.neurodock.core.files.FileHandler;
import org.intellij.lang.annotations.MagicConstant; import org.intellij.lang.annotations.MagicConstant;
@@ -15,6 +11,7 @@ import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileReader; import java.io.FileReader;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -56,6 +53,14 @@ public class OllamaObject {
* The keep alive of the Ollama Object. * The keep alive of the Ollama Object.
*/ */
String keep_alive; String keep_alive;
/**
* The system prompt object used to re-generate the system prompt.
*/
LLMSystemPrompt systemPrompt;
/**
* The level of thining the model shuld do
*/
Thinking thinking;
/** /**
* Creates a new instance of OllamaObject. * Creates a new instance of OllamaObject.
@@ -66,10 +71,14 @@ public class OllamaObject {
* @param options The options of the Ollama Object. see {@link OllamaObject#options} * @param options The options of the Ollama Object. see {@link OllamaObject#options}
* @param stream If the Ollama Object is streamed. see {@link OllamaObject#stream} * @param 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} * @param keep_alive The keep alive of the Ollama Object. see {@link OllamaObject#keep_alive}
* @param prompt The system prompt object of the Ollama Object. See {@link OllamaObject#systemPrompt}
*/ */
private OllamaObject(String model, ArrayList<OllamaMessage> messages, ArrayList<Pair<OllamaTool, String>> tools, JSONObject format, Map<String, Object> options, boolean stream, String keep_alive, LLMSystemPrompt prompt) { private OllamaObject(String model, ArrayList<OllamaMessage> messages, ArrayList<Pair<OllamaTool, String>> tools, JSONObject format, Map<String, Object> options, boolean stream, String keep_alive, LLMSystemPrompt prompt, Thinking thinking) {
this.model = model; this.model = model;
this.messages = messages; this.messages = messages;
this.systemPrompt = prompt;
this.thinking = thinking;
this.stream = stream;
// Reflecting the reflection injection for ALL tools that was added in the OllamaObjectBuilder, etc, etc comment.. its past midnignt and i'm tired. // 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) for(Pair<OllamaTool, String> tool : tools)
@@ -104,7 +113,7 @@ public class OllamaObject {
LaunchOptions launchOptions = LaunchOptions.getInstance(); LaunchOptions launchOptions = LaunchOptions.getInstance();
if(launchOptions.isLoadOld()) { if(launchOptions.isLoadOld()) {
System.out.println("Loading old data..."); System.out.println("Loading old data...");
File f = new File(Core.DATA_DIR+"/messages.json"); File f = new File(Options.getInstance().getDataDir(), "messages.json");
if(f.exists()) { if(f.exists()) {
try { try {
BufferedReader br = new BufferedReader(new FileReader(f)); BufferedReader br = new BufferedReader(new FileReader(f));
@@ -135,34 +144,39 @@ public class OllamaObject {
} }
} }
if(prompt == null) { if(prompt != null) {
// Do nothing prompt.generateCapabilities(this);
} if (!this.messages.isEmpty()) {
else if(!this.messages.isEmpty()) OllamaMessage systemPrompt = this.messages.getFirst();
{ if (systemPrompt.getRole() != OllamaMessageRole.SYSTEM) {
OllamaMessage systemPrompt = this.messages.getFirst(); System.out.println("FIRST MESSAGE ISN'T A SYSTEM PROMPT!. Pushing all messages forward and injecting the system prompt");
if(systemPrompt.getRole() != OllamaMessageRole.SYSTEM) Core.writeLog("FIRST MESSAGE ISN'T A SYSTEM PROMPT!. Pushing all messages forward and injecting the system prompt");
{ ArrayList<OllamaMessage> newMessages = new ArrayList<>();
System.out.println("FIRST MESSAGE ISN'T A SYSTEM PROMPT!. Pushing all messages forward and injecting the system prompt"); newMessages.add(prompt.generateSystemPrompt());
Core.writeLog("FIRST MESSAGE ISN'T A SYSTEM PROMPT!. Pushing all messages forward and injecting the system prompt"); newMessages.addAll(messages);
ArrayList<OllamaMessage> newMessages = new ArrayList<>(); this.messages = newMessages;
newMessages.add(prompt.generateSystemPrompt()); } else {
newMessages.addAll(messages); System.out.println("Replacing System prompt...");
this.messages = newMessages; Core.writeLog("Replacing System prompt...");
} systemPrompt = prompt.generateSystemPrompt();
else this.messages.set(0, systemPrompt);
{ }
System.out.println("Replacing System prompt..."); } else {
Core.writeLog("Replacing System prompt..."); System.out.println("No old prompts. Inserting system prompt...");
systemPrompt = prompt.generateSystemPrompt(); Core.writeLog("No old prompts. Inserting system prompt...");
this.messages.set(0, systemPrompt); this.messages.add(prompt.generateSystemPrompt());
} }
} }
else }
{
System.out.println("No old prompts. Inserting system prompt..."); /**
Core.writeLog("No old prompts. Inserting system prompt..."); * Re-generates and sets the system promp according to the {@link LLMSystemPrompt} stored as {@link OllamaObject#systemPrompt}
this.messages.add(prompt.generateSystemPrompt()); */
public void reGenerateSystemPrompt()
{
if(systemPrompt != null) {
systemPrompt.generateCapabilities(this);
setSystemPrompt(systemPrompt);
} }
} }
@@ -335,6 +349,7 @@ public class OllamaObject {
json.put("options", options); json.put("options", options);
json.put("stream", stream); json.put("stream", stream);
json.put("keep_alive", keep_alive); json.put("keep_alive", keep_alive);
thinking.putInto(json, "think");
return json; return json;
} }
@@ -383,6 +398,10 @@ public class OllamaObject {
* This represents the System Prompt for the LLM * This represents the System Prompt for the LLM
*/ */
LLMSystemPrompt systemPrompt; LLMSystemPrompt systemPrompt;
/**
* Weather the model should think or not (requires supportive model)
*/
Thinking think;
/** /**
* Creates a new instance of {@link OllamaObjectBuilder}. * Creates a new instance of {@link OllamaObjectBuilder}.
@@ -423,10 +442,8 @@ public class OllamaObject {
/** /**
* Sets if the Ollama Object is streamed. * Sets if the Ollama Object is streamed.
* @param stream If the Ollama Object is streamed * @param stream If the Ollama Object is streamed
* @deprecated This should be false due to being broken in the current version of this system
* @return The {@link OllamaObjectBuilder} * @return The {@link OllamaObjectBuilder}
*/ */
@Deprecated
public OllamaObjectBuilder stream(boolean stream) { public OllamaObjectBuilder stream(boolean stream) {
this.stream = stream; this.stream = stream;
return this; return this;
@@ -564,10 +581,10 @@ public class OllamaObject {
* object construction. Calling this after {@link #build()} is technically possible * object construction. Calling this after {@link #build()} is technically possible
* but not intended. * but not intended.
* </p> * </p>
* @param baseDirectory the base directory for file access, see {@link FileHandlerLocation} * @param baseDirectory the base directory for file access.
* @return The {@link OllamaObjectBuilder} * @return The {@link OllamaObjectBuilder}
*/ */
public OllamaObjectBuilder addFileTools(@MagicConstant(valuesFromClass = FileHandlerLocation.class) String baseDirectory) public OllamaObjectBuilder addFileTools(Path baseDirectory)
{ {
new FileHandler(baseDirectory); new FileHandler(baseDirectory);
@@ -581,12 +598,133 @@ public class OllamaObject {
return this; return this;
} }
/**
* Sets the thinking tier that should be requested from the model when this
* {@link OllamaObject} is built.
* <p>
* <b>Not all models support this.</b> Whether {@code think} has any effect depends
* entirely on the model being queried — models without native thinking support will
* simply ignore it.
* <p>
* Among models that <i>do</i> support it, the accepted scheme is not consistent:
* <ul>
* <li>Some models only support a boolean on/off toggle (thinking enabled or disabled)</li>
* <li>Others support graded levels, e.g. {@code "low"}, {@code "medium"}, {@code "high"},
* or {@code "max"}</li>
* </ul>
* Callers are responsible for knowing which scheme the target model expects and supplying
* a {@link Thinking} value it can actually interpret — an unsupported value is not
* guaranteed to fail cleanly and may instead be silently ignored by Ollama.
*
* @param think the thinking tier to request, or {@code null}/equivalent to leave
* thinking behavior at the model's default
* @return this builder, for chaining
*/
public OllamaObjectBuilder setThinkingTier(Thinking think)
{
this.think = think;
return this;
}
/** /**
* Builds the {@link OllamaObject} * Builds the {@link OllamaObject}
* @return The {@link OllamaObject} * @return The {@link OllamaObject}
*/ */
public OllamaObject build() { public OllamaObject build() {
return new OllamaObject(model, messages, tools, format, options, stream, keep_alive, systemPrompt); return new OllamaObject(model, messages, tools, format, options, stream, keep_alive, systemPrompt, think);
}
}
/**
* Represents the "thinking" tier that can be requested from an Ollama model via the
* {@code think} request field.
* <p>
* Support for this varies by model, and models that do support it do not agree on a
* single scheme:
* <ul>
* <li>Some models only understand a boolean toggle — {@link #TRUE} / {@link #FALSE}</li>
* <li>Others understand graded levels — {@link #LOW}, {@link #MEDIUM}, {@link #HIGH},
* {@link #MAX}</li>
* </ul>
* All six values live in this single enum for convenience, but mixing schemes on a model
* that doesn't support the one you pick is a caller error — e.g. sending {@link #HIGH} to
* a model that only understands {@link #TRUE}/{@link #FALSE} is not guaranteed to fail
* cleanly and may simply be ignored. Callers must know which scheme their target model
* expects before choosing a value here.
*/
public static enum Thinking {
/** Thinking enabled, for models using the boolean on/off scheme. */
TRUE("true"),
/** Thinking disabled, for models using the boolean on/off scheme. */
FALSE("false"),
/** Lowest thinking effort, for models using the graded-level scheme. */
LOW("low"),
/** Moderate thinking effort, for models using the graded-level scheme. */
MEDIUM("medium"),
/** High thinking effort, for models using the graded-level scheme. */
HIGH("high"),
/** Maximum thinking effort, for models using the graded-level scheme. */
MAX("max");
/** The raw string value sent to Ollama's {@code think} request field. */
final String value;
private Thinking(String value)
{
this.value = value;
}
/** True if this constant belongs to the boolean on/off scheme, false if it's a graded level. */
public boolean isBoolean() {
return this == TRUE || this == FALSE;
}
/**
* Puts this thinking tier into the given JSON object under the given key, using
* whichever JSON type Ollama expects for this constant — a real boolean for
* {@link #TRUE}/{@link #FALSE}, or a string for the graded levels.
*/
public void putInto(JSONObject json, String key) {
if (isBoolean()) {
json.put(key, Boolean.parseBoolean(value)); // real JSON boolean, no quotes
} else {
json.put(key, value); // JSON string, e.g. "high"
}
}
/**
* Returns the raw string form of this tier, as expected by Ollama's {@code think}
* request field.
*
* @return the raw value, e.g. {@code "true"} or {@code "high"}
*/
public String getValue()
{
return value;
}
/**
* Resolves a {@link Thinking} constant from its raw string value.
* <p>
* Matching is case-insensitive; the input is lowercased before comparison.
*
* @param value the raw value to resolve, e.g. {@code "true"} or {@code "HIGH"}
* @return the matching {@link Thinking} constant
* @throws IllegalArgumentException if no constant matches the given value
*/
public static Thinking fromValue(String value) {
for(Thinking roleRole : values()) {
if(roleRole.value.equals(value.toLowerCase()))
return roleRole;
}
throw new IllegalArgumentException("Invalid value: " + value);
} }
} }
} }
+3 -1
View File
@@ -3,11 +3,13 @@ import me.neurodock.core.files.FileHandlerException;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.nio.file.Path;
public class FileTest { public class FileTest {
@Test @Test
void TestError() void TestError()
{ {
FileHandler fileHandler = new FileHandler("./test/files"); FileHandler fileHandler = new FileHandler(Path.of("./test/files"));
Assertions.assertThrowsExactly(FileHandlerException.class, () -> fileHandler.readFile("../build.gradle")); Assertions.assertThrowsExactly(FileHandlerException.class, () -> fileHandler.readFile("../build.gradle"));
} }
} }
+55
View File
@@ -0,0 +1,55 @@
import me.neurodock.core.Core;
import me.neurodock.core.LaunchOptions;
import me.neurodock.core.Options;
import me.neurodock.core.PrintMessageHandler;
import me.neurodock.ollama.OllamaMessage;
import me.neurodock.ollama.OllamaMessageRole;
import me.neurodock.ollama.OllamaObject;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import javax.imageio.ImageIO;
import java.awt.*;
import java.io.File;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicReference;
public class OllamaImages {
@Test
public void test1()
{
AtomicReference<Image> i = new AtomicReference<>();
assertDoesNotThrow(() -> {
File input = new File("../testImage.png");
i.set(ImageIO.read(input));
});
assertNotNull(i.get());
LaunchOptions.getInstance().setLoadOld(false);
Options.getInstance().setDataDir(Path.of("AI-Chat/Test"), false);
Core core = new Core(new PrintMessageHandler() {
@Override
public void printMessage(String message) {
System.out.println(message);
}
@Override
public boolean color() {
return false;
}
});
core.setOllamaObjectNoMemory(OllamaObject.builder()
.setModel("qwen3.5:4b")
.keep_alive(0)
.build());
assertDoesNotThrow(() -> {
core.getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.USER, "Describe this image", i.get()));
});
core.qurryOllama().thenAccept(core::handleResponse).join();
}
}
@@ -1,15 +1,16 @@
package me.neurodock.display; package me.neurodock.display;
import me.neurodock.core.Core; import me.neurodock.core.Core;
import me.neurodock.core.Options;
import me.neurodock.core.Pair; import me.neurodock.core.Pair;
import me.neurodock.core.PrintMessageHandler; import me.neurodock.core.PrintMessageHandler;
import me.neurodock.core.files.FileHandlerLocation;
import me.neurodock.core.memory.CoreMemory; import me.neurodock.core.memory.CoreMemory;
import me.neurodock.ollama.*; import me.neurodock.ollama.*;
import me.neurodock.ollama.utils.SystemMessage; import me.neurodock.ollama.utils.SystemMessage;
import org.json.JSONObject; import org.json.JSONObject;
import java.io.*; import java.io.*;
import java.nio.file.Path;
import java.util.*; import java.util.*;
import static me.neurodock.core.Core.writeLog; import static me.neurodock.core.Core.writeLog;
@@ -43,15 +44,17 @@ public class Display {
public Display() public Display()
{ {
Options.getInstance().setDataDir(Path.of("AI-Chat"), false);
core.setOllamaObject/*NoMemory*/(OllamaObject.builder() core.setOllamaObject/*NoMemory*/(OllamaObject.builder()
.setModel("llama3.1:8b") .setModel("llama3.1:8b")
.keep_alive(10) .keep_alive(10)
//.stream(false) //.stream(false)
.addFileTools(FileHandlerLocation.DATA_FILES) .addFileTools(Options.getInstance().getFileHandlerDataLocation())
.addMessages(new SystemMessage("You need to emulate feelings and emotions.")) .addMessages(new SystemMessage("You need to emulate feelings and emotions."))
.build()); .build());
core.enablePlugins(Core.PLUGIN_DIRECTORY); core.enablePlugins(Options.getInstance().getPluginDirectory());
core.addTool(new TimeTool(), Core.Source.CTP); core.addTool(new TimeTool(), Core.Source.CTP);
// TODO: Well Docker failes when luanched.... Fuck // TODO: Well Docker failes when luanched.... Fuck
@@ -158,9 +161,9 @@ public class Display {
break; break;
case "working": case "working":
System.out.println("Working directories:\n" + System.out.println("Working directories:\n" +
" Data: " + Core.DATA_DIR.getAbsolutePath() + "\n" + " Data: " + Options.getInstance().getDataDir().getAbsolutePath() + "\n" +
" DateFiles: " + FileHandlerLocation.DATA_FILES + "\n" + " DateFiles: " + Options.getInstance().getFileHandlerDataLocation() + "\n" +
" Plugins: " + Core.PLUGIN_DIRECTORY.getAbsolutePath()); " Plugins: " + Options.getInstance().getPluginDirectory().getAbsolutePath());
break; break;
default: default:
System.out.println("Unknown command: " + message); System.out.println("Unknown command: " + message);
@@ -7,6 +7,7 @@ import com.github.dockerjava.core.DockerClientImpl;
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
import com.github.dockerjava.transport.DockerHttpClient; import com.github.dockerjava.transport.DockerHttpClient;
import me.neurodock.core.Core; import me.neurodock.core.Core;
import me.neurodock.core.Options;
import me.neurodock.core.Pair; import me.neurodock.core.Pair;
import me.neurodock.ollama.OllamaFunctionArgument; import me.neurodock.ollama.OllamaFunctionArgument;
import me.neurodock.ollama.OllamaFunctionTool; import me.neurodock.ollama.OllamaFunctionTool;
@@ -434,7 +435,7 @@ public class PythonRunner extends OllamaFunctionTool {
fullCmd.append(arg).append(" "); fullCmd.append(arg).append(" ");
} }
File program = new File(Core.CACHE_DIRECTORY, "cmd.sh"); File program = new File(Options.getInstance().getCacheDirectory(), "cmd.sh");
if(program.exists()){ if(program.exists()){
program.delete(); program.delete();
} }
@@ -5,6 +5,7 @@ import io.github.classgraph.ClassInfo;
import io.github.classgraph.ClassInfoList; import io.github.classgraph.ClassInfoList;
import io.github.classgraph.ScanResult; import io.github.classgraph.ScanResult;
import me.neurodock.core.Core; import me.neurodock.core.Core;
import me.neurodock.core.Options;
import me.neurodock.ollama.OllamaFunctionTool; import me.neurodock.ollama.OllamaFunctionTool;
import me.neurodock.ollama.OllamaFunctionTools; import me.neurodock.ollama.OllamaFunctionTools;
import org.json.JSONObject; import org.json.JSONObject;
@@ -27,13 +28,13 @@ public class GeniusTools {
public final String Access_Token; public final String Access_Token;
public final String BaseURL = "https://api.genius.com"; public final String BaseURL = "https://api.genius.com";
public final OllamaFunctionTools GeniusTools; public final OllamaFunctionTools GeniusTools;
public final File CacheFile = new File(Core.DATA_DIR + "/genius_cache.json"); public final File CacheFile = new File(Options.getInstance().getDataDir(), "genius_cache.json");
public JSONObject CacheData; public JSONObject CacheData;
public GeniusTools() { public GeniusTools() {
super(); super();
try { try {
JSONObject obj = new JSONObject(Files.readString(Path.of(Core.DATA_DIR + "/geniusapi.json"))); JSONObject obj = new JSONObject(Files.readString(Path.of(Options.getInstance().getDataDir().getPath(), "geniusapi.json")));
this.Client_ID = obj.getString("client_id"); this.Client_ID = obj.getString("client_id");
this.Client_Secret = obj.getString("client_secret"); this.Client_Secret = obj.getString("client_secret");
this.Access_Token = obj.getString("access_token"); this.Access_Token = obj.getString("access_token");
@@ -5,6 +5,7 @@ import io.github.classgraph.ClassInfo;
import io.github.classgraph.ClassInfoList; import io.github.classgraph.ClassInfoList;
import io.github.classgraph.ScanResult; import io.github.classgraph.ScanResult;
import me.neurodock.core.Core; import me.neurodock.core.Core;
import me.neurodock.core.Options;
import me.neurodock.ollama.OllamaFunctionTool; import me.neurodock.ollama.OllamaFunctionTool;
import me.neurodock.ollama.OllamaFunctionTools; import me.neurodock.ollama.OllamaFunctionTools;
import org.json.JSONObject; import org.json.JSONObject;
@@ -30,7 +31,7 @@ public class MALAPITool {
public MALAPITool() { public MALAPITool() {
super(); super();
try { try {
JSONObject obj = new JSONObject(Files.readString(Path.of(Core.DATA_DIR + "/malapi.json"))); JSONObject obj = new JSONObject(Files.readString(Path.of(Options.getInstance().getDataDir().getPath(), "malapi.json")));
this.Client_ID = obj.getString("client_id"); this.Client_ID = obj.getString("client_id");
this.Client_Secret = obj.getString("client_secret"); this.Client_Secret = obj.getString("client_secret");
} catch (IOException e) { } catch (IOException e) {
+1 -1
View File
@@ -2,7 +2,7 @@ plugins {
id 'java-library' id 'java-library'
} }
version = '0.1.4.1' version = pluginAPIVersion
repositories { repositories {
mavenCentral() mavenCentral()
+6
View File
@@ -18,6 +18,12 @@ The author **does not endorse or encourage** scraping or any other use that viol
Use this software **at your own risk**. The author disclaims any liability for legal or technical consequences arising from its use. Use this software **at your own risk**. The author disclaims any liability for legal or technical consequences arising from its use.
## How to report issues?
This is a personal, self-hosted Gitea instance, and I haven't set up account
registration or figured out permissions for letting new accounts only open
issues — and honestly, I'd rather not have random accounts created here anyway.<br>
All issues for NeuroDock should be filed on the **[Codeberg mirror](https://codeberg.org/alienfromdia/NeuroDock/issues)**. Thanks for understanding.
## API ## API
The documentation for the API is available at the gitea wiki under [API docs](https://git.server.4zellen.se/neurodock/NeuroDock/wiki/API-Docs) The documentation for the API is available at the gitea wiki under [API docs](https://git.server.4zellen.se/neurodock/NeuroDock/wiki/API-Docs)
+5 -1
View File
@@ -1,3 +1,7 @@
# This is used in sub-project that dosent explicitly require the API sub-project to only include if thay shuld be included project wide. # This is used in sub-project that dosent explicitly require the API sub-project to only include if thay shuld be included project wide.
useAPI = true useAPI = true
javaVersion = 26 javaVersion = 25
coreVersion = 1.10.1
pluginAPIVersion = 0.1.4.1
APIVersion = 1.0-SNAPSHOT