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"> <component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" /> <file type="web" url="file://$PROJECT_DIR$" />
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_26" default="true" project-jdk-name="26" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="graalvm-25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>
+3 -2
View File
@@ -5,7 +5,7 @@ plugins {
id 'io.spring.dependency-management' version '1.1.4' id 'io.spring.dependency-management' version '1.1.4'
} }
version = '1.0-SNAPSHOT' version = APIVersion
dependencies { dependencies {
implementation project(":Core") implementation project(":Core")
@@ -14,9 +14,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:4.1.0-M4' implementation 'org.springframework.boot:spring-boot-starter-web:4.1.0-M4'
implementation 'org.springframework.boot:spring-boot-starter-webflux:4.1.0-M4' implementation 'org.springframework.boot:spring-boot-starter-webflux:4.1.0-M4'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.0-M1' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.0-M1'
//implementation 'org.springframework.boot:spring-boot-starter-actuator'
testImplementation 'org.springframework.boot:spring-boot-starter-test:4.1.0-M4' testImplementation 'org.springframework.boot:spring-boot-starter-test:4.1.0-M4'
//implementation 'org.springframework.boot:spring-boot-starter-actuator'
//runtimeOnly('org.springframework.boot:spring-boot-starter-web') //runtimeOnly('org.springframework.boot:spring-boot-starter-web')
} }
@@ -3,7 +3,6 @@ package me.neurodock.api;
import me.neurodock.api.payload.request.NewQurryResponceHook; import me.neurodock.api.payload.request.NewQurryResponceHook;
import me.neurodock.api.payload.request.NewToolRequest; import me.neurodock.api.payload.request.NewToolRequest;
import me.neurodock.core.Core; import me.neurodock.core.Core;
import me.neurodock.core.GlobalObjects;
import me.neurodock.core.PrintMessageHandler; import me.neurodock.core.PrintMessageHandler;
import me.neurodock.ollama.OllamaObject; import me.neurodock.ollama.OllamaObject;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
@@ -41,8 +40,9 @@ public class APIApplication {
instance = this; instance = this;
if(GlobalObjects.getObject("core") instanceof Core coreInstance) { if(false) {
this.core = coreInstance; // TODO: This needs to be properly refactored.
//this.core = coreInstance;
} else { } else {
this.core = new Core(new PrintMessageHandler() { this.core = new Core(new PrintMessageHandler() {
@Override @Override
+4 -1
View File
@@ -2,11 +2,14 @@ plugins {
id 'java-library' id 'java-library'
} }
version = '1.9.0' version = coreVersion
dependencies { dependencies {
implementation project(":Plugin-API") implementation project(":Plugin-API")
api "org.json:json:20250107" api "org.json:json:20250107"
implementation 'org.graalvm.polyglot:polyglot:25.1.3'
implementation 'org.graalvm.polyglot:js:25.1.3'
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
} }
java { 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);
});
}
}
+143 -457
View File
@@ -1,34 +1,36 @@
package me.neurodock.core; package me.neurodock.core;
import me.neurodock.core.memory.*; import me.neurodock.core.memory.*;
import me.neurodock.ollama.*; import me.neurodock.llm.ChatObject;
import me.neurodock.ollama.exceptions.OllamaToolErrorException; 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.Data;
import me.neurodock.plugin.LoadedPlugin; import me.neurodock.plugin.LoadedPlugin;
import me.neurodock.plugin.loader.Loader; import me.neurodock.plugin.loader.Loader;
import me.neurodock.plugin.exceptions.PluginLoadingException; import me.neurodock.plugin.exceptions.PluginLoadingException;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import javax.naming.OperationNotSupportedException;
import java.io.*; import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.Locale;
import java.util.concurrent.Executors; import java.util.concurrent.*;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.TimeUnit; import java.util.function.Consumer;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarFile; 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. * 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. * The OllamaObject to use.
*/ */
private OllamaObject ollamaObject; private Model model;
/** /**
* The list of tools to use. * 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; private AtomicBoolean cancelled = new AtomicBoolean(false);
/**
* The port of the Ollama API.
*/
private int ollamaPort = 11434;
/**
* The URL of the Ollama API.
*/
private URL url;
/** /**
* The PrintMessageHandler to use. * The PrintMessageHandler to use.
*/ */
private final PrintAdvanceMessageHandler printMessageHandler; private final PrintAdvanceMessageHandler printMessageHandler;
public static String DATA;
public static File DATA_DIR;
public static File PLUGIN_DIRECTORY;
public static File CACHE_DIRECTORY;
/** /**
* Creates a new instance of Core with the provided PrintMessageHandler, * Creates a new instance of Core with the provided PrintMessageHandler,
* defaulting the Ollama backend to {@code localhost}. * defaulting the Ollama backend to {@code localhost}.
* *
* @param printMessageHandler The PrintMessageHandler to use as the default output * @param printMessageHandler The PrintMessageHandler to use as the default output
*/ */
public Core(@NotNull PrintAdvanceMessageHandler printMessageHandler) { public Core(@NotNull PrintAdvanceMessageHandler printMessageHandler)
this(printMessageHandler, "localhost");
}
/**
* Creates a new instance of Core with the provided PrintMessageHandler
* and a specific Ollama backend address.
*
* @param printMessageHandler The PrintMessageHandler to use as the default output
* @param ollamaIP The IP or hostname of the Ollama backend
*/
public Core(@NotNull PrintAdvanceMessageHandler printMessageHandler, @NotNull String ollamaIP)
{ {
this.printMessageHandler = printMessageHandler; this.printMessageHandler = printMessageHandler;
this.ollamaIP = ollamaIP;
initDirectories(); initDirectories();
initOllamaUrl();
confirmOllama();
initLogWriter(); initLogWriter();
initScheduler(); initScheduler();
initShutdownHook(); initShutdownHook();
} }
static {
// This should not be enforced like this, instead this should read a data field, or wait for an init to be called....
// Unsure of how to properly do this at this time, however.
// After looking at things, just run {@link Core.setDataDirectory(String)} somewhere else before initilising the Core object and seems to be fine
setDataDirectory("AI-Chat", false);
}
public static void setLogDirectory(String logDirectory) public static void setLogDirectory(String logDirectory)
{ {
logDir = new File(logDirectory); logDir = new File(logDirectory);
@@ -126,55 +96,6 @@ public class Core {
logFile = new File(logDirectory, "latest.log"); logFile = new File(logDirectory, "latest.log");
} }
/**
* Set the data directory in appropriate locations depending on the host OS, falling back to $WORKING_DIR/data
* @param dataDirectory the data directory to use
*/
public static void setDataDirectory(String dataDirectory, boolean fullDirectory) {
String data;
if(System.getenv("AI_CHAT_DEBUG") != null) {
data = "./data";
}
if(fullDirectory) {
data = dataDirectory;
}
else if(System.getProperty("os.name").toLowerCase().contains("windows")) {
String localappdata = System.getenv("LOCALAPPDATA");
if(localappdata == null) {
localappdata = System.getenv("APPDATA");
}
data = localappdata + "/"+ dataDirectory;
}
else if (System.getProperty("os.name").toLowerCase().contains("linux")) {
data = System.getenv("HOME") + "/.local/share/" + dataDirectory;
}
else if (System.getProperty("os.name").toLowerCase().contains("mac")) {
data = System.getProperty("user.home") + "/Library/Application Support/"+ dataDirectory;
}
else {
data = "./data";
}
DATA = data;
DATA_DIR = new File(DATA);
if(!DATA_DIR.exists()) {
DATA_DIR.mkdirs();
}
String pluginDir = DATA + "/plugins";
PLUGIN_DIRECTORY = new File(pluginDir);
if(!PLUGIN_DIRECTORY.exists()) {
PLUGIN_DIRECTORY.mkdirs();
}
CACHE_DIRECTORY = new File(DATA + "/cache");
if(!CACHE_DIRECTORY.exists()) {
CACHE_DIRECTORY.mkdirs();
}
}
/** /**
* Creates the base directories required by the application. * Creates the base directories required by the application.
* <p> * <p>
@@ -183,7 +104,8 @@ public class Core {
*/ */
private void initDirectories() { private void initDirectories() {
ensureDir(logDir.getAbsolutePath()); ensureDir(logDir.getAbsolutePath());
ensureDir(DATA_DIR.getAbsolutePath() + "/messages"); ensureDir(Options.getInstance().getDataDir() + "/messages");
Options.getInstance().initiateDirectories();
} }
/** /**
@@ -196,19 +118,6 @@ public class Core {
if (!dir.exists()) dir.mkdir(); 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. * 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} * a timestamped archive file under {@code ./messages/}, and a rolling {@code messages.json}
* under {@link #DATA_DIR} for resuming the session later. * under {@link Options#getDataDir()} for resuming the session later.
* *
* @see #buildMessagesArray() * @see #buildMessagesArray()
* @see #writeMessagesTo(File, JSONArray) * @see #writeMessagesTo(File, JSONArray)
@@ -310,23 +219,34 @@ public class Core {
JSONArray messages = buildMessagesArray(); JSONArray messages = buildMessagesArray();
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd_HH-mm-ss")); String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd_HH-mm-ss"));
writeMessagesTo(new File(DATA_DIR.getAbsolutePath()+"/messages/" + timestamp + ".json"), messages); writeMessagesTo(new File(Options.getInstance().getDataDir().getAbsolutePath()+"/messages/" + timestamp + ".json"), messages);
writeMessagesTo(new File(DATA_DIR, "messages.json"), messages); writeMessagesTo(new File(Options.getInstance().getDataDir(), "messages.json"), messages);
} }
/** /**
* 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 * @return A {@link JSONArray} containing all current messages
*/ */
private JSONArray buildMessagesArray() { private JSONArray buildMessagesArray() {
JSONArray messages = new JSONArray(); JSONArray messages = new JSONArray();
for (OllamaMessage message : ollamaObject.getMessages()) { for (Message msg : this.chatObject.getConversation()) {
messages.put(message.toJSON()); 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; return messages;
} }
public ChatObject getChatObject() {
return chatObject;
}
/** /**
* Writes a {@link JSONArray} of messages to the given file, overwriting it if it already exists. * 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. * 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
* TODO: Fix a better name and refactor this method. * @param model The Model to use
*/ */
@Deprecated(forRemoval = true) public void setModel(Model model) {
private void constructCore(){ this.model = model;
File dir = new File("./logs/");
if (!dir.exists()) {
dir.mkdir();
}
dir = new File("./pythonFiles/");
if (!dir.exists()) {
dir.mkdir();
}
dir = new File("./messages");
if (!dir.exists()) {
dir.mkdir();
}
try { addTool(new AddMemoryFunction());
url = new URI("http://"+ollamaIP+":"+ollamaPort+"/api/chat").toURL(); addTool(new RemoveMemoryFunction());
} catch (MalformedURLException e) { addTool(new GetMemoryFunction());
throw new RuntimeException(e); addTool(new GetMemoriesFunction());
} catch (URISyntaxException e) { addTool(new GetMemoryIdentitiesFunction());
throw new RuntimeException(e);
}
try {
if (logFile.exists()) {
BufferedReader br = new BufferedReader(new FileReader(logFile));
String line = br.readLine();
br.close();
if (line != null) {
String date = line.substring(0, line.indexOf(">")).replaceAll("[/:]", "-");
logFile.renameTo(new File(logFile.getParentFile(), date + ".log"));
logFile = new File("./logs/latest.log");
}
else {
System.out.println("Exisitng log file is empty, overwriting it!");
logFile.delete();
}
logFile.createNewFile();
}
logWriter = new BufferedWriter(new FileWriter(logFile));
}catch (IOException e) {
throw new RuntimeException(e);
}
this.scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
try {
logWriter.flush();
//System.out.println("Buffer flushed to file.");
} catch (IOException e) {
e.printStackTrace();
}
}, 0, 3, TimeUnit.MINUTES);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
scheduler.shutdownNow();
try {
try {
logWriter.flush();
logWriter.close();
}catch (IOException ignore)
{
// This exception is kinda expected. Since it can often occur that the logWriter is already closed
System.out.println("Failed to flush log file, but that is not a problem.");
}
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd_HH-mm-ss");
File messagesFile = new File("./messages/"+now.format(formatter)+".json");
BufferedWriter messagesWriter = new BufferedWriter(new FileWriter(messagesFile));
JSONArray messages = new JSONArray();
for(OllamaMessage message : ollamaObject.getMessages()) {
messages.put(message.toJSON());
}
messagesWriter.write(messages.toString());
messagesWriter.close();
File f = new File(DATA_DIR,"messages.json");
if(f.exists())
{
f.delete();
}
f.createNewFile();
messagesWriter = new BufferedWriter(new FileWriter(f));
messagesWriter.write(messages.toString());
messagesWriter.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}));
} }
/** /**
* A check to exit early if Ollama isn't reachable. * 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() public void setModelNoMemory(Model model) {
{ this.model = model;
try {
URL url = new URL("http://" + ollamaIP + ":" + ollamaPort + "/api/version");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Content-Type", "application/json");
connection.setDoOutput(true);
connection.setConnectTimeout(3 * 1000);
int responseCode = connection.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
new RuntimeException("Ollama is un-responsive on url: " + url.toString()).printStackTrace();
System.exit(1);
}
}catch (IOException ex)
{
ex.printStackTrace();
System.out.println("Can not reach Ollama!");
System.exit(1);
}
}
/**
* Sets the {@link #ollamaObject} object to the provided argument,
* Also adds the memory base system. See {@link Core#setOllamaObjectNoMemory} if you don't want to add memory functions
* @param ollamaObject The OllamaObject to use
*/
public void setOllamaObject(OllamaObject ollamaObject) {
if(this.ollamaObject == null) {
this.ollamaObject = ollamaObject;
for(Pair<OllamaTool, String> tool : ollamaObject.getTools()) {
if(tool.getKey() instanceof OllamaFunctionTool functionTool)
{
funtionTools.add(new Pair<>(functionTool, tool.getValue()));
}
}
addTool(new AddMemoryFunction(), Source.CORE);
addTool(new RemoveMemoryFunction(), Source.CORE);
addTool(new GetMemoryFunction(), Source.CORE);
addTool(new GetMemoriesFunction(), Source.CORE);
addTool(new GetMemoryIdentitiesFunction(), Source.CORE);
}
else {
throw new IllegalArgumentException("Ollama object is already set");
}
}
/**
* Sets the {@link #ollamaObject} object to the provided argument,
* Dose not add the base system for memory. see {@link #setOllamaObject} if you want to add memory function
* @param ollamaObject The OllamaObject to use
*/
public void setOllamaObjectNoMemory(OllamaObject ollamaObject) {
if(this.ollamaObject == null) {
this.ollamaObject = ollamaObject;
for(Pair<OllamaTool, String> tool : ollamaObject.getTools()) {
if(tool.getKey() instanceof OllamaFunctionTool functionTool)
{
funtionTools.add(new Pair<>(functionTool, tool.getValue()));
}
}
}
else {
throw new IllegalArgumentException("Ollama object is already set");
}
} }
/** /**
* Adds a new tool to the System * Adds a new tool to the System
* @param functionTool The tool to add * @param functionTool The tool to add
* @param source The source of the tool
*/ */
public void addTool(OllamaFunctionTool functionTool, @MagicConstant(valuesFromClass = Source.class) String source) { public void addTool(Tool functionTool) {
funtionTools.add(new Pair<>(functionTool, source)); tools.add(functionTool);
ollamaObject.addTool(functionTool, source);
} }
public void addMessage(Message message) {
chatObject.addMessage(message);
}
/*
/** /**
* Adds a list of tools to the System * Adds a list of tools to the System
* @param tools The tools to add * @param tools The tools to add
*/ */
@SuppressWarnings("MagicConstant") /*@SuppressWarnings("MagicConstant")
public void addTools(OllamaFunctionTools tools) public void addTools(Tools tools)
{ {
for(Pair<OllamaFunctionTool, String> tool : tools) for(Pair<Tool, String> tool : tools)
{ {
addTool(tool.getKey(), tool.getValue()); addTool(tool.getKey(), tool.getValue());
} }
} }*/
/** /**
* Gets the list of tools added to the System * 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 * @return The list of tools added to the System compressed as Pairs of the tool and the source
*/ */
public ArrayList<Pair<OllamaFunctionTool, String>> getFuntionTools() { public ArrayList<Tool> getTools() {
return funtionTools; return tools;
} }
/** /**
* Gets the Ollama Object * Gets the Ollama Object
* @return The Ollama Object * @return The Ollama Object
*/ */
public OllamaObject getOllamaObject() { public Model getModel() {
return ollamaObject; return model;
} }
/** /**
@@ -570,14 +337,10 @@ public class Core {
* @param name The tool to remove * @param name The tool to remove
*/ */
public void removeTool(String name) { public void removeTool(String name) {
Pair<OllamaFunctionTool, String> funtionTool = funtionTools.stream().filter(tool -> tool.getKey().name().equalsIgnoreCase(name)).findFirst().orElse(null); tools.stream()
funtionTools.stream() .filter(tool -> tool.name().equalsIgnoreCase(name))
.filter(tool -> tool.getKey().name().equalsIgnoreCase(name))
.findFirst() .findFirst()
.ifPresentOrElse(tool -> { .ifPresentOrElse(tools::remove, () -> {
funtionTools.remove(tool);
ollamaObject.removeTool(tool.getKey());
}, () -> {
new IllegalArgumentException("Function tool with name '"+name+"' does not exist") new IllegalArgumentException("Function tool with name '"+name+"' does not exist")
.printStackTrace(); .printStackTrace();
@@ -593,107 +356,52 @@ public class Core {
logWriter.flush(); logWriter.flush();
}catch (IOException e) {} }catch (IOException e) {}
} }
/** public CompletableFuture<Message> queryModel()
* Sends the OllamaObject to Ollama
* @return The response from Ollama
*/
public CompletableFuture<JSONObject> qurryOllama()
{ {
return CompletableFuture.supplyAsync(() -> { return model.qurryModel(chatObject, tools);
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(); public CompletableFuture<Message> queryModel(Consumer<JSONObject> consumer) throws OperationNotSupportedException {
if(model instanceof StreamingModel streamingModel) {
ollamaObjectString = ollamaObjectString.replace("\n", "\\n"); return streamingModel.qurryModel(chatObject, tools, consumer);
}
try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) { throw new OperationNotSupportedException("The chosen backend of type \""+model.getClass().getSimpleName()+"\" dose not support streaming");
wr.write(ollamaObjectString.getBytes(StandardCharsets.UTF_8));
wr.flush();
}
int responseCode = connection.getResponseCode();
// HTTP_OK or 200 response code generally means that the server ran successfully without any errors
StringBuilder response = new StringBuilder();
// Read response content
// connection.getInputStream() purpose is to obtain an input stream for reading the server's response.
try (
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
response.append(line); // Adds every line to response till the end of file.
}
} catch (Exception ex) {
// If the server returns an error, we read the error stream instead
InputStream errorStream = connection.getErrorStream();
if (errorStream != null) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream))) {
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
}
}
}
if (responseCode == HttpURLConnection.HTTP_OK) {
connection.disconnect();
return new JSONObject(response.toString());
} else {
connection.disconnect();
printMessageHandler.printErrorMessage(new OllamaMessage(OllamaMessageRole.SYSTEM,"Error: HTTP Response code - " + responseCode + "\n" + response.toString()));
throw new RuntimeException("HTTP Response code - " + responseCode);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
});
} }
/** /**
* Handles the response from Ollama. * Handles the response from Ollama.
* * <p>
* Processes tool calls, logs information, appends messages to the OllamaObject, * Processes tool calls, logs information, appends messages to the OllamaObject,
* and prints output to the user. * and prints output to the user.
* *
* @param response The response from Ollama * @param response The response from Ollama
*/ */
public void handleResponse(JSONObject response) { public void handleResponse(Message response) {
if(response == null) return; if(response == null) return;
writeLog("Raw response: " + response.toString()); //chatObject.addMessage(response);
JSONObject message = response.getJSONObject("message");
if(!message.has("tool_calls")) {
checkIfResponceMessage(response);
return;
}
ollamaObject.addMessage(new OllamaMessageToolCall(
OllamaMessageRole.fromRole(message.optString("role")),
message.getString("content"),
message.getJSONArray("tool_calls")
));
List<CompletableFuture<Void>> futures = new ArrayList<>(); List<CompletableFuture<Void>> futures = new ArrayList<>();
// Process each tool call // 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)); futures.add(processToolCall(call));
} }
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenAccept(result -> { CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenAccept(result -> {
checkIfResponceMessage(response); 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) { private CompletableFuture<Void> processToolCall(Object call) {
return CompletableFuture.runAsync(() -> { return CompletableFuture.runAsync(() -> {
if (!(call instanceof JSONObject jsonObject)) return; if (!(call instanceof JSONObject jsonObject)) return;
if (!jsonObject.has("function")) return;
JSONObject function = jsonObject.getJSONObject("function"); JSONObject function = jsonObject.getJSONObject("function");
OllamaFunctionTool func = findTool(function); Tool func = findTool(function);
if (func == null) { if (func == null) {
reportToolNotFound(function); reportToolNotFound(function);
return; return;
} }
JSONObject arguments = function.getJSONObject("arguments"); JSONObject arguments = new JSONObject(function.getString("arguments"));
renderToolCalling(func.renderCalling(function), function, arguments); renderToolCalling(func.renderCalling(function), function, arguments);
executeToolCall(func, arguments); executeToolCall(func, arguments);
}); });
@@ -730,11 +437,10 @@ public class Core {
* @param function the function JSON containing the tool name * @param function the function JSON containing the tool name
* @return the OllamaFunctionTool if found, {@code null} otherwise * @return the OllamaFunctionTool if found, {@code null} otherwise
*/ */
private OllamaFunctionTool findTool(JSONObject function) { private Tool findTool(JSONObject function) {
return funtionTools.stream() return tools.stream()
.filter(f -> (f.getKey().name() + "_" + f.getValue()) .filter(f -> (f.name())
.equalsIgnoreCase(function.getString("name"))) .equalsIgnoreCase(function.getString("name")))
.map(Pair::getKey)
.findFirst() .findFirst()
.orElse(null); .orElse(null);
} }
@@ -745,11 +451,13 @@ public class Core {
* @param function the function JSON representing a hallucinated or removed function * @param function the function JSON representing a hallucinated or removed function
*/ */
private void reportToolNotFound(JSONObject 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" "Function '" + function.getString("name") + "' does not exist"
); );
ollamaObject.addMessage(error); chatObject.addMessage(error);
printMessageHandler.printToolCalling(error.getError()); printMessageHandler.printToolCalling(error.getContent().toString());
} }
/** /**
@@ -786,37 +494,45 @@ public class Core {
* @param func the {@link OllamaFunctionTool} to execute * @param func the {@link OllamaFunctionTool} to execute
* @param arguments the raw JSON arguments for the tool * @param arguments the raw JSON arguments for the tool
*/ */
private void executeToolCall(OllamaFunctionTool func, JSONObject arguments) { private void executeToolCall(Tool tool, JSONObject arguments) {
ArrayList<OllamaFunctionArgument> args = new ArrayList<>(); if(tool instanceof FunctionTool func) {
for(String key : arguments.keySet()) { ToolArguments args = new ToolArguments();
args.add(new OllamaFunctionArgument(key, arguments.get(key))); for (String key : arguments.keySet()) {
} args.addArgument(key, arguments.get(key));
}
try { try {
OllamaToolResponse response = func.function(args.toArray(new OllamaFunctionArgument[0])); ToolResponse response = func.function(args);
ollamaObject.addMessage(response); if(response.getToolID() == null) {
printMessageHandler.printMessage(response); response.setToolID(tool.name());
writeLog("Successfully function call " + func.name() + " output: " + response.getResponse()); }
} catch(OllamaToolErrorException e) { chatObject.addMessage(response);
OllamaToolError error = new OllamaToolError(e.getMessage()); printMessageHandler.printMessage(response);
ollamaObject.addMessage(error); writeLog("Successfully function call " + func.name() + " output: " + response.getResponse());
printMessageHandler.printErrorMessage(error); } catch (ToolException e) {
writeLog("ERROR: " + e.getMessage()); 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 * Checks if the response contains a message and if so, prints it to the user
* @param responce the Ollama response * @param responce the Ollama response
*/ */
private void checkIfResponceMessage(JSONObject responce) { private void checkIfResponceMessage(Message responce) {
String message = responce.getJSONObject("message").getString("content"); if(responce.getContent() instanceof String str)
if(responce.getJSONObject("message").has("content") && !message.isBlank())
{ {
OllamaMessage ollamaMessage = new OllamaMessage(OllamaMessageRole.ASSISTANT, message); printMessageHandler.printMessage(responce);
printMessageHandler.printMessage(ollamaMessage); writeLog("Response content: "+ str);
writeLog("Response content: "+ message); chatObject.addMessage(responce);
ollamaObject.addMessage(ollamaMessage);
} }
} }
@@ -879,9 +595,9 @@ public class Core {
} }
data.plugins.forEach(loadedPlugin -> { 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 @Override
public File getDataDictionary() { public File getDataDictionary() {
return DATA_DIR; return Options.getInstance().getDataDir();
} }
@Override @Override
@@ -902,7 +618,7 @@ public class Core {
@Override @Override
public File getCacheDirectory() { public File getCacheDirectory() {
return CACHE_DIRECTORY; return Options.getInstance().getCacheDirectory();
} }
public void addPlugin(LoadedPlugin plugin) public void addPlugin(LoadedPlugin plugin)
@@ -915,34 +631,4 @@ public class Core {
plugins.remove(plugin); plugins.remove(plugin);
} }
} }
/**
* Represents the source of a tool.
* <p>
* This is intended for use with {@link Core#addTool(OllamaFunctionTool, String)}
* to indicate the category from which a tool originates.
*/
public static class Source {
/**
* Tools boundeld with the Core runtime (memory, file access, etc.)
* DO NOT USE THIS unless you are poking at core stuff :).
*/
public static final String CORE = "Core";
/**
* Compile-Time Plugins: boundeld at build time as part of the application.
* Examples: MALAPITool, GeniusAPI, WikipediaTool.
*/
public static final String CTP = "CTP";
/**
* Runtime Pre-Compiled Plugins: external plugins loaded at runtime via Plugin-API.
*/
public static final String RPCP = RPCP_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; package me.neurodock.core;
import com.sun.jdi.connect.spi.TransportService; import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.ollama.*; import me.neurodock.llm.tools.Tool;
import me.neurodock.llm.Message;
import me.neurodock.llm.Model;
import org.json.JSONObject; import org.json.JSONObject;
import java.io.*; import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
@@ -42,7 +42,7 @@ import java.util.Map;
* <p> * <p>
* Instances are assembled via {@link #builder()}. Callers who need capability routing * Instances are assembled via {@link #builder()}. Callers who need capability routing
* hints compiled from a live set of registered tools must call * 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 * see that method's docs for why this is a separate step rather than being folded into
* construction. * construction.
* *
@@ -63,12 +63,12 @@ public class LLMSystemPrompt {
* Usage-hint definitions for registered tools, or {@code null} if this prompt doesn't * 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 * 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 * 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; private Capabilities capabilities;
/** /**
* The compiled, ready-to-render {@code <Capabilities>} body, built by * 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 * set of registered tools. {@code null} until that method has been called at least
* once, or if {@link #capabilities} was never set. * 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 * 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> * <p>
* This is intentionally a separate step from construction rather than something the * 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 * 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 * compiled capabilities text needs to be regenerated on demand against whatever the
* current tool set actually is, not frozen at construction time. * current tool set actually is, not frozen at construction time.
* <p> * <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 * 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 — * 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 * 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. * up front in a system prompt; conflating the two would blur what each is for.
* <p> * <p>
@@ -125,19 +125,19 @@ public class LLMSystemPrompt {
* section at all), this clears {@link #compiledCapabilities} to {@code null} and * section at all), this clears {@link #compiledCapabilities} to {@code null} and
* returns without inspecting {@code ollamaObject}. * 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) { if(capabilities == null) {
compiledCapabilities = null; compiledCapabilities = null;
return; return;
} }
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
for (Pair<OllamaTool, String> tool : ollamaObject.getTools()) { for (Tool tool : tools) {
if(tool.getKey() instanceof OllamaFunctionTool funcTool) { if(tool instanceof FunctionTool funcTool) {
String llmName = funcTool.name() + "_" + (funcTool.getSource() != null ? funcTool.getSource() : ""); String llmName = funcTool.name();
String hint = capabilities.getUsageHints().get(tool.getClass()); String hint = capabilities.getUsageHints().get(funcTool.getClass());
// fall back to description() if no explicit routing hint was set // fall back to description() if no explicit routing hint was set
String line = hint != null ? hint : funcTool.description(); String line = hint != null ? hint : funcTool.description();
@@ -151,7 +151,7 @@ public class LLMSystemPrompt {
/** /**
* Renders this prompt's currently-set sections into a single system-role * 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> * <p>
* Each section — {@link #identity}, {@link #context}, {@link #compiledCapabilities}, * Each section — {@link #identity}, {@link #context}, {@link #compiledCapabilities},
* {@link #behavior}, {@link #outputFormat} — is wrapped in its own XML-style tag and * {@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. * skipped rather than emitted as empty tags.
* <p> * <p>
* Note that {@link #compiledCapabilities} reflects whatever tool set was live the last * 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. * 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 * containing the compiled prompt text
*/ */
public OllamaMessage generateSystemPrompt() { public Message generateSystemPrompt() {
StringBuilder builder = new StringBuilder(); 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()); 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 * 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 * 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> * <p>
* Hints are keyed by the tool's {@link Class} rather than by a registered instance or * 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 — * 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 * see {@link #use(Class, String)}. This class only holds the hint definitions; the
* actual compiled text is produced by * actual compiled text is produced by
* {@link LLMSystemPrompt#generateCapabilities(OllamaObject)}. * {@link LLMSystemPrompt#generateCapabilities(ArrayList)}.
*/ */
public static class Capabilities { public static class Capabilities {
/** /**
* Usage hints keyed by tool class, in the order they were registered. * 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 * Registers a usage-routing hint for a tool class — guidance on when the model
@@ -324,13 +324,13 @@ public class LLMSystemPrompt {
* <p> * <p>
* Keyed by class rather than instance because a tool's LLM-facing name (which * 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 * 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 toolClass the tool class this hint applies to
* @param usageHint the routing guidance text for this tool * @param usageHint the routing guidance text for this tool
* @return this {@link Capabilities}, for chaining * @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); usageHints.put(toolClass, usageHint);
return this; return this;
} }
@@ -340,7 +340,7 @@ public class LLMSystemPrompt {
* *
* @return the live map backing this {@link Capabilities} * @return the live map backing this {@link Capabilities}
*/ */
Map<Class<? extends OllamaFunctionTool>, String> getUsageHints() { Map<Class<? extends FunctionTool>, String> getUsageHints() {
return usageHints; return usageHints;
} }
} }
@@ -448,7 +448,7 @@ public class LLMSystemPrompt {
* Loads {@link #behavior} from a text file, resolved in order against: * Loads {@link #behavior} from a text file, resolved in order against:
* <ol> * <ol>
* <li>the classpath</li> * <li>the classpath</li>
* <li>{@link Core#DATA_DIR}</li> * <li>{@link Options#getDataDir()}</li>
* <li>the current runtime/working directory</li> * <li>the current runtime/working directory</li>
* <li>an exact file path match</li> * <li>an exact file path match</li>
* </ol> * </ol>
@@ -488,7 +488,7 @@ public class LLMSystemPrompt {
/** /**
* Sets the capability usage-hint definitions for the prompt being built. These are * Sets the capability usage-hint definitions for the prompt being built. These are
* only compiled into prompt text once * 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. * instance.
* *
* @param capabilities the usage-hint definitions to use * @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, * Resolves and reads a behavior file as UTF-8 text, checking the classpath,
* {@link Core#DATA_DIR}, the working directory, and an exact path match, in that order. * {@link Options#getDataDir()}, the working directory, and an exact path match, in that order.
* *
* @param path the file to look for * @param path the file to look for
* @return the full file contents, with each line terminated by {@code \n} * @return the full file contents, with each line terminated by {@code \n}
@@ -566,7 +566,7 @@ public class LLMSystemPrompt {
if (classpathResourceExists("/"+path)) { if (classpathResourceExists("/"+path)) {
in = BehaviorLoader.class.getResourceAsStream("/"+path); in = BehaviorLoader.class.getResourceAsStream("/"+path);
} else { } else {
Path dataDirPath = Path.of(Core.DATA_DIR.getPath(), path); Path dataDirPath = Path.of(Options.getInstance().getDataDir().getPath(), path);
Path relativePath = Path.of(path); Path relativePath = Path.of(path);
if (Files.exists(dataDirPath)) { if (Files.exists(dataDirPath)) {
in = new FileInputStream(dataDirPath.toFile()); in = new FileInputStream(dataDirPath.toFile());
@@ -0,0 +1,123 @@
package me.neurodock.core;
import java.io.File;
import java.nio.file.Path;
public class Options {
/**
* The singleton options object
*/
private static Options instance = new Options();
/**
* Provides the singleton options object for modification to options
* @return the current {@link Options} singleton object
*/
public static Options getInstance()
{
return instance;
}
private Path data;
private File dataDir;
private File pluginDirectory;
private File cacheDirectory;
/**
* Sets a new singleton options object
* @param options the new singleton object
*/
public static void setInstance(Options options)
{
instance = options;
}
public Options setDataDir(Path dataDir, boolean fullDirectory)
{
Path data = Path.of("./data");
String os = System.getProperty("os.name").toLowerCase();
Path cache;
if(System.getenv("AI_CHAT_DEBUG") == null) {
if (fullDirectory) {
data = dataDir;
} else {
if (os.contains("windows")) {
String localappdata = System.getenv("LOCALAPPDATA");
if (localappdata == null) {
localappdata = System.getenv("APPDATA");
}
data = Path.of(localappdata, dataDir.toFile().getPath());
} else if (os.contains("linux")) {
data = Path.of(System.getenv("HOME"), ".local/share", dataDir.toFile().getPath());
} else if (os.contains("mac")) {
data = Path.of(System.getProperty("user.home"), "Library/Application Support", dataDir.toFile().getPath());
}
}
}
if (os.contains("win")) {
String localAppData = System.getenv("LOCALAPPDATA");
cache = Path.of(localAppData != null ? localAppData
: System.getProperty("user.home") + "\\AppData\\Local");
} else if (os.contains("mac")) {
cache = Path.of(System.getProperty("user.home"), "Library", "Caches");
} else {
// Linux / other unix
String xdgCache = System.getenv("XDG_CACHE_HOME");
cache = Path.of(xdgCache != null && !xdgCache.isBlank()
? xdgCache
: System.getProperty("user.home") + "/.cache");
}
this.data = data;
this.cacheDirectory = new File(cache.toFile(), data.getFileName().toFile().toString());
return this;
}
public Options initiateDirectories()
{
this.dataDir = this.data.toFile();
if(!this.dataDir.exists()) {
this.dataDir.mkdirs();
}
String pluginDir = this.data + "/plugins";
pluginDirectory = new File(pluginDir);
if(!pluginDirectory.exists()) {
pluginDirectory.mkdirs();
}
if(!cacheDirectory.exists()) {
cacheDirectory.mkdirs();
}
return this;
}
public Path getData() {
return data;
}
public File getDataDir() {
return dataDir;
}
public File getPluginDirectory() {
return pluginDirectory;
}
public File getCacheDirectory() {
return cacheDirectory;
}
public Path getFileHandlerDataLocation()
{
return Path.of(dataDir.toString(), "files");
}
}
@@ -1,19 +1,18 @@
package me.neurodock.core; package me.neurodock.core;
import me.neurodock.ollama.OllamaMessage; import me.neurodock.llm.Message;
import me.neurodock.ollama.OllamaMessageRole;
/** /**
* Represents a {@link PrintAdvanceMessageHandler}. * 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} * For a simpler version see {@link PrintMessageHandler}
*/ */
public interface PrintAdvanceMessageHandler { public interface PrintAdvanceMessageHandler {
/** /**
* Expected to handle the printing of the provided {@link OllamaMessage}. * Expected to handle the printing of the provided {@link Message}.
* @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}
*/ */
void printMessage(OllamaMessage message); void printMessage(Message message);
/** /**
* Default method to print error messages to the user or API Client. * 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. * @param errorMessage The error message to be printed.
* If color is not supported, it will print the message without color. * 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 * Used when a tool is to have it's calling rendered
@@ -1,9 +1,7 @@
package me.neurodock.core; 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. * 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} * 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()) { 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 TOOL -> (color() ?"\u001b[31m":"")+message.getContent()+(color()?"\u001b[0m":"");
case USER, SYSTEM -> (color() ?"\u001b[37m":"")+message.getContent()+(color()?"\u001b[0m":""); case USER, SYSTEM -> (color() ?"\u001b[37m":"")+message.getContent()+(color()?"\u001b[0m":"");
default -> throw new IllegalArgumentException("Invalid message role"); default -> throw new IllegalArgumentException("Invalid message role");
@@ -62,8 +60,8 @@ public interface PrintMessageHandler extends PrintAdvanceMessageHandler {
* @param errorMessage The error message to be printed. * @param errorMessage The error message to be printed.
* If color is not supported, it will print the message without color. * If color is not supported, it will print the message without color.
*/ */
default void printErrorMessage(OllamaMessage errorMessage) { default void printErrorMessage(Message errorMessage) {
printError(errorMessage.getContent()); printError(errorMessage.getContent().toString());
} }
@Override @Override
@@ -1,11 +1,11 @@
package me.neurodock.core.files; package me.neurodock.core.files;
import me.neurodock.core.Core; import me.neurodock.core.Core;
import me.neurodock.core.Options;
import me.neurodock.core.Pair; import me.neurodock.core.Pair;
import me.neurodock.core.files.tools.ReadFileTool; import me.neurodock.core.files.tools.ReadFileTool;
import me.neurodock.core.files.tools.WriteFileTool; import me.neurodock.core.files.tools.WriteFileTool;
import me.neurodock.ollama.OllamaTool; import me.neurodock.llm.tools.Tool;
import org.intellij.lang.annotations.MagicConstant;
import java.io.*; import java.io.*;
import java.nio.file.Path; import java.nio.file.Path;
@@ -27,11 +27,12 @@ public class FileHandler {
/** /**
* Creates a new instance as well as setting the {@link #instance} to this new one * Creates a new instance as well as setting the {@link #instance} to this new one
* A good start is to use {@link Options#getFileHandlerDataLocation()} as it's located along all other files
* @param baseDirectory the directory to be used as base directory * @param baseDirectory the directory to be used as base directory
*/ */
public FileHandler(@MagicConstant(valuesFromClass = FileHandlerLocation.class) String baseDirectory) { public FileHandler(Path baseDirectory) {
try { try {
root = Path.of(baseDirectory).toAbsolutePath().normalize(); root = baseDirectory.toAbsolutePath().normalize();
if (!root.toFile().exists()) { if (!root.toFile().exists()) {
root.toFile().mkdirs(); root.toFile().mkdirs();
} }
@@ -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 * @return
*/ */
public static ArrayList<Pair<? extends OllamaTool, String>> getTools() { public static ArrayList<Tool> getTools() {
ArrayList<Pair<? extends OllamaTool, String>> fileTools = new ArrayList<>(); ArrayList<Tool> fileTools = new ArrayList<>();
fileTools.add(new Pair<>(new ReadFileTool(), Core.Source.CORE)); fileTools.add(new ReadFileTool());
fileTools.add(new Pair<>(new WriteFileTool(), Core.Source.CORE)); fileTools.add(new WriteFileTool());
return fileTools; 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; package me.neurodock.core.files.tools;
import me.neurodock.core.files.FileHandler; import me.neurodock.core.files.FileHandler;
import me.neurodock.ollama.OllamaFunctionArgument; import me.neurodock.llm.exceptions.ToolException;
import me.neurodock.ollama.OllamaFunctionTool; import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.ollama.OllamaPerameter; import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.ollama.OllamaToolResponse; import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.ollama.exceptions.OllamaToolErrorException; import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.*; import java.io.*;
import java.nio.file.Path; import java.nio.file.Path;
public class ReadFileTool extends OllamaFunctionTool { public class ReadFileTool extends FunctionTool {
FileHandler fs = FileHandler.getInstance(); FileHandler fs = FileHandler.getInstance();
@Override @Override
public @NotNull String name() { public @NotNull String name() {
@@ -24,23 +24,18 @@ public class ReadFileTool extends OllamaFunctionTool {
} }
@Override @Override
public @NotNull OllamaPerameter parameters() { public @NotNull ToolParameters parameters() {
return OllamaPerameter.builder() return ToolParameters.builder()
.addProperty("file_path", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The path to the file to be read", true) .addProperty("file_path", ToolParameters.ToolParametersBuilder.Type.STRING, "The path to the file to be read", true)
.build(); .build();
} }
@Override @Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { public @NotNull ToolResponse function(ToolArguments args) {
String orgPath = null; String orgPath = args.optArgument("file_path", String.class);
for (OllamaFunctionArgument arg : args) {
if(arg.argument().equals("file_path")) {
orgPath = (String) arg.value();
}
}
if(orgPath == null) { 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; Path filePath = null;
@@ -49,19 +44,19 @@ public class ReadFileTool extends OllamaFunctionTool {
filePath = fs.resolve(orgPath); filePath = fs.resolve(orgPath);
} }
catch (IOException ex) { catch (IOException ex) {
throw new OllamaToolErrorException(this.name(), ex); throw new ToolException(this, ex);
} }
File file = filePath.toFile(); File file = filePath.toFile();
if(!file.exists()) { 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; BufferedReader br = null;
try{ try{
br = new BufferedReader(new FileReader(file)); br = new BufferedReader(new FileReader(file));
}catch (FileNotFoundException ex) { }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(); StringBuilder sb = new StringBuilder();
String tmp = null; String tmp = null;
@@ -71,8 +66,8 @@ public class ReadFileTool extends OllamaFunctionTool {
} }
} }
catch (IOException e) { 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; package me.neurodock.core.files.tools;
import me.neurodock.core.files.FileHandler; import me.neurodock.core.files.FileHandler;
import me.neurodock.ollama.OllamaFunctionArgument; import me.neurodock.llm.exceptions.ToolException;
import me.neurodock.ollama.OllamaFunctionTool; import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.ollama.OllamaPerameter; import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.ollama.OllamaToolResponse; import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.ollama.OllamaPerameter.OllamaPerameterBuilder.Type; import me.neurodock.llm.tools.ToolResponse;
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import static me.neurodock.llm.tools.ToolParameters.ToolParametersBuilder.*;
import java.io.*; import java.io.*;
import java.nio.file.Path; import java.nio.file.Path;
public class WriteFileTool extends OllamaFunctionTool { public class WriteFileTool extends FunctionTool {
FileHandler fs = FileHandler.getInstance(); FileHandler fs = FileHandler.getInstance();
@Override @Override
@@ -21,8 +21,8 @@ public class WriteFileTool extends OllamaFunctionTool {
} }
@Override @Override
public @NotNull OllamaPerameter parameters() { public @NotNull ToolParameters parameters() {
return OllamaPerameter.builder() return ToolParameters.builder()
.addProperty("file_path", Type.STRING, "The path to the file to write to", true) .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("file_content", Type.STRING, "The content to write to the file", true)
.addProperty("overwrite", Type.BOOLEAN, "Overwrite the file, defaults to false", false) .addProperty("overwrite", Type.BOOLEAN, "Overwrite the file, defaults to false", false)
@@ -30,30 +30,14 @@ public class WriteFileTool extends OllamaFunctionTool {
} }
@Override @Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { public @NotNull ToolResponse function(ToolArguments args) {
String path = null; String path = args.getArgument("file_path", String.class);
String content = null; String content = args.getArgument("file_content", String.class);
boolean overwrite = false; boolean overwrite = args.getArgument("overwrite", Boolean.class);
for(OllamaFunctionArgument arg : args)
{
switch (arg.argument())
{
case "file_path" -> {
path = (String) arg.value();
}
case "file_content" -> {
content = (String) arg.value();
}
case "overwrite" -> {
overwrite = (boolean) arg.value();
}
}
}
if(path == null || content == null || path.isBlank() || content.isBlank()) 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; Path filePath = null;
@@ -62,12 +46,12 @@ public class WriteFileTool extends OllamaFunctionTool {
filePath = fs.resolve(path); filePath = fs.resolve(path);
} }
catch (IOException ex) { catch (IOException ex) {
throw new OllamaToolErrorException(this.name(), ex); throw new ToolException(this, ex);
} }
File file = filePath.toFile(); File file = filePath.toFile();
if(file.exists() && !overwrite) { 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) else if(file.exists() && overwrite)
{ {
@@ -77,17 +61,17 @@ public class WriteFileTool extends OllamaFunctionTool {
try { try {
file.createNewFile(); file.createNewFile();
} catch (IOException e) { } catch (IOException e) {
throw new OllamaToolErrorException(name(), e); throw new ToolException(this, e);
} }
try(BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)))) try(BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file))))
{ {
bw.write(content); 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) catch (IOException ex)
{ {
throw new OllamaToolErrorException(name(), ex); throw new ToolException(this, ex);
} }
} }
} }
@@ -1,17 +1,17 @@
package me.neurodock.core.memory; package me.neurodock.core.memory;
import me.neurodock.ollama.OllamaFunctionArgument; import me.neurodock.llm.exceptions.ToolException;
import me.neurodock.ollama.OllamaFunctionTool; import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.ollama.OllamaPerameter; import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.ollama.OllamaToolResponse; import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.ollama.exceptions.OllamaToolErrorException; import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
/** /**
* Provides the add_memory function.<br> * Provides the add_memory function.<br>
* This function adds a string to the memory. * This function adds a string to the memory.
*/ */
public class AddMemoryFunction extends OllamaFunctionTool { public class AddMemoryFunction extends FunctionTool {
/** /**
* The CoreMemory instance. * The CoreMemory instance.
*/ */
@@ -28,37 +28,23 @@ public class AddMemoryFunction extends OllamaFunctionTool {
} }
@Override @Override
public @NotNull OllamaPerameter parameters() { public @NotNull ToolParameters parameters() {
return OllamaPerameter.builder() return ToolParameters.builder()
.addProperty("memory", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The memory to remember", true) .addProperty("memory", ToolParameters.ToolParametersBuilder.Type.STRING, "The memory to remember", true)
.addProperty("identity", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The identity of the memory to remember", true) .addProperty("identity", ToolParameters.ToolParametersBuilder.Type.STRING, "The identity of the memory to remember", true)
.build(); .build();
} }
@Override @Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { public @NotNull ToolResponse function(ToolArguments args) {
if (args.length == 0) { String memory = args.optArgument("memory", String.class);
throw new OllamaToolErrorException(name(), "Missing memory argument"); String identity = args.optArgument("identity", String.class);
}
String memory = null;
String identity = null;
for(OllamaFunctionArgument arg : args) {
if (arg.argument().equals("memory")) {
memory = (String) arg.value();
} else if (arg.argument().equals("identity")) {
identity = (String) arg.value();
} else {
throw new OllamaToolErrorException(name(), "Unknown argument: " + arg.argument());
}
}
if (memory == null || identity == null) { 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); 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; package me.neurodock.core.memory;
import me.neurodock.core.Core; import me.neurodock.core.Options;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
@@ -17,7 +17,7 @@ public class CoreMemory {
/** /**
* The singleton instance of CoreMemory. * The singleton instance of CoreMemory.
*/ */
private static final CoreMemory instance = new CoreMemory(Core.DATA + "/CoreMemory.json"); private static final CoreMemory instance = new CoreMemory(Options.getInstance().getDataDir() + "/CoreMemory.json");
/** /**
* Memory type identifier for key-value mapped memory storage. * Memory type identifier for key-value mapped memory storage.
@@ -1,12 +1,12 @@
package me.neurodock.core.memory; package me.neurodock.core.memory;
import me.neurodock.ollama.OllamaFunctionArgument; import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.ollama.OllamaFunctionTool; import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.ollama.OllamaPerameter; import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.ollama.OllamaToolResponse; import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class GetMemoriesFunction extends OllamaFunctionTool { public class GetMemoriesFunction extends FunctionTool {
@Override @Override
public @NotNull String name() { public @NotNull String name() {
return "get_memories"; return "get_memories";
@@ -18,12 +18,12 @@ public class GetMemoriesFunction extends OllamaFunctionTool {
} }
@Override @Override
public @NotNull OllamaPerameter parameters() { public @NotNull ToolParameters parameters() {
return null; return ToolParameters.empty();
} }
@Override @Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { public @NotNull ToolResponse function(ToolArguments args) {
return new OllamaToolResponse(name(), CoreMemory.getInstance().getMappedMemories()); return new ToolResponse(name(), CoreMemory.getInstance().getMappedMemories());
} }
} }
@@ -1,16 +1,16 @@
package me.neurodock.core.memory; package me.neurodock.core.memory;
import me.neurodock.ollama.OllamaFunctionArgument; import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.ollama.OllamaFunctionTool; import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.ollama.OllamaPerameter; import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.ollama.OllamaToolResponse; import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
/** /**
* Provides the get_memory function.<br> * Provides the get_memory function.<br>
* This function retrives all the memory. * This function retrives all the memory.
*/ */
public class GetMemoryFunction extends OllamaFunctionTool { public class GetMemoryFunction extends FunctionTool {
/** /**
* The CoreMemory instance. * The CoreMemory instance.
*/ */
@@ -27,16 +27,16 @@ public class GetMemoryFunction extends OllamaFunctionTool {
} }
@Override @Override
public @NotNull OllamaPerameter parameters() { public @NotNull ToolParameters parameters() {
return OllamaPerameter.builder() return ToolParameters.builder()
.addProperty("identity", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The identity of the memory to retrieve", true) .addProperty("identity", ToolParameters.ToolParametersBuilder.Type.STRING, "The identity of the memory to retrieve", true)
.build(); .build();
} }
@Override @Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { public @NotNull ToolResponse function(ToolArguments args) {
return memory.getMemory((String) (args[0].value())) return memory.getMemory(args.optArgument("identity", String.class))
.map(value -> new OllamaToolResponse(name(), value)) .map(value -> new ToolResponse(name(), value))
.orElse(OllamaToolResponse.empty("No memory found for key: " + args[0].value())); .orElse(ToolResponse.empty(name(), "No memory found for key: " + args.optArgument("identity", String.class)));
} }
} }
@@ -1,13 +1,13 @@
package me.neurodock.core.memory; package me.neurodock.core.memory;
import me.neurodock.ollama.OllamaFunctionArgument; import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.ollama.OllamaFunctionTool; import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.ollama.OllamaPerameter; import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.ollama.OllamaToolResponse; import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.json.JSONArray; import org.json.JSONArray;
public class GetMemoryIdentitiesFunction extends OllamaFunctionTool { public class GetMemoryIdentitiesFunction extends FunctionTool {
CoreMemory memory = CoreMemory.getInstance(); CoreMemory memory = CoreMemory.getInstance();
@Override @Override
@@ -21,12 +21,12 @@ public class GetMemoryIdentitiesFunction extends OllamaFunctionTool {
} }
@Override @Override
public @NotNull OllamaPerameter parameters() { public @NotNull ToolParameters parameters() {
return null; return ToolParameters.empty();
} }
@Override @Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { public @NotNull ToolResponse function(ToolArguments args) {
return new OllamaToolResponse(this.name(), new JSONArray(memory.getMemoriesIdentity()).toString()); return new ToolResponse(this.name(), new JSONArray(memory.getMemoriesIdentity()).toString());
} }
} }
@@ -1,17 +1,17 @@
package me.neurodock.core.memory; package me.neurodock.core.memory;
import me.neurodock.ollama.OllamaFunctionArgument; import me.neurodock.llm.exceptions.ToolException;
import me.neurodock.ollama.OllamaFunctionTool; import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.ollama.OllamaPerameter; import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.ollama.OllamaToolResponse; import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.ollama.exceptions.OllamaToolErrorException; import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
/** /**
* Provides the remove_memory function.<br> * Provides the remove_memory function.<br>
* This function removes a value from the memory. * This function removes a value from the memory.
*/ */
public class RemoveMemoryFunction extends OllamaFunctionTool { public class RemoveMemoryFunction extends FunctionTool {
/** /**
* The CoreMemory instance. * The CoreMemory instance.
*/ */
@@ -28,19 +28,19 @@ public class RemoveMemoryFunction extends OllamaFunctionTool {
} }
@Override @Override
public @NotNull OllamaPerameter parameters() { public @NotNull ToolParameters parameters() {
return OllamaPerameter.builder() return ToolParameters.builder()
.addProperty("identity", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The identity of the memory to forget", true) .addProperty("identity", ToolParameters.ToolParametersBuilder.Type.STRING, "The identity of the memory to forget", true)
.build(); .build();
} }
@Override @Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { public @NotNull ToolResponse function(ToolArguments args) {
if (args.length == 0) { String value = args.optArgument("identity", String.class);
throw new OllamaToolErrorException(name(), "Missing memory argument"); if(value == null || value.isEmpty()) {
throw new ToolException(this, "Missing identity argument");
} }
String value = (String) args[0].value();
memory.removeMemory(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; package me.neurodock.core.memory.array;
import me.neurodock.core.memory.CoreMemory; import me.neurodock.core.memory.CoreMemory;
import me.neurodock.ollama.OllamaFunctionArgument; import me.neurodock.llm.exceptions.ToolException;
import me.neurodock.ollama.OllamaFunctionTool; import me.neurodock.llm.tools.*;
import me.neurodock.ollama.OllamaPerameter;
import me.neurodock.ollama.OllamaPerameter.OllamaPerameterBuilder.Type;
import me.neurodock.ollama.OllamaToolResponse;
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class AddArrayMemory extends OllamaFunctionTool { public class AddArrayMemory extends FunctionTool {
private CoreMemory memory = CoreMemory.getInstance(); private CoreMemory memory = CoreMemory.getInstance();
@@ -19,37 +15,29 @@ public class AddArrayMemory extends OllamaFunctionTool {
} }
@Override @Override
public @NotNull OllamaPerameter parameters() { public @NotNull ToolParameters parameters() {
return OllamaPerameter.builder() return ToolParameters.builder()
.addProperty("memory", Type.STRING, "The memory to remember", true) .addProperty("memory", ToolParameters.ToolParametersBuilder.Type.STRING, "The memory to remember", true)
.addProperty("index", Type.INT, "The index to put it at. Should be avoided, unless overwriting", false) .addProperty("index", ToolParameters.ToolParametersBuilder.Type.INT, "The index to put it at. Should be avoided, unless overwriting", false)
.build(); .build();
} }
@Override @Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { public @NotNull ToolResponse function(ToolArguments args) {
if (args.length > 1) { if (args.hasArgument("index")) {
String memory = null; String memory = args.optArgument("memory", String.class);
int index = -1; int index = args.optArgument("index", Integer.class);
for (OllamaFunctionArgument arg : args) {
if(arg.argument().equals("memory")) {
memory = (String) arg.value();
}
else if(arg.argument().equals("index")) {
index = (Integer) arg.value();
}
}
if (memory == null || index < 0) { 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); this.memory.addMemory(memory, index);
} }
else { else {
if(!args[0].argument().equals("memory")) { if(!args.hasArgument("memory")) {
throw new OllamaToolErrorException(name(), "no memory provided"); 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; package me.neurodock.core.memory.array;
import me.neurodock.core.memory.CoreMemory; import me.neurodock.core.memory.CoreMemory;
import me.neurodock.ollama.OllamaFunctionArgument; import me.neurodock.llm.exceptions.ToolException;
import me.neurodock.ollama.OllamaFunctionTool; import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.ollama.OllamaPerameter; import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.ollama.OllamaToolResponse; import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.ollama.OllamaPerameter.OllamaPerameterBuilder.Type; import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class GetArrayMemory extends OllamaFunctionTool { public class GetArrayMemory extends FunctionTool {
CoreMemory memory = CoreMemory.getInstance(); CoreMemory memory = CoreMemory.getInstance();
@@ -18,19 +18,17 @@ public class GetArrayMemory extends OllamaFunctionTool {
} }
@Override @Override
public @NotNull OllamaPerameter parameters() { public @NotNull ToolParameters parameters() {
return OllamaPerameter.builder() return ToolParameters.builder()
.addProperty("index", Type.STRING, "The index to retrieve memory from", false) .addProperty("index", ToolParameters.ToolParametersBuilder.Type.STRING, "The index to retrieve memory from", false)
.build(); .build();
} }
@Override @Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { public @NotNull ToolResponse function(ToolArguments args) {
if (args.length != 1) { if(!args.hasArgument("index")) throw new ToolException(this, "Missing index");
return new OllamaToolResponse(name(), memory.getArrayMemories().toString()); return memory.getMemory(args.optArgument("index", Integer.class, -1))
} .map(value -> new ToolResponse(name(), value))
return memory.getMemory((Integer) args[0].value()) .orElse(ToolResponse.empty("No memory found for key: " + args.optArgument("index", -1)));
.map(value -> new OllamaToolResponse(name(), value))
.orElse(OllamaToolResponse.empty("No memory found for key: " + args[0].value()));
} }
} }
@@ -1,13 +1,13 @@
package me.neurodock.core.memory.array; package me.neurodock.core.memory.array;
import me.neurodock.core.memory.CoreMemory; import me.neurodock.core.memory.CoreMemory;
import me.neurodock.ollama.OllamaFunctionArgument; import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.ollama.OllamaFunctionTool; import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.ollama.OllamaPerameter; import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.ollama.OllamaToolResponse; import me.neurodock.llm.tools.ToolResponse;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class GetArrayedMemories extends OllamaFunctionTool { public class GetArrayedMemories extends FunctionTool {
private CoreMemory memory = CoreMemory.getInstance(); private CoreMemory memory = CoreMemory.getInstance();
@Override @Override
public @NotNull String name() { public @NotNull String name() {
@@ -15,12 +15,12 @@ public class GetArrayedMemories extends OllamaFunctionTool {
} }
@Override @Override
public @NotNull OllamaPerameter parameters() { public @NotNull ToolParameters parameters() {
return OllamaPerameter.empty(); return ToolParameters.empty();
} }
@Override @Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { public @NotNull ToolResponse function(ToolArguments args) {
return new OllamaToolResponse(name(), memory.getArrayMemories().toString()); 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; import org.json.JSONObject;
/** /**
* Represents a response from a tool. * Represents a response from a tool.
*/ */
public class OllamaToolResponse extends OllamaMessage { public class ToolResponse extends Message {
/** /**
* Returns an empty tool response * 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 * @param tool The tool that responded
* @return an empty tool response * @return an empty tool response
*/ */
public static OllamaToolResponse empty(String tool) public static ToolResponse empty(String tool)
{ {
return empty(tool, "No reason provided"); return empty(tool, "No reason provided");
} }
/** /**
* Returns an empty tool response with a reason/description. * 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 tool The tool that responded
* @param description A description for why this is empty * @param description A description for why this is empty
* @return an empty tool response with a reason * @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; 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 tool The tool that responded
* @param response The response from the tool * @param response The response from the tool
*/ */
public OllamaToolResponse(String tool, String response) { public ToolResponse(String tool, String response) {
super(OllamaMessageRole.TOOL, new JSONObject().put("tool", tool).put("result", response).toString()); super(Message.Role.TOOL, new JSONObject().put("tool", tool).put("result", response).toString());
this.tool = tool; this.tool = tool;
this.response = response; this.response = response;
} }
@@ -65,4 +67,8 @@ public class OllamaToolResponse extends OllamaMessage {
public String getResponse() { public String getResponse() {
return response; 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 jdk.jshell.spi.ExecutionControl;
import me.neurodock.core.Pair; import me.neurodock.core.Pair;
import me.neurodock.ollama.*; import me.neurodock.llm.tools.FunctionTool;
import me.neurodock.ollama.exceptions.OllamaToolErrorException; import me.neurodock.llm.tools.ToolArguments;
import me.neurodock.llm.tools.ToolParameters;
import me.neurodock.plugin.Data; import me.neurodock.plugin.Data;
import me.neurodock.plugin.LoadedPlugin; import me.neurodock.plugin.LoadedPlugin;
import me.neurodock.plugin.Plugin; 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.PluginLoadingException;
import me.neurodock.plugin.exceptions.ToolRuntimeException; import me.neurodock.plugin.exceptions.ToolRuntimeException;
import me.neurodock.plugin.tool.Tool; import me.neurodock.plugin.tool.Tool;
import me.neurodock.plugin.tool.ToolArguments;
import me.neurodock.plugin.tool.ToolResponse; import me.neurodock.plugin.tool.ToolResponse;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.json.JSONObject; import org.json.JSONObject;
import javax.naming.OperationNotSupportedException;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
@@ -40,12 +41,12 @@ public class Loader {
* Mock example! * Mock example!
* @return * @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()) { for(Tool tool : plugin.getTools()) {
tools.add(new OllamaFunctionTool() { tools.add(new FunctionTool() {
@Override @Override
public @NotNull String name() { public @NotNull String name() {
return tool.name() + "_" + plugin.getMetadata().getName(); return tool.name() + "_" + plugin.getMetadata().getName();
@@ -57,14 +58,16 @@ public class Loader {
} }
@Override @Override
public @NotNull OllamaPerameter parameters() { public @NotNull ToolParameters parameters() {
return OllamaPerameter.builder().of(tool.parameters()).build(); throw new RuntimeException(new OperationNotSupportedException("No"));
//return ToolParameters.builder().of(tool.parameters()).build();
} }
@Override @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) // Wrap OllamaFunctionArguments[] to ether ToolArgument[] or ToolArguments(current implementation)
ToolArguments toolArgs = new ToolArguments(); /*ToolArguments toolArgs = new ToolArguments();
for (OllamaFunctionArgument arg : args) { for (OllamaFunctionArgument arg : args) {
toolArgs.addArgument(arg.argument(), arg.value()); toolArgs.addArgument(arg.argument(), arg.value());
} }
@@ -75,17 +78,12 @@ public class Loader {
throw new OllamaToolErrorException(name(), e); throw new OllamaToolErrorException(name(), e);
} }
// Wrap ToolResponce to an OllamaToolRespnce(Well I see a typo here now) // Wrap ToolResponce to an OllamaToolRespnce(Well I see a typo here now)
return new OllamaToolResponse(toolResponse.name(), toolResponse.response()); return new OllamaToolResponse(toolResponse.name(), toolResponse.response());*/
}
@Override
public JSONObject toJSON() {
return tool.getToolJSON();
} }
}); });
} }
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.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.nio.file.Path;
public class FileTest { public class FileTest {
@Test @Test
void TestError() void TestError()
{ {
FileHandler fileHandler = new FileHandler("./test/files"); FileHandler fileHandler = new FileHandler(Path.of("./test/files"));
Assertions.assertThrowsExactly(FileHandlerException.class, () -> fileHandler.readFile("../build.gradle")); Assertions.assertThrowsExactly(FileHandlerException.class, () -> fileHandler.readFile("../build.gradle"));
} }
} }
-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; package me.neurodock.display;
import me.neurodock.backend.open.ai.OpenAIModel;
import me.neurodock.core.Core; import me.neurodock.core.Core;
import me.neurodock.core.Options;
import me.neurodock.core.Pair; import me.neurodock.core.Pair;
import me.neurodock.core.PrintMessageHandler; import me.neurodock.core.PrintMessageHandler;
import me.neurodock.core.files.FileHandlerLocation;
import me.neurodock.core.memory.CoreMemory; import me.neurodock.core.memory.CoreMemory;
import me.neurodock.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.*;
import me.neurodock.ollama.utils.SystemMessage; import me.neurodock.ollama.utils.SystemMessage;
import org.jetbrains.annotations.NotNull;
import org.json.JSONObject; import org.json.JSONObject;
import java.io.*; import java.io.*;
import java.nio.file.Path;
import java.util.*; import java.util.*;
import static me.neurodock.core.Core.writeLog; import static me.neurodock.core.Core.writeLog;
@@ -20,7 +27,14 @@ import static me.neurodock.core.Core.writeLog;
* Somewhat meant to be used as a Debug tool for testing your API. * Somewhat meant to be used as a Debug tool for testing your API.
*/ */
public class Display { public class Display {
/*
* Inizilizer
*/
{
Options.getInstance().setDataDir(Path.of("AI-Chat"), false);
}
/** /**
* The Core instance. * The Core instance.
*/ */
@@ -42,16 +56,19 @@ public class Display {
*/ */
public 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") .setModel("llama3.1:8b")
.keep_alive(10) .keep_alive(10)
//.stream(false) //.stream(false)
.addFileTools(FileHandlerLocation.DATA_FILES) .addFileTools(Options.getInstance().getFileHandlerDataLocation())
.addMessages(new SystemMessage("You need to emulate feelings and emotions.")) .addMessages(new SystemMessage("You need to emulate feelings and emotions."))
.build()); .build());*/
core.enablePlugins(Core.PLUGIN_DIRECTORY); core.setModel(model);
core.enablePlugins(Options.getInstance().getPluginDirectory());
core.addTool(new TimeTool(), Core.Source.CTP); core.addTool(new TimeTool(), Core.Source.CTP);
// TODO: Well Docker failes when luanched.... Fuck // TODO: Well Docker failes when luanched.... Fuck
@@ -158,9 +175,9 @@ public class Display {
break; break;
case "working": case "working":
System.out.println("Working directories:\n" + System.out.println("Working directories:\n" +
" Data: " + Core.DATA_DIR.getAbsolutePath() + "\n" + " Data: " + Options.getInstance().getDataDir().getAbsolutePath() + "\n" +
" DateFiles: " + FileHandlerLocation.DATA_FILES + "\n" + " DateFiles: " + Options.getInstance().getFileHandlerDataLocation() + "\n" +
" Plugins: " + Core.PLUGIN_DIRECTORY.getAbsolutePath()); " Plugins: " + Options.getInstance().getPluginDirectory().getAbsolutePath());
break; break;
default: default:
System.out.println("Unknown command: " + message); System.out.println("Unknown command: " + message);
@@ -7,6 +7,7 @@ import com.github.dockerjava.core.DockerClientImpl;
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
import com.github.dockerjava.transport.DockerHttpClient; import com.github.dockerjava.transport.DockerHttpClient;
import me.neurodock.core.Core; import me.neurodock.core.Core;
import me.neurodock.core.Options;
import me.neurodock.core.Pair; import me.neurodock.core.Pair;
import me.neurodock.ollama.OllamaFunctionArgument; import me.neurodock.ollama.OllamaFunctionArgument;
import me.neurodock.ollama.OllamaFunctionTool; import me.neurodock.ollama.OllamaFunctionTool;
@@ -434,7 +435,7 @@ public class PythonRunner extends OllamaFunctionTool {
fullCmd.append(arg).append(" "); fullCmd.append(arg).append(" ");
} }
File program = new File(Core.CACHE_DIRECTORY, "cmd.sh"); File program = new File(Options.getInstance().getCacheDirectory(), "cmd.sh");
if(program.exists()){ if(program.exists()){
program.delete(); program.delete();
} }
@@ -1,8 +1,8 @@
package me.neurodock.genius; 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; protected GeniusTools geniusToolsInstance;
public GeniusEndpointTool(GeniusTools geniusTools) { public GeniusEndpointTool(GeniusTools geniusTools) {
@@ -5,6 +5,7 @@ import io.github.classgraph.ClassInfo;
import io.github.classgraph.ClassInfoList; import io.github.classgraph.ClassInfoList;
import io.github.classgraph.ScanResult; import io.github.classgraph.ScanResult;
import me.neurodock.core.Core; import me.neurodock.core.Core;
import me.neurodock.core.Options;
import me.neurodock.ollama.OllamaFunctionTool; import me.neurodock.ollama.OllamaFunctionTool;
import me.neurodock.ollama.OllamaFunctionTools; import me.neurodock.ollama.OllamaFunctionTools;
import org.json.JSONObject; import org.json.JSONObject;
@@ -27,13 +28,13 @@ public class GeniusTools {
public final String Access_Token; public final String Access_Token;
public final String BaseURL = "https://api.genius.com"; public final String BaseURL = "https://api.genius.com";
public final OllamaFunctionTools GeniusTools; public final OllamaFunctionTools GeniusTools;
public final File CacheFile = new File(Core.DATA_DIR + "/genius_cache.json"); public final File CacheFile = new File(Options.getInstance().getDataDir(), "genius_cache.json");
public JSONObject CacheData; public JSONObject CacheData;
public GeniusTools() { public GeniusTools() {
super(); super();
try { try {
JSONObject obj = new JSONObject(Files.readString(Path.of(Core.DATA_DIR + "/geniusapi.json"))); JSONObject obj = new JSONObject(Files.readString(Path.of(Options.getInstance().getDataDir().getPath(), "geniusapi.json")));
this.Client_ID = obj.getString("client_id"); this.Client_ID = obj.getString("client_id");
this.Client_Secret = obj.getString("client_secret"); this.Client_Secret = obj.getString("client_secret");
this.Access_Token = obj.getString("access_token"); this.Access_Token = obj.getString("access_token");
@@ -1,12 +1,11 @@
package me.neurodock.genius.endpoints; 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.GeniusEndpoint;
import me.neurodock.genius.GeniusEndpointTool; import me.neurodock.genius.GeniusEndpointTool;
import me.neurodock.genius.GeniusTools; 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.jetbrains.annotations.NotNull;
import org.json.JSONObject; import org.json.JSONObject;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
@@ -49,20 +48,16 @@ public class GetLyrics extends GeniusEndpointTool {
} }
@Override @Override
public @NotNull OllamaPerameter parameters() { public @NotNull ToolParameters parameters() {
return OllamaPerameter.builder() return ToolParameters.builder()
.addProperty("song_id", OllamaPerameter.OllamaPerameterBuilder.Type.INT, "The ID of the song to get lyrics for.", true) .addProperty("song_id", ToolParameters.ToolParametersBuilder.Type.INT, "The ID of the song to get lyrics for.", true)
.build(); .build();
} }
@Override @Override
public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { public @NotNull ToolResponse function(ToolArguments args) {
if(!( args[0].value() instanceof Integer)) { String lyricsStr = geniusToolsInstance.hasCache(args.getArgument("song_id", Integer.class));
throw new OllamaToolErrorException(this.name(), "The song_id must be an integer.");
}
String lyricsStr = geniusToolsInstance.hasCache((int) args[0].value());
if(lyricsStr != null) if(lyricsStr != null)
{ {
@@ -5,6 +5,7 @@ import io.github.classgraph.ClassInfo;
import io.github.classgraph.ClassInfoList; import io.github.classgraph.ClassInfoList;
import io.github.classgraph.ScanResult; import io.github.classgraph.ScanResult;
import me.neurodock.core.Core; import me.neurodock.core.Core;
import me.neurodock.core.Options;
import me.neurodock.ollama.OllamaFunctionTool; import me.neurodock.ollama.OllamaFunctionTool;
import me.neurodock.ollama.OllamaFunctionTools; import me.neurodock.ollama.OllamaFunctionTools;
import org.json.JSONObject; import org.json.JSONObject;
@@ -30,7 +31,7 @@ public class MALAPITool {
public MALAPITool() { public MALAPITool() {
super(); super();
try { try {
JSONObject obj = new JSONObject(Files.readString(Path.of(Core.DATA_DIR + "/malapi.json"))); JSONObject obj = new JSONObject(Files.readString(Path.of(Options.getInstance().getDataDir().getPath(), "malapi.json")));
this.Client_ID = obj.getString("client_id"); this.Client_ID = obj.getString("client_id");
this.Client_Secret = obj.getString("client_secret"); this.Client_Secret = obj.getString("client_secret");
} catch (IOException e) { } catch (IOException e) {
+1 -1
View File
@@ -2,7 +2,7 @@ plugins {
id 'java-library' id 'java-library'
} }
version = '0.1.4.1' version = pluginAPIVersion
repositories { repositories {
mavenCentral() mavenCentral()
+6
View File
@@ -18,6 +18,12 @@ The author **does not endorse or encourage** scraping or any other use that viol
Use this software **at your own risk**. The author disclaims any liability for legal or technical consequences arising from its use. Use this software **at your own risk**. The author disclaims any liability for legal or technical consequences arising from its use.
## How to report issues?
This is a personal, self-hosted Gitea instance, and I haven't set up account
registration or figured out permissions for letting new accounts only open
issues — and honestly, I'd rather not have random accounts created here anyway.<br>
All issues for NeuroDock should be filed on the **[Codeberg mirror](https://codeberg.org/alienfromdia/NeuroDock/issues)**. Thanks for understanding.
## API ## API
The documentation for the API is available at the gitea wiki under [API docs](https://git.server.4zellen.se/neurodock/NeuroDock/wiki/API-Docs) The documentation for the API is available at the gitea wiki under [API docs](https://git.server.4zellen.se/neurodock/NeuroDock/wiki/API-Docs)
+5 -1
View File
@@ -1,3 +1,7 @@
# This is used in sub-project that dosent explicitly require the API sub-project to only include if thay shuld be included project wide. # This is used in sub-project that dosent explicitly require the API sub-project to only include if thay shuld be included project wide.
useAPI = true useAPI = true
javaVersion = 26 javaVersion = 25
coreVersion = 2.1.5.1
pluginAPIVersion = 1.1.4.1
APIVersion = 1.0-SNAPSHOT