From 2949a0d2fe40b28f2955a6e71ccac0d213d86a6a Mon Sep 17 00:00:00 2001 From: Zacharias Date: Sat, 29 Aug 2026 23:52:02 +0200 Subject: [PATCH] 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 --- Core/build.gradle | 1 + .../open/ai/OpenAIAssistentMessage.java | 63 ++ .../backend/open/ai/OpenAIModel.java | 629 +++++++++++++++ .../src/main/java/me/neurodock/core/Core.java | 481 ++++-------- .../me/neurodock/core/LLMSystemPrompt.java | 53 +- .../core/PrintAdvanceMessageHandler.java | 13 +- .../neurodock/core/PrintMessageHandler.java | 14 +- .../me/neurodock/core/files/FileHandler.java | 12 +- .../core/files/tools/ReadFileTool.java | 39 +- .../core/files/tools/WriteFileTool.java | 54 +- .../core/memory/AddMemoryFunction.java | 44 +- .../core/memory/GetMemoriesFunction.java | 18 +- .../core/memory/GetMemoryFunction.java | 24 +- .../memory/GetMemoryIdentitiesFunction.java | 18 +- .../core/memory/RemoveMemoryFunction.java | 28 +- .../core/memory/array/AddArrayMemory.java | 44 +- .../core/memory/array/GetArrayMemory.java | 30 +- .../core/memory/array/GetArrayedMemories.java | 18 +- .../java/me/neurodock/llm/ChatObject.java | 18 + .../main/java/me/neurodock/llm/Message.java | 70 ++ .../src/main/java/me/neurodock/llm/Model.java | 60 ++ .../java/me/neurodock/llm/StreamingModel.java | 17 + .../exceptions/ModelNotFoundException.java | 11 + .../llm/exceptions/ToolException.java | 21 + .../me/neurodock/llm/tools/FunctionTool.java | 41 + .../java/me/neurodock/llm/tools/Tool.java | 27 + .../me/neurodock/llm/tools/ToolArguments.java | 66 ++ .../neurodock/llm/tools/ToolParameters.java | 212 +++++ .../tools/ToolResponse.java} | 28 +- .../llm/tools/serializer/ToolSerializer.java | 36 + .../llm/tools/serializer/ToolToJSON.java | 14 + .../ollama/OllamaFunctionArgument.java | 69 -- .../neurodock/ollama/OllamaFunctionTool.java | 96 --- .../neurodock/ollama/OllamaFunctionTools.java | 111 --- .../me/neurodock/ollama/OllamaMessage.java | 86 --- .../neurodock/ollama/OllamaMessageRole.java | 58 -- .../ollama/OllamaMessageToolCall.java | 37 - .../me/neurodock/ollama/OllamaObject.java | 730 ------------------ .../me/neurodock/ollama/OllamaPerameter.java | 298 ------- .../java/me/neurodock/ollama/OllamaTool.java | 10 - .../me/neurodock/ollama/OllamaToolError.java | 31 - .../exceptions/OllamaToolErrorException.java | 50 -- .../neurodock/ollama/utils/SystemMessage.java | 15 - .../me/neurodock/plugin/loader/Loader.java | 32 +- Core/src/test/java/OllamaImages.java | 55 -- Core/src/test/java/plugin/Test.java | 12 - Core/src/test/java/plugin/Tool.java | 37 - .../java/me/neurodock/display/Display.java | 24 +- .../neurodock/genius/GeniusEndpointTool.java | 4 +- .../neurodock/genius/endpoints/GetLyrics.java | 21 +- gradle.properties | 4 +- 51 files changed, 1659 insertions(+), 2325 deletions(-) create mode 100644 Core/src/main/java/me/neurodock/backend/open/ai/OpenAIAssistentMessage.java create mode 100644 Core/src/main/java/me/neurodock/backend/open/ai/OpenAIModel.java create mode 100644 Core/src/main/java/me/neurodock/llm/ChatObject.java create mode 100644 Core/src/main/java/me/neurodock/llm/Message.java create mode 100644 Core/src/main/java/me/neurodock/llm/Model.java create mode 100644 Core/src/main/java/me/neurodock/llm/StreamingModel.java create mode 100644 Core/src/main/java/me/neurodock/llm/exceptions/ModelNotFoundException.java create mode 100644 Core/src/main/java/me/neurodock/llm/exceptions/ToolException.java create mode 100644 Core/src/main/java/me/neurodock/llm/tools/FunctionTool.java create mode 100644 Core/src/main/java/me/neurodock/llm/tools/Tool.java create mode 100644 Core/src/main/java/me/neurodock/llm/tools/ToolArguments.java create mode 100644 Core/src/main/java/me/neurodock/llm/tools/ToolParameters.java rename Core/src/main/java/me/neurodock/{ollama/OllamaToolResponse.java => llm/tools/ToolResponse.java} (59%) create mode 100644 Core/src/main/java/me/neurodock/llm/tools/serializer/ToolSerializer.java create mode 100644 Core/src/main/java/me/neurodock/llm/tools/serializer/ToolToJSON.java delete mode 100644 Core/src/main/java/me/neurodock/ollama/OllamaFunctionArgument.java delete mode 100644 Core/src/main/java/me/neurodock/ollama/OllamaFunctionTool.java delete mode 100644 Core/src/main/java/me/neurodock/ollama/OllamaFunctionTools.java delete mode 100644 Core/src/main/java/me/neurodock/ollama/OllamaMessage.java delete mode 100644 Core/src/main/java/me/neurodock/ollama/OllamaMessageRole.java delete mode 100644 Core/src/main/java/me/neurodock/ollama/OllamaMessageToolCall.java delete mode 100644 Core/src/main/java/me/neurodock/ollama/OllamaObject.java delete mode 100644 Core/src/main/java/me/neurodock/ollama/OllamaPerameter.java delete mode 100644 Core/src/main/java/me/neurodock/ollama/OllamaTool.java delete mode 100644 Core/src/main/java/me/neurodock/ollama/OllamaToolError.java delete mode 100644 Core/src/main/java/me/neurodock/ollama/exceptions/OllamaToolErrorException.java delete mode 100644 Core/src/main/java/me/neurodock/ollama/utils/SystemMessage.java delete mode 100644 Core/src/test/java/OllamaImages.java delete mode 100644 Core/src/test/java/plugin/Test.java delete mode 100644 Core/src/test/java/plugin/Tool.java diff --git a/Core/build.gradle b/Core/build.gradle index 4b061d1..1aa691f 100644 --- a/Core/build.gradle +++ b/Core/build.gradle @@ -9,6 +9,7 @@ dependencies { 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 { diff --git a/Core/src/main/java/me/neurodock/backend/open/ai/OpenAIAssistentMessage.java b/Core/src/main/java/me/neurodock/backend/open/ai/OpenAIAssistentMessage.java new file mode 100644 index 0000000..2f0e99e --- /dev/null +++ b/Core/src/main/java/me/neurodock/backend/open/ai/OpenAIAssistentMessage.java @@ -0,0 +1,63 @@ +package me.neurodock.backend.open.ai; + +import me.neurodock.llm.Message; +import org.json.JSONArray; + +public class OpenAIAssistentMessage extends Message { + private Reason endReason; + + public OpenAIAssistentMessage(Role role, Object content, Reason endReason) { + super(role, content); + this.endReason = endReason; + } + + public OpenAIAssistentMessage(Role role, Object content, JSONArray toolCalls, Reason endReason) { + super(role, content, toolCalls); + this.endReason = endReason; + } + + public OpenAIAssistentMessage(Role role, String toolID, Object content, Reason endReason) { + super(role, toolID, content); + this.endReason = endReason; + } + + public Reason getEndReason() { + return endReason; + } + + enum Reason { + STOP, + LENGTH, + TOOL_CALLS, + CONTENT_FILTER, + FUNCTION_CALL, + /** + * Still streaming, or null + */ + NULL; + + public static Reason fromString(String reason) { + if(reason == null) { + return NULL; + } + else if(reason.equalsIgnoreCase("STOP")) { + return STOP; + } + else if(reason.equalsIgnoreCase("LENGTH")) { + return LENGTH; + } + else if(reason.equalsIgnoreCase("TOOL_CALLS")) { + return TOOL_CALLS; + } + else if(reason.equalsIgnoreCase("CONTENT_FILTER")) { + return CONTENT_FILTER; + } + else if(reason.equalsIgnoreCase("FUNCTION_CALL")) { + return FUNCTION_CALL; + } + else { + return NULL; + } + } + } +} diff --git a/Core/src/main/java/me/neurodock/backend/open/ai/OpenAIModel.java b/Core/src/main/java/me/neurodock/backend/open/ai/OpenAIModel.java new file mode 100644 index 0000000..83deb6c --- /dev/null +++ b/Core/src/main/java/me/neurodock/backend/open/ai/OpenAIModel.java @@ -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) 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. + *

+ * 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.
+ * 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. + *

+ * 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 qurryModel(ChatObject obj, List 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 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 singleFire(Message msg, List 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 queryJSON(JSONObject payload, String endpoint, String method, + Consumer 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 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 qurryModel(ChatObject obj, List tools, Consumer 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 singleFire(Message msg, Consumer 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 singleFire(Message msg, List tools, Consumer 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); + }); + } +} diff --git a/Core/src/main/java/me/neurodock/core/Core.java b/Core/src/main/java/me/neurodock/core/Core.java index 7cbee0d..14386c0 100644 --- a/Core/src/main/java/me/neurodock/core/Core.java +++ b/Core/src/main/java/me/neurodock/core/Core.java @@ -1,33 +1,36 @@ package me.neurodock.core; import me.neurodock.core.memory.*; -import me.neurodock.ollama.*; -import me.neurodock.ollama.exceptions.OllamaToolErrorException; +import me.neurodock.llm.ChatObject; +import me.neurodock.llm.Message; +import me.neurodock.llm.Model; +import me.neurodock.llm.StreamingModel; +import me.neurodock.llm.exceptions.ToolException; +import me.neurodock.llm.tools.FunctionTool; +import me.neurodock.llm.tools.Tool; +import me.neurodock.llm.tools.ToolArguments; +import me.neurodock.llm.tools.ToolResponse; import me.neurodock.plugin.Data; import me.neurodock.plugin.LoadedPlugin; import me.neurodock.plugin.loader.Loader; import me.neurodock.plugin.exceptions.PluginLoadingException; -import org.intellij.lang.annotations.MagicConstant; import org.jetbrains.annotations.NotNull; import org.json.JSONArray; import org.json.JSONObject; +import javax.naming.OperationNotSupportedException; import java.io.*; -import java.net.*; -import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.jar.JarEntry; import java.util.jar.JarFile; -import static me.neurodock.ollama.OllamaFunctionArgument.deconstructOllamaFunctionArguments; -import static me.neurodock.plugin.tool.Tool.RPCP_SOURCE; - /** * The Main class for the System, responsible for managing the OllamaObject, tools, and the Ollama API. */ @@ -49,32 +52,18 @@ public class Core { /** * The OllamaObject to use. */ - private OllamaObject ollamaObject; + private Model model; /** * The list of tools to use. */ - private ArrayList> funtionTools = new ArrayList<>(); - - /** - * The IP of the Ollama API. - */ - private String ollamaIP; - /** - * The port of the Ollama API. - */ - private int ollamaPort = 11434; - /** - * The URL of the Ollama API. - */ - private URL url; + private final ArrayList tools = new ArrayList<>(); + + private ChatObject chatObject = new ChatObject(); + /** * Used to cancel/stop a responce, only usefull if */ private AtomicBoolean cancelled = new AtomicBoolean(false); - /** - * The current connection to Ollama - */ - private HttpURLConnection activeConnection; /** * The PrintMessageHandler to use. @@ -87,25 +76,11 @@ public class Core { * * @param printMessageHandler The PrintMessageHandler to use as the default output */ - public Core(@NotNull PrintAdvanceMessageHandler printMessageHandler) { - this(printMessageHandler, "localhost"); - } - - /** - * Creates a new instance of Core with the provided PrintMessageHandler - * and a specific Ollama backend address. - * - * @param printMessageHandler The PrintMessageHandler to use as the default output - * @param ollamaIP The IP or hostname of the Ollama backend - */ - public Core(@NotNull PrintAdvanceMessageHandler printMessageHandler, @NotNull String ollamaIP) + public Core(@NotNull PrintAdvanceMessageHandler printMessageHandler) { this.printMessageHandler = printMessageHandler; - this.ollamaIP = ollamaIP; initDirectories(); - initOllamaUrl(); - confirmOllama(); initLogWriter(); initScheduler(); initShutdownHook(); @@ -143,19 +118,6 @@ public class Core { if (!dir.exists()) dir.mkdir(); } - /** - * Constructs the Ollama API {@link #url} from {@link #ollamaIP} and {@link #ollamaPort}. - * - * @throws RuntimeException If the resulting URL is malformed or the URI syntax is invalid - */ - private void initOllamaUrl() { - try { - url = new URI("http://" + ollamaIP + ":" + ollamaPort + "/api/chat").toURL(); - } catch (MalformedURLException | URISyntaxException e) { - throw new RuntimeException("Failed to construct Ollama URL", e); - } - } - /** * Initializes the {@link #logWriter}, rotating any pre-existing log file beforehand. * @@ -246,7 +208,7 @@ public class Core { } /** - * Persists the current session's messages from the {@link OllamaObject} to two locations: + * Persists the current session's messages from the {@link Model} to two locations: * a timestamped archive file under {@code ./messages/}, and a rolling {@code messages.json} * under {@link Options#getDataDir()} for resuming the session later. * @@ -262,18 +224,29 @@ public class Core { } /** - * Collects all messages from the {@link OllamaObject} and serializes them into a {@link JSONArray}. + * Collects all messages from the {@link Model} and serializes them into a {@link JSONArray}. * * @return A {@link JSONArray} containing all current messages */ private JSONArray buildMessagesArray() { JSONArray messages = new JSONArray(); - for (OllamaMessage message : ollamaObject.getMessages()) { - messages.put(message.toJSON()); + for (Message msg : this.chatObject.getConversation()) { + JSONObject message = new JSONObject(); + String role = msg.getRole().name().toLowerCase(Locale.ROOT); + message.put("role", role); + message.put("content", msg.getContent()); + if (msg.getRole() == Message.Role.TOOL) { + message.put("tool_call_id", msg.getToolID()); + } + messages.put(message); } return messages; } + public ChatObject getChatObject() { + return chatObject; + } + /** * Writes a {@link JSONArray} of messages to the given file, overwriting it if it already exists. * @@ -294,106 +267,69 @@ public class Core { } /** - * A check to exit early if Ollama isn't reachable. + * Sets the {@link #model} object to the provided argument, + * Also adds the memory base system. See {@link Core#setModelNoMemory(Model)} if you don't want to add memory functions + * @param model The Model to use */ - private boolean confirmOllama() - { - try { - URL url = URI.create("http://" + ollamaIP + ":" + ollamaPort + "/api/version").toURL(); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setDoOutput(true); - connection.setConnectTimeout(3 * 1000); + public void setModel(Model model) { + this.model = model; - int responseCode = connection.getResponseCode(); - - if (responseCode != HttpURLConnection.HTTP_OK) { - new RuntimeException("Ollama is un-responsive on url: " + url.toString()).printStackTrace(); - return false; - } - }catch (IOException ex) - { - ex.printStackTrace(); - return false; - } - return true; + addTool(new AddMemoryFunction()); + addTool(new RemoveMemoryFunction()); + addTool(new GetMemoryFunction()); + addTool(new GetMemoriesFunction()); + addTool(new GetMemoryIdentitiesFunction()); } /** - * 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 + * Sets the {@link #model} object to the provided argument, + * Does not add the base system for memory. see {@link #setModel(Model)} if you want to add memory function + * @param model The Model to use */ - public void setOllamaObject(OllamaObject ollamaObject) { - this.ollamaObject = ollamaObject; - - for(Pair 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); - } - - /** - * 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) { - this.ollamaObject = ollamaObject; - for(Pair tool : ollamaObject.getTools()) { - if(tool.getKey() instanceof OllamaFunctionTool functionTool) - { - funtionTools.add(new Pair<>(functionTool, tool.getValue())); - } - } + public void setModelNoMemory(Model model) { + this.model = model; } /** * Adds a new tool to the System * @param functionTool The tool to add - * @param source The source of the tool */ - public void addTool(OllamaFunctionTool functionTool, @MagicConstant(valuesFromClass = Source.class) String source) { - funtionTools.add(new Pair<>(functionTool, source)); - ollamaObject.addTool(functionTool, source); + public void addTool(Tool functionTool) { + tools.add(functionTool); } + public void addMessage(Message message) { + chatObject.addMessage(message); + } + + /* /** * Adds a list of tools to the System * @param tools The tools to add */ - @SuppressWarnings("MagicConstant") - public void addTools(OllamaFunctionTools tools) + /*@SuppressWarnings("MagicConstant") + public void addTools(Tools tools) { - for(Pair tool : tools) + for(Pair tool : tools) { addTool(tool.getKey(), tool.getValue()); } - } + }*/ /** * Gets the list of tools added to the System * @return The list of tools added to the System compressed as Pairs of the tool and the source */ - public ArrayList> getFuntionTools() { - return funtionTools; + public ArrayList getTools() { + return tools; } /** * Gets the Ollama Object * @return The Ollama Object */ - public OllamaObject getOllamaObject() { - return ollamaObject; + public Model getModel() { + return model; } /** @@ -401,14 +337,10 @@ public class Core { * @param name The tool to remove */ public void removeTool(String name) { - Pair funtionTool = funtionTools.stream().filter(tool -> tool.getKey().name().equalsIgnoreCase(name)).findFirst().orElse(null); - funtionTools.stream() - .filter(tool -> tool.getKey().name().equalsIgnoreCase(name)) + tools.stream() + .filter(tool -> tool.name().equalsIgnoreCase(name)) .findFirst() - .ifPresentOrElse(tool -> { - funtionTools.remove(tool); - ollamaObject.removeTool(tool.getKey()); - }, () -> { + .ifPresentOrElse(tools::remove, () -> { new IllegalArgumentException("Function tool with name '"+name+"' does not exist") .printStackTrace(); @@ -425,182 +357,51 @@ public class Core { }catch (IOException e) {} } - public void cancel() + public CompletableFuture queryModel() { - cancelled.set(true); - HttpURLConnection conn = activeConnection; - if (conn != null) { - conn.disconnect(); + return model.qurryModel(chatObject, tools); + } + + public CompletableFuture queryModel(Consumer consumer) throws OperationNotSupportedException { + if(model instanceof StreamingModel streamingModel) { + return streamingModel.qurryModel(chatObject, tools, consumer); } - } - - /** - * Queries Ollama and resolves once the full response is available, discarding any - * intermediate streaming chunks along the way. - *

- * This is a convenience overload of {@link #qurryOllama(Consumer)} with a no-op chunk - * listener. Whether the underlying request is actually sent as a streaming or - * non-streaming HTTP request is determined entirely by this query's {@link OllamaObject}'s - * {@code stream} setting — this method does not itself impose a mode. If the - * {@link OllamaObject} is configured to stream, each intermediate chunk is still received - * and parsed internally, just not exposed to the caller; use - * {@link #qurryOllama(Consumer)} if intermediate chunks are needed. - * - * @return a future that resolves to the final response object once generation completes - */ - public CompletableFuture qurryOllama() - { - return qurryOllama(_ -> {}); - } - - /** - * Queries Ollama, invoking {@code onChunk} for each JSON object received from the server, - * and resolves once the final response is available. - *

- * Whether this is a streaming or non-streaming exchange is determined by this query's - * {@link OllamaObject}'s {@code stream} setting, not by which overload was called: - *

- * In both cases, the returned future resolves once, to the final ({@code done: true}) - * response object — {@code onChunk} is for observing progress, the returned future is for - * "the response is complete." - *

- * If {@link #cancel()} is called while this query is in flight, the underlying connection - * is closed and the returned future completes exceptionally with a - * {@link java.util.concurrent.CancellationException}. - * - * @param onChunk callback invoked for each response object received from Ollama, in - * arrival order; never invoked with {@code null} - * @return a future that resolves to the final response object once generation completes - */ - public CompletableFuture qurryOllama(Consumer onChunk) { - return CompletableFuture.supplyAsync(() -> { - HttpURLConnection connection = null; - try { - connection = (HttpURLConnection) url.openConnection(); - activeConnection = connection; - - if (cancelled.get()) { - throw new CancellationException("Query cancelled before request was sent"); - } - - connection.setRequestMethod("POST"); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setDoOutput(true); - connection.setConnectTimeout(80 * 1000); - - String ollamaObjectString = ollamaObject.toJSON().toString(); - ollamaObjectString = ollamaObjectString.replace("\n", "\\n"); - - try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) { - wr.write(ollamaObjectString.getBytes(StandardCharsets.UTF_8)); - wr.flush(); - } - - int responseCode = connection.getResponseCode(); - boolean isStreaming = ollamaObject.isStream(); // whatever the real accessor is - JSONObject last = null; - StringBuilder rawErrorOrDump = new StringBuilder(); - StringBuilder messageContent = new StringBuilder(); - - try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { - String line; - while ((line = reader.readLine()) != null) { - if (cancelled.get()) throw new CancellationException("Query cancelled mid-response"); - if (line.isBlank()) continue; - - rawErrorOrDump.append(line); - - if (isStreaming) { - // Ollama emits one JSON object per line when streaming — parse and - // dispatch each one as it arrives. - JSONObject chunkObj = new JSONObject(line); - last = chunkObj; - if(chunkObj.has("message")) { - messageContent.append(chunkObj.getJSONObject("message").optString("content", "")); - } - onChunk.accept(chunkObj); - } - else - { - last = new JSONObject(line); - } - } - } catch (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); - } - } - } - - if (isStreaming && last != null && last.has("message")) { - JSONObject finalObj = new JSONObject(last.toString()); // deep-ish copy via re-parse - finalObj.getJSONObject("message").put("content", messageContent.toString()); - last = finalObj; - } - - if (responseCode == HttpURLConnection.HTTP_OK) { - return last; - } else { - printMessageHandler.printErrorMessage(new OllamaMessage(OllamaMessageRole.SYSTEM, - "Error: HTTP Response code - " + responseCode + "\n" + rawErrorOrDump)); - throw new RuntimeException("HTTP Response code - " + responseCode); - } - } catch (IOException e) { - if (cancelled.get()) throw new CancellationException("Query cancelled"); - throw new RuntimeException(e); - } finally { - if (connection != null) connection.disconnect(); - activeConnection = null; - } - }); + throw new OperationNotSupportedException("The chosen backend of type \""+model.getClass().getSimpleName()+"\" dose not support streaming"); } /** * Handles the response from Ollama. - * + *

* Processes tool calls, logs information, appends messages to the OllamaObject, * and prints output to the user. * * @param response The response from Ollama */ - public void handleResponse(JSONObject response) { + public void handleResponse(Message response) { if(response == null) return; - writeLog("Raw response: " + response.toString()); - JSONObject message = response.getJSONObject("message"); - - if(!message.has("tool_calls")) { - checkIfResponceMessage(response); - return; - } - - ollamaObject.addMessage(new OllamaMessageToolCall( - OllamaMessageRole.fromRole(message.optString("role")), - message.getString("content"), - message.getJSONArray("tool_calls") - )); + //chatObject.addMessage(response); List> futures = new ArrayList<>(); // Process each tool call - for(Object call : message.getJSONArray("tool_calls")) { + JSONArray toolCalls = response.getToolCalls(); + + if(toolCalls == null) toolCalls = new JSONArray(); + + if(toolCalls.length() == 0) + { + checkIfResponceMessage(response); + return; + } + + for(Object call : toolCalls) { futures.add(processToolCall(call)); } CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenAccept(result -> { checkIfResponceMessage(response); - qurryOllama().thenAccept(this::handleResponse); + model.qurryModel(chatObject, tools).thenAccept(this::handleResponse); }); } @@ -615,17 +416,16 @@ public class Core { private CompletableFuture processToolCall(Object call) { return CompletableFuture.runAsync(() -> { if (!(call instanceof JSONObject jsonObject)) return; - if (!jsonObject.has("function")) return; JSONObject function = jsonObject.getJSONObject("function"); - OllamaFunctionTool func = findTool(function); + Tool func = findTool(function); if (func == null) { reportToolNotFound(function); return; } - JSONObject arguments = function.getJSONObject("arguments"); + JSONObject arguments = new JSONObject(function.getString("arguments")); renderToolCalling(func.renderCalling(function), function, arguments); executeToolCall(func, arguments); }); @@ -637,11 +437,10 @@ public class Core { * @param function the function JSON containing the tool name * @return the OllamaFunctionTool if found, {@code null} otherwise */ - private OllamaFunctionTool findTool(JSONObject function) { - return funtionTools.stream() - .filter(f -> (f.getKey().name() + "_" + f.getValue()) + private Tool findTool(JSONObject function) { + return tools.stream() + .filter(f -> (f.name()) .equalsIgnoreCase(function.getString("name"))) - .map(Pair::getKey) .findFirst() .orElse(null); } @@ -652,11 +451,13 @@ public class Core { * @param function the function JSON representing a hallucinated or removed function */ private void reportToolNotFound(JSONObject function) { - OllamaToolError error = new OllamaToolError( + Message error = new Message( + Message.Role.TOOL, + function.getString("name"), "Function '" + function.getString("name") + "' does not exist" ); - ollamaObject.addMessage(error); - printMessageHandler.printToolCalling(error.getError()); + chatObject.addMessage(error); + printMessageHandler.printToolCalling(error.getContent().toString()); } /** @@ -693,37 +494,45 @@ public class Core { * @param func the {@link OllamaFunctionTool} to execute * @param arguments the raw JSON arguments for the tool */ - private void executeToolCall(OllamaFunctionTool func, JSONObject arguments) { - ArrayList args = new ArrayList<>(); - for(String key : arguments.keySet()) { - args.add(new OllamaFunctionArgument(key, arguments.get(key))); - } + private void executeToolCall(Tool tool, JSONObject arguments) { + if(tool instanceof FunctionTool func) { + ToolArguments args = new ToolArguments(); + for (String key : arguments.keySet()) { + args.addArgument(key, arguments.get(key)); + } - try { - OllamaToolResponse response = func.function(args.toArray(new OllamaFunctionArgument[0])); - ollamaObject.addMessage(response); - printMessageHandler.printMessage(response); - writeLog("Successfully function call " + func.name() + " output: " + response.getResponse()); - } catch(OllamaToolErrorException e) { - OllamaToolError error = new OllamaToolError(e.getMessage()); - ollamaObject.addMessage(error); - printMessageHandler.printErrorMessage(error); - writeLog("ERROR: " + e.getMessage()); + try { + ToolResponse response = func.function(args); + if(response.getToolID() == null) { + response.setToolID(tool.name()); + } + chatObject.addMessage(response); + printMessageHandler.printMessage(response); + writeLog("Successfully function call " + func.name() + " output: " + response.getResponse()); + } catch (ToolException e) { + Message error = new Message( + Message.Role.TOOL, + tool.name(), + e.getMessage() + ); + chatObject.addMessage(error); + printMessageHandler.printErrorMessage(error); + writeLog("ERROR: " + e.getMessage()); + } } + // TODO: Add support for misc tools } /** * Checks if the response contains a message and if so, prints it to the user * @param responce the Ollama response */ - private void checkIfResponceMessage(JSONObject responce) { - String message = responce.getJSONObject("message").getString("content"); - if(responce.getJSONObject("message").has("content") && !message.isBlank()) + private void checkIfResponceMessage(Message responce) { + if(responce.getContent() instanceof String str) { - OllamaMessage ollamaMessage = new OllamaMessage(OllamaMessageRole.ASSISTANT, message); - printMessageHandler.printMessage(ollamaMessage); - writeLog("Response content: "+ message); - ollamaObject.addMessage(ollamaMessage); + printMessageHandler.printMessage(responce); + writeLog("Response content: "+ str); + chatObject.addMessage(responce); } } @@ -786,9 +595,9 @@ public class Core { } data.plugins.forEach(loadedPlugin -> { - for(OllamaFunctionTool tool : loader.getTools(loadedPlugin.plugin())) + for(Tool tool : loader.getTools(loadedPlugin.plugin())) { - addTool(tool, Source.RPCP); + addTool(tool); } }); } @@ -822,34 +631,4 @@ public class Core { plugins.remove(plugin); } } - - /** - * Represents the source of a tool. - *

- * 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"; - } } diff --git a/Core/src/main/java/me/neurodock/core/LLMSystemPrompt.java b/Core/src/main/java/me/neurodock/core/LLMSystemPrompt.java index 67c3448..87ec192 100644 --- a/Core/src/main/java/me/neurodock/core/LLMSystemPrompt.java +++ b/Core/src/main/java/me/neurodock/core/LLMSystemPrompt.java @@ -1,6 +1,9 @@ package me.neurodock.core; -import me.neurodock.ollama.*; +import me.neurodock.llm.tools.FunctionTool; +import me.neurodock.llm.tools.Tool; +import me.neurodock.llm.Message; +import me.neurodock.llm.Model; import org.json.JSONObject; import java.io.*; @@ -39,7 +42,7 @@ import java.util.Map; *

* Instances are assembled via {@link #builder()}. Callers who need capability routing * hints compiled from a live set of registered tools must call - * {@link #generateCapabilities(OllamaObject)} before {@link #generateSystemPrompt()}; + * {@link #generateCapabilities(ArrayList)} before {@link #generateSystemPrompt()}; * see that method's docs for why this is a separate step rather than being folded into * construction. * @@ -60,12 +63,12 @@ public class LLMSystemPrompt { * Usage-hint definitions for registered tools, or {@code null} if this prompt doesn't * compile a capabilities section. This holds the *rules* for what to say about each * tool; the actual compiled text lives in {@link #compiledCapabilities} and is only - * populated once {@link #generateCapabilities(OllamaObject)} has been called. + * populated once {@link #generateCapabilities(ArrayList)} has been called. */ private Capabilities capabilities; /** * The compiled, ready-to-render {@code } body, built by - * {@link #generateCapabilities(OllamaObject)} from {@link #capabilities} and a live + * {@link #generateCapabilities(ArrayList)} from {@link #capabilities} and a live * set of registered tools. {@code null} until that method has been called at least * once, or if {@link #capabilities} was never set. */ @@ -103,18 +106,18 @@ public class LLMSystemPrompt { /** * Compiles the {@code } section from the tools currently registered on - * the given {@link OllamaObject}, using the usage hints defined in {@link #capabilities}. + * the given {@link ArrayList}, using the usage hints defined in {@link #capabilities}. *

* 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} * can change after this prompt is built (tools added/removed at runtime), so the * compiled capabilities text needs to be regenerated on demand against whatever the * current tool set actually is, not frozen at construction time. *

- * For each registered {@link OllamaFunctionTool}, this looks up a usage hint by the + * For each registered {@link FunctionTool}, this looks up a usage hint by the * tool's {@link Class} via {@link Capabilities#getUsageHints()}. If no hint was * registered for that class, the tool is silently omitted from the compiled output — - * there is no fallback to {@link OllamaFunctionTool#description()}, since that text + * there is no fallback to {@link FunctionTool#description()}, since that text * is written to help the model choose a tool mid-conversation, not to explain routing * up front in a system prompt; conflating the two would blur what each is for. *

@@ -122,18 +125,18 @@ public class LLMSystemPrompt { * section at all), this clears {@link #compiledCapabilities} to {@code null} and * returns without inspecting {@code ollamaObject}. * - * @param ollamaObject the object whose currently-registered tools should be inspected + * @param tools the object whose currently-registered tools should be inspected */ - public void generateCapabilities(OllamaObject ollamaObject) { + public void generateCapabilities(ArrayList tools) { if(capabilities == null) { compiledCapabilities = null; return; } StringBuilder builder = new StringBuilder(); - for (Pair tool : ollamaObject.getTools()) { - if(tool.getKey() instanceof OllamaFunctionTool funcTool) { - String llmName = funcTool.name() + "_" + (funcTool.getSource() != null ? funcTool.getSource() : ""); + for (Tool tool : tools) { + if(tool instanceof FunctionTool funcTool) { + String llmName = funcTool.name(); String hint = capabilities.getUsageHints().get(funcTool.getClass()); // fall back to description() if no explicit routing hint was set @@ -148,7 +151,7 @@ public class LLMSystemPrompt { /** * Renders this prompt's currently-set sections into a single system-role - * {@link OllamaMessage}, ready to be sent to Ollama. + * {@link Message}, ready to be sent to Ollama. *

* Each section — {@link #identity}, {@link #context}, {@link #compiledCapabilities}, * {@link #behavior}, {@link #outputFormat} — is wrapped in its own XML-style tag and @@ -157,13 +160,13 @@ public class LLMSystemPrompt { * skipped rather than emitted as empty tags. *

* Note that {@link #compiledCapabilities} reflects whatever tool set was live the last - * time {@link #generateCapabilities(OllamaObject)} was called — call that first if the + * time {@link #generateCapabilities(ArrayList)} was called — call that first if the * registered tools may have changed since. * - * @return a new {@link OllamaMessage} with role {@link OllamaMessageRole#SYSTEM} + * @return a new {@link Message} with role {@link Message.Role#SYSTEM} * containing the compiled prompt text */ - public OllamaMessage generateSystemPrompt() { + public Message generateSystemPrompt() { StringBuilder builder = new StringBuilder(); @@ -185,7 +188,7 @@ public class LLMSystemPrompt { builder.append("").append(System.lineSeparator()).append(outputFormat.build()).append(System.lineSeparator()).append("").append(System.lineSeparator()); } - return new OllamaMessage(OllamaMessageRole.SYSTEM, builder.toString()); + return new Message(Message.Role.SYSTEM, builder.toString()); } /** @@ -300,19 +303,19 @@ public class LLMSystemPrompt { /** * Defines per-tool usage-routing hints for the {@code } section of a * system prompt — i.e. "use this tool only in these cases" guidance, distinct from a - * tool's own {@link OllamaFunctionTool#description()}. + * tool's own {@link FunctionTool#description()}. *

* Hints are keyed by the tool's {@link Class} rather than by a registered instance or * its LLM-facing name, since NeuroDock's convention is one instance per tool class — * see {@link #use(Class, String)}. This class only holds the hint definitions; the * actual compiled text is produced by - * {@link LLMSystemPrompt#generateCapabilities(OllamaObject)}. + * {@link LLMSystemPrompt#generateCapabilities(ArrayList)}. */ public static class Capabilities { /** * Usage hints keyed by tool class, in the order they were registered. */ - private final LinkedHashMap, String> usageHints = new LinkedHashMap<>(); + private final LinkedHashMap, String> usageHints = new LinkedHashMap<>(); /** * Registers a usage-routing hint for a tool class — guidance on when the model @@ -321,13 +324,13 @@ public class LLMSystemPrompt { *

* Keyed by class rather than instance because a tool's LLM-facing name (which * depends on its registration source) isn't known until it's actually registered - * on an {@link OllamaObject}; class identity is stable and known up front. + * on an {@link Model}; class identity is stable and known up front. * * @param toolClass the tool class this hint applies to * @param usageHint the routing guidance text for this tool * @return this {@link Capabilities}, for chaining */ - public Capabilities use(Class toolClass, String usageHint) { + public Capabilities use(Class toolClass, String usageHint) { usageHints.put(toolClass, usageHint); return this; } @@ -337,7 +340,7 @@ public class LLMSystemPrompt { * * @return the live map backing this {@link Capabilities} */ - Map, String> getUsageHints() { + Map, String> getUsageHints() { return usageHints; } } @@ -485,7 +488,7 @@ public class LLMSystemPrompt { /** * Sets the capability usage-hint definitions for the prompt being built. These are * only compiled into prompt text once - * {@link LLMSystemPrompt#generateCapabilities(OllamaObject)} is called on the built + * {@link LLMSystemPrompt#generateCapabilities(ArrayList)} is called on the built * instance. * * @param capabilities the usage-hint definitions to use diff --git a/Core/src/main/java/me/neurodock/core/PrintAdvanceMessageHandler.java b/Core/src/main/java/me/neurodock/core/PrintAdvanceMessageHandler.java index 01c347f..f8e6f3d 100644 --- a/Core/src/main/java/me/neurodock/core/PrintAdvanceMessageHandler.java +++ b/Core/src/main/java/me/neurodock/core/PrintAdvanceMessageHandler.java @@ -1,19 +1,18 @@ package me.neurodock.core; -import me.neurodock.ollama.OllamaMessage; -import me.neurodock.ollama.OllamaMessageRole; +import me.neurodock.llm.Message; /** * Represents a {@link PrintAdvanceMessageHandler}. - * This is used by the {@link Core} to print messages for the user, LLM, Tools, or potentialy System, see {@link OllamaMessageRole}. + * This is used by the {@link Core} to print messages for the user, LLM, Tools, or potentialy System, see {@link Message.Role}. * For a simpler version see {@link PrintMessageHandler} */ public interface PrintAdvanceMessageHandler { /** - * Expected to handle the printing of the provided {@link OllamaMessage}. - * @param message The {@link OllamaMessage} requested to be printed from a veriity of sources, see {@link OllamaMessageRole} + * Expected to handle the printing of the provided {@link Message}. + * @param message The {@link Message} requested to be printed from a veriity of sources, see {@link Message} */ - void printMessage(OllamaMessage message); + void printMessage(Message message); /** * Default method to print error messages to the user or API Client. @@ -21,7 +20,7 @@ public interface PrintAdvanceMessageHandler { * @param errorMessage The error message to be printed. * If color is not supported, it will print the message without color. */ - void printErrorMessage(OllamaMessage errorMessage); + void printErrorMessage(Message errorMessage); /** * Used when a tool is to have it's calling rendered diff --git a/Core/src/main/java/me/neurodock/core/PrintMessageHandler.java b/Core/src/main/java/me/neurodock/core/PrintMessageHandler.java index 48e34c5..396f043 100644 --- a/Core/src/main/java/me/neurodock/core/PrintMessageHandler.java +++ b/Core/src/main/java/me/neurodock/core/PrintMessageHandler.java @@ -1,9 +1,7 @@ package me.neurodock.core; -import me.neurodock.ollama.OllamaMessage; -import me.neurodock.ollama.OllamaMessageRole; -import static me.neurodock.ollama.OllamaFunctionArgument.deconstructOllamaFunctionArguments; +import me.neurodock.llm.Message; /** * Represents a PrintMessageHandler. @@ -21,11 +19,11 @@ public interface PrintMessageHandler extends PrintAdvanceMessageHandler { * This is a default implementation when wrapping {@link PrintAdvanceMessageHandler} to this "simpler" {@link PrintMessageHandler} * * - * @param message The {@link OllamaMessage} requested to be printed from a veriity of sources, see {@link OllamaMessageRole} + * @param message The {@link Message} requested to be printed from a veriity of sources, see {@link Message.Role} */ - default void printMessage(OllamaMessage message) { + default void printMessage(Message message) { printMessage(switch (message.getRole()) { - case ASSISTANT -> (color()?"\u001b[32m":"")+(LaunchOptions.getInstance().isShowFullMessage()? message.getContent() : message.getContent().replaceAll("(?s).*?", "")) +(color()?"\u001b[0m":""); + case ASSISTANT -> (color()?"\u001b[32m":"")+(LaunchOptions.getInstance().isShowFullMessage()? message.getContent() : message.getContent().toString().replaceAll("(?s).*?", "")) +(color()?"\u001b[0m":""); case TOOL -> (color() ?"\u001b[31m":"")+message.getContent()+(color()?"\u001b[0m":""); case USER, SYSTEM -> (color() ?"\u001b[37m":"")+message.getContent()+(color()?"\u001b[0m":""); default -> throw new IllegalArgumentException("Invalid message role"); @@ -62,8 +60,8 @@ public interface PrintMessageHandler extends PrintAdvanceMessageHandler { * @param errorMessage The error message to be printed. * If color is not supported, it will print the message without color. */ - default void printErrorMessage(OllamaMessage errorMessage) { - printError(errorMessage.getContent()); + default void printErrorMessage(Message errorMessage) { + printError(errorMessage.getContent().toString()); } @Override diff --git a/Core/src/main/java/me/neurodock/core/files/FileHandler.java b/Core/src/main/java/me/neurodock/core/files/FileHandler.java index 6e9abc6..f0ad225 100644 --- a/Core/src/main/java/me/neurodock/core/files/FileHandler.java +++ b/Core/src/main/java/me/neurodock/core/files/FileHandler.java @@ -5,7 +5,7 @@ import me.neurodock.core.Options; import me.neurodock.core.Pair; import me.neurodock.core.files.tools.ReadFileTool; import me.neurodock.core.files.tools.WriteFileTool; -import me.neurodock.ollama.OllamaTool; +import me.neurodock.llm.tools.Tool; import java.io.*; import java.nio.file.Path; @@ -50,14 +50,14 @@ public class FileHandler { } /** - * Returns a list of all {@link OllamaTool}'s this module adds + * Returns a list of all {@link Tool}'s this module adds * @return */ - public static ArrayList> getTools() { - ArrayList> fileTools = new ArrayList<>(); + public static ArrayList getTools() { + ArrayList fileTools = new ArrayList<>(); - fileTools.add(new Pair<>(new ReadFileTool(), Core.Source.CORE)); - fileTools.add(new Pair<>(new WriteFileTool(), Core.Source.CORE)); + fileTools.add(new ReadFileTool()); + fileTools.add(new WriteFileTool()); return fileTools; } diff --git a/Core/src/main/java/me/neurodock/core/files/tools/ReadFileTool.java b/Core/src/main/java/me/neurodock/core/files/tools/ReadFileTool.java index 4cdbdd0..74da253 100644 --- a/Core/src/main/java/me/neurodock/core/files/tools/ReadFileTool.java +++ b/Core/src/main/java/me/neurodock/core/files/tools/ReadFileTool.java @@ -1,17 +1,17 @@ package me.neurodock.core.files.tools; import me.neurodock.core.files.FileHandler; -import me.neurodock.ollama.OllamaFunctionArgument; -import me.neurodock.ollama.OllamaFunctionTool; -import me.neurodock.ollama.OllamaPerameter; -import me.neurodock.ollama.OllamaToolResponse; -import me.neurodock.ollama.exceptions.OllamaToolErrorException; +import me.neurodock.llm.exceptions.ToolException; +import me.neurodock.llm.tools.FunctionTool; +import me.neurodock.llm.tools.ToolArguments; +import me.neurodock.llm.tools.ToolParameters; +import me.neurodock.llm.tools.ToolResponse; import org.jetbrains.annotations.NotNull; import java.io.*; import java.nio.file.Path; -public class ReadFileTool extends OllamaFunctionTool { +public class ReadFileTool extends FunctionTool { FileHandler fs = FileHandler.getInstance(); @Override public @NotNull String name() { @@ -24,23 +24,18 @@ public class ReadFileTool extends OllamaFunctionTool { } @Override - public @NotNull OllamaPerameter parameters() { - return OllamaPerameter.builder() - .addProperty("file_path", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The path to the file to be read", true) + public @NotNull ToolParameters parameters() { + return ToolParameters.builder() + .addProperty("file_path", ToolParameters.ToolParametersBuilder.Type.STRING, "The path to the file to be read", true) .build(); } @Override - public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { - String orgPath = null; - for (OllamaFunctionArgument arg : args) { - if(arg.argument().equals("file_path")) { - orgPath = (String) arg.value(); - } - } + public @NotNull ToolResponse function(ToolArguments args) { + String orgPath = args.optArgument("file_path", String.class); if(orgPath == null) { - throw new OllamaToolErrorException(this.name(), "Missing required argument 'file_path'"); + throw new ToolException(this, "Missing required argument 'file_path'"); } Path filePath = null; @@ -49,19 +44,19 @@ public class ReadFileTool extends OllamaFunctionTool { filePath = fs.resolve(orgPath); } catch (IOException ex) { - throw new OllamaToolErrorException(this.name(), ex); + throw new ToolException(this, ex); } File file = filePath.toFile(); if(!file.exists()) { - throw new OllamaToolErrorException(this.name(), "File does not exist: " + fs.path(file)); + throw new ToolException(this, "File does not exist: " + fs.path(file)); } BufferedReader br = null; try{ br = new BufferedReader(new FileReader(file)); }catch (FileNotFoundException ex) { - throw new OllamaToolErrorException(this.name(), "File not found: " + fs.path(file)); + throw new ToolException(this, "File not found: " + fs.path(file)); } StringBuilder sb = new StringBuilder(); String tmp = null; @@ -71,8 +66,8 @@ public class ReadFileTool extends OllamaFunctionTool { } } catch (IOException e) { - throw new OllamaToolErrorException(this.name(), "Error reading file: " + fs.path(file)); + throw new ToolException(this, "Error reading file: " + fs.path(file)); } - return new OllamaToolResponse(this.name(), sb.toString()); + return new ToolResponse(this.name(), sb.toString()); } } diff --git a/Core/src/main/java/me/neurodock/core/files/tools/WriteFileTool.java b/Core/src/main/java/me/neurodock/core/files/tools/WriteFileTool.java index e54791b..b884581 100644 --- a/Core/src/main/java/me/neurodock/core/files/tools/WriteFileTool.java +++ b/Core/src/main/java/me/neurodock/core/files/tools/WriteFileTool.java @@ -1,18 +1,18 @@ package me.neurodock.core.files.tools; import me.neurodock.core.files.FileHandler; -import me.neurodock.ollama.OllamaFunctionArgument; -import me.neurodock.ollama.OllamaFunctionTool; -import me.neurodock.ollama.OllamaPerameter; -import me.neurodock.ollama.OllamaToolResponse; -import me.neurodock.ollama.OllamaPerameter.OllamaPerameterBuilder.Type; -import me.neurodock.ollama.exceptions.OllamaToolErrorException; +import me.neurodock.llm.exceptions.ToolException; +import me.neurodock.llm.tools.FunctionTool; +import me.neurodock.llm.tools.ToolArguments; +import me.neurodock.llm.tools.ToolParameters; +import me.neurodock.llm.tools.ToolResponse; import org.jetbrains.annotations.NotNull; +import static me.neurodock.llm.tools.ToolParameters.ToolParametersBuilder.*; import java.io.*; import java.nio.file.Path; -public class WriteFileTool extends OllamaFunctionTool { +public class WriteFileTool extends FunctionTool { FileHandler fs = FileHandler.getInstance(); @Override @@ -21,8 +21,8 @@ public class WriteFileTool extends OllamaFunctionTool { } @Override - public @NotNull OllamaPerameter parameters() { - return OllamaPerameter.builder() + public @NotNull ToolParameters parameters() { + return ToolParameters.builder() .addProperty("file_path", Type.STRING, "The path to the file to write to", true) .addProperty("file_content", Type.STRING, "The content to write to the file", true) .addProperty("overwrite", Type.BOOLEAN, "Overwrite the file, defaults to false", false) @@ -30,30 +30,14 @@ public class WriteFileTool extends OllamaFunctionTool { } @Override - public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { - String path = null; - String content = null; - boolean overwrite = false; - - for(OllamaFunctionArgument arg : args) - { - switch (arg.argument()) - { - case "file_path" -> { - path = (String) arg.value(); - } - case "file_content" -> { - content = (String) arg.value(); - } - case "overwrite" -> { - overwrite = (boolean) arg.value(); - } - } - } + public @NotNull ToolResponse function(ToolArguments args) { + String path = args.getArgument("file_path", String.class); + String content = args.getArgument("file_content", String.class); + boolean overwrite = args.getArgument("overwrite", Boolean.class); if(path == null || content == null || path.isBlank() || content.isBlank()) { - throw new OllamaToolErrorException(name(), "file_content or file_path is empty or null"); + throw new ToolException(this, "file_content or file_path is empty or null"); } Path filePath = null; @@ -62,12 +46,12 @@ public class WriteFileTool extends OllamaFunctionTool { filePath = fs.resolve(path); } catch (IOException ex) { - throw new OllamaToolErrorException(this.name(), ex); + throw new ToolException(this, ex); } File file = filePath.toFile(); if(file.exists() && !overwrite) { - throw new OllamaToolErrorException(this.name(), "File already exists, and instructed to not be overwritten: " + fs.path(file)); + throw new ToolException(this, "File already exists, and instructed to not be overwritten: " + fs.path(file)); } else if(file.exists() && overwrite) { @@ -77,17 +61,17 @@ public class WriteFileTool extends OllamaFunctionTool { try { file.createNewFile(); } catch (IOException e) { - throw new OllamaToolErrorException(name(), e); + throw new ToolException(this, e); } try(BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)))) { bw.write(content); - return new OllamaToolResponse(name(), "Successfully wrote data to "+fs.path(file)); + return new ToolResponse(name(), "Successfully wrote data to "+fs.path(file)); } catch (IOException ex) { - throw new OllamaToolErrorException(name(), ex); + throw new ToolException(this, ex); } } } diff --git a/Core/src/main/java/me/neurodock/core/memory/AddMemoryFunction.java b/Core/src/main/java/me/neurodock/core/memory/AddMemoryFunction.java index ef64ad0..14bdefa 100644 --- a/Core/src/main/java/me/neurodock/core/memory/AddMemoryFunction.java +++ b/Core/src/main/java/me/neurodock/core/memory/AddMemoryFunction.java @@ -1,17 +1,17 @@ package me.neurodock.core.memory; -import me.neurodock.ollama.OllamaFunctionArgument; -import me.neurodock.ollama.OllamaFunctionTool; -import me.neurodock.ollama.OllamaPerameter; -import me.neurodock.ollama.OllamaToolResponse; -import me.neurodock.ollama.exceptions.OllamaToolErrorException; +import me.neurodock.llm.exceptions.ToolException; +import me.neurodock.llm.tools.FunctionTool; +import me.neurodock.llm.tools.ToolArguments; +import me.neurodock.llm.tools.ToolParameters; +import me.neurodock.llm.tools.ToolResponse; import org.jetbrains.annotations.NotNull; /** * Provides the add_memory function.
* This function adds a string to the memory. */ -public class AddMemoryFunction extends OllamaFunctionTool { +public class AddMemoryFunction extends FunctionTool { /** * The CoreMemory instance. */ @@ -28,37 +28,23 @@ public class AddMemoryFunction extends OllamaFunctionTool { } @Override - public @NotNull OllamaPerameter parameters() { - return OllamaPerameter.builder() - .addProperty("memory", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The memory to remember", true) - .addProperty("identity", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The identity of the memory to remember", true) + public @NotNull ToolParameters parameters() { + return ToolParameters.builder() + .addProperty("memory", ToolParameters.ToolParametersBuilder.Type.STRING, "The memory to remember", true) + .addProperty("identity", ToolParameters.ToolParametersBuilder.Type.STRING, "The identity of the memory to remember", true) .build(); } @Override - public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { - if (args.length == 0) { - throw new OllamaToolErrorException(name(), "Missing memory argument"); - } - - String memory = null; - String identity = null; - - for(OllamaFunctionArgument arg : args) { - if (arg.argument().equals("memory")) { - memory = (String) arg.value(); - } else if (arg.argument().equals("identity")) { - identity = (String) arg.value(); - } else { - throw new OllamaToolErrorException(name(), "Unknown argument: " + arg.argument()); - } - } + public @NotNull ToolResponse function(ToolArguments args) { + String memory = args.optArgument("memory", String.class); + String identity = args.optArgument("identity", String.class); if (memory == null || identity == null) { - throw new OllamaToolErrorException(name(), "Missing memory or identity argument"); + throw new ToolException(this, "Missing memory or identity argument"); } this.memory.addMemory(identity, memory); - return new OllamaToolResponse(name(), "Added "+identity+" to the memory"); + return new ToolResponse(name(), "Added "+identity+" to the memory"); } } diff --git a/Core/src/main/java/me/neurodock/core/memory/GetMemoriesFunction.java b/Core/src/main/java/me/neurodock/core/memory/GetMemoriesFunction.java index d9f84a0..fca72ed 100644 --- a/Core/src/main/java/me/neurodock/core/memory/GetMemoriesFunction.java +++ b/Core/src/main/java/me/neurodock/core/memory/GetMemoriesFunction.java @@ -1,12 +1,12 @@ package me.neurodock.core.memory; -import me.neurodock.ollama.OllamaFunctionArgument; -import me.neurodock.ollama.OllamaFunctionTool; -import me.neurodock.ollama.OllamaPerameter; -import me.neurodock.ollama.OllamaToolResponse; +import me.neurodock.llm.tools.FunctionTool; +import me.neurodock.llm.tools.ToolArguments; +import me.neurodock.llm.tools.ToolParameters; +import me.neurodock.llm.tools.ToolResponse; import org.jetbrains.annotations.NotNull; -public class GetMemoriesFunction extends OllamaFunctionTool { +public class GetMemoriesFunction extends FunctionTool { @Override public @NotNull String name() { return "get_memories"; @@ -18,12 +18,12 @@ public class GetMemoriesFunction extends OllamaFunctionTool { } @Override - public @NotNull OllamaPerameter parameters() { - return null; + public @NotNull ToolParameters parameters() { + return ToolParameters.empty(); } @Override - public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { - return new OllamaToolResponse(name(), CoreMemory.getInstance().getMappedMemories()); + public @NotNull ToolResponse function(ToolArguments args) { + return new ToolResponse(name(), CoreMemory.getInstance().getMappedMemories()); } } diff --git a/Core/src/main/java/me/neurodock/core/memory/GetMemoryFunction.java b/Core/src/main/java/me/neurodock/core/memory/GetMemoryFunction.java index fe136e2..4529e39 100644 --- a/Core/src/main/java/me/neurodock/core/memory/GetMemoryFunction.java +++ b/Core/src/main/java/me/neurodock/core/memory/GetMemoryFunction.java @@ -1,16 +1,16 @@ package me.neurodock.core.memory; -import me.neurodock.ollama.OllamaFunctionArgument; -import me.neurodock.ollama.OllamaFunctionTool; -import me.neurodock.ollama.OllamaPerameter; -import me.neurodock.ollama.OllamaToolResponse; +import me.neurodock.llm.tools.FunctionTool; +import me.neurodock.llm.tools.ToolArguments; +import me.neurodock.llm.tools.ToolParameters; +import me.neurodock.llm.tools.ToolResponse; import org.jetbrains.annotations.NotNull; /** * Provides the get_memory function.
* This function retrives all the memory. */ -public class GetMemoryFunction extends OllamaFunctionTool { +public class GetMemoryFunction extends FunctionTool { /** * The CoreMemory instance. */ @@ -27,16 +27,16 @@ public class GetMemoryFunction extends OllamaFunctionTool { } @Override - public @NotNull OllamaPerameter parameters() { - return OllamaPerameter.builder() - .addProperty("identity", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The identity of the memory to retrieve", true) + public @NotNull ToolParameters parameters() { + return ToolParameters.builder() + .addProperty("identity", ToolParameters.ToolParametersBuilder.Type.STRING, "The identity of the memory to retrieve", true) .build(); } @Override - public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { - return memory.getMemory((String) (args[0].value())) - .map(value -> new OllamaToolResponse(name(), value)) - .orElse(OllamaToolResponse.empty("No memory found for key: " + args[0].value())); + public @NotNull ToolResponse function(ToolArguments args) { + return memory.getMemory(args.optArgument("identity", String.class)) + .map(value -> new ToolResponse(name(), value)) + .orElse(ToolResponse.empty(name(), "No memory found for key: " + args.optArgument("identity", String.class))); } } diff --git a/Core/src/main/java/me/neurodock/core/memory/GetMemoryIdentitiesFunction.java b/Core/src/main/java/me/neurodock/core/memory/GetMemoryIdentitiesFunction.java index a1d8ac6..31de717 100644 --- a/Core/src/main/java/me/neurodock/core/memory/GetMemoryIdentitiesFunction.java +++ b/Core/src/main/java/me/neurodock/core/memory/GetMemoryIdentitiesFunction.java @@ -1,13 +1,13 @@ package me.neurodock.core.memory; -import me.neurodock.ollama.OllamaFunctionArgument; -import me.neurodock.ollama.OllamaFunctionTool; -import me.neurodock.ollama.OllamaPerameter; -import me.neurodock.ollama.OllamaToolResponse; +import me.neurodock.llm.tools.FunctionTool; +import me.neurodock.llm.tools.ToolArguments; +import me.neurodock.llm.tools.ToolParameters; +import me.neurodock.llm.tools.ToolResponse; import org.jetbrains.annotations.NotNull; import org.json.JSONArray; -public class GetMemoryIdentitiesFunction extends OllamaFunctionTool { +public class GetMemoryIdentitiesFunction extends FunctionTool { CoreMemory memory = CoreMemory.getInstance(); @Override @@ -21,12 +21,12 @@ public class GetMemoryIdentitiesFunction extends OllamaFunctionTool { } @Override - public @NotNull OllamaPerameter parameters() { - return null; + public @NotNull ToolParameters parameters() { + return ToolParameters.empty(); } @Override - public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { - return new OllamaToolResponse(this.name(), new JSONArray(memory.getMemoriesIdentity()).toString()); + public @NotNull ToolResponse function(ToolArguments args) { + return new ToolResponse(this.name(), new JSONArray(memory.getMemoriesIdentity()).toString()); } } diff --git a/Core/src/main/java/me/neurodock/core/memory/RemoveMemoryFunction.java b/Core/src/main/java/me/neurodock/core/memory/RemoveMemoryFunction.java index 4928c27..91edaf5 100644 --- a/Core/src/main/java/me/neurodock/core/memory/RemoveMemoryFunction.java +++ b/Core/src/main/java/me/neurodock/core/memory/RemoveMemoryFunction.java @@ -1,17 +1,17 @@ package me.neurodock.core.memory; -import me.neurodock.ollama.OllamaFunctionArgument; -import me.neurodock.ollama.OllamaFunctionTool; -import me.neurodock.ollama.OllamaPerameter; -import me.neurodock.ollama.OllamaToolResponse; -import me.neurodock.ollama.exceptions.OllamaToolErrorException; +import me.neurodock.llm.exceptions.ToolException; +import me.neurodock.llm.tools.FunctionTool; +import me.neurodock.llm.tools.ToolArguments; +import me.neurodock.llm.tools.ToolParameters; +import me.neurodock.llm.tools.ToolResponse; import org.jetbrains.annotations.NotNull; /** * Provides the remove_memory function.
* This function removes a value from the memory. */ -public class RemoveMemoryFunction extends OllamaFunctionTool { +public class RemoveMemoryFunction extends FunctionTool { /** * The CoreMemory instance. */ @@ -28,19 +28,19 @@ public class RemoveMemoryFunction extends OllamaFunctionTool { } @Override - public @NotNull OllamaPerameter parameters() { - return OllamaPerameter.builder() - .addProperty("identity", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "The identity of the memory to forget", true) + public @NotNull ToolParameters parameters() { + return ToolParameters.builder() + .addProperty("identity", ToolParameters.ToolParametersBuilder.Type.STRING, "The identity of the memory to forget", true) .build(); } @Override - public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { - if (args.length == 0) { - throw new OllamaToolErrorException(name(), "Missing memory argument"); + public @NotNull ToolResponse function(ToolArguments args) { + String value = args.optArgument("identity", String.class); + if(value == null || value.isEmpty()) { + throw new ToolException(this, "Missing identity argument"); } - String value = (String) args[0].value(); memory.removeMemory(value); - return new OllamaToolResponse(name(), "Removed "+value+" to the memory"); + return new ToolResponse(name(), "Removed "+value+" to the memory"); } } diff --git a/Core/src/main/java/me/neurodock/core/memory/array/AddArrayMemory.java b/Core/src/main/java/me/neurodock/core/memory/array/AddArrayMemory.java index 86562d3..314582d 100644 --- a/Core/src/main/java/me/neurodock/core/memory/array/AddArrayMemory.java +++ b/Core/src/main/java/me/neurodock/core/memory/array/AddArrayMemory.java @@ -1,15 +1,11 @@ package me.neurodock.core.memory.array; import me.neurodock.core.memory.CoreMemory; -import me.neurodock.ollama.OllamaFunctionArgument; -import me.neurodock.ollama.OllamaFunctionTool; -import me.neurodock.ollama.OllamaPerameter; -import me.neurodock.ollama.OllamaPerameter.OllamaPerameterBuilder.Type; -import me.neurodock.ollama.OllamaToolResponse; -import me.neurodock.ollama.exceptions.OllamaToolErrorException; +import me.neurodock.llm.exceptions.ToolException; +import me.neurodock.llm.tools.*; import org.jetbrains.annotations.NotNull; -public class AddArrayMemory extends OllamaFunctionTool { +public class AddArrayMemory extends FunctionTool { private CoreMemory memory = CoreMemory.getInstance(); @@ -19,37 +15,29 @@ public class AddArrayMemory extends OllamaFunctionTool { } @Override - public @NotNull OllamaPerameter parameters() { - return OllamaPerameter.builder() - .addProperty("memory", Type.STRING, "The memory to remember", true) - .addProperty("index", Type.INT, "The index to put it at. Should be avoided, unless overwriting", false) + public @NotNull ToolParameters parameters() { + return ToolParameters.builder() + .addProperty("memory", ToolParameters.ToolParametersBuilder.Type.STRING, "The memory to remember", true) + .addProperty("index", ToolParameters.ToolParametersBuilder.Type.INT, "The index to put it at. Should be avoided, unless overwriting", false) .build(); } @Override - public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { - if (args.length > 1) { - String memory = null; - int index = -1; - for (OllamaFunctionArgument arg : args) { - if(arg.argument().equals("memory")) { - memory = (String) arg.value(); - } - else if(arg.argument().equals("index")) { - index = (Integer) arg.value(); - } - } + public @NotNull ToolResponse function(ToolArguments args) { + if (args.hasArgument("index")) { + String memory = args.optArgument("memory", String.class); + int index = args.optArgument("index", Integer.class); if (memory == null || index < 0) { - throw new OllamaToolErrorException(name(), "no memory or index provided"); + throw new ToolException(this, "no memory or index provided"); } this.memory.addMemory(memory, index); } else { - if(!args[0].argument().equals("memory")) { - throw new OllamaToolErrorException(name(), "no memory provided"); + if(!args.hasArgument("memory")) { + throw new ToolException(this, "no memory provided"); } - this.memory.addMemory(name(), (String) args[0].value()); + this.memory.addMemory(name(), args.optArgument("memory", String.class)); } - return new OllamaToolResponse(name(), "Added arrayed memory"); + return new ToolResponse(name(), "Added arrayed memory"); } } diff --git a/Core/src/main/java/me/neurodock/core/memory/array/GetArrayMemory.java b/Core/src/main/java/me/neurodock/core/memory/array/GetArrayMemory.java index 18ca92b..4b57148 100644 --- a/Core/src/main/java/me/neurodock/core/memory/array/GetArrayMemory.java +++ b/Core/src/main/java/me/neurodock/core/memory/array/GetArrayMemory.java @@ -1,14 +1,14 @@ package me.neurodock.core.memory.array; import me.neurodock.core.memory.CoreMemory; -import me.neurodock.ollama.OllamaFunctionArgument; -import me.neurodock.ollama.OllamaFunctionTool; -import me.neurodock.ollama.OllamaPerameter; -import me.neurodock.ollama.OllamaToolResponse; -import me.neurodock.ollama.OllamaPerameter.OllamaPerameterBuilder.Type; +import me.neurodock.llm.exceptions.ToolException; +import me.neurodock.llm.tools.FunctionTool; +import me.neurodock.llm.tools.ToolArguments; +import me.neurodock.llm.tools.ToolParameters; +import me.neurodock.llm.tools.ToolResponse; import org.jetbrains.annotations.NotNull; -public class GetArrayMemory extends OllamaFunctionTool { +public class GetArrayMemory extends FunctionTool { CoreMemory memory = CoreMemory.getInstance(); @@ -18,19 +18,17 @@ public class GetArrayMemory extends OllamaFunctionTool { } @Override - public @NotNull OllamaPerameter parameters() { - return OllamaPerameter.builder() - .addProperty("index", Type.STRING, "The index to retrieve memory from", false) + public @NotNull ToolParameters parameters() { + return ToolParameters.builder() + .addProperty("index", ToolParameters.ToolParametersBuilder.Type.STRING, "The index to retrieve memory from", false) .build(); } @Override - public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { - if (args.length != 1) { - return new OllamaToolResponse(name(), memory.getArrayMemories().toString()); - } - return memory.getMemory((Integer) args[0].value()) - .map(value -> new OllamaToolResponse(name(), value)) - .orElse(OllamaToolResponse.empty("No memory found for key: " + args[0].value())); + public @NotNull ToolResponse function(ToolArguments args) { + if(!args.hasArgument("index")) throw new ToolException(this, "Missing index"); + return memory.getMemory(args.optArgument("index", Integer.class, -1)) + .map(value -> new ToolResponse(name(), value)) + .orElse(ToolResponse.empty("No memory found for key: " + args.optArgument("index", -1))); } } diff --git a/Core/src/main/java/me/neurodock/core/memory/array/GetArrayedMemories.java b/Core/src/main/java/me/neurodock/core/memory/array/GetArrayedMemories.java index 5a83928..4d8de3c 100644 --- a/Core/src/main/java/me/neurodock/core/memory/array/GetArrayedMemories.java +++ b/Core/src/main/java/me/neurodock/core/memory/array/GetArrayedMemories.java @@ -1,13 +1,13 @@ package me.neurodock.core.memory.array; import me.neurodock.core.memory.CoreMemory; -import me.neurodock.ollama.OllamaFunctionArgument; -import me.neurodock.ollama.OllamaFunctionTool; -import me.neurodock.ollama.OllamaPerameter; -import me.neurodock.ollama.OllamaToolResponse; +import me.neurodock.llm.tools.FunctionTool; +import me.neurodock.llm.tools.ToolArguments; +import me.neurodock.llm.tools.ToolParameters; +import me.neurodock.llm.tools.ToolResponse; import org.jetbrains.annotations.NotNull; -public class GetArrayedMemories extends OllamaFunctionTool { +public class GetArrayedMemories extends FunctionTool { private CoreMemory memory = CoreMemory.getInstance(); @Override public @NotNull String name() { @@ -15,12 +15,12 @@ public class GetArrayedMemories extends OllamaFunctionTool { } @Override - public @NotNull OllamaPerameter parameters() { - return OllamaPerameter.empty(); + public @NotNull ToolParameters parameters() { + return ToolParameters.empty(); } @Override - public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { - return new OllamaToolResponse(name(), memory.getArrayMemories().toString()); + public @NotNull ToolResponse function(ToolArguments args) { + return new ToolResponse(name(), memory.getArrayMemories().toString()); } } diff --git a/Core/src/main/java/me/neurodock/llm/ChatObject.java b/Core/src/main/java/me/neurodock/llm/ChatObject.java new file mode 100644 index 0000000..b2ac4ef --- /dev/null +++ b/Core/src/main/java/me/neurodock/llm/ChatObject.java @@ -0,0 +1,18 @@ +package me.neurodock.llm; + +import java.util.ArrayList; +import java.util.List; + +public class ChatObject { + private ArrayList messages = new ArrayList<>(); + + public List getConversation() + { + return messages; + } + + public void addMessage(Message msg) + { + messages.add(msg); + } +} diff --git a/Core/src/main/java/me/neurodock/llm/Message.java b/Core/src/main/java/me/neurodock/llm/Message.java new file mode 100644 index 0000000..7bbe9e0 --- /dev/null +++ b/Core/src/main/java/me/neurodock/llm/Message.java @@ -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. + * DO NOT 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 SHULD NEVER 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 + + '}'; + } +} diff --git a/Core/src/main/java/me/neurodock/llm/Model.java b/Core/src/main/java/me/neurodock/llm/Model.java new file mode 100644 index 0000000..f93ffdc --- /dev/null +++ b/Core/src/main/java/me/neurodock/llm/Model.java @@ -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 qurryModel(ChatObject obj, List 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 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 singleFire(Message msg, List 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(); + +} diff --git a/Core/src/main/java/me/neurodock/llm/StreamingModel.java b/Core/src/main/java/me/neurodock/llm/StreamingModel.java new file mode 100644 index 0000000..7780690 --- /dev/null +++ b/Core/src/main/java/me/neurodock/llm/StreamingModel.java @@ -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 qurryModel(ChatObject obj, List tools, Consumer chunkConsumer); + CompletableFuture singleFire(Message msg, Consumer chunkConsumer); + CompletableFuture singleFire(Message msg, List tools, Consumer chunkConsumer); +} diff --git a/Core/src/main/java/me/neurodock/llm/exceptions/ModelNotFoundException.java b/Core/src/main/java/me/neurodock/llm/exceptions/ModelNotFoundException.java new file mode 100644 index 0000000..856f823 --- /dev/null +++ b/Core/src/main/java/me/neurodock/llm/exceptions/ModelNotFoundException.java @@ -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 + "\""); + } +} diff --git a/Core/src/main/java/me/neurodock/llm/exceptions/ToolException.java b/Core/src/main/java/me/neurodock/llm/exceptions/ToolException.java new file mode 100644 index 0000000..5ce1ad4 --- /dev/null +++ b/Core/src/main/java/me/neurodock/llm/exceptions/ToolException.java @@ -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()); + } +} diff --git a/Core/src/main/java/me/neurodock/llm/tools/FunctionTool.java b/Core/src/main/java/me/neurodock/llm/tools/FunctionTool.java new file mode 100644 index 0000000..e3103c3 --- /dev/null +++ b/Core/src/main/java/me/neurodock/llm/tools/FunctionTool.java @@ -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; +} diff --git a/Core/src/main/java/me/neurodock/llm/tools/Tool.java b/Core/src/main/java/me/neurodock/llm/tools/Tool.java new file mode 100644 index 0000000..8916d46 --- /dev/null +++ b/Core/src/main/java/me/neurodock/llm/tools/Tool.java @@ -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 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(); + } +} diff --git a/Core/src/main/java/me/neurodock/llm/tools/ToolArguments.java b/Core/src/main/java/me/neurodock/llm/tools/ToolArguments.java new file mode 100644 index 0000000..a79e99a --- /dev/null +++ b/Core/src/main/java/me/neurodock/llm/tools/ToolArguments.java @@ -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 arguments = new HashMap<>(); + + public void addArguments(Map 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 getArgument(String name, Class 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 optArgument(String name, Class 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 optArgument(String name, Class 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); + } +} diff --git a/Core/src/main/java/me/neurodock/llm/tools/ToolParameters.java b/Core/src/main/java/me/neurodock/llm/tools/ToolParameters.java new file mode 100644 index 0000000..f97b671 --- /dev/null +++ b/Core/src/main/java/me/neurodock/llm/tools/ToolParameters.java @@ -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 propertyMap, ArrayList required) { + this.propertyMap = propertyMap; + this.required = required; + }; + + /** + * The properties of the parameters. + */ + private Map propertyMap; + /** + * The required parameters. + */ + private ArrayList required; + + /** + * Gets the properties of the {@link ToolParameters} + * @return The properties of the {@link ToolParameters} + */ + public Map getProperties() { + return propertyMap; + } + + /** + * Gets the required parameters of the {@link ToolParameters} + * @return The required parameters of the {@link ToolParameters} + */ + public ArrayList 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 + *

{@code
+     *    ToolParameters.builder().build();
+     * }
+ */ + public static ToolParameters empty() { + return builder().build(); + } + + /** + * Represents a builder for {@link ToolParameters}. + */ + public static class ToolParametersBuilder { + /** + * The properties of the parameters. + */ + private Map propertyMap = new HashMap<>(); + /** + * The required parameters. + */ + private ArrayList 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; + } + } + } +} diff --git a/Core/src/main/java/me/neurodock/ollama/OllamaToolResponse.java b/Core/src/main/java/me/neurodock/llm/tools/ToolResponse.java similarity index 59% rename from Core/src/main/java/me/neurodock/ollama/OllamaToolResponse.java rename to Core/src/main/java/me/neurodock/llm/tools/ToolResponse.java index e623dc7..d7c4672 100644 --- a/Core/src/main/java/me/neurodock/ollama/OllamaToolResponse.java +++ b/Core/src/main/java/me/neurodock/llm/tools/ToolResponse.java @@ -1,33 +1,35 @@ -package me.neurodock.ollama; +package me.neurodock.llm.tools; +import me.neurodock.llm.Message; +import org.jetbrains.annotations.NotNull; import org.json.JSONObject; /** * Represents a response from a tool. */ -public class OllamaToolResponse extends OllamaMessage { +public class ToolResponse extends Message { /** * Returns an empty tool response - * See {@link OllamaToolResponse#empty(String, String)} for a reasoned/described response + * See {@link ToolResponse#empty(String, String)} for a reasoned/described response * @param tool The tool that responded * @return an empty tool response */ - public static OllamaToolResponse empty(String tool) + public static ToolResponse empty(String tool) { return empty(tool, "No reason provided"); } /** * Returns an empty tool response with a reason/description. - * See {@link OllamaToolResponse#empty(String)} for reason/description less response + * See {@link ToolResponse#empty(String)} for reason/description less response * @param tool The tool that responded * @param description A description for why this is empty * @return an empty tool response with a reason */ - public static OllamaToolResponse empty(String tool, String description) + public static ToolResponse empty(String tool, String description) { - return new OllamaToolResponse(tool, "Empty! reason: " + description); + return new ToolResponse(tool, "Empty! reason: " + description); } /** @@ -40,12 +42,12 @@ public class OllamaToolResponse extends OllamaMessage { private final String response; /** - * Creates a new instance of {@link OllamaToolResponse}. + * Creates a new instance of {@link ToolResponse}. * @param tool The tool that responded * @param response The response from the tool */ - public OllamaToolResponse(String tool, String response) { - super(OllamaMessageRole.TOOL, new JSONObject().put("tool", tool).put("result", response).toString()); + public ToolResponse(String tool, String response) { + super(Message.Role.TOOL, new JSONObject().put("tool", tool).put("result", response).toString()); this.tool = tool; this.response = response; } @@ -65,4 +67,8 @@ public class OllamaToolResponse extends OllamaMessage { public String getResponse() { return response; } -} + + public void setToolID(@NotNull String name) { + this.toolID = name; + } +} \ No newline at end of file diff --git a/Core/src/main/java/me/neurodock/llm/tools/serializer/ToolSerializer.java b/Core/src/main/java/me/neurodock/llm/tools/serializer/ToolSerializer.java new file mode 100644 index 0000000..eab4f54 --- /dev/null +++ b/Core/src/main/java/me/neurodock/llm/tools/serializer/ToolSerializer.java @@ -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, ToolToJSON> serializers = new HashMap<>(); + + public boolean addSerializer(Class clazz, ToolToJSON serializer) { + if(!serializers.containsKey(clazz)) { + serializers.put(clazz, serializer); + return true; + } + return false; + } + + @SuppressWarnings({"unchecked"}) + public ToolToJSON getSerializer(Class clazz) { + return (ToolToJSON) serializers.get(clazz); + } + + public JSONObject serialize(T tool) + { + if(!serializers.containsKey(tool.getClass())) throw new IllegalArgumentException("Tool " + tool.getClass() + " not found!"); + @SuppressWarnings({"unchecked"}) + ToolToJSON serializer = (ToolToJSON) serializers.get(tool.getClass()); + return serializer.toJSON(tool); + } + + public boolean canSerialize(Class clazz) { + return serializers.containsKey(clazz); + } +} diff --git a/Core/src/main/java/me/neurodock/llm/tools/serializer/ToolToJSON.java b/Core/src/main/java/me/neurodock/llm/tools/serializer/ToolToJSON.java new file mode 100644 index 0000000..5e3eb7b --- /dev/null +++ b/Core/src/main/java/me/neurodock/llm/tools/serializer/ToolToJSON.java @@ -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 { + /** + * @param tool the tool to serialize + * @return the JSON representation of {@code tool}, in the shape this + * backend expects + */ + JSONObject toJSON(T tool); +} diff --git a/Core/src/main/java/me/neurodock/ollama/OllamaFunctionArgument.java b/Core/src/main/java/me/neurodock/ollama/OllamaFunctionArgument.java deleted file mode 100644 index b579b8a..0000000 --- a/Core/src/main/java/me/neurodock/ollama/OllamaFunctionArgument.java +++ /dev/null @@ -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.
- * 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.
- * 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 getValue(Class 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 arguments) { - StringBuilder sb = new StringBuilder(); - for (OllamaFunctionArgument argument : arguments) { - sb.append(deconstructOllamaFunctionArgument(argument)).append(", "); - } - return sb.toString().replaceAll(", $", ""); - } -} diff --git a/Core/src/main/java/me/neurodock/ollama/OllamaFunctionTool.java b/Core/src/main/java/me/neurodock/ollama/OllamaFunctionTool.java deleted file mode 100644 index 67bca85..0000000 --- a/Core/src/main/java/me/neurodock/ollama/OllamaFunctionTool.java +++ /dev/null @@ -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.
- * This is used by Ollama to call the tool.
- * 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); -} diff --git a/Core/src/main/java/me/neurodock/ollama/OllamaFunctionTools.java b/Core/src/main/java/me/neurodock/ollama/OllamaFunctionTools.java deleted file mode 100644 index a68cc8b..0000000 --- a/Core/src/main/java/me/neurodock/ollama/OllamaFunctionTools.java +++ /dev/null @@ -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> { - /** - * 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 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 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 tools, ArrayList 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> iterator() { - ArrayList> 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> action) { - for (Pair pair : this) { - action.accept(pair); - } - } - - @Override - public Spliterator> spliterator() { - // TODO: Implement this method - throw new UnsupportedOperationException("Not implemented yet"); - //return Iterable.super.spliterator(); - } - - public static class OllamaFunctionToolsBuilder { - private ArrayList tools = new ArrayList<>(); - private ArrayList 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 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); - } - } -} diff --git a/Core/src/main/java/me/neurodock/ollama/OllamaMessage.java b/Core/src/main/java/me/neurodock/ollama/OllamaMessage.java deleted file mode 100644 index 52aa4f4..0000000 --- a/Core/src/main/java/me/neurodock/ollama/OllamaMessage.java +++ /dev/null @@ -1,86 +0,0 @@ -package me.neurodock.ollama; - -import org.jetbrains.annotations.NotNull; -import org.json.JSONArray; -import org.json.JSONObject; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.RenderedImage; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Base64; - -/** - * Represents a message sent by a Tool, Assistant(Ollama), or User. - */ -public class OllamaMessage { - /** - * The role of the message. - */ - OllamaMessageRole role; - /** - * The content of the message. - */ - String content; - /** - * The image content of a message. - * This is a base64 encoded string of either an Image or an Audio file (this is how Ollama dose this, ask them, not me) - */ - ArrayList images; - - /** - * 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; - } - - /** - * Creates a new instance of OllamaMessage. - * @param role The role of the message - * @param content The content of the message - * @param images The image of the message - * @throws IOException if an error occurs during writing or when not able to create the required ImageOutputStream. - */ - public OllamaMessage(OllamaMessageRole role, String content, @NotNull Image... images) throws IOException - { - this(role, content); - ArrayList base65Images = new ArrayList<>(); - for(Image i : images) { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - ImageIO.write((RenderedImage) i, "PNG", stream); - base65Images.add(Base64.getEncoder().encodeToString(stream.toByteArray())); - } - this.images = base65Images; - } - - /** - * @return The role of who sent this "message", represented as {@link OllamaMessageRole} - */ - 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")); - if(images != null && !images.isEmpty()) - { - json.put("images", new JSONArray(images)); - } - return json; - } -} diff --git a/Core/src/main/java/me/neurodock/ollama/OllamaMessageRole.java b/Core/src/main/java/me/neurodock/ollama/OllamaMessageRole.java deleted file mode 100644 index 83d672a..0000000 --- a/Core/src/main/java/me/neurodock/ollama/OllamaMessageRole.java +++ /dev/null @@ -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); - } -} diff --git a/Core/src/main/java/me/neurodock/ollama/OllamaMessageToolCall.java b/Core/src/main/java/me/neurodock/ollama/OllamaMessageToolCall.java deleted file mode 100644 index 0dbf97a..0000000 --- a/Core/src/main/java/me/neurodock/ollama/OllamaMessageToolCall.java +++ /dev/null @@ -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; - } -} diff --git a/Core/src/main/java/me/neurodock/ollama/OllamaObject.java b/Core/src/main/java/me/neurodock/ollama/OllamaObject.java deleted file mode 100644 index 30b2efc..0000000 --- a/Core/src/main/java/me/neurodock/ollama/OllamaObject.java +++ /dev/null @@ -1,730 +0,0 @@ -package me.neurodock.ollama; - -import me.neurodock.core.*; -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.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Represents an Ollama Object. - * This is used to represent the state of the Ollama Object. - * This is used by the Core to store the state of the Ollama Object. - * This is used by the API to send the state of the Ollama Object to the client. - * @see Core#setOllamaObject(OllamaObject) - */ -public class OllamaObject { - /** - * The model of the Ollama Object. - */ - String model; - /** - * The messages of the Ollama Object. - */ - ArrayList messages; - /** - * The tools of the Ollama Object. - */ - ArrayList> tools; - /** - * The format of the Ollama Object. - */ - JSONObject format; - /** - * The options of the Ollama Object. - */ - Map options; - /** - * If the Ollama Object is streamed. - */ - boolean stream; - /** - * The keep alive of the Ollama Object. - */ - String keep_alive; - /** - * The system prompt object used to re-generate the system prompt. - */ - LLMSystemPrompt systemPrompt; - /** - * The level of thining the model shuld do - */ - Thinking thinking; - - /** - * Creates a new instance of OllamaObject. - * @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} - * @param prompt The system prompt object of the Ollama Object. See {@link OllamaObject#systemPrompt} - */ - private OllamaObject(String model, ArrayList messages, ArrayList> tools, JSONObject format, Map options, boolean stream, String keep_alive, LLMSystemPrompt prompt, Thinking thinking) { - this.model = model; - this.messages = messages; - this.systemPrompt = prompt; - this.thinking = thinking; - this.stream = stream; - - // Reflecting the reflection injection for ALL tools that was added in the OllamaObjectBuilder, etc, etc comment.. its past midnignt and i'm tired. - for(Pair 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(Options.getInstance().getDataDir(), "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) { - prompt.generateCapabilities(this); - 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 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()); - } - } - } - - /** - * Re-generates and sets the system promp according to the {@link LLMSystemPrompt} stored as {@link OllamaObject#systemPrompt} - */ - public void reGenerateSystemPrompt() - { - if(systemPrompt != null) { - systemPrompt.generateCapabilities(this); - setSystemPrompt(systemPrompt); - } - } - - /** - * 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 getMessages() { - return messages; - } - - /** - * Gets the tools - * @return The tools - */ - public ArrayList> 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 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 dumpMessages() - { - ArrayList 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 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 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); - thinking.putInto(json, "think"); - 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 messages = new ArrayList<>(); - /** - * The tools of the Ollama Object. - */ - ArrayList> tools = new ArrayList<>(); - /** - * The format of the Ollama Object. - */ - JSONObject format; - /** - * The options of the Ollama Object. - */ - Map 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; - /** - * Weather the model should think or not (requires supportive model) - */ - Thinking think; - - /** - * 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 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 - * @return The {@link OllamaObjectBuilder} - */ - public OllamaObjectBuilder stream(boolean stream) { - this.stream = stream; - return this; - } - - /** - * Sets the keep alive of the Ollama Object.
- * 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.
- * @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 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> tools) { - for(Pair 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... 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. - *

- * 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. - *

- * @param baseDirectory the base directory for file access. - * @return The {@link OllamaObjectBuilder} - */ - public OllamaObjectBuilder addFileTools(Path 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; - } - - /** - * Sets the thinking tier that should be requested from the model when this - * {@link OllamaObject} is built. - *

- * Not all models support this. Whether {@code think} has any effect depends - * entirely on the model being queried — models without native thinking support will - * simply ignore it. - *

- * Among models that do support it, the accepted scheme is not consistent: - *

    - *
  • Some models only support a boolean on/off toggle (thinking enabled or disabled)
  • - *
  • Others support graded levels, e.g. {@code "low"}, {@code "medium"}, {@code "high"}, - * or {@code "max"}
  • - *
- * Callers are responsible for knowing which scheme the target model expects and supplying - * a {@link Thinking} value it can actually interpret — an unsupported value is not - * guaranteed to fail cleanly and may instead be silently ignored by Ollama. - * - * @param think the thinking tier to request, or {@code null}/equivalent to leave - * thinking behavior at the model's default - * @return this builder, for chaining - */ - public OllamaObjectBuilder setThinkingTier(Thinking think) - { - this.think = think; - return this; - } - - /** - * Builds the {@link OllamaObject} - * @return The {@link OllamaObject} - */ - public OllamaObject build() { - return new OllamaObject(model, messages, tools, format, options, stream, keep_alive, systemPrompt, think); - } - } - - /** - * Represents the "thinking" tier that can be requested from an Ollama model via the - * {@code think} request field. - *

- * Support for this varies by model, and models that do support it do not agree on a - * single scheme: - *

    - *
  • Some models only understand a boolean toggle — {@link #TRUE} / {@link #FALSE}
  • - *
  • Others understand graded levels — {@link #LOW}, {@link #MEDIUM}, {@link #HIGH}, - * {@link #MAX}
  • - *
- * All six values live in this single enum for convenience, but mixing schemes on a model - * that doesn't support the one you pick is a caller error — e.g. sending {@link #HIGH} to - * a model that only understands {@link #TRUE}/{@link #FALSE} is not guaranteed to fail - * cleanly and may simply be ignored. Callers must know which scheme their target model - * expects before choosing a value here. - */ - public static enum Thinking { - - /** Thinking enabled, for models using the boolean on/off scheme. */ - TRUE("true"), - - /** Thinking disabled, for models using the boolean on/off scheme. */ - FALSE("false"), - - /** Lowest thinking effort, for models using the graded-level scheme. */ - LOW("low"), - - /** Moderate thinking effort, for models using the graded-level scheme. */ - MEDIUM("medium"), - - /** High thinking effort, for models using the graded-level scheme. */ - HIGH("high"), - - /** Maximum thinking effort, for models using the graded-level scheme. */ - MAX("max"); - - /** The raw string value sent to Ollama's {@code think} request field. */ - final String value; - - private Thinking(String value) - { - this.value = value; - } - - /** True if this constant belongs to the boolean on/off scheme, false if it's a graded level. */ - public boolean isBoolean() { - return this == TRUE || this == FALSE; - } - - - /** - * Puts this thinking tier into the given JSON object under the given key, using - * whichever JSON type Ollama expects for this constant — a real boolean for - * {@link #TRUE}/{@link #FALSE}, or a string for the graded levels. - */ - public void putInto(JSONObject json, String key) { - if (isBoolean()) { - json.put(key, Boolean.parseBoolean(value)); // real JSON boolean, no quotes - } else { - json.put(key, value); // JSON string, e.g. "high" - } - } - - /** - * Returns the raw string form of this tier, as expected by Ollama's {@code think} - * request field. - * - * @return the raw value, e.g. {@code "true"} or {@code "high"} - */ - public String getValue() - { - return value; - } - - /** - * Resolves a {@link Thinking} constant from its raw string value. - *

- * Matching is case-insensitive; the input is lowercased before comparison. - * - * @param value the raw value to resolve, e.g. {@code "true"} or {@code "HIGH"} - * @return the matching {@link Thinking} constant - * @throws IllegalArgumentException if no constant matches the given value - */ - public static Thinking fromValue(String value) { - for(Thinking roleRole : values()) { - if(roleRole.value.equals(value.toLowerCase())) - return roleRole; - } - throw new IllegalArgumentException("Invalid value: " + value); - } - } -} diff --git a/Core/src/main/java/me/neurodock/ollama/OllamaPerameter.java b/Core/src/main/java/me/neurodock/ollama/OllamaPerameter.java deleted file mode 100644 index 2e7c538..0000000 --- a/Core/src/main/java/me/neurodock/ollama/OllamaPerameter.java +++ /dev/null @@ -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 - *

{@code
-     *    OllamaPerameter.builder().build();
-     * }
- */ - public static OllamaPerameter empty() { - return builder().build(); - } - - /** - * Represents a builder for {@link OllamaPerameter}. - */ - public static class OllamaPerameterBuilder { - /** - * The properties of the parameters. - */ - Map propertyMap = new HashMap<>(); - /** - * The required parameters. - */ - ArrayList 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 > OllamaPerameterBuilder addEnumProperty(String name, Class 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 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 > OllamaPerameterBuilder addEnumProperty(Class 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); - }; - } - } - } -} diff --git a/Core/src/main/java/me/neurodock/ollama/OllamaTool.java b/Core/src/main/java/me/neurodock/ollama/OllamaTool.java deleted file mode 100644 index f548f48..0000000 --- a/Core/src/main/java/me/neurodock/ollama/OllamaTool.java +++ /dev/null @@ -1,10 +0,0 @@ -package me.neurodock.ollama; - -import org.json.JSONObject; - -/** - * Represents a tool. - */ -public interface OllamaTool { - public JSONObject toJSON(); -} diff --git a/Core/src/main/java/me/neurodock/ollama/OllamaToolError.java b/Core/src/main/java/me/neurodock/ollama/OllamaToolError.java deleted file mode 100644 index 656c0d7..0000000 --- a/Core/src/main/java/me/neurodock/ollama/OllamaToolError.java +++ /dev/null @@ -1,31 +0,0 @@ -package me.neurodock.ollama; - -import org.json.JSONObject; - -/** - * Represents an error from a tool.
- * 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; - } -} diff --git a/Core/src/main/java/me/neurodock/ollama/exceptions/OllamaToolErrorException.java b/Core/src/main/java/me/neurodock/ollama/exceptions/OllamaToolErrorException.java deleted file mode 100644 index 0074529..0000000 --- a/Core/src/main/java/me/neurodock/ollama/exceptions/OllamaToolErrorException.java +++ /dev/null @@ -1,50 +0,0 @@ -package me.neurodock.ollama.exceptions; - -import me.neurodock.core.Core; -import org.json.JSONObject; - -/** - * Represents an error from a tool.
- * 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; - } -} \ No newline at end of file diff --git a/Core/src/main/java/me/neurodock/ollama/utils/SystemMessage.java b/Core/src/main/java/me/neurodock/ollama/utils/SystemMessage.java deleted file mode 100644 index 0dc4a58..0000000 --- a/Core/src/main/java/me/neurodock/ollama/utils/SystemMessage.java +++ /dev/null @@ -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); - } -} diff --git a/Core/src/main/java/me/neurodock/plugin/loader/Loader.java b/Core/src/main/java/me/neurodock/plugin/loader/Loader.java index fb5a2c4..10d3d2b 100644 --- a/Core/src/main/java/me/neurodock/plugin/loader/Loader.java +++ b/Core/src/main/java/me/neurodock/plugin/loader/Loader.java @@ -2,8 +2,9 @@ package me.neurodock.plugin.loader; import jdk.jshell.spi.ExecutionControl; import me.neurodock.core.Pair; -import me.neurodock.ollama.*; -import me.neurodock.ollama.exceptions.OllamaToolErrorException; +import me.neurodock.llm.tools.FunctionTool; +import me.neurodock.llm.tools.ToolArguments; +import me.neurodock.llm.tools.ToolParameters; import me.neurodock.plugin.Data; import me.neurodock.plugin.LoadedPlugin; import me.neurodock.plugin.Plugin; @@ -11,11 +12,11 @@ import me.neurodock.plugin.PluginMetadata; import me.neurodock.plugin.exceptions.PluginLoadingException; import me.neurodock.plugin.exceptions.ToolRuntimeException; import me.neurodock.plugin.tool.Tool; -import me.neurodock.plugin.tool.ToolArguments; import me.neurodock.plugin.tool.ToolResponse; import org.jetbrains.annotations.NotNull; import org.json.JSONObject; +import javax.naming.OperationNotSupportedException; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -40,12 +41,12 @@ public class Loader { * Mock example! * @return */ - public OllamaFunctionTool[] getTools(Plugin plugin) { + public me.neurodock.llm.tools.Tool[] getTools(Plugin plugin) { - ArrayList tools = new ArrayList<>(); + ArrayList tools = new ArrayList<>(); for(Tool tool : plugin.getTools()) { - tools.add(new OllamaFunctionTool() { + tools.add(new FunctionTool() { @Override public @NotNull String name() { return tool.name() + "_" + plugin.getMetadata().getName(); @@ -57,14 +58,16 @@ public class Loader { } @Override - public @NotNull OllamaPerameter parameters() { - return OllamaPerameter.builder().of(tool.parameters()).build(); + public @NotNull ToolParameters parameters() { + throw new RuntimeException(new OperationNotSupportedException("No")); + //return ToolParameters.builder().of(tool.parameters()).build(); } @Override - public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { + public @NotNull me.neurodock.llm.tools.ToolResponse function(ToolArguments args) { + throw new UnsupportedOperationException("no"); // Wrap OllamaFunctionArguments[] to ether ToolArgument[] or ToolArguments(current implementation) - ToolArguments toolArgs = new ToolArguments(); + /*ToolArguments toolArgs = new ToolArguments(); for (OllamaFunctionArgument arg : args) { toolArgs.addArgument(arg.argument(), arg.value()); } @@ -75,17 +78,12 @@ public class Loader { throw new OllamaToolErrorException(name(), e); } // Wrap ToolResponce to an OllamaToolRespnce(Well I see a typo here now) - return new OllamaToolResponse(toolResponse.name(), toolResponse.response()); - } - - @Override - public JSONObject toJSON() { - return tool.getToolJSON(); + return new OllamaToolResponse(toolResponse.name(), toolResponse.response());*/ } }); } - return tools.toArray(tools.toArray(new OllamaFunctionTool[0])); + return tools.toArray(tools.toArray(new me.neurodock.llm.tools.Tool[0])); } diff --git a/Core/src/test/java/OllamaImages.java b/Core/src/test/java/OllamaImages.java deleted file mode 100644 index 948c914..0000000 --- a/Core/src/test/java/OllamaImages.java +++ /dev/null @@ -1,55 +0,0 @@ -import me.neurodock.core.Core; -import me.neurodock.core.LaunchOptions; -import me.neurodock.core.Options; -import me.neurodock.core.PrintMessageHandler; -import me.neurodock.ollama.OllamaMessage; -import me.neurodock.ollama.OllamaMessageRole; -import me.neurodock.ollama.OllamaObject; -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.io.File; -import java.nio.file.Path; -import java.util.concurrent.atomic.AtomicReference; - -public class OllamaImages { - @Test - public void test1() - { - AtomicReference i = new AtomicReference<>(); - assertDoesNotThrow(() -> { - File input = new File("../testImage.png"); - i.set(ImageIO.read(input)); - }); - assertNotNull(i.get()); - - LaunchOptions.getInstance().setLoadOld(false); - Options.getInstance().setDataDir(Path.of("AI-Chat/Test"), false); - - Core core = new Core(new PrintMessageHandler() { - @Override - public void printMessage(String message) { - System.out.println(message); - } - - @Override - public boolean color() { - return false; - } - }); - - core.setOllamaObjectNoMemory(OllamaObject.builder() - .setModel("qwen3.5:4b") - .keep_alive(0) - .build()); - - assertDoesNotThrow(() -> { - core.getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.USER, "Describe this image", i.get())); - }); - - core.qurryOllama().thenAccept(core::handleResponse).join(); - - } -} diff --git a/Core/src/test/java/plugin/Test.java b/Core/src/test/java/plugin/Test.java deleted file mode 100644 index 027250e..0000000 --- a/Core/src/test/java/plugin/Test.java +++ /dev/null @@ -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; - } -} diff --git a/Core/src/test/java/plugin/Tool.java b/Core/src/test/java/plugin/Tool.java deleted file mode 100644 index 7cc9397..0000000 --- a/Core/src/test/java/plugin/Tool.java +++ /dev/null @@ -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; - } -} diff --git a/Display/src/main/java/me/neurodock/display/Display.java b/Display/src/main/java/me/neurodock/display/Display.java index 79dab11..c83748d 100644 --- a/Display/src/main/java/me/neurodock/display/Display.java +++ b/Display/src/main/java/me/neurodock/display/Display.java @@ -1,12 +1,18 @@ package me.neurodock.display; +import me.neurodock.backend.open.ai.OpenAIModel; import me.neurodock.core.Core; import me.neurodock.core.Options; import me.neurodock.core.Pair; import me.neurodock.core.PrintMessageHandler; import me.neurodock.core.memory.CoreMemory; +import me.neurodock.llm.ChatObject; +import me.neurodock.llm.Message; +import me.neurodock.llm.exceptions.ToolException; +import me.neurodock.llm.tools.*; import me.neurodock.ollama.*; import me.neurodock.ollama.utils.SystemMessage; +import org.jetbrains.annotations.NotNull; import org.json.JSONObject; import java.io.*; @@ -21,7 +27,14 @@ import static me.neurodock.core.Core.writeLog; * Somewhat meant to be used as a Debug tool for testing your API. */ public class Display { - + + /* + * Inizilizer + */ + { + Options.getInstance().setDataDir(Path.of("AI-Chat"), false); + } + /** * The Core instance. */ @@ -43,16 +56,17 @@ public class Display { */ public Display() { + OpenAIModel model = new OpenAIModel("http://localhost:8080", "MiniCPM5"); - Options.getInstance().setDataDir(Path.of("AI-Chat"), false); - - core.setOllamaObject/*NoMemory*/(OllamaObject.builder() + /*core.setOllamaObject/*NoMemory*//*(OllamaObject.builder() .setModel("llama3.1:8b") .keep_alive(10) //.stream(false) .addFileTools(Options.getInstance().getFileHandlerDataLocation()) .addMessages(new SystemMessage("You need to emulate feelings and emotions.")) - .build()); + .build());*/ + + core.setModel(model); core.enablePlugins(Options.getInstance().getPluginDirectory()); diff --git a/GeniusAPI/src/main/java/me/neurodock/genius/GeniusEndpointTool.java b/GeniusAPI/src/main/java/me/neurodock/genius/GeniusEndpointTool.java index 7c89c01..1088df6 100644 --- a/GeniusAPI/src/main/java/me/neurodock/genius/GeniusEndpointTool.java +++ b/GeniusAPI/src/main/java/me/neurodock/genius/GeniusEndpointTool.java @@ -1,8 +1,8 @@ package me.neurodock.genius; -import me.neurodock.ollama.OllamaFunctionTool; +import me.neurodock.llm.tools.FunctionTool; -public abstract class GeniusEndpointTool extends OllamaFunctionTool { +public abstract class GeniusEndpointTool extends FunctionTool { protected GeniusTools geniusToolsInstance; public GeniusEndpointTool(GeniusTools geniusTools) { diff --git a/GeniusAPI/src/main/java/me/neurodock/genius/endpoints/GetLyrics.java b/GeniusAPI/src/main/java/me/neurodock/genius/endpoints/GetLyrics.java index 1705217..0956e9c 100644 --- a/GeniusAPI/src/main/java/me/neurodock/genius/endpoints/GetLyrics.java +++ b/GeniusAPI/src/main/java/me/neurodock/genius/endpoints/GetLyrics.java @@ -1,12 +1,11 @@ package me.neurodock.genius.endpoints; -import me.neurodock.ollama.OllamaFunctionArgument; -import me.neurodock.ollama.OllamaPerameter; -import me.neurodock.ollama.OllamaToolResponse; -import me.neurodock.ollama.exceptions.OllamaToolErrorException; import me.neurodock.genius.GeniusEndpoint; import me.neurodock.genius.GeniusEndpointTool; import me.neurodock.genius.GeniusTools; +import me.neurodock.llm.tools.ToolArguments; +import me.neurodock.llm.tools.ToolParameters; +import me.neurodock.llm.tools.ToolResponse; import org.jetbrains.annotations.NotNull; import org.json.JSONObject; import org.jsoup.Jsoup; @@ -49,20 +48,16 @@ public class GetLyrics extends GeniusEndpointTool { } @Override - public @NotNull OllamaPerameter parameters() { - return OllamaPerameter.builder() - .addProperty("song_id", OllamaPerameter.OllamaPerameterBuilder.Type.INT, "The ID of the song to get lyrics for.", true) + public @NotNull ToolParameters parameters() { + return ToolParameters.builder() + .addProperty("song_id", ToolParameters.ToolParametersBuilder.Type.INT, "The ID of the song to get lyrics for.", true) .build(); } @Override - public @NotNull OllamaToolResponse function(OllamaFunctionArgument... args) { + public @NotNull ToolResponse function(ToolArguments args) { - if(!( args[0].value() instanceof Integer)) { - throw new OllamaToolErrorException(this.name(), "The song_id must be an integer."); - } - - String lyricsStr = geniusToolsInstance.hasCache((int) args[0].value()); + String lyricsStr = geniusToolsInstance.hasCache(args.getArgument("song_id", Integer.class)); if(lyricsStr != null) { diff --git a/gradle.properties b/gradle.properties index 13c8b6d..7bd6632 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,6 +2,6 @@ useAPI = true javaVersion = 25 -coreVersion = 1.10.1 -pluginAPIVersion = 0.1.4.1 +coreVersion = 2.1.5 +pluginAPIVersion = 1.1.4.1 APIVersion = 1.0-SNAPSHOT \ No newline at end of file