Author SHA1 Message Date
Zacharias 5be402b196 Added a corrected toString method to OpenAIAssistentMessage 2026-08-30 00:19:54 +02:00
Zacharias 53a9781ed4 fixed minor access bug 2026-08-29 23:59:53 +02:00
Zacharias 2949a0d2fe Whonestly, i have forgoten what this contains, sooo ye :)
Removed Ollama integration
swaped it for llama.cpp integration
and made backend integration more generic/abstract.. soo ollama will return.. maybe.. probobly

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

This begain some implementations to support streaming from Ollama, and the ability to cancel a request.
2026-07-20 22:13:33 +02:00
Zacharias 330d7df389 Fixed some errors with the System prompt
Chaged java version to make this more compadible
2026-07-20 18:03:31 +02:00
63 changed files with 1853 additions and 2264 deletions
+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_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" />
</component>
</project>
+3 -2
View File
@@ -5,7 +5,7 @@ plugins {
id 'io.spring.dependency-management' version '1.1.4'
}
version = '1.0-SNAPSHOT'
version = APIVersion
dependencies {
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-webflux:4.1.0-M4'
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'
//implementation 'org.springframework.boot:spring-boot-starter-actuator'
//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.NewToolRequest;
import me.neurodock.core.Core;
import me.neurodock.core.GlobalObjects;
import me.neurodock.core.PrintMessageHandler;
import me.neurodock.ollama.OllamaObject;
import org.springframework.boot.SpringApplication;
@@ -41,8 +40,9 @@ public class APIApplication {
instance = this;
if(GlobalObjects.getObject("core") instanceof Core coreInstance) {
this.core = coreInstance;
if(false) {
// TODO: This needs to be properly refactored.
//this.core = coreInstance;
} else {
this.core = new Core(new PrintMessageHandler() {
@Override
+4 -1
View File
@@ -2,11 +2,14 @@ plugins {
id 'java-library'
}
version = '1.9.0'
version = coreVersion
dependencies {
implementation project(":Plugin-API")
api "org.json:json:20250107"
implementation 'org.graalvm.polyglot:polyglot:25.1.3'
implementation 'org.graalvm.polyglot:js:25.1.3'
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
}
java {
@@ -0,0 +1,74 @@
package me.neurodock.backend.open.ai;
import me.neurodock.llm.Message;
import org.json.JSONArray;
public class OpenAIAssistentMessage extends Message {
private Reason endReason;
public OpenAIAssistentMessage(Role role, Object content, Reason endReason) {
super(role, content);
this.endReason = endReason;
}
public OpenAIAssistentMessage(Role role, Object content, JSONArray toolCalls, Reason endReason) {
super(role, content, toolCalls);
this.endReason = endReason;
}
public OpenAIAssistentMessage(Role role, String toolID, Object content, Reason endReason) {
super(role, toolID, content);
this.endReason = endReason;
}
public Reason getEndReason() {
return endReason;
}
@Override
public String toString() {
return "OpenAIAssistentMessage{" +
"endReason=" + endReason +
", role=" + role +
", content=" + content +
", toolID='" + toolID + '\'' +
", toolCalls=" + toolCalls +
'}';
}
public static enum Reason {
STOP,
LENGTH,
TOOL_CALLS,
CONTENT_FILTER,
FUNCTION_CALL,
/**
* Still streaming, or null
*/
NULL;
public static Reason fromString(String reason) {
if(reason == null) {
return NULL;
}
else if(reason.equalsIgnoreCase("STOP")) {
return STOP;
}
else if(reason.equalsIgnoreCase("LENGTH")) {
return LENGTH;
}
else if(reason.equalsIgnoreCase("TOOL_CALLS")) {
return TOOL_CALLS;
}
else if(reason.equalsIgnoreCase("CONTENT_FILTER")) {
return CONTENT_FILTER;
}
else if(reason.equalsIgnoreCase("FUNCTION_CALL")) {
return FUNCTION_CALL;
}
else {
return NULL;
}
}
}
}
@@ -0,0 +1,629 @@
package me.neurodock.backend.open.ai;
import me.neurodock.llm.*;
import me.neurodock.llm.exceptions.ModelNotFoundException;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.Tool;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.serializer.ToolSerializer;
import me.neurodock.llm.tools.serializer.ToolToJSON;
import me.xdrop.fuzzywuzzy.FuzzySearch;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
public class OpenAIModel implements StreamingModel {
/** Minimum fuzzy-match ratio (0-100) required for a model name to be considered a match. */
private static final int MIN_MODEL_MATCH_RATIO = 40;
private static final ToolSerializer DEFAULT_SERIALIZER = new ToolSerializer();
private URL backendURL;
private String model;
private HttpURLConnection activeConnection;
private AtomicBoolean cancelled = new AtomicBoolean(false);
static {
DEFAULT_SERIALIZER.addSerializer(FunctionTool.class, (ToolToJSON<FunctionTool>) tool -> {
JSONObject result = new JSONObject();
result.put("type", "function");
JSONObject function = new JSONObject();
ToolParameters parms = tool.parameters();
JSONObject parameters = new JSONObject();
parameters.put("type", "object");
parameters.put("required", parms.getRequired());
JSONObject properties = new JSONObject();
parms.getProperties().forEach((str,prop) -> {
JSONObject obj = new JSONObject();
obj.put("description", prop.description());
obj.put("type", prop.type().name().toLowerCase(Locale.ROOT));
properties.put(str, obj);
});
parameters.put("properties", properties);
function.put("parameters", parameters);
function.put("name", tool.name());
function.put("description", tool.description());
result.put("function", function);
return result;
});
}
/**
* Constructs an {@code me.neurodock.backend.open.ai.OpenAIModel} bound to a specific backend and model.
* <p>
* This performs a blocking request against the backend to fetch the list of
* available models, then fuzzy-matches {@code modelName} against the returned
* model IDs, selecting the closest match above a minimum confidence threshold.
*
* @param backendURI the base URL of the OpenAI-compatible backend
* (e.g. {@code http://localhost:11434})
* @param modelName the model name, or partial name, to fuzzy-match against
* the backend's available models
*
* @apiNote This {@code me.neurodock.backend.open.ai.OpenAIModel} does NOT support OpenAI's ChatGPT or
* OpenAI's hosted API — it sends no API key. Pointing {@code backendURI} at
* {@code api.openai.com} will not be rejected here; the request will instead
* fail server-side (typically with a 401 Unauthorized), which {@link #queryJSON}
* surfaces as a {@code null} response, causing this constructor to throw
* {@link ModelNotFoundException} — even though no model is actually missing.
* This class is intended for locally hosted, non-keyed OpenAI-compatible
* APIs only.
*
* @throws ModelNotFoundException if no models could be retrieved from the
* backend (e.g. the server is unreachable, misconfigured, or — see
* {@code @apiNote} — rejects the request due to a missing API key),
* or if none of the returned models sufficiently match {@code modelName}
* @throws RuntimeException if {@code backendURI} is malformed
*/
public OpenAIModel(@NotNull String backendURI, @NotNull String modelName) throws ModelNotFoundException {
try {
modelName = Objects.requireNonNull(modelName, "modelName cannot be null");
this.backendURL = new URI(Objects.requireNonNull(backendURI, "backendURI cannot be null")).toURL();
JSONObject models = queryJSON(null, "v1/models", "GET");
if(models == null || models.isEmpty()) {
throw new ModelNotFoundException("No models found");
}
int highestMatch = 0;
String highestMatchString = "";
for(Object obj : models.optJSONArray("data", new JSONArray()))
{
if(!(obj instanceof JSONObject)) continue;
JSONObject model = (JSONObject)obj;
int matchRatio = FuzzySearch.partialRatio(modelName, model.getString("id"));
if(matchRatio > highestMatch) {
highestMatch = matchRatio;
highestMatchString = model.getString("id");
}
// 100% match... might as well return?
if(matchRatio == 100) {
break;
}
}
// Too low of a match, failing
if(highestMatch < MIN_MODEL_MATCH_RATIO) {
throw new ModelNotFoundException(modelName, backendURI);
}
this.model = highestMatchString;
// Creating a shutdown hook to unload the model after the program shutdowns, this is to converse RAM!
Runtime.getRuntime().addShutdownHook(new Thread(this::unload));
} catch (URISyntaxException e) {
throw new RuntimeException(e);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
/**
* Unloads the current model.<br>
* And ignoring any errors
*/
public void unload()
{
try {
queryJSON(new JSONObject().put("model", model), "/models/unload", "POST");
}
catch (Exception _)
{
// We catch the exception, but since expected exception here is SOLY a 400 Bad Request for when a model was already unloaded, we throw it out.
}
}
/**
* Sends the conversation history in {@code obj} to the backend and requests
* the next assistant response.
* <p>
* This performs the request asynchronously; the returned future completes
* once the backend has responded (or completes exceptionally if the request
* fails at the transport level — see {@link #queryJSON}).
*
* @param obj the chat context, containing the full conversation history
* to send to the model
* @param tools the tools available to the model for this request (currently
* unused — tool support is not yet implemented)
* @return a future resolving to the assistant's reply as a {@link Message}.
* If the backend returns no choices, or the response is otherwise
* malformed/missing, resolves to a {@link Message} with empty content
* rather than throwing.
*/
@Override
public CompletableFuture<Message> qurryModel(ChatObject obj, List<Tool> tools) {
return CompletableFuture.supplyAsync(() -> {
JSONObject request = new JSONObject();
request.put("model", model);
JSONArray messages = new JSONArray();
for (Message msg : obj.getConversation()) {
JSONObject message = new JSONObject();
String role = msg.getRole().name().toLowerCase(Locale.ROOT);
message.put("role", role);
message.put("content", msg.getContent());
if (msg.getRole() == Message.Role.TOOL) {
message.put("tool_call_id", msg.getToolID());
}
messages.put(message);
}
request.put("messages", messages);
JSONArray toolArray = new JSONArray();
if(tools == null || tools.isEmpty()) {
for(Tool tool : tools) {
toolArray.put(DEFAULT_SERIALIZER.serialize(tool));
}
}
JSONObject response = queryJSON(request, "v1/chat/completions", "POST");
if(response == null) {
response = new JSONObject();
}
JSONObject message = response
.optJSONArray("choices", new JSONArray())
.optJSONObject(0, new JSONObject())
.optJSONObject("message", new JSONObject());
String content = message.optString("content", "");
String roleStr = message.optString("role", "assistant");
OpenAIAssistentMessage.Reason endReason = OpenAIAssistentMessage.Reason.fromString(response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).optString("endReason", null));
Message msg;
if(response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).has("tool_calls")) {
msg = new OpenAIAssistentMessage(
Message.Role.valueOf(roleStr.toUpperCase(Locale.ROOT)),
(Object) content,
response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).getJSONArray("tool_calls"),
endReason
);
}
else
{
msg = new OpenAIAssistentMessage(Message.Role.valueOf(roleStr.toUpperCase(Locale.ROOT)), content, endReason);
}
return msg;
});
}
@Override
public CompletableFuture<Message> singleFire(Message msg) {
return CompletableFuture.supplyAsync(() -> {
JSONObject request = new JSONObject();
request.put("model", model);
JSONArray messages = new JSONArray();
JSONObject message = new JSONObject();
String role = msg.getRole().name().toLowerCase(Locale.ROOT);
message.put("role", role);
message.put("content", msg.getContent());
if (msg.getRole() == Message.Role.TOOL) {
message.put("tool_call_id", msg.getToolID());
}
messages.put(message);
request.put("messages", messages);
JSONArray toolArray = new JSONArray();
JSONObject response = queryJSON(request, "v1/chat/completions", "POST");
if(response == null) {
response = new JSONObject();
}
JSONObject resMessage = response
.optJSONArray("choices", new JSONArray())
.optJSONObject(0, new JSONObject())
.optJSONObject("message", new JSONObject());
String content = resMessage.optString("content", "");
String roleStr = resMessage.optString("role", "assistant");
OpenAIAssistentMessage.Reason endReason = OpenAIAssistentMessage.Reason.fromString(response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).optString("endReason", null));
return new OpenAIAssistentMessage(Message.Role.valueOf(roleStr.toUpperCase(Locale.ROOT)), content, endReason);
});
}
@Override
public CompletableFuture<Message> singleFire(Message msg, List<Tool> tools) {
return CompletableFuture.supplyAsync(() -> {
JSONObject request = new JSONObject();
request.put("model", model);
JSONArray messages = new JSONArray();
JSONObject message = new JSONObject();
String role = msg.getRole().name().toLowerCase(Locale.ROOT);
message.put("role", role);
message.put("content", msg.getContent());
if (msg.getRole() == Message.Role.TOOL) {
message.put("tool_call_id", msg.getToolID());
}
messages.put(message);
request.put("messages", messages);
JSONArray toolArray = new JSONArray();
if(tools == null || tools.isEmpty()) {
for(Tool tool : tools) {
toolArray.put(DEFAULT_SERIALIZER.serialize(tool));
}
}
JSONObject response = queryJSON(request, "v1/chat/completions", "POST");
if(response == null) {
response = new JSONObject();
}
JSONObject resMessage = response
.optJSONArray("choices", new JSONArray())
.optJSONObject(0, new JSONObject())
.optJSONObject("message", new JSONObject());
String content = resMessage.optString("content", "");
String roleStr = resMessage.optString("role", "assistant");
OpenAIAssistentMessage.Reason endReason = OpenAIAssistentMessage.Reason.fromString(response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).optString("endReason", null));
return new OpenAIAssistentMessage(Message.Role.valueOf(roleStr.toUpperCase(Locale.ROOT)), content, endReason);
});
}
@Override
public ToolSerializer getToolSerializer() {
return DEFAULT_SERIALIZER;
}
public static ToolSerializer getDefaultToolSerializer() {
return DEFAULT_SERIALIZER;
}
public void cancel()
{
cancelled.set(true);
HttpURLConnection conn = activeConnection;
if (conn != null) {
conn.disconnect();
}
}
private JSONObject queryJSON(JSONObject payload, String endpoint, String method) {
return Objects.requireNonNull(queryJSON(payload, endpoint, method, null)).join();
}
/**
* Sends a blocking HTTP request to the given endpoint on the configured backend
* and parses the response body as JSON.
*
* @param payload the JSON request body to send, or {@code null} to send no body
* @param endpoint the endpoint to request, resolved against {@link #backendURL}
* (e.g. {@code "v1/models"})
* @param method the HTTP method to use (e.g. {@code "GET"}, {@code "POST"})
* @return the parsed {@link JSONObject} response body if the request succeeds
* with a 200 status, or {@code null} if a non-200 status is received
*
* @throws RuntimeException if an I/O error occurs while communicating with
* the backend, or if {@code endpoint} cannot be resolved into a valid URL
*/
@Nullable
public CompletableFuture<JSONObject> queryJSON(JSONObject payload, String endpoint, String method,
Consumer<JSONObject> onChunk) {
return CompletableFuture.supplyAsync(() -> {
HttpURLConnection connection = null;
try {
URL url = backendURL.toURI().resolve(endpoint).toURL();
connection = (HttpURLConnection) url.openConnection();
activeConnection = connection;
if (cancelled.get()) {
throw new CancellationException("Query cancelled before request was sent");
}
boolean isStreaming = onChunk != null;
connection.setRequestMethod(method);
connection.setRequestProperty("Content-Type", "application/json");
if (isStreaming) {
connection.setRequestProperty("Accept", "text/event-stream");
}
connection.setDoOutput(true);
connection.setConnectTimeout(80 * 1000);
if (payload != null) {
if (isStreaming) {
payload.put("stream", true);
}
String payloadString = payload.toString().replace("\n", "\\n");
try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
wr.write(payloadString.getBytes(StandardCharsets.UTF_8));
wr.flush();
}
}
int responseCode = connection.getResponseCode();
StringBuilder rawErrorOrDump = new StringBuilder();
JSONObject result;
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
if (isStreaming) {
result = readStream(reader, onChunk);
} else {
// Non-streaming: OpenAI-compatible endpoints return one JSON object body
StringBuilder messageContent = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
if (cancelled.get()) throw new CancellationException("Query cancelled mid-response");
messageContent.append(line);
}
result = new JSONObject(messageContent.toString());
}
} catch (IOException ex) {
if (cancelled.get()) throw new CancellationException("Query cancelled mid-response");
InputStream errorStream = connection.getErrorStream();
if (errorStream != null) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream))) {
String line;
while ((line = reader.readLine()) != null) rawErrorOrDump.append(line);
}
}
result = null;
}
if (responseCode == HttpURLConnection.HTTP_OK) {
return result;
} else {
throw new RuntimeException("HTTP Response code - " + responseCode);
}
} catch (IOException | URISyntaxException e) {
if (cancelled.get()) throw new CancellationException("Query cancelled");
throw new RuntimeException(e);
} finally {
if (connection != null) connection.disconnect();
activeConnection = null;
}
});
}
/**
* Consumes an OpenAI-style SSE stream, dispatching each parsed chunk to
* {@code onChunk} and accumulating the deltas into a single final
* {@link JSONObject} shaped like a non-streaming chat completion response
* ({@code choices[0].message.content} holding the full assembled text).
*/
private JSONObject readStream(BufferedReader reader, Consumer<JSONObject> onChunk) throws IOException {
StringBuilder messageContent = new StringBuilder();
JSONObject last = null;
String role = "assistant";
String line;
while ((line = reader.readLine()) != null) {
if (cancelled.get()) throw new CancellationException("Query cancelled mid-response");
if (line.isBlank() || !line.startsWith("data:")) continue;
String data = line.substring(5).trim();
if (data.equals("[DONE]")) break;
JSONObject chunk = new JSONObject(data);
onChunk.accept(chunk);
last = chunk;
JSONObject choice = chunk.getJSONArray("choices").getJSONObject(0);
JSONObject delta = choice.getJSONObject("delta");
if (delta.has("role") && delta.get("role") != null)
{
role = delta.optString("role");
}
if (delta.has("content"))
{
messageContent.append(delta.optString("content", ""));
}
}
if (last == null) return null;
// Re-shape the final chunk into a single OpenAI-style response object,
// so callers get the same shape whether they streamed or not.
JSONObject finalObj = new JSONObject(last.toString());
JSONObject message = new JSONObject();
message.put("role", role);
message.put("content", messageContent.toString());
finalObj.getJSONArray("choices").getJSONObject(0).put("message", message);
return finalObj;
}
@Override
public CompletableFuture<Message> qurryModel(ChatObject obj, List<Tool> tools, Consumer<JSONObject> chunkConsumer) {
return CompletableFuture.supplyAsync(() -> {
JSONObject request = new JSONObject();
request.put("model", model);
JSONArray messages = new JSONArray();
for (Message msg : obj.getConversation()) {
JSONObject message = new JSONObject();
String role = msg.getRole().name().toLowerCase(Locale.ROOT);
message.put("role", role);
message.put("content", msg.getContent());
if (msg.getRole() == Message.Role.TOOL) {
message.put("tool_call_id", msg.getToolID());
}
messages.put(message);
}
request.put("messages", messages);
JSONArray toolArray = new JSONArray();
if(tools == null || tools.isEmpty()) {
for(Tool tool : tools) {
toolArray.put(DEFAULT_SERIALIZER.serialize(tool));
}
}
JSONObject response = queryJSON(request, "v1/chat/completions", "POST", chunkConsumer).join();
if(response == null) {
response = new JSONObject();
}
JSONObject message = response
.optJSONArray("choices", new JSONArray())
.optJSONObject(0, new JSONObject())
.optJSONObject("message", new JSONObject());
String content = message.optString("content", "");
String roleStr = message.optString("role", "assistant");
OpenAIAssistentMessage.Reason endReason = OpenAIAssistentMessage.Reason.fromString(response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).optString("endReason", null));
Message msg;
if(response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).has("tool_calls")) {
msg = new OpenAIAssistentMessage(
Message.Role.valueOf(roleStr.toUpperCase(Locale.ROOT)),
(Object) content,
response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).getJSONArray("tool_calls"),
endReason
);
}
else
{
msg = new OpenAIAssistentMessage(Message.Role.valueOf(roleStr.toUpperCase(Locale.ROOT)), content, endReason);
}
return msg;
});
}
@Override
public CompletableFuture<Message> singleFire(Message msg, Consumer<JSONObject> chunkConsumer) {
return CompletableFuture.supplyAsync(() -> {
JSONObject request = new JSONObject();
request.put("model", model);
JSONArray messages = new JSONArray();
JSONObject message = new JSONObject();
String role = msg.getRole().name().toLowerCase(Locale.ROOT);
message.put("role", role);
message.put("content", msg.getContent());
if (msg.getRole() == Message.Role.TOOL) {
message.put("tool_call_id", msg.getToolID());
}
messages.put(message);
request.put("messages", messages);
JSONArray toolArray = new JSONArray();
JSONObject response = queryJSON(request, "v1/chat/completions", "POST", chunkConsumer).join();
if(response == null) {
response = new JSONObject();
}
JSONObject resMessage = response
.optJSONArray("choices", new JSONArray())
.optJSONObject(0, new JSONObject())
.optJSONObject("message", new JSONObject());
String content = resMessage.optString("content", "");
String roleStr = resMessage.optString("role", "assistant");
OpenAIAssistentMessage.Reason endReason = OpenAIAssistentMessage.Reason.fromString(response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).optString("endReason", null));
return new OpenAIAssistentMessage(Message.Role.valueOf(roleStr.toUpperCase(Locale.ROOT)), content, endReason);
});
}
@Override
public CompletableFuture<Message> singleFire(Message msg, List<Tool> tools, Consumer<JSONObject> chunkConsumer) {
return CompletableFuture.supplyAsync(() -> {
JSONObject request = new JSONObject();
request.put("model", model);
JSONArray messages = new JSONArray();
JSONObject message = new JSONObject();
String role = msg.getRole().name().toLowerCase(Locale.ROOT);
message.put("role", role);
message.put("content", msg.getContent());
if (msg.getRole() == Message.Role.TOOL) {
message.put("tool_call_id", msg.getToolID());
}
messages.put(message);
request.put("messages", messages);
JSONArray toolArray = new JSONArray();
if(tools == null || tools.isEmpty()) {
for(Tool tool : tools) {
toolArray.put(DEFAULT_SERIALIZER.serialize(tool));
}
}
JSONObject response = queryJSON(request, "v1/chat/completions", "POST", chunkConsumer).join();
if(response == null) {
response = new JSONObject();
}
JSONObject resMessage = response
.optJSONArray("choices", new JSONArray())
.optJSONObject(0, new JSONObject())
.optJSONObject("message", new JSONObject());
String content = resMessage.optString("content", "");
String roleStr = resMessage.optString("role", "assistant");
OpenAIAssistentMessage.Reason endReason = OpenAIAssistentMessage.Reason.fromString(response.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).optString("endReason", null));
return new OpenAIAssistentMessage(Message.Role.valueOf(roleStr.toUpperCase(Locale.ROOT)), content, endReason);
});
}
}
+132 -446
View File
@@ -1,34 +1,36 @@
package me.neurodock.core;
import me.neurodock.core.memory.*;
import me.neurodock.ollama.*;
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
import me.neurodock.llm.ChatObject;
import me.neurodock.llm.Message;
import me.neurodock.llm.Model;
import me.neurodock.llm.StreamingModel;
import me.neurodock.llm.exceptions.ToolException;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.Tool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolResponse;
import me.neurodock.plugin.Data;
import me.neurodock.plugin.LoadedPlugin;
import me.neurodock.plugin.loader.Loader;
import me.neurodock.plugin.exceptions.PluginLoadingException;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.NotNull;
import org.json.JSONArray;
import org.json.JSONObject;
import javax.naming.OperationNotSupportedException;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
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.Locale;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import static me.neurodock.ollama.OllamaFunctionArgument.deconstructOllamaFunctionArguments;
import static me.neurodock.plugin.tool.Tool.RPCP_SOURCE;
/**
* The Main class for the System, responsible for managing the OllamaObject, tools, and the Ollama API.
*/
@@ -50,72 +52,40 @@ public class Core {
/**
* The OllamaObject to use.
*/
private OllamaObject ollamaObject;
private Model model;
/**
* The list of tools to use.
*/
private ArrayList<Pair<OllamaFunctionTool, String>> funtionTools = new ArrayList<>();
private final ArrayList<Tool> tools = new ArrayList<>();
private ChatObject chatObject = new ChatObject();
/**
* The IP of the Ollama API.
* Used to cancel/stop a responce, only usefull if
*/
private String ollamaIP;
/**
* The port of the Ollama API.
*/
private int ollamaPort = 11434;
/**
* The URL of the Ollama API.
*/
private URL url;
private AtomicBoolean cancelled = new AtomicBoolean(false);
/**
* 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)
public Core(@NotNull PrintAdvanceMessageHandler printMessageHandler)
{
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);
@@ -126,55 +96,6 @@ public class Core {
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>
@@ -183,7 +104,8 @@ public class Core {
*/
private void initDirectories() {
ensureDir(logDir.getAbsolutePath());
ensureDir(DATA_DIR.getAbsolutePath() + "/messages");
ensureDir(Options.getInstance().getDataDir() + "/messages");
Options.getInstance().initiateDirectories();
}
/**
@@ -196,19 +118,6 @@ public class Core {
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.
*
@@ -299,9 +208,9 @@ 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 Model} 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.
* under {@link Options#getDataDir()} for resuming the session later.
*
* @see #buildMessagesArray()
* @see #writeMessagesTo(File, JSONArray)
@@ -310,23 +219,34 @@ public class Core {
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);
writeMessagesTo(new File(Options.getInstance().getDataDir().getAbsolutePath()+"/messages/" + timestamp + ".json"), messages);
writeMessagesTo(new File(Options.getInstance().getDataDir(), "messages.json"), messages);
}
/**
* Collects all messages from the {@link OllamaObject} and serializes them into a {@link JSONArray}.
* Collects all messages from the {@link Model} and serializes them into a {@link JSONArray}.
*
* @return A {@link JSONArray} containing all current messages
*/
private JSONArray buildMessagesArray() {
JSONArray messages = new JSONArray();
for (OllamaMessage message : ollamaObject.getMessages()) {
messages.put(message.toJSON());
for (Message msg : this.chatObject.getConversation()) {
JSONObject message = new JSONObject();
String role = msg.getRole().name().toLowerCase(Locale.ROOT);
message.put("role", role);
message.put("content", msg.getContent());
if (msg.getRole() == Message.Role.TOOL) {
message.put("tool_call_id", msg.getToolID());
}
messages.put(message);
}
return messages;
}
public ChatObject getChatObject() {
return chatObject;
}
/**
* Writes a {@link JSONArray} of messages to the given file, overwriting it if it already exists.
*
@@ -347,222 +267,69 @@ 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.
* Sets the {@link #model} object to the provided argument,
* Also adds the memory base system. See {@link Core#setModelNoMemory(Model)} if you don't want to add memory functions
* @param model The Model to use
*/
@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();
}
public void setModel(Model model) {
this.model = model;
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);
}
}));
addTool(new AddMemoryFunction());
addTool(new RemoveMemoryFunction());
addTool(new GetMemoryFunction());
addTool(new GetMemoriesFunction());
addTool(new GetMemoryIdentitiesFunction());
}
/**
* A check to exit early if Ollama isn't reachable.
* Sets the {@link #model} object to the provided argument,
* Does not add the base system for memory. see {@link #setModel(Model)} if you want to add memory function
* @param model The Model to use
*/
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");
}
public void setModelNoMemory(Model model) {
this.model = model;
}
/**
* 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);
public void addTool(Tool functionTool) {
tools.add(functionTool);
}
public void addMessage(Message message) {
chatObject.addMessage(message);
}
/*
/**
* Adds a list of tools to the System
* @param tools The tools to add
*/
@SuppressWarnings("MagicConstant")
public void addTools(OllamaFunctionTools tools)
/*@SuppressWarnings("MagicConstant")
public void addTools(Tools tools)
{
for(Pair<OllamaFunctionTool, String> tool : tools)
for(Pair<Tool, String> tool : tools)
{
addTool(tool.getKey(), tool.getValue());
}
}
}*/
/**
* Gets the list of tools added to the System
* @return The list of tools added to the System compressed as Pairs of the tool and the source
*/
public ArrayList<Pair<OllamaFunctionTool, String>> getFuntionTools() {
return funtionTools;
public ArrayList<Tool> getTools() {
return tools;
}
/**
* Gets the Ollama Object
* @return The Ollama Object
*/
public OllamaObject getOllamaObject() {
return ollamaObject;
public Model getModel() {
return model;
}
/**
@@ -570,14 +337,10 @@ public class Core {
* @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))
tools.stream()
.filter(tool -> tool.name().equalsIgnoreCase(name))
.findFirst()
.ifPresentOrElse(tool -> {
funtionTools.remove(tool);
ollamaObject.removeTool(tool.getKey());
}, () -> {
.ifPresentOrElse(tools::remove, () -> {
new IllegalArgumentException("Function tool with name '"+name+"' does not exist")
.printStackTrace();
@@ -594,106 +357,51 @@ public class Core {
}catch (IOException e) {}
}
/**
* Sends the OllamaObject to Ollama
* @return The response from Ollama
*/
public CompletableFuture<JSONObject> qurryOllama()
public CompletableFuture<Message> queryModel()
{
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.toJSON().toString();
ollamaObjectString = ollamaObjectString.replace("\n", "\\n");
try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
wr.write(ollamaObjectString.getBytes(StandardCharsets.UTF_8));
wr.flush();
return model.qurryModel(chatObject, tools);
}
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.
public CompletableFuture<Message> queryModel(Consumer<JSONObject> consumer) throws OperationNotSupportedException {
if(model instanceof StreamingModel streamingModel) {
return streamingModel.qurryModel(chatObject, tools, consumer);
}
} 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);
}
});
throw new OperationNotSupportedException("The chosen backend of type \""+model.getClass().getSimpleName()+"\" dose not support streaming");
}
/**
* Handles the response from Ollama.
*
* <p>
* Processes tool calls, logs information, appends messages to the OllamaObject,
* and prints output to the user.
*
* @param response The response from Ollama
*/
public void handleResponse(JSONObject response) {
public void handleResponse(Message 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")
));
//chatObject.addMessage(response);
List<CompletableFuture<Void>> futures = new ArrayList<>();
// Process each tool call
for(Object call : message.getJSONArray("tool_calls")) {
JSONArray toolCalls = response.getToolCalls();
if(toolCalls == null) toolCalls = new JSONArray();
if(toolCalls.length() == 0)
{
checkIfResponceMessage(response);
return;
}
for(Object call : toolCalls) {
futures.add(processToolCall(call));
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenAccept(result -> {
checkIfResponceMessage(response);
qurryOllama().thenAccept(this::handleResponse);
model.qurryModel(chatObject, tools).thenAccept(this::handleResponse);
});
}
@@ -708,17 +416,16 @@ public class Core {
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);
Tool func = findTool(function);
if (func == null) {
reportToolNotFound(function);
return;
}
JSONObject arguments = function.getJSONObject("arguments");
JSONObject arguments = new JSONObject(function.getString("arguments"));
renderToolCalling(func.renderCalling(function), function, arguments);
executeToolCall(func, arguments);
});
@@ -730,11 +437,10 @@ public class Core {
* @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())
private Tool findTool(JSONObject function) {
return tools.stream()
.filter(f -> (f.name())
.equalsIgnoreCase(function.getString("name")))
.map(Pair::getKey)
.findFirst()
.orElse(null);
}
@@ -745,11 +451,13 @@ public class Core {
* @param function the function JSON representing a hallucinated or removed function
*/
private void reportToolNotFound(JSONObject function) {
OllamaToolError error = new OllamaToolError(
Message error = new Message(
Message.Role.TOOL,
function.getString("name"),
"Function '" + function.getString("name") + "' does not exist"
);
ollamaObject.addMessage(error);
printMessageHandler.printToolCalling(error.getError());
chatObject.addMessage(error);
printMessageHandler.printToolCalling(error.getContent().toString());
}
/**
@@ -786,37 +494,45 @@ public class Core {
* @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<>();
private void executeToolCall(Tool tool, JSONObject arguments) {
if(tool instanceof FunctionTool func) {
ToolArguments args = new ToolArguments();
for (String key : arguments.keySet()) {
args.add(new OllamaFunctionArgument(key, arguments.get(key)));
args.addArgument(key, arguments.get(key));
}
try {
OllamaToolResponse response = func.function(args.toArray(new OllamaFunctionArgument[0]));
ollamaObject.addMessage(response);
ToolResponse response = func.function(args);
if(response.getToolID() == null) {
response.setToolID(tool.name());
}
chatObject.addMessage(response);
printMessageHandler.printMessage(response);
writeLog("Successfully function call " + func.name() + " output: " + response.getResponse());
} catch(OllamaToolErrorException e) {
OllamaToolError error = new OllamaToolError(e.getMessage());
ollamaObject.addMessage(error);
} catch (ToolException e) {
Message error = new Message(
Message.Role.TOOL,
tool.name(),
e.getMessage()
);
chatObject.addMessage(error);
printMessageHandler.printErrorMessage(error);
writeLog("ERROR: " + e.getMessage());
}
}
// TODO: Add support for misc tools
}
/**
* Checks if the response contains a message and if so, prints it to the user
* @param responce the Ollama response
*/
private void checkIfResponceMessage(JSONObject responce) {
String message = responce.getJSONObject("message").getString("content");
if(responce.getJSONObject("message").has("content") && !message.isBlank())
private void checkIfResponceMessage(Message responce) {
if(responce.getContent() instanceof String str)
{
OllamaMessage ollamaMessage = new OllamaMessage(OllamaMessageRole.ASSISTANT, message);
printMessageHandler.printMessage(ollamaMessage);
writeLog("Response content: "+ message);
ollamaObject.addMessage(ollamaMessage);
printMessageHandler.printMessage(responce);
writeLog("Response content: "+ str);
chatObject.addMessage(responce);
}
}
@@ -879,9 +595,9 @@ public class Core {
}
data.plugins.forEach(loadedPlugin -> {
for(OllamaFunctionTool tool : loader.getTools(loadedPlugin.plugin()))
for(Tool tool : loader.getTools(loadedPlugin.plugin()))
{
addTool(tool, Source.RPCP);
addTool(tool);
}
});
}
@@ -892,7 +608,7 @@ public class Core {
@Override
public File getDataDictionary() {
return DATA_DIR;
return Options.getInstance().getDataDir();
}
@Override
@@ -902,7 +618,7 @@ public class Core {
@Override
public File getCacheDirectory() {
return CACHE_DIRECTORY;
return Options.getInstance().getCacheDirectory();
}
public void addPlugin(LoadedPlugin plugin)
@@ -915,34 +631,4 @@ public class Core {
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_SOURCE;
/**
* Tools defined via the RESt API interface dynamically.
*/
public static final String API = "API";
}
}
@@ -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,15 @@
package me.neurodock.core;
import com.sun.jdi.connect.spi.TransportService;
import me.neurodock.ollama.*;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.Tool;
import me.neurodock.llm.Message;
import me.neurodock.llm.Model;
import org.json.JSONObject;
import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -42,7 +42,7 @@ import java.util.Map;
* <p>
* Instances are assembled via {@link #builder()}. Callers who need capability routing
* hints compiled from a live set of registered tools must call
* {@link #generateCapabilities(OllamaObject)} before {@link #generateSystemPrompt()};
* {@link #generateCapabilities(ArrayList)} before {@link #generateSystemPrompt()};
* see that method's docs for why this is a separate step rather than being folded into
* construction.
*
@@ -63,12 +63,12 @@ public class LLMSystemPrompt {
* Usage-hint definitions for registered tools, or {@code null} if this prompt doesn't
* compile a capabilities section. This holds the *rules* for what to say about each
* tool; the actual compiled text lives in {@link #compiledCapabilities} and is only
* populated once {@link #generateCapabilities(OllamaObject)} has been called.
* populated once {@link #generateCapabilities(ArrayList)} has been called.
*/
private Capabilities capabilities;
/**
* The compiled, ready-to-render {@code <Capabilities>} body, built by
* {@link #generateCapabilities(OllamaObject)} from {@link #capabilities} and a live
* {@link #generateCapabilities(ArrayList)} from {@link #capabilities} and a live
* set of registered tools. {@code null} until that method has been called at least
* once, or if {@link #capabilities} was never set.
*/
@@ -106,18 +106,18 @@ public class LLMSystemPrompt {
/**
* Compiles the {@code <Capabilities>} section from the tools currently registered on
* the given {@link OllamaObject}, using the usage hints defined in {@link #capabilities}.
* the given {@link ArrayList<Tool>}, using the usage hints defined in {@link #capabilities}.
* <p>
* This is intentionally a separate step from construction rather than something the
* constructor does automatically: which tools are registered on an {@link OllamaObject}
* constructor does automatically: which tools are registered on an {@link ArrayList<Tool>}
* can change after this prompt is built (tools added/removed at runtime), so the
* compiled capabilities text needs to be regenerated on demand against whatever the
* current tool set actually is, not frozen at construction time.
* <p>
* For each registered {@link OllamaFunctionTool}, this looks up a usage hint by the
* For each registered {@link FunctionTool}, this looks up a usage hint by the
* tool's {@link Class} via {@link Capabilities#getUsageHints()}. If no hint was
* registered for that class, the tool is silently omitted from the compiled output —
* there is no fallback to {@link OllamaFunctionTool#description()}, since that text
* there is no fallback to {@link FunctionTool#description()}, since that text
* is written to help the model choose a tool mid-conversation, not to explain routing
* up front in a system prompt; conflating the two would blur what each is for.
* <p>
@@ -125,19 +125,19 @@ public class LLMSystemPrompt {
* section at all), this clears {@link #compiledCapabilities} to {@code null} and
* returns without inspecting {@code ollamaObject}.
*
* @param ollamaObject the object whose currently-registered tools should be inspected
* @param tools the object whose currently-registered tools should be inspected
*/
public void generateCapabilities(OllamaObject ollamaObject) {
public void generateCapabilities(ArrayList<Tool> tools) {
if(capabilities == null) {
compiledCapabilities = null;
return;
}
StringBuilder builder = new StringBuilder();
for (Pair<OllamaTool, String> tool : ollamaObject.getTools()) {
if(tool.getKey() instanceof OllamaFunctionTool funcTool) {
String llmName = funcTool.name() + "_" + (funcTool.getSource() != null ? funcTool.getSource() : "");
String hint = capabilities.getUsageHints().get(tool.getClass());
for (Tool tool : tools) {
if(tool instanceof FunctionTool funcTool) {
String llmName = funcTool.name();
String hint = capabilities.getUsageHints().get(funcTool.getClass());
// fall back to description() if no explicit routing hint was set
String line = hint != null ? hint : funcTool.description();
@@ -151,7 +151,7 @@ public class LLMSystemPrompt {
/**
* Renders this prompt's currently-set sections into a single system-role
* {@link OllamaMessage}, ready to be sent to Ollama.
* {@link Message}, ready to be sent to Ollama.
* <p>
* Each section — {@link #identity}, {@link #context}, {@link #compiledCapabilities},
* {@link #behavior}, {@link #outputFormat} — is wrapped in its own XML-style tag and
@@ -160,13 +160,13 @@ public class LLMSystemPrompt {
* skipped rather than emitted as empty tags.
* <p>
* Note that {@link #compiledCapabilities} reflects whatever tool set was live the last
* time {@link #generateCapabilities(OllamaObject)} was called — call that first if the
* time {@link #generateCapabilities(ArrayList)} was called — call that first if the
* registered tools may have changed since.
*
* @return a new {@link OllamaMessage} with role {@link OllamaMessageRole#SYSTEM}
* @return a new {@link Message} with role {@link Message.Role#SYSTEM}
* containing the compiled prompt text
*/
public OllamaMessage generateSystemPrompt() {
public Message generateSystemPrompt() {
StringBuilder builder = new StringBuilder();
@@ -188,7 +188,7 @@ public class LLMSystemPrompt {
builder.append("<Output>").append(System.lineSeparator()).append(outputFormat.build()).append(System.lineSeparator()).append("</Output>").append(System.lineSeparator());
}
return new OllamaMessage(OllamaMessageRole.SYSTEM, builder.toString());
return new Message(Message.Role.SYSTEM, builder.toString());
}
/**
@@ -303,19 +303,19 @@ public class LLMSystemPrompt {
/**
* Defines per-tool usage-routing hints for the {@code <Capabilities>} section of a
* system prompt — i.e. "use this tool only in these cases" guidance, distinct from a
* tool's own {@link OllamaFunctionTool#description()}.
* tool's own {@link FunctionTool#description()}.
* <p>
* Hints are keyed by the tool's {@link Class} rather than by a registered instance or
* its LLM-facing name, since NeuroDock's convention is one instance per tool class —
* see {@link #use(Class, String)}. This class only holds the hint definitions; the
* actual compiled text is produced by
* {@link LLMSystemPrompt#generateCapabilities(OllamaObject)}.
* {@link LLMSystemPrompt#generateCapabilities(ArrayList)}.
*/
public static class Capabilities {
/**
* Usage hints keyed by tool class, in the order they were registered.
*/
private final LinkedHashMap<Class<? extends OllamaFunctionTool>, String> usageHints = new LinkedHashMap<>();
private final LinkedHashMap<Class<? extends FunctionTool>, String> usageHints = new LinkedHashMap<>();
/**
* Registers a usage-routing hint for a tool class — guidance on when the model
@@ -324,13 +324,13 @@ public class LLMSystemPrompt {
* <p>
* Keyed by class rather than instance because a tool's LLM-facing name (which
* depends on its registration source) isn't known until it's actually registered
* on an {@link OllamaObject}; class identity is stable and known up front.
* on an {@link Model}; class identity is stable and known up front.
*
* @param toolClass the tool class this hint applies to
* @param usageHint the routing guidance text for this tool
* @return this {@link Capabilities}, for chaining
*/
public Capabilities use(Class<? extends OllamaFunctionTool> toolClass, String usageHint) {
public Capabilities use(Class<? extends FunctionTool> toolClass, String usageHint) {
usageHints.put(toolClass, usageHint);
return this;
}
@@ -340,7 +340,7 @@ public class LLMSystemPrompt {
*
* @return the live map backing this {@link Capabilities}
*/
Map<Class<? extends OllamaFunctionTool>, String> getUsageHints() {
Map<Class<? extends FunctionTool>, String> getUsageHints() {
return usageHints;
}
}
@@ -448,7 +448,7 @@ public class LLMSystemPrompt {
* Loads {@link #behavior} from a text file, resolved in order against:
* <ol>
* <li>the classpath</li>
* <li>{@link Core#DATA_DIR}</li>
* <li>{@link Options#getDataDir()}</li>
* <li>the current runtime/working directory</li>
* <li>an exact file path match</li>
* </ol>
@@ -488,7 +488,7 @@ public class LLMSystemPrompt {
/**
* Sets the capability usage-hint definitions for the prompt being built. These are
* only compiled into prompt text once
* {@link LLMSystemPrompt#generateCapabilities(OllamaObject)} is called on the built
* {@link LLMSystemPrompt#generateCapabilities(ArrayList)} is called on the built
* instance.
*
* @param capabilities the usage-hint definitions to use
@@ -553,7 +553,7 @@ public class LLMSystemPrompt {
/**
* 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
* @return the full file contents, with each line terminated by {@code \n}
@@ -566,7 +566,7 @@ public class LLMSystemPrompt {
if (classpathResourceExists("/"+path)) {
in = BehaviorLoader.class.getResourceAsStream("/"+path);
} else {
Path dataDirPath = Path.of(Core.DATA_DIR.getPath(), path);
Path dataDirPath = Path.of(Options.getInstance().getDataDir().getPath(), path);
Path relativePath = Path.of(path);
if (Files.exists(dataDirPath)) {
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,19 +1,18 @@
package me.neurodock.core;
import me.neurodock.ollama.OllamaMessage;
import me.neurodock.ollama.OllamaMessageRole;
import me.neurodock.llm.Message;
/**
* Represents a {@link PrintAdvanceMessageHandler}.
* This is used by the {@link Core} to print messages for the user, LLM, Tools, or potentialy System, see {@link OllamaMessageRole}.
* This is used by the {@link Core} to print messages for the user, LLM, Tools, or potentialy System, see {@link Message.Role}.
* For a simpler version see {@link PrintMessageHandler}
*/
public interface PrintAdvanceMessageHandler {
/**
* Expected to handle the printing of the provided {@link OllamaMessage}.
* @param message The {@link OllamaMessage} requested to be printed from a veriity of sources, see {@link OllamaMessageRole}
* Expected to handle the printing of the provided {@link Message}.
* @param message The {@link Message} requested to be printed from a veriity of sources, see {@link Message}
*/
void printMessage(OllamaMessage message);
void printMessage(Message message);
/**
* Default method to print error messages to the user or API Client.
@@ -21,7 +20,7 @@ public interface PrintAdvanceMessageHandler {
* @param errorMessage The error message to be printed.
* If color is not supported, it will print the message without color.
*/
void printErrorMessage(OllamaMessage errorMessage);
void printErrorMessage(Message errorMessage);
/**
* Used when a tool is to have it's calling rendered
@@ -1,9 +1,7 @@
package me.neurodock.core;
import me.neurodock.ollama.OllamaMessage;
import me.neurodock.ollama.OllamaMessageRole;
import static me.neurodock.ollama.OllamaFunctionArgument.deconstructOllamaFunctionArguments;
import me.neurodock.llm.Message;
/**
* Represents a PrintMessageHandler.
@@ -21,11 +19,11 @@ public interface PrintMessageHandler extends PrintAdvanceMessageHandler {
* 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}
* @param message The {@link Message} requested to be printed from a veriity of sources, see {@link Message.Role}
*/
default void printMessage(OllamaMessage message) {
default void printMessage(Message 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 ASSISTANT -> (color()?"\u001b[32m":"")+(LaunchOptions.getInstance().isShowFullMessage()? message.getContent() : message.getContent().toString().replaceAll("(?s)<think>.*?</think>", "")) +(color()?"\u001b[0m":"");
case TOOL -> (color() ?"\u001b[31m":"")+message.getContent()+(color()?"\u001b[0m":"");
case USER, SYSTEM -> (color() ?"\u001b[37m":"")+message.getContent()+(color()?"\u001b[0m":"");
default -> throw new IllegalArgumentException("Invalid message role");
@@ -62,8 +60,8 @@ public interface PrintMessageHandler extends PrintAdvanceMessageHandler {
* @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());
default void printErrorMessage(Message errorMessage) {
printError(errorMessage.getContent().toString());
}
@Override
@@ -1,11 +1,11 @@
package me.neurodock.core.files;
import me.neurodock.core.Core;
import me.neurodock.core.Options;
import me.neurodock.core.Pair;
import me.neurodock.core.files.tools.ReadFileTool;
import me.neurodock.core.files.tools.WriteFileTool;
import me.neurodock.ollama.OllamaTool;
import org.intellij.lang.annotations.MagicConstant;
import me.neurodock.llm.tools.Tool;
import java.io.*;
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
* A good start is to use {@link Options#getFileHandlerDataLocation()} as it's located along all other files
* @param baseDirectory the directory to be used as base directory
*/
public FileHandler(@MagicConstant(valuesFromClass = FileHandlerLocation.class) String baseDirectory) {
public FileHandler(Path baseDirectory) {
try {
root = Path.of(baseDirectory).toAbsolutePath().normalize();
root = baseDirectory.toAbsolutePath().normalize();
if (!root.toFile().exists()) {
root.toFile().mkdirs();
}
@@ -49,14 +50,14 @@ public class FileHandler {
}
/**
* Returns a list of all {@link OllamaTool}'s this module adds
* Returns a list of all {@link Tool}'s this module adds
* @return
*/
public static ArrayList<Pair<? extends OllamaTool, String>> getTools() {
ArrayList<Pair<? extends OllamaTool, String>> fileTools = new ArrayList<>();
public static ArrayList<Tool> getTools() {
ArrayList<Tool> fileTools = new ArrayList<>();
fileTools.add(new Pair<>(new ReadFileTool(), Core.Source.CORE));
fileTools.add(new Pair<>(new WriteFileTool(), Core.Source.CORE));
fileTools.add(new ReadFileTool());
fileTools.add(new WriteFileTool());
return fileTools;
}
@@ -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,17 +1,17 @@
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 me.neurodock.llm.exceptions.ToolException;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.nio.file.Path;
public class ReadFileTool extends OllamaFunctionTool {
public class ReadFileTool extends FunctionTool {
FileHandler fs = FileHandler.getInstance();
@Override
public @NotNull String name() {
@@ -24,23 +24,18 @@ public class ReadFileTool extends OllamaFunctionTool {
}
@Override
public @NotNull OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("file_path", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The path to the file to be read", true)
public @NotNull ToolParameters parameters() {
return ToolParameters.builder()
.addProperty("file_path", ToolParameters.ToolParametersBuilder.Type.STRING, "The path to the file to be read", true)
.build();
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
String orgPath = null;
for (OllamaFunctionArgument arg : args) {
if(arg.argument().equals("file_path")) {
orgPath = (String) arg.value();
}
}
public @NotNull ToolResponse function(ToolArguments args) {
String orgPath = args.optArgument("file_path", String.class);
if(orgPath == null) {
throw new OllamaToolErrorException(this.name(), "Missing required argument 'file_path'");
throw new ToolException(this, "Missing required argument 'file_path'");
}
Path filePath = null;
@@ -49,19 +44,19 @@ public class ReadFileTool extends OllamaFunctionTool {
filePath = fs.resolve(orgPath);
}
catch (IOException ex) {
throw new OllamaToolErrorException(this.name(), ex);
throw new ToolException(this, ex);
}
File file = filePath.toFile();
if(!file.exists()) {
throw new OllamaToolErrorException(this.name(), "File does not exist: " + fs.path(file));
throw new ToolException(this, "File does not exist: " + fs.path(file));
}
BufferedReader br = null;
try{
br = new BufferedReader(new FileReader(file));
}catch (FileNotFoundException ex) {
throw new OllamaToolErrorException(this.name(), "File not found: " + fs.path(file));
throw new ToolException(this, "File not found: " + fs.path(file));
}
StringBuilder sb = new StringBuilder();
String tmp = null;
@@ -71,8 +66,8 @@ public class ReadFileTool extends OllamaFunctionTool {
}
}
catch (IOException e) {
throw new OllamaToolErrorException(this.name(), "Error reading file: " + fs.path(file));
throw new ToolException(this, "Error reading file: " + fs.path(file));
}
return new OllamaToolResponse(this.name(), sb.toString());
return new ToolResponse(this.name(), sb.toString());
}
}
@@ -1,18 +1,18 @@
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 me.neurodock.llm.exceptions.ToolException;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull;
import static me.neurodock.llm.tools.ToolParameters.ToolParametersBuilder.*;
import java.io.*;
import java.nio.file.Path;
public class WriteFileTool extends OllamaFunctionTool {
public class WriteFileTool extends FunctionTool {
FileHandler fs = FileHandler.getInstance();
@Override
@@ -21,8 +21,8 @@ public class WriteFileTool extends OllamaFunctionTool {
}
@Override
public @NotNull OllamaPerameter parameters() {
return OllamaPerameter.builder()
public @NotNull ToolParameters parameters() {
return ToolParameters.builder()
.addProperty("file_path", Type.STRING, "The path to the file to write to", true)
.addProperty("file_content", Type.STRING, "The content to write to the file", true)
.addProperty("overwrite", Type.BOOLEAN, "Overwrite the file, defaults to false", false)
@@ -30,30 +30,14 @@ public class WriteFileTool extends OllamaFunctionTool {
}
@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();
}
}
}
public @NotNull ToolResponse function(ToolArguments args) {
String path = args.getArgument("file_path", String.class);
String content = args.getArgument("file_content", String.class);
boolean overwrite = args.getArgument("overwrite", Boolean.class);
if(path == null || content == null || path.isBlank() || content.isBlank())
{
throw new OllamaToolErrorException(name(), "file_content or file_path is empty or null");
throw new ToolException(this, "file_content or file_path is empty or null");
}
Path filePath = null;
@@ -62,12 +46,12 @@ public class WriteFileTool extends OllamaFunctionTool {
filePath = fs.resolve(path);
}
catch (IOException ex) {
throw new OllamaToolErrorException(this.name(), ex);
throw new ToolException(this, 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));
throw new ToolException(this, "File already exists, and instructed to not be overwritten: " + fs.path(file));
}
else if(file.exists() && overwrite)
{
@@ -77,17 +61,17 @@ public class WriteFileTool extends OllamaFunctionTool {
try {
file.createNewFile();
} catch (IOException e) {
throw new OllamaToolErrorException(name(), e);
throw new ToolException(this, 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));
return new ToolResponse(name(), "Successfully wrote data to "+fs.path(file));
}
catch (IOException ex)
{
throw new OllamaToolErrorException(name(), ex);
throw new ToolException(this, ex);
}
}
}
@@ -1,17 +1,17 @@
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 me.neurodock.ollama.exceptions.OllamaToolErrorException;
import me.neurodock.llm.exceptions.ToolException;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull;
/**
* Provides the add_memory function.<br>
* This function adds a string to the memory.
*/
public class AddMemoryFunction extends OllamaFunctionTool {
public class AddMemoryFunction extends FunctionTool {
/**
* The CoreMemory instance.
*/
@@ -28,37 +28,23 @@ public class AddMemoryFunction extends OllamaFunctionTool {
}
@Override
public @NotNull 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)
public @NotNull ToolParameters parameters() {
return ToolParameters.builder()
.addProperty("memory", ToolParameters.ToolParametersBuilder.Type.STRING, "The memory to remember", true)
.addProperty("identity", ToolParameters.ToolParametersBuilder.Type.STRING, "The identity of the memory to remember", true)
.build();
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
if (args.length == 0) {
throw new OllamaToolErrorException(name(), "Missing memory argument");
}
String memory = null;
String identity = null;
for(OllamaFunctionArgument arg : args) {
if (arg.argument().equals("memory")) {
memory = (String) arg.value();
} else if (arg.argument().equals("identity")) {
identity = (String) arg.value();
} else {
throw new OllamaToolErrorException(name(), "Unknown argument: " + arg.argument());
}
}
public @NotNull ToolResponse function(ToolArguments args) {
String memory = args.optArgument("memory", String.class);
String identity = args.optArgument("identity", String.class);
if (memory == null || identity == null) {
throw new OllamaToolErrorException(name(), "Missing memory or identity argument");
throw new ToolException(this, "Missing memory or identity argument");
}
this.memory.addMemory(identity, memory);
return new OllamaToolResponse(name(), "Added "+identity+" to the memory");
return new ToolResponse(name(), "Added "+identity+" to the memory");
}
}
@@ -1,6 +1,6 @@
package me.neurodock.core.memory;
import me.neurodock.core.Core;
import me.neurodock.core.Options;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -17,7 +17,7 @@ public class 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.
@@ -1,12 +1,12 @@
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 me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull;
public class GetMemoriesFunction extends OllamaFunctionTool {
public class GetMemoriesFunction extends FunctionTool {
@Override
public @NotNull String name() {
return "get_memories";
@@ -18,12 +18,12 @@ public class GetMemoriesFunction extends OllamaFunctionTool {
}
@Override
public @NotNull OllamaPerameter parameters() {
return null;
public @NotNull ToolParameters parameters() {
return ToolParameters.empty();
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
return new OllamaToolResponse(name(), CoreMemory.getInstance().getMappedMemories());
public @NotNull ToolResponse function(ToolArguments args) {
return new ToolResponse(name(), CoreMemory.getInstance().getMappedMemories());
}
}
@@ -1,16 +1,16 @@
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 me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull;
/**
* Provides the get_memory function.<br>
* This function retrives all the memory.
*/
public class GetMemoryFunction extends OllamaFunctionTool {
public class GetMemoryFunction extends FunctionTool {
/**
* The CoreMemory instance.
*/
@@ -27,16 +27,16 @@ public class GetMemoryFunction extends OllamaFunctionTool {
}
@Override
public @NotNull OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("identity", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The identity of the memory to retrieve", true)
public @NotNull ToolParameters parameters() {
return ToolParameters.builder()
.addProperty("identity", ToolParameters.ToolParametersBuilder.Type.STRING, "The identity of the memory to retrieve", true)
.build();
}
@Override
public @NotNull 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 @NotNull ToolResponse function(ToolArguments args) {
return memory.getMemory(args.optArgument("identity", String.class))
.map(value -> new ToolResponse(name(), value))
.orElse(ToolResponse.empty(name(), "No memory found for key: " + args.optArgument("identity", String.class)));
}
}
@@ -1,13 +1,13 @@
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 me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull;
import org.json.JSONArray;
public class GetMemoryIdentitiesFunction extends OllamaFunctionTool {
public class GetMemoryIdentitiesFunction extends FunctionTool {
CoreMemory memory = CoreMemory.getInstance();
@Override
@@ -21,12 +21,12 @@ public class GetMemoryIdentitiesFunction extends OllamaFunctionTool {
}
@Override
public @NotNull OllamaPerameter parameters() {
return null;
public @NotNull ToolParameters parameters() {
return ToolParameters.empty();
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
return new OllamaToolResponse(this.name(), new JSONArray(memory.getMemoriesIdentity()).toString());
public @NotNull ToolResponse function(ToolArguments args) {
return new ToolResponse(this.name(), new JSONArray(memory.getMemoriesIdentity()).toString());
}
}
@@ -1,17 +1,17 @@
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 me.neurodock.ollama.exceptions.OllamaToolErrorException;
import me.neurodock.llm.exceptions.ToolException;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull;
/**
* Provides the remove_memory function.<br>
* This function removes a value from the memory.
*/
public class RemoveMemoryFunction extends OllamaFunctionTool {
public class RemoveMemoryFunction extends FunctionTool {
/**
* The CoreMemory instance.
*/
@@ -28,19 +28,19 @@ public class RemoveMemoryFunction extends OllamaFunctionTool {
}
@Override
public @NotNull OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("identity", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The identity of the memory to forget", true)
public @NotNull ToolParameters parameters() {
return ToolParameters.builder()
.addProperty("identity", ToolParameters.ToolParametersBuilder.Type.STRING, "The identity of the memory to forget", true)
.build();
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
if (args.length == 0) {
throw new OllamaToolErrorException(name(), "Missing memory argument");
public @NotNull ToolResponse function(ToolArguments args) {
String value = args.optArgument("identity", String.class);
if(value == null || value.isEmpty()) {
throw new ToolException(this, "Missing identity argument");
}
String value = (String) args[0].value();
memory.removeMemory(value);
return new OllamaToolResponse(name(), "Removed "+value+" to the memory");
return new ToolResponse(name(), "Removed "+value+" to the memory");
}
}
@@ -1,15 +1,11 @@
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 me.neurodock.llm.exceptions.ToolException;
import me.neurodock.llm.tools.*;
import org.jetbrains.annotations.NotNull;
public class AddArrayMemory extends OllamaFunctionTool {
public class AddArrayMemory extends FunctionTool {
private CoreMemory memory = CoreMemory.getInstance();
@@ -19,37 +15,29 @@ public class AddArrayMemory extends OllamaFunctionTool {
}
@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)
public @NotNull ToolParameters parameters() {
return ToolParameters.builder()
.addProperty("memory", ToolParameters.ToolParametersBuilder.Type.STRING, "The memory to remember", true)
.addProperty("index", ToolParameters.ToolParametersBuilder.Type.INT, "The index to put it at. Should be avoided, unless overwriting", false)
.build();
}
@Override
public @NotNull 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();
}
}
public @NotNull ToolResponse function(ToolArguments args) {
if (args.hasArgument("index")) {
String memory = args.optArgument("memory", String.class);
int index = args.optArgument("index", Integer.class);
if (memory == null || index < 0) {
throw new OllamaToolErrorException(name(), "no memory or index provided");
throw new ToolException(this, "no memory or index provided");
}
this.memory.addMemory(memory, index);
}
else {
if(!args[0].argument().equals("memory")) {
throw new OllamaToolErrorException(name(), "no memory provided");
if(!args.hasArgument("memory")) {
throw new ToolException(this, "no memory provided");
}
this.memory.addMemory(name(), (String) args[0].value());
this.memory.addMemory(name(), args.optArgument("memory", String.class));
}
return new OllamaToolResponse(name(), "Added arrayed memory");
return new ToolResponse(name(), "Added arrayed memory");
}
}
@@ -1,14 +1,14 @@
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 me.neurodock.llm.exceptions.ToolException;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull;
public class GetArrayMemory extends OllamaFunctionTool {
public class GetArrayMemory extends FunctionTool {
CoreMemory memory = CoreMemory.getInstance();
@@ -18,19 +18,17 @@ public class GetArrayMemory extends OllamaFunctionTool {
}
@Override
public @NotNull OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("index", Type.STRING, "The index to retrieve memory from", false)
public @NotNull ToolParameters parameters() {
return ToolParameters.builder()
.addProperty("index", ToolParameters.ToolParametersBuilder.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()));
public @NotNull ToolResponse function(ToolArguments args) {
if(!args.hasArgument("index")) throw new ToolException(this, "Missing index");
return memory.getMemory(args.optArgument("index", Integer.class, -1))
.map(value -> new ToolResponse(name(), value))
.orElse(ToolResponse.empty("No memory found for key: " + args.optArgument("index", -1)));
}
}
@@ -1,13 +1,13 @@
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.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull;
public class GetArrayedMemories extends OllamaFunctionTool {
public class GetArrayedMemories extends FunctionTool {
private CoreMemory memory = CoreMemory.getInstance();
@Override
public @NotNull String name() {
@@ -15,12 +15,12 @@ public class GetArrayedMemories extends OllamaFunctionTool {
}
@Override
public @NotNull OllamaPerameter parameters() {
return OllamaPerameter.empty();
public @NotNull ToolParameters parameters() {
return ToolParameters.empty();
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
return new OllamaToolResponse(name(), memory.getArrayMemories().toString());
public @NotNull ToolResponse function(ToolArguments args) {
return new ToolResponse(name(), memory.getArrayMemories().toString());
}
}
@@ -0,0 +1,18 @@
package me.neurodock.llm;
import java.util.ArrayList;
import java.util.List;
public class ChatObject {
private ArrayList<Message> messages = new ArrayList<>();
public List<Message> getConversation()
{
return messages;
}
public void addMessage(Message msg)
{
messages.add(msg);
}
}
@@ -0,0 +1,70 @@
package me.neurodock.llm;
import org.json.JSONArray;
public class Message {
protected Role role;
protected Object content;
protected String toolID;
protected JSONArray toolCalls;
/**
*
* @param role
* @param content
* @throws IllegalArgumentException If the backend does not support the content or role provided
* @implSpec Implementers of a backend is expected to sanitize the content to be valid for your backend.
* Throw {@link IllegalArgumentException} for cases where your backend does not support the arguments provided.
* <b>DO NOT</b> default to somthing else that is for the user of your backend to handle.
* You may however provide your own builder/contstuctor that can make such assumptions or defaults but this contractor <b>SHULD NEVER</b> do that
*/
public Message(Role role, Object content)
{
this.role = role;
this.content = content;
}
public Message(Role role, Object content, JSONArray toolCalls)
{
this(role, content);
this.toolCalls = toolCalls;
}
public Message(Role role, String toolID, Object content)
{
this(role, content);
this.toolID = toolID;
}
public Role getRole() {
return role;
}
public Object getContent() {
return content;
}
public String getToolID() {
return toolID;
}
public JSONArray getToolCalls() {
return toolCalls;
}
public static enum Role {
USER,
ASSISTANT,
TOOL,
SYSTEM;
}
@Override
public String toString() {
return "Message{" +
"role=" + role +
", content=" + content +
", toolID='" + toolID + '\'' +
", toolCalls=" + toolCalls +
'}';
}
}
@@ -0,0 +1,60 @@
package me.neurodock.llm;
import me.neurodock.llm.tools.Tool;
import me.neurodock.llm.tools.serializer.ToolSerializer;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* This interface is used for when a backend dose not supports streaming responses, otherwise use {@link StreamingModel}
*/
public interface Model {
/**
* Sends the given chat context to the model and requests the next response.
*
* @param obj the chat context, containing the conversation history to
* send to the model
* @param tools the tools available to the model for this request, or an
* empty list if none
* @return a future resolving to the model's reply as a {@link Message}
*
* @apiNote This method does not append the resulting {@link Message} to
* {@code obj} itself. The caller is expected to append it to
* {@code obj}'s conversation once the future completes, if the reply
* should persist as part of the chat history.
*/
CompletableFuture<Message> qurryModel(ChatObject obj, List<Tool> tools);
/**
* Sends a single, standalone message to the model, without any prior
* conversation context.
*
* @param msg the message to send
* @return a future resolving to the model's reply as a {@link Message}
*/
CompletableFuture<Message> singleFire(Message msg);
/**
* Sends a single, standalone message to the model with tool support,
* without any prior conversation context.
*
* @apiNote If no tools are needed for this request, prefer
* {@link #singleFire(Message)} instead of passing an empty or null
* {@code tools} list here.
* @param msg the message to send
* @param tools the tools available to the model for this request
* @return a future resolving to the model's reply as a {@link Message}
*/
CompletableFuture<Message> singleFire(Message msg, List<Tool> tools);
/**
* @return this backend's {@link ToolSerializer}, used to convert
* {@link Tool}s into the JSON shape this backend expects.
* Each backend instance owns its own — this is not shared
* across the JVM.
*/
ToolSerializer getToolSerializer();
}
@@ -0,0 +1,17 @@
package me.neurodock.llm;
import me.neurodock.llm.tools.Tool;
import org.json.JSONObject;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
/**
* This interface is used for when a backend supports streaming responses, otherwise use {@link Model}
*/
public interface StreamingModel extends Model {
CompletableFuture<Message> qurryModel(ChatObject obj, List<Tool> tools, Consumer<JSONObject> chunkConsumer);
CompletableFuture<Message> singleFire(Message msg, Consumer<JSONObject> chunkConsumer);
CompletableFuture<Message> singleFire(Message msg, List<Tool> tools, Consumer<JSONObject> chunkConsumer);
}
@@ -0,0 +1,11 @@
package me.neurodock.llm.exceptions;
public class ModelNotFoundException extends RuntimeException {
public ModelNotFoundException(String modelName) {
super("Model not found: " + modelName);
}
public ModelNotFoundException(String modelName, String backend) {
super("Model not found: \"" + modelName + "\" at backend: \"" + backend + "\"");
}
}
@@ -0,0 +1,21 @@
package me.neurodock.llm.exceptions;
import me.neurodock.llm.Message;
import me.neurodock.llm.tools.Tool;
public class ToolException extends RuntimeException {
protected Tool exceptingTool;
public ToolException(Tool tool, String message) {
super(message);
exceptingTool = tool;
}
public ToolException(Tool tool, Throwable cause) {
this(tool, cause.getMessage());
}
public Message getErrorMessage()
{
return new Message(Message.Role.TOOL, getMessage());
}
}
@@ -0,0 +1,41 @@
package me.neurodock.llm.tools;
import me.neurodock.core.ToolCallingRender;
import me.neurodock.llm.exceptions.ToolException;
import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;
public abstract class FunctionTool implements Tool {
/**
* The name of the tool. Used by the model to identify what it's calling.
* @return the tool's name
*/
@NotNull
@Override
public abstract String name();
/**
* The description of the tool. Used by the model to understand what the
* tool does. May be {@code null} to omit it from the serialized JSON.
* @return the tool's description, or {@code null}
*/
public String description() {
return null;
}
/**
* The parameters this tool accepts.
* @return the tool's parameter schema
*/
@NotNull
public abstract ToolParameters parameters();
/**
* Invokes the tool.
* @param args the arguments passed by the model
* @return the tool's response
* @throws ToolException if the tool encounters an error
*/
@NotNull
public abstract ToolResponse function(ToolArguments args) throws ToolException;
}
@@ -0,0 +1,27 @@
package me.neurodock.llm.tools;
import me.neurodock.core.ToolCallingRender;
import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
public interface Tool {
static List<Tool> emptyTools() {
return new ArrayList<>();
}
/**
* The name of the tool, as sent to the model. Used to route an
* incoming tool call back to this tool.
*
* @return the tool's name
*/
@NotNull
String name();
default ToolCallingRender renderCalling(JSONObject calling) {
return new ToolCallingRender.Default();
}
}
@@ -0,0 +1,66 @@
package me.neurodock.llm.tools;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
public class ToolArguments {
Map<String, Object> arguments = new HashMap<>();
public void addArguments(Map<String, Object> args)
{
arguments.putAll(args);
}
public void addArgument(String name, Object value)
{
arguments.put(name, value);
}
public Object getArgument(String name)
{
if(!arguments.containsKey(name)) throw new NoSuchElementException("Missing required argument: " + name);;
return arguments.get(name);
}
public <T> T getArgument(String name, Class<T> type)
{
if(!arguments.containsKey(name)) throw new NoSuchElementException("Missing required argument: " + name);;
Object o = arguments.get(name);
if(type.isAssignableFrom(o.getClass())) return type.cast(o);
throw new ClassCastException("Argument '" + name + "' is " + o.getClass().getSimpleName() + ", expected " + type.getSimpleName());
}
public Object optArgument(String name)
{
if(!arguments.containsKey(name)) return null;
return arguments.get(name);
}
public Object optArgument(String name, Object defaultValue)
{
if(!arguments.containsKey(name)) return defaultValue;
return arguments.get(name);
}
public <T> T optArgument(String name, Class<T> type)
{
if(!arguments.containsKey(name)) return null;
Object o = arguments.get(name);
if(type.isAssignableFrom(o.getClass())) return type.cast(o);
return null;
}
public <T> T optArgument(String name, Class<T> type, T defaultValue)
{
if(!arguments.containsKey(name)) return defaultValue;
Object o = arguments.get(name);
if(type.isAssignableFrom(o.getClass())) return type.cast(o);
return defaultValue;
}
public boolean hasArgument(String name)
{
return arguments.containsKey(name);
}
}
@@ -0,0 +1,212 @@
package me.neurodock.llm.tools;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* Represents the parameters of a tool.
* This is used by Ollama to determine the parameters of a tool.
*/
public class ToolParameters {
/**
* Creates a new instance of {@link ToolParameters}.
* @param properties The properties of the parameters
* @param required The required parameters
*/
private ToolParameters(Map<String, ToolParametersBuilder.Property> propertyMap, ArrayList<String> required) {
this.propertyMap = propertyMap;
this.required = required;
};
/**
* The properties of the parameters.
*/
private Map<String, ToolParametersBuilder.Property> propertyMap;
/**
* The required parameters.
*/
private ArrayList<String> required;
/**
* Gets the properties of the {@link ToolParameters}
* @return The properties of the {@link ToolParameters}
*/
public Map<String, ToolParametersBuilder.Property> getProperties() {
return propertyMap;
}
/**
* Gets the required parameters of the {@link ToolParameters}
* @return The required parameters of the {@link ToolParameters}
*/
public ArrayList<String> getRequired() {
return required;
}
/**
* Creates a new instance of {@link ToolParametersBuilder}.
* @return The {@link ToolParametersBuilder}
*/
public static ToolParametersBuilder builder() {
return new ToolParametersBuilder();
}
/**
* Creates an empty {@link ToolParameters}
* @return an empty {@link ToolParameters}
* @apiNote This is equvalent to
* <pre>{@code
* ToolParameters.builder().build();
* }</pre>
*/
public static ToolParameters empty() {
return builder().build();
}
/**
* Represents a builder for {@link ToolParameters}.
*/
public static class ToolParametersBuilder {
/**
* The properties of the parameters.
*/
private Map<String, Property> propertyMap = new HashMap<>();
/**
* The required parameters.
*/
private ArrayList<String> required = new ArrayList<>();
/**
* Add an optinal perameter to this {@link ToolParametersBuilder}
* @param name The name of the parameter
* @param type The type of the parameter
* @param description The description of the parameter
* @return The {@link ToolParametersBuilder}
* @apiNote Prefer {@link #addProperty(String, Type, String, boolean)} to be explicit about required state
*/
public ToolParametersBuilder addProperty(String name, Type type, String description) {
return addProperty(name, type, description, false);
}
/**
* Add a potentialy required peremeter to this {@link ToolParametersBuilder}.
* @param name The name of the parameter
* @param type The type of the parameter
* @param description The description of the parameter
* @param required The required state of the parameter
* @return The {@link ToolParametersBuilder}
*/
public ToolParametersBuilder addProperty(String name, Type type, String description, boolean required) {
if(name == null || type == null || description == null) {
return this;
}
propertyMap.put(name, new Property(type, description));
if(required) {
this.required.add(name);
}
return this;
}
/**
* Makes a previusly optinal perameter required for this {@link ToolParametersBuilder}
* @param name The name of the parameter
* @return The {@link ToolParametersBuilder}
*/
public ToolParametersBuilder required(String name) {
if (!propertyMap.containsKey(name)) {
throw new IllegalArgumentException("Cannot require unknown property: " + name);
}
required.add(name);
return this;
}
/**
* Removes a property from the parameters.
* @param name The name of the property to remove
* @return The {@link ToolParametersBuilder}
*/
public ToolParametersBuilder removeProperty(String name) {
propertyMap.remove(name);
required.remove(name);
return this;
}
/**
* Builds the {@link ToolParameters}
* @return The {@link ToolParameters}
*/
public ToolParameters build() {
return new ToolParameters(propertyMap, required);
}
/**
* Represents a property of a parameter.
*
* @param type The type of the property.
* @param description The description of the property.
*/
public record Property(Type type, String description) {
/**
* Creates a new instance of {@link Property}.
*
* @param type The type of the property
* @param description The description of the property
*/
public Property {
}
}
/**
* Represents the type of parameter.
*/
public enum Type {
/**
* Represents a string parameter.
*/
STRING("string"),
/**
* Represents an integer parameter.
*/
INT("integer"),
/**
* Represents a boolean parameter.
*/
BOOLEAN("boolean"),
/**
* Represents a enum parameter.
*/
ENUM("enum"),
/**
* Represents a array parameter.
*/
ARRAY("array"),
/**
* Represents a object parameter.
*/
OBJECT("object");
/**
* The type of the parameter.
*/
private final String type;
/**
* Gets the type of the parameter.
* @return The type of the parameter
*/
public String getType() {
return type;
}
/**
* Creates a new instance of {@link Type}.
* @param type The type of the parameter
*/
Type(String type) {
this.type = type;
}
}
}
}
@@ -1,33 +1,35 @@
package me.neurodock.ollama;
package me.neurodock.llm.tools;
import me.neurodock.llm.Message;
import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;
/**
* Represents a response from a tool.
*/
public class OllamaToolResponse extends OllamaMessage {
public class ToolResponse extends Message {
/**
* Returns an empty tool response
* See {@link OllamaToolResponse#empty(String, String)} for a reasoned/described response
* See {@link ToolResponse#empty(String, String)} for a reasoned/described response
* @param tool The tool that responded
* @return an empty tool response
*/
public static OllamaToolResponse empty(String tool)
public static ToolResponse 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
* See {@link ToolResponse#empty(String)} for reason/description less response
* @param tool The tool that responded
* @param description A description for why this is empty
* @return an empty tool response with a reason
*/
public static OllamaToolResponse empty(String tool, String description)
public static ToolResponse empty(String tool, String description)
{
return new OllamaToolResponse(tool, "Empty! reason: " + description);
return new ToolResponse(tool, "Empty! reason: " + description);
}
/**
@@ -40,12 +42,12 @@ public class OllamaToolResponse extends OllamaMessage {
private final String response;
/**
* Creates a new instance of {@link OllamaToolResponse}.
* Creates a new instance of {@link ToolResponse}.
* @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());
public ToolResponse(String tool, String response) {
super(Message.Role.TOOL, new JSONObject().put("tool", tool).put("result", response).toString());
this.tool = tool;
this.response = response;
}
@@ -65,4 +67,8 @@ public class OllamaToolResponse extends OllamaMessage {
public String getResponse() {
return response;
}
public void setToolID(@NotNull String name) {
this.toolID = name;
}
}
@@ -0,0 +1,36 @@
package me.neurodock.llm.tools.serializer;
import me.neurodock.llm.tools.Tool;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
public class ToolSerializer {
private final Map<Class<? extends Tool>, ToolToJSON<? extends Tool>> serializers = new HashMap<>();
public <T extends Tool> boolean addSerializer(Class<T> clazz, ToolToJSON<T> serializer) {
if(!serializers.containsKey(clazz)) {
serializers.put(clazz, serializer);
return true;
}
return false;
}
@SuppressWarnings({"unchecked"})
public <T extends Tool> ToolToJSON<T> getSerializer(Class<T> clazz) {
return (ToolToJSON<T>) serializers.get(clazz);
}
public <T extends Tool> JSONObject serialize(T tool)
{
if(!serializers.containsKey(tool.getClass())) throw new IllegalArgumentException("Tool " + tool.getClass() + " not found!");
@SuppressWarnings({"unchecked"})
ToolToJSON<T> serializer = (ToolToJSON<T>) serializers.get(tool.getClass());
return serializer.toJSON(tool);
}
public boolean canSerialize(Class<? extends Tool> clazz) {
return serializers.containsKey(clazz);
}
}
@@ -0,0 +1,14 @@
package me.neurodock.llm.tools.serializer;
import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.llm.tools.Tool;
import org.json.JSONObject;
public interface ToolToJSON<T extends Tool> {
/**
* @param tool the tool to serialize
* @return the JSON representation of {@code tool}, in the shape this
* backend expects
*/
JSONObject toJSON(T tool);
}
@@ -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,96 +0,0 @@
package me.neurodock.ollama;
import me.neurodock.core.Core;
import me.neurodock.core.ToolCallingRender;
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
import org.jetbrains.annotations.NotNull;
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 = "";
/**
* returns the source of this tool
* @return String source
*/
public final String getSource()
{
return source;
}
public JSONObject toJSON() {
JSONObject ret = new JSONObject();
ret.put("type", "function");
JSONObject function = new JSONObject();
function.put("name", name()+"_"+(source != null ? source : ""));
if(description() != null) {
function.put("description", description());
}
function.put("parameters", (parameters() == null?
new JSONObject() : parameters().toJSON()));
ret.put("function", function);
return ret;
}
/**
* 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();
/**
* The description of the tool
* This is used by Ollama to know what the tool does
* @return The description of the tool
*/
public String description(){
return null;
}
/**
* 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)}
* @param args The arguments to pass to the tool, if any
* @return The response from the tool, if null return {@link OllamaToolResponse}
* @throws OllamaToolErrorException If the tool encounters an error
*/
abstract public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args);
}
@@ -1,111 +0,0 @@
package me.neurodock.ollama;
import me.neurodock.core.Core;
import me.neurodock.core.Pair;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Spliterator;
import java.util.function.Consumer;
public class OllamaFunctionTools implements Iterable<Pair<OllamaFunctionTool, String>> {
/**
* A list of tools for the OllamaObject.
* OPS! Shuld only be ussed to add a set of tools to the OllamaObject, not to be used for storage of tools internaly or externaly
*/
private ArrayList<OllamaFunctionTool> tools;
/**
* A list of source for the tools.
* OPS! Shuld only be ussed to add a set of tools to the OllamaObject, not to be used for storage of tools internaly or externaly
* OPS! most be the same size as the tools list! since each tool matches to a source!
*/
private ArrayList<String> source;
/**
* Gets the tools of the {@link OllamaFunctionTools}
* @param tools A list of {@link OllamaFunctionTool}
* @param source The source of the tools
*/
private OllamaFunctionTools(ArrayList<OllamaFunctionTool> tools, ArrayList<String> source) {
if(source == null || tools == null || source.size() != tools.size() || source.isEmpty())
throw new IllegalArgumentException("The source and tools must be the same size! and not empty!");
this.tools = tools;
this.source = source;
}
/**
* Gets the tools of the {@link OllamaFunctionTools}
* @param tools A list of {@link OllamaFunctionTool}
* @param source The source of the tools
*/
private OllamaFunctionTools(OllamaFunctionTool[] tools, @MagicConstant(valuesFromClass = Core.Source.class) String[] source) {
if(source == null || tools == null || source.length != tools.length || source.length == 0)
throw new IllegalArgumentException("The source and tools must be the same size! and not empty!");
this.tools = new ArrayList<>();
this.source = new ArrayList<>();
for (OllamaFunctionTool tool : tools) {
this.tools.add(tool);
this.source.add(tool.name());
}
for (String s : source) {
if (s.equals(Core.Source.CTP)) {
this.source.add(s);
}
}
}
public static OllamaFunctionToolsBuilder builder() {
return new OllamaFunctionToolsBuilder();
}
@Override
public @NotNull Iterator<Pair<OllamaFunctionTool, String>> iterator() {
ArrayList<Pair<OllamaFunctionTool, String>> pairs = new ArrayList<>();
for (int i = 0; i < tools.size(); i++) {
pairs.add(new Pair<>(tools.get(i), source.get(i)));
}
return pairs.iterator();
}
@Override
public void forEach(Consumer<? super Pair<OllamaFunctionTool, String>> action) {
for (Pair<OllamaFunctionTool, String> pair : this) {
action.accept(pair);
}
}
@Override
public Spliterator<Pair<OllamaFunctionTool, String>> spliterator() {
// TODO: Implement this method
throw new UnsupportedOperationException("Not implemented yet");
//return Iterable.super.spliterator();
}
public static class OllamaFunctionToolsBuilder {
private ArrayList<OllamaFunctionTool> tools = new ArrayList<>();
private ArrayList<String> source = new ArrayList<>();
public OllamaFunctionToolsBuilder addTool(OllamaFunctionTool tool, @MagicConstant(valuesFromClass = Core.Source.class) String source) {
this.tools.add(tool);
this.source.add(source);
return this;
}
public OllamaFunctionToolsBuilder addTools(HashMap<OllamaFunctionTool, String> tools) {
for (OllamaFunctionTool tool : tools.keySet()) {
this.tools.add(tool);
this.source.add(tools.get(tool));
}
return this;
}
public OllamaFunctionTools build() {
return new OllamaFunctionTools(tools, source);
}
}
}
@@ -1,48 +0,0 @@
package me.neurodock.ollama;
import org.json.JSONObject;
/**
* Represents a message sent by a Tool, Assistant(Ollama), or User.
*/
public class OllamaMessage {
/**
* The role of the message.
*/
OllamaMessageRole role;
/**
* The content of the message.
*/
String content;
/**
* Creates a new instance of OllamaMessage.
* @param role The role of the message
* @param content The content of the message
*/
public OllamaMessage(OllamaMessageRole role, String content) {
this.role = role;
this.content = content;
}
/**
* @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;
}
public JSONObject toJSON() {
JSONObject json = new JSONObject();
json.put("role", role.getRole());
json.put("content", content.replace("\n", "\\n"));
return json;
}
}
@@ -1,58 +0,0 @@
package me.neurodock.ollama;
/**
* Represents the role of a message.
* This is used by Ollama to determine the role of a message.
*/
public enum OllamaMessageRole {
/**
* Represents a user message.
*/
USER("user"),
/**
* Represents an assistant message.
*/
ASSISTANT("assistant"),
/**
* Represents a tool message
*/
TOOL("tool"),
/**
* Represents a system message.
*/
SYSTEM("system");
/**
* The role of the message.
*/
private String role;
/**
* Creates a new instance of OllamaMessageRole.
* @param role The role of the message
*/
OllamaMessageRole(String role) {
this.role = role;
}
/**
* Gets the role of the message.
* @return The role of the message
*/
public String getRole() {
return role;
}
/**
* Gets the role of the message from a string.
* @param role The role of the message as a string
* @return The role of the message
*/
public static OllamaMessageRole fromRole(String role) {
for(OllamaMessageRole roleRole : values()) {
if(roleRole.role.equals(role.toLowerCase()))
return roleRole;
}
throw new IllegalArgumentException("Invalid role: " + role);
}
}
@@ -1,37 +0,0 @@
package me.neurodock.ollama;
import org.json.JSONArray;
import org.json.JSONObject;
/**
* Represents a message sent by a Tool.
*/
public class OllamaMessageToolCall extends OllamaMessage{
/**
* The tool calls in the message
*/
private JSONArray tool_calls;
/**
* Creates a new instance of OllamaMessage
* @param role The role of the message
* @param content The content of the message
* @param tool_calls The tool calls in the message
*/
public OllamaMessageToolCall(OllamaMessageRole role, String content, JSONArray tool_calls) {
super(role, content);
this.tool_calls = tool_calls;
}
@Override
public JSONObject toJSON() {
JSONObject json = new JSONObject();
json.put("role", role);
json.put("content", content);
json.put("tool_calls", tool_calls);
return json;
}
}
@@ -1,592 +0,0 @@
package me.neurodock.ollama;
import me.neurodock.core.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 org.intellij.lang.annotations.MagicConstant;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Represents an Ollama Object.
* This is used to represent the state of the Ollama Object.
* This is used by the Core to store the state of the Ollama Object.
* This is used by the API to send the state of the Ollama Object to the client.
* @see Core#setOllamaObject(OllamaObject)
*/
public class OllamaObject {
/**
* The model of the Ollama Object.
*/
String model;
/**
* The messages of the Ollama Object.
*/
ArrayList<OllamaMessage> messages;
/**
* The tools of the Ollama Object.
*/
ArrayList<Pair<OllamaTool, String>> tools;
/**
* The format of the Ollama Object.
*/
JSONObject format;
/**
* The options of the Ollama Object.
*/
Map<String, Object> options;
/**
* If the Ollama Object is streamed.
*/
boolean stream;
/**
* The keep alive of the Ollama Object.
*/
String keep_alive;
/**
* Creates a new instance of OllamaObject.
* @param model The model of the Ollama Object. see {@link OllamaObject#model}
* @param messages The messages of the Ollama Object. see {@link OllamaObject#messages}
* @param tools The tools of the Ollama Object. see {@link OllamaObject#tools}
* @param format The format of the Ollama Object. see {@link OllamaObject#format}
* @param options The options of the Ollama Object. see {@link OllamaObject#options}
* @param stream If the Ollama Object is streamed. see {@link OllamaObject#stream}
* @param keep_alive The keep alive of the Ollama Object. see {@link OllamaObject#keep_alive}
*/
private OllamaObject(String model, ArrayList<OllamaMessage> messages, ArrayList<Pair<OllamaTool, String>> tools, JSONObject format, Map<String, Object> options, boolean stream, String keep_alive, LLMSystemPrompt prompt) {
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();
if(launchOptions.isLoadOld()) {
System.out.println("Loading old data...");
File f = new File(Core.DATA_DIR+"/messages.json");
if(f.exists()) {
try {
BufferedReader br = new BufferedReader(new FileReader(f));
StringBuilder data = new StringBuilder();
String buffer = null;
while ((buffer = br.readLine()) != null) {
data.append(buffer).append("\n");
}
JSONArray jsonArray = new JSONArray(data.toString());
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject obj = jsonArray.getJSONObject(i);
OllamaMessage message;
if (!obj.has("tool_calls")) {
message = new OllamaMessage(OllamaMessageRole.fromRole(obj.getString("role")), obj.getString("content"));
} else {
message = new OllamaMessageToolCall(OllamaMessageRole.fromRole(obj.getString("role")), obj.getString("content"), obj.getJSONArray("tool_calls"));
}
this.messages.add(message);
}
}catch (Exception e) {
System.out.println("Error loading old data");
e.printStackTrace();
}
}
else
{
System.out.println("No old data found, skipping loading old data.");
}
}
if(prompt == null) {
// Do nothing
}
else if(!this.messages.isEmpty())
{
OllamaMessage systemPrompt = this.messages.getFirst();
if(systemPrompt.getRole() != OllamaMessageRole.SYSTEM)
{
System.out.println("FIRST MESSAGE ISN'T A SYSTEM PROMPT!. Pushing all messages forward and injecting the system prompt");
Core.writeLog("FIRST MESSAGE ISN'T A SYSTEM PROMPT!. Pushing all messages forward and injecting the system prompt");
ArrayList<OllamaMessage> newMessages = new ArrayList<>();
newMessages.add(prompt.generateSystemPrompt());
newMessages.addAll(messages);
this.messages = newMessages;
}
else
{
System.out.println("Replacing System prompt...");
Core.writeLog("Replacing System prompt...");
systemPrompt = prompt.generateSystemPrompt();
this.messages.set(0, systemPrompt);
}
}
else
{
System.out.println("No old prompts. Inserting system prompt...");
Core.writeLog("No old prompts. Inserting system prompt...");
this.messages.add(prompt.generateSystemPrompt());
}
}
/**
* Gets the model of the Ollama Object.
* @return The model of the Ollama Object
*/
public String getModel() {
return model;
}
/**
* Gets the messages
* @return The messages
*/
public ArrayList<OllamaMessage> getMessages() {
return messages;
}
/**
* Gets the tools
* @return The tools
*/
public ArrayList<Pair<OllamaTool, String>> getTools() {
return tools;
}
/**
* 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 removeTool(OllamaTool tool) {
tools.remove(tool);
}
/**
* Gets the format of the Ollama Object.
* @return The format of the Ollama Object
*/
public JSONObject getFormat() {
return format;
}
/**
* Gets the options of the Ollama Object.
* @return The options of the Ollama Object
*/
public Map<String, Object> getOptions() {
return options;
}
/**
* Gets if the Ollama Object is streamed.
* @return If the Ollama Object is streamed
*/
public boolean isStream() {
return stream;
}
/**
* Gets the keep alive of the Ollama Object.
* @return The keep alive of the Ollama Object
*/
public String getKeep_alive() {
return keep_alive;
}
/**
* Adds a message to the Ollama Object
* @param message The message to add
*/
public void addMessage(OllamaMessage message) {
messages.add(message);
}
/**
* 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);
}
/**
* Sets the system message, replacing any existing one at the start of {@link #messages},
* or inserting one at the start if none exists yet.
* @param message the system message text
*/
public void setSystemMessage(String message) {
applySystemPrompt(new OllamaMessage(OllamaMessageRole.SYSTEM, message));
}
/**
* Replaces the current system prompt if one exists at the start of {@link #messages},
* or inserts one at the start if none exists yet.
*/
public void setSystemPrompt(LLMSystemPrompt prompt) {
applySystemPrompt(prompt.generateSystemPrompt()); // shared with the constructor
}
private void applySystemPrompt(OllamaMessage message) {
if (message == null) return;
if (messages.isEmpty()) {
messages.add(message);
} else if (messages.getFirst().getRole() != OllamaMessageRole.SYSTEM) {
messages.addFirst(message);
} else {
messages.set(0, message);
}
}
public JSONObject toJSON()
{
JSONObject json = new JSONObject();
JSONArray tools = new JSONArray();
for (Pair<OllamaTool, String> tool : this.tools) {
if(tool.getKey().getClass().isInterface()) continue;
JSONObject obj = tool.getKey().toJSON();
//obj.put("name", obj.getString("name") + tool.getValue()); // Injects the source of the tool into the name
tools.put(obj);
}
JSONArray messages = new JSONArray();
for (OllamaMessage message : this.messages) {
messages.put(message.toJSON());
}
json.put("model", model);
json.put("messages", messages);
json.put("tools", tools);
json.put("format", format);
json.put("options", options);
json.put("stream", stream);
json.put("keep_alive", keep_alive);
return json;
}
/**
* Creates a new instance of OllamaObjectBuilder.
* @return The {@link OllamaObjectBuilder}
*/
public static OllamaObjectBuilder builder()
{
return new OllamaObjectBuilder();
}
/**
* Represents a builder for OllamaObject.
*/
public static class OllamaObjectBuilder {
/**
* The model of the Ollama Object.
*/
String model;
/**
* The messages of the Ollama Object.
*/
ArrayList<OllamaMessage> messages = new ArrayList<>();
/**
* The tools of the Ollama Object.
*/
ArrayList<Pair<OllamaTool, String>> tools = new ArrayList<>();
/**
* The format of the Ollama Object.
*/
JSONObject format;
/**
* The options of the Ollama Object.
*/
Map<String, Object> options = new HashMap<>();
/**
* If the Ollama Object is streamed.
*/
boolean stream = false;
/**
* The keep alive of the Ollama Object.
*/
String keep_alive;
/**
* This represents the System Prompt for the LLM
*/
LLMSystemPrompt systemPrompt;
/**
* Creates a new instance of {@link OllamaObjectBuilder}.
*/
public OllamaObjectBuilder() {}
/**
* Sets the format of the Ollama Object as a JSON schema.
* @param format The format of the Ollama Object as a JSON schema
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder format(String format) {
this.format = new JSONObject(format);
return this;
}
/**
* Sets the options of the Ollama Object.
* @param options The options of the Ollama Object
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder options(Map<String, Object> options) {
this.options.putAll(options);
return this;
}
/**
* Sets an option of the Ollama Object.
* @param key The key of the option
* @param value The value of the option
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder option(String key, String value) {
this.options.put(key, value);
return this;
}
/**
* Sets if the Ollama Object is streamed.
* @param stream If the Ollama Object is streamed
* @deprecated This should be false due to being broken in the current version of this system
* @return The {@link OllamaObjectBuilder}
*/
@Deprecated
public OllamaObjectBuilder stream(boolean stream) {
this.stream = stream;
return this;
}
/**
* Sets the keep alive of the Ollama Object.<br>
* This is a string formated as "minutes"m or "hours"h or "days"d
* @param keep_alive The keep alive of the Ollama Object
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder keep_alive(String keep_alive) {
this.keep_alive = keep_alive;
return this;
}
/**
* Sets the keep alive of the Ollama Object.<br>
* @param minutes The keep alive of the Ollama Object in minutes
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder keep_alive(int minutes) {
this.keep_alive = minutes+"m";
return this;
}
/**
* Adds a tool to the Ollama Object
* 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));
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()));
}
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;
}
/**
* Adds messages to the Ollama Object
* @param messages The messages to add
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder addMessages(OllamaMessage... messages) {
this.messages.addAll(List.of(messages));
return this;
}
/**
* Adds a message to the Ollama Object
* @param messages The message to add
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder addMessage(OllamaMessage messages) {
this.messages.add(messages);
return this;
}
/**
* Sets the model of the Ollama Object
* @param model The model of the Ollama Object
* @return The {@link OllamaObjectBuilder}
*/
public OllamaObjectBuilder setModel(String model) {
this.model = model;
return this;
}
/**
* 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);
//throw new IllegalArgumentException("FileHandler is not supported yet!");
return addTools(FileHandler.getTools());
}
public OllamaObjectBuilder setSystemPrompt(LLMSystemPrompt systemPrompt) {
this.systemPrompt = systemPrompt;
return this;
}
/**
* Builds the {@link OllamaObject}
* @return The {@link OllamaObject}
*/
public OllamaObject build() {
return new OllamaObject(model, messages, tools, format, options, stream, keep_alive, systemPrompt);
}
}
}
@@ -1,298 +0,0 @@
package me.neurodock.ollama;
import me.neurodock.plugin.tool.ToolParameters;
import org.json.JSONObject;
import java.util.*;
/**
* 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
* @param required The required parameters
*/
private OllamaPerameter(JSONObject properties, String[] required) {
this.properties = properties;
this.required = required;
};
public JSONObject toJSON() {
JSONObject json = new JSONObject();
json.put("type", "object");
json.put("properties", properties);
json.put("required", required);
return json;
}
/**
* the properties of the parameters
*/
private final JSONObject properties;
/**
* the required parameters
*/
private final String[] required;
/**
* Gets the properties of the {@link OllamaPerameter}
* @return The properties of the {@link OllamaPerameter}
*/
public JSONObject getProperties() {
return properties;
}
/**
* Gets the required parameters of the {@link OllamaPerameter}
* @return The required parameters of the {@link OllamaPerameter}
*/
public String[] getRequired() {
return required;
}
/**
* Creates a new instance of {@link OllamaPerameterBuilder}.
* @return The {@link OllamaPerameterBuilder}
*/
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}.
*/
public static class OllamaPerameterBuilder {
/**
* The properties of the parameters.
*/
Map<String, Property> propertyMap = new HashMap<>();
/**
* The required parameters.
*/
ArrayList<String> required = new ArrayList<>();
/**
* Add an optinal perameter to this {@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);
}
/**
* Add a potentialy required peremeter to this {@link OllamaPerameterBuilder}.
* @param name The name of the parameter
* @param type The type of the parameter
* @param description The description of the parameter
* @param required The required state of the parameter
* @return The {@link OllamaPerameterBuilder}
*/
public OllamaPerameterBuilder addProperty(String name, Type type, String description, boolean required) {
if(name == null || type == null || description == null) {
return this;
}
propertyMap.put(name, new Property(type.getType(), description));
if(required) {
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}
* @param name The name of the parameter
* @return The {@link OllamaPerameterBuilder}
*/
public OllamaPerameterBuilder required(String name) {
required.add(name);
return this;
}
/**
* Removes a property from the parameters.
* @param name The name of the property to remove
* @return The {@link OllamaPerameterBuilder}
*/
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}
*/
public OllamaPerameter build() {
JSONObject properties = new JSONObject();
for(String name : propertyMap.keySet()) {
properties.put(name, propertyMap.get(name).toJSON());
}
return new OllamaPerameter(properties, required.toArray(new String[0]));
}
/**
* Represents a property of a parameter.
*/
private class Property {
/**
* The type of the property.
*/
String type;
/**
* The description of the property.
*/
String description;
/**
* Creates a new instance of {@link Property}.
* @param type The type of the property
* @param description The description of the property
*/
public Property(String type, String description) {
this.type = type;
this.description = description;
}
public JSONObject toJSON() {
JSONObject json = new JSONObject();
json.put("type", type);
json.put("description", description);
return json;
}
}
/**
* Represents the type of parameter.
*/
public enum Type {
/**
* Represents a string parameter.
*/
STRING("string"),
/**
* Represents an integer parameter.
*/
INT("int"),
/**
* Represents a boolean parameter.
*/
BOOLEAN("boolean"),
/**
* Represents a enum parameter.
*/
ENUM("enum"),
/**
* Represents a array parameter.
*/
ARRAY("array"),
/**
* Represents a object parameter.
*/
OBJECT("object");
/**
* The type of the parameter.
*/
private final String type;
/**
* Gets the type of the parameter.
* @return The type of the parameter
*/
public String getType() {
return type;
}
/**
* Creates a new instance of {@link Type}.
* @param type The type of the parameter
*/
Type(String type) {
this.type = type;
}
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,10 +0,0 @@
package me.neurodock.ollama;
import org.json.JSONObject;
/**
* Represents a tool.
*/
public interface OllamaTool {
public JSONObject toJSON();
}
@@ -1,31 +0,0 @@
package me.neurodock.ollama;
import org.json.JSONObject;
/**
* Represents an error from a tool.<br>
* This is used by a tool to indicate to Ollama that an error occurred.
*/
public class OllamaToolError extends OllamaMessage {
/**
* The error from the tool.
*/
String error;
/**
* Creates a new instance of OllamaToolError.
* @param error The error from the tool
*/
public OllamaToolError(String error) {
super(OllamaMessageRole.TOOL, new JSONObject().put("error", error).toString());
this.error = error;
}
/**
* Gets the error from the tool.
* @return The error from the tool
*/
public String getError() {
return error;
}
}
@@ -1,50 +0,0 @@
package me.neurodock.ollama.exceptions;
import me.neurodock.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)}
*/
public class OllamaToolErrorException extends RuntimeException {
/**
* The tool that caused the error.
*/
private final String tool;
/**
* The error from the tool.
*/
private final String error;
/**
* Creates a new instance of OllamaToolErrorException.
* @param tool The tool that caused the error
* @param error The error from the tool
*/
public OllamaToolErrorException(String tool, String error) {
super(tool + ": " + error);
this.tool = tool;
this.error = error;
}
public OllamaToolErrorException(String tool, Exception ex) {
this(tool, ex.getMessage());
}
/**
* Gets the tool that caused the error.
* @return The tool that caused the error
*/
public String getTool() {
return tool;
}
/**
* Gets the error from the tool.
* @return The error from the tool
*/
public String getError() {
return error;
}
}
@@ -1,15 +0,0 @@
package me.neurodock.ollama.utils;
import me.neurodock.ollama.OllamaMessage;
import me.neurodock.ollama.OllamaMessageRole;
public class SystemMessage extends OllamaMessage {
/**
* Creates a new instance of OllamaMessage.
*
* @param systemMessage The content of the message
*/
public SystemMessage(String systemMessage) {
super(OllamaMessageRole.SYSTEM, systemMessage);
}
}
@@ -2,8 +2,9 @@ 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.llm.tools.FunctionTool;
import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.plugin.Data;
import me.neurodock.plugin.LoadedPlugin;
import me.neurodock.plugin.Plugin;
@@ -11,11 +12,11 @@ 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 javax.naming.OperationNotSupportedException;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
@@ -40,12 +41,12 @@ public class Loader {
* Mock example!
* @return
*/
public OllamaFunctionTool[] getTools(Plugin plugin) {
public me.neurodock.llm.tools.Tool[] getTools(Plugin plugin) {
ArrayList<OllamaFunctionTool> tools = new ArrayList<>();
ArrayList<me.neurodock.llm.tools.Tool> tools = new ArrayList<>();
for(Tool tool : plugin.getTools()) {
tools.add(new OllamaFunctionTool() {
tools.add(new FunctionTool() {
@Override
public @NotNull String name() {
return tool.name() + "_" + plugin.getMetadata().getName();
@@ -57,14 +58,16 @@ public class Loader {
}
@Override
public @NotNull OllamaPerameter parameters() {
return OllamaPerameter.builder().of(tool.parameters()).build();
public @NotNull ToolParameters parameters() {
throw new RuntimeException(new OperationNotSupportedException("No"));
//return ToolParameters.builder().of(tool.parameters()).build();
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
public @NotNull me.neurodock.llm.tools.ToolResponse function(ToolArguments args) {
throw new UnsupportedOperationException("no");
// Wrap OllamaFunctionArguments[] to ether ToolArgument[] or ToolArguments(current implementation)
ToolArguments toolArgs = new ToolArguments();
/*ToolArguments toolArgs = new ToolArguments();
for (OllamaFunctionArgument arg : args) {
toolArgs.addArgument(arg.argument(), arg.value());
}
@@ -75,17 +78,12 @@ public class Loader {
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 JSONObject toJSON() {
return tool.getToolJSON();
return new OllamaToolResponse(toolResponse.name(), toolResponse.response());*/
}
});
}
return tools.toArray(tools.toArray(new OllamaFunctionTool[0]));
return tools.toArray(tools.toArray(new me.neurodock.llm.tools.Tool[0]));
}
+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.Test;
import java.nio.file.Path;
public class FileTest {
@Test
void TestError()
{
FileHandler fileHandler = new FileHandler("./test/files");
FileHandler fileHandler = new FileHandler(Path.of("./test/files"));
Assertions.assertThrowsExactly(FileHandlerException.class, () -> fileHandler.readFile("../build.gradle"));
}
}
-12
View File
@@ -1,12 +0,0 @@
package plugin;
import me.neurodock.plugin.Plugin;
import me.neurodock.plugin.PluginMetadata;
import org.jetbrains.annotations.NotNull;
public class Test extends Plugin {
@Override
public @NotNull PluginMetadata getMetadata() {
return null;
}
}
-37
View File
@@ -1,37 +0,0 @@
package plugin;
//// This class is broken due to the current refactoring on the RPCP part of this project.
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
public class Tool extends OllamaFunctionTool {
//@InjectPlugin(classType = Test.class)
Test core;
@Override
public @NotNull String name() {
return "";
}
@Override
public String description() {
return "";
}
@Override
public @NotNull OllamaPerameter parameters() {
return null;
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
return null;
}
}
@@ -1,15 +1,22 @@
package me.neurodock.display;
import me.neurodock.backend.open.ai.OpenAIModel;
import me.neurodock.core.Core;
import me.neurodock.core.Options;
import me.neurodock.core.Pair;
import me.neurodock.core.PrintMessageHandler;
import me.neurodock.core.files.FileHandlerLocation;
import me.neurodock.core.memory.CoreMemory;
import me.neurodock.llm.ChatObject;
import me.neurodock.llm.Message;
import me.neurodock.llm.exceptions.ToolException;
import me.neurodock.llm.tools.*;
import me.neurodock.ollama.*;
import me.neurodock.ollama.utils.SystemMessage;
import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;
import java.io.*;
import java.nio.file.Path;
import java.util.*;
import static me.neurodock.core.Core.writeLog;
@@ -21,6 +28,13 @@ import static me.neurodock.core.Core.writeLog;
*/
public class Display {
/*
* Inizilizer
*/
{
Options.getInstance().setDataDir(Path.of("AI-Chat"), false);
}
/**
* The Core instance.
*/
@@ -42,16 +56,19 @@ public class Display {
*/
public Display()
{
OpenAIModel model = new OpenAIModel("http://localhost:8080", "MiniCPM5");
core.setOllamaObject/*NoMemory*/(OllamaObject.builder()
/*core.setOllamaObject/*NoMemory*//*(OllamaObject.builder()
.setModel("llama3.1:8b")
.keep_alive(10)
//.stream(false)
.addFileTools(FileHandlerLocation.DATA_FILES)
.addFileTools(Options.getInstance().getFileHandlerDataLocation())
.addMessages(new SystemMessage("You need to emulate feelings and emotions."))
.build());
.build());*/
core.enablePlugins(Core.PLUGIN_DIRECTORY);
core.setModel(model);
core.enablePlugins(Options.getInstance().getPluginDirectory());
core.addTool(new TimeTool(), Core.Source.CTP);
// TODO: Well Docker failes when luanched.... Fuck
@@ -158,9 +175,9 @@ public class Display {
break;
case "working":
System.out.println("Working directories:\n" +
" Data: " + Core.DATA_DIR.getAbsolutePath() + "\n" +
" DateFiles: " + FileHandlerLocation.DATA_FILES + "\n" +
" Plugins: " + Core.PLUGIN_DIRECTORY.getAbsolutePath());
" Data: " + Options.getInstance().getDataDir().getAbsolutePath() + "\n" +
" DateFiles: " + Options.getInstance().getFileHandlerDataLocation() + "\n" +
" Plugins: " + Options.getInstance().getPluginDirectory().getAbsolutePath());
break;
default:
System.out.println("Unknown command: " + message);
@@ -7,6 +7,7 @@ import com.github.dockerjava.core.DockerClientImpl;
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
import com.github.dockerjava.transport.DockerHttpClient;
import me.neurodock.core.Core;
import me.neurodock.core.Options;
import me.neurodock.core.Pair;
import me.neurodock.ollama.OllamaFunctionArgument;
import me.neurodock.ollama.OllamaFunctionTool;
@@ -434,7 +435,7 @@ public class PythonRunner extends OllamaFunctionTool {
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()){
program.delete();
}
@@ -1,8 +1,8 @@
package me.neurodock.genius;
import me.neurodock.ollama.OllamaFunctionTool;
import me.neurodock.llm.tools.FunctionTool;
public abstract class GeniusEndpointTool extends OllamaFunctionTool {
public abstract class GeniusEndpointTool extends FunctionTool {
protected GeniusTools geniusToolsInstance;
public GeniusEndpointTool(GeniusTools geniusTools) {
@@ -5,6 +5,7 @@ import io.github.classgraph.ClassInfo;
import io.github.classgraph.ClassInfoList;
import io.github.classgraph.ScanResult;
import me.neurodock.core.Core;
import me.neurodock.core.Options;
import me.neurodock.ollama.OllamaFunctionTool;
import me.neurodock.ollama.OllamaFunctionTools;
import org.json.JSONObject;
@@ -27,13 +28,13 @@ public class GeniusTools {
public final String Access_Token;
public final String BaseURL = "https://api.genius.com";
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 GeniusTools() {
super();
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_Secret = obj.getString("client_secret");
this.Access_Token = obj.getString("access_token");
@@ -1,12 +1,11 @@
package me.neurodock.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 me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;
import org.jsoup.Jsoup;
@@ -49,20 +48,16 @@ public class GetLyrics extends GeniusEndpointTool {
}
@Override
public @NotNull OllamaPerameter parameters() {
return OllamaPerameter.builder()
.addProperty("song_id", OllamaPerameter.OllamaPerameterBuilder.Type.INT, "The ID of the song to get lyrics for.", true)
public @NotNull ToolParameters parameters() {
return ToolParameters.builder()
.addProperty("song_id", ToolParameters.ToolParametersBuilder.Type.INT, "The ID of the song to get lyrics for.", true)
.build();
}
@Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) {
public @NotNull ToolResponse function(ToolArguments args) {
if(!( args[0].value() instanceof Integer)) {
throw new OllamaToolErrorException(this.name(), "The song_id must be an integer.");
}
String lyricsStr = geniusToolsInstance.hasCache((int) args[0].value());
String lyricsStr = geniusToolsInstance.hasCache(args.getArgument("song_id", Integer.class));
if(lyricsStr != null)
{
@@ -5,6 +5,7 @@ import io.github.classgraph.ClassInfo;
import io.github.classgraph.ClassInfoList;
import io.github.classgraph.ScanResult;
import me.neurodock.core.Core;
import me.neurodock.core.Options;
import me.neurodock.ollama.OllamaFunctionTool;
import me.neurodock.ollama.OllamaFunctionTools;
import org.json.JSONObject;
@@ -30,7 +31,7 @@ public class MALAPITool {
public MALAPITool() {
super();
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_Secret = obj.getString("client_secret");
} catch (IOException e) {
+1 -1
View File
@@ -2,7 +2,7 @@ plugins {
id 'java-library'
}
version = '0.1.4.1'
version = pluginAPIVersion
repositories {
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.
## 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
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.
useAPI = true
javaVersion = 26
javaVersion = 25
coreVersion = 2.1.5.1
pluginAPIVersion = 1.1.4.1
APIVersion = 1.0-SNAPSHOT