!! PARTIAL COMMIT !!

This is a partial commit bc i felt like it...

This begain some implementations to support streaming from Ollama, and the ability to cancel a request.
This commit is contained in:
2026-07-20 22:13:33 +02:00
parent 330d7df389
commit 663ab68172
4 changed files with 209 additions and 136 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ plugins {
id 'java-library' id 'java-library'
} }
version = '1.9.5' version = '1.9.6'
dependencies { dependencies {
implementation project(":Plugin-API") implementation project(":Plugin-API")
+55 -131
View File
@@ -19,10 +19,9 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.*;
import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.ScheduledExecutorService; import java.util.function.Consumer;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarFile; import java.util.jar.JarFile;
@@ -68,6 +67,14 @@ public class Core {
* The URL of the Ollama API. * The URL of the Ollama API.
*/ */
private URL url; private URL url;
/**
* Used to cancel/stop a responce, only usefull if
*/
private AtomicBoolean cancelled = new AtomicBoolean(false);
/**
* The current connection to Ollama
*/
private HttpURLConnection activeConnection;
/** /**
* The PrintMessageHandler to use. * The PrintMessageHandler to use.
@@ -346,116 +353,10 @@ public class Core {
} }
} }
/**
* This function is pending a better name, as this was introduced from being a generic block. This was ugly, and ideally things here should be refactored more.
*
* TODO: Fix a better name and refactor this method.
*/
@Deprecated(forRemoval = true)
private void constructCore(){
File dir = new File("./logs/");
if (!dir.exists()) {
dir.mkdir();
}
dir = new File("./pythonFiles/");
if (!dir.exists()) {
dir.mkdir();
}
dir = new File("./messages");
if (!dir.exists()) {
dir.mkdir();
}
try {
url = new URI("http://"+ollamaIP+":"+ollamaPort+"/api/chat").toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
try {
if (logFile.exists()) {
BufferedReader br = new BufferedReader(new FileReader(logFile));
String line = br.readLine();
br.close();
if (line != null) {
String date = line.substring(0, line.indexOf(">")).replaceAll("[/:]", "-");
logFile.renameTo(new File(logFile.getParentFile(), date + ".log"));
logFile = new File("./logs/latest.log");
}
else {
System.out.println("Exisitng log file is empty, overwriting it!");
logFile.delete();
}
logFile.createNewFile();
}
logWriter = new BufferedWriter(new FileWriter(logFile));
}catch (IOException e) {
throw new RuntimeException(e);
}
this.scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
try {
logWriter.flush();
//System.out.println("Buffer flushed to file.");
} catch (IOException e) {
e.printStackTrace();
}
}, 0, 3, TimeUnit.MINUTES);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
scheduler.shutdownNow();
try {
try {
logWriter.flush();
logWriter.close();
}catch (IOException ignore)
{
// This exception is kinda expected. Since it can often occur that the logWriter is already closed
System.out.println("Failed to flush log file, but that is not a problem.");
}
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd_HH-mm-ss");
File messagesFile = new File("./messages/"+now.format(formatter)+".json");
BufferedWriter messagesWriter = new BufferedWriter(new FileWriter(messagesFile));
JSONArray messages = new JSONArray();
for(OllamaMessage message : ollamaObject.getMessages()) {
messages.put(message.toJSON());
}
messagesWriter.write(messages.toString());
messagesWriter.close();
File f = new File(DATA_DIR,"messages.json");
if(f.exists())
{
f.delete();
}
f.createNewFile();
messagesWriter = new BufferedWriter(new FileWriter(f));
messagesWriter.write(messages.toString());
messagesWriter.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}));
}
/** /**
* A check to exit early if Ollama isn't reachable. * A check to exit early if Ollama isn't reachable.
*/ */
private void confirmOllama() private boolean confirmOllama()
{ {
try { try {
URL url = new URL("http://" + ollamaIP + ":" + ollamaPort + "/api/version"); URL url = new URL("http://" + ollamaIP + ":" + ollamaPort + "/api/version");
@@ -469,14 +370,14 @@ public class Core {
if (responseCode != HttpURLConnection.HTTP_OK) { if (responseCode != HttpURLConnection.HTTP_OK) {
new RuntimeException("Ollama is un-responsive on url: " + url.toString()).printStackTrace(); new RuntimeException("Ollama is un-responsive on url: " + url.toString()).printStackTrace();
System.exit(1); return false;
} }
}catch (IOException ex) }catch (IOException ex)
{ {
ex.printStackTrace(); ex.printStackTrace();
System.out.println("Can not reach Ollama!"); return false;
System.exit(1);
} }
return true;
} }
/** /**
@@ -594,22 +495,42 @@ public class Core {
}catch (IOException e) {} }catch (IOException e) {}
} }
public void cancel()
{
cancelled.set(true);
HttpURLConnection conn = activeConnection;
if (conn != null) {
conn.disconnect();
}
}
public CompletableFuture<JSONObject> qurryOllama()
{
return qurryOllama(_ -> {});
}
/** /**
* Sends the OllamaObject to Ollama * Sends the OllamaObject to Ollama
* @return The response from Ollama * @return The response from Ollama
*/ */
public CompletableFuture<JSONObject> qurryOllama() public CompletableFuture<JSONObject> qurryOllama(Consumer<JSONObject> onChunk)
{ {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
HttpURLConnection connection = null;
try { try {
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection = (HttpURLConnection) url.openConnection();
activeConnection = connection;
if (cancelled.get()) {
throw new CancellationException("Query cancelled before request was sent");
}
connection.setRequestMethod("POST"); connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json"); connection.setRequestProperty("Content-Type", "application/json");
connection.setDoOutput(true); connection.setDoOutput(true);
connection.setConnectTimeout(80 * 1000); connection.setConnectTimeout(80 * 1000);
String ollamaObjectString = ollamaObject.toJSON().toString(); String ollamaObjectString = ollamaObject.toJSON().toString();
ollamaObjectString = ollamaObjectString.replace("\n", "\\n"); ollamaObjectString = ollamaObjectString.replace("\n", "\\n");
try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) { try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
@@ -618,20 +539,20 @@ public class Core {
} }
int responseCode = connection.getResponseCode(); int responseCode = connection.getResponseCode();
// HTTP_OK or 200 response code generally means that the server ran successfully without any errors
StringBuilder response = new StringBuilder(); StringBuilder response = new StringBuilder();
// Read response content try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
// connection.getInputStream() purpose is to obtain an input stream for reading the server's response.
try (
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
String line; String line;
while ((line = reader.readLine()) != null) { while ((line = reader.readLine()) != null) {
response.append(line); // Adds every line to response till the end of file. if (cancelled.get()) {
throw new CancellationException("Query cancelled mid-response");
}
response.append(line);
}
} catch (IOException ex) {
if (cancelled.get()) {
throw new CancellationException("Query cancelled mid-response");
} }
} catch (Exception ex) {
// If the server returns an error, we read the error stream instead
InputStream errorStream = connection.getErrorStream(); InputStream errorStream = connection.getErrorStream();
if (errorStream != null) { if (errorStream != null) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream))) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream))) {
@@ -644,17 +565,20 @@ public class Core {
} }
if (responseCode == HttpURLConnection.HTTP_OK) { if (responseCode == HttpURLConnection.HTTP_OK) {
connection.disconnect();
return new JSONObject(response.toString()); return new JSONObject(response.toString());
} else { } else {
connection.disconnect(); printMessageHandler.printErrorMessage(new OllamaMessage(OllamaMessageRole.SYSTEM,
printMessageHandler.printErrorMessage(new OllamaMessage(OllamaMessageRole.SYSTEM,"Error: HTTP Response code - " + responseCode + "\n" + response.toString())); "Error: HTTP Response code - " + responseCode + "\n" + response));
throw new RuntimeException("HTTP Response code - " + responseCode); throw new RuntimeException("HTTP Response code - " + responseCode);
} }
} catch (IOException e) { } catch (IOException e) {
if (cancelled.get()) {
throw new CancellationException("Query cancelled");
}
throw new RuntimeException(e); throw new RuntimeException(e);
} finally {
if (connection != null) connection.disconnect();
activeConnection = null;
} }
}); });
} }
@@ -1,7 +1,17 @@
package me.neurodock.ollama; package me.neurodock.ollama;
import org.jetbrains.annotations.NotNull;
import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.RenderedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
/** /**
* Represents a message sent by a Tool, Assistant(Ollama), or User. * Represents a message sent by a Tool, Assistant(Ollama), or User.
*/ */
@@ -14,6 +24,11 @@ public class OllamaMessage {
* The content of the message. * The content of the message.
*/ */
String content; String content;
/**
* The image content of a message.
* This is a base64 encoded string of either an Image or an Audio file (this is how Ollama dose this, ask them, not me)
*/
ArrayList<String> images;
/** /**
* Creates a new instance of OllamaMessage. * Creates a new instance of OllamaMessage.
@@ -25,6 +40,25 @@ public class OllamaMessage {
this.content = content; this.content = content;
} }
/**
* Creates a new instance of OllamaMessage.
* @param role The role of the message
* @param content The content of the message
* @param images The image of the message
* @throws IOException if an error occurs during writing or when not able to create the required ImageOutputStream.
*/
public OllamaMessage(OllamaMessageRole role, String content, @NotNull Image... images) throws IOException
{
this(role, content);
ArrayList<String> base65Images = new ArrayList<>();
for(Image i : images) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
ImageIO.write((RenderedImage) i, "PNG", stream);
base65Images.add(Base64.getEncoder().encodeToString(stream.toByteArray()));
}
this.images = base65Images;
}
/** /**
* @return The role of who sent this "message", represented as {@link OllamaMessageRole} * @return The role of who sent this "message", represented as {@link OllamaMessageRole}
*/ */
@@ -43,6 +77,10 @@ public class OllamaMessage {
JSONObject json = new JSONObject(); JSONObject json = new JSONObject();
json.put("role", role.getRole()); json.put("role", role.getRole());
json.put("content", content.replace("\n", "\\n")); json.put("content", content.replace("\n", "\\n"));
if(images != null && !images.isEmpty())
{
json.put("images", new JSONArray(images));
}
return json; return json;
} }
} }
@@ -60,6 +60,10 @@ public class OllamaObject {
* The system prompt object used to re-generate the system prompt. * The system prompt object used to re-generate the system prompt.
*/ */
LLMSystemPrompt systemPrompt; LLMSystemPrompt systemPrompt;
/**
* The level of thining the model shuld do
*/
Thinking thinking;
/** /**
* Creates a new instance of OllamaObject. * Creates a new instance of OllamaObject.
@@ -72,10 +76,12 @@ public class OllamaObject {
* @param keep_alive The keep alive of the Ollama Object. see {@link OllamaObject#keep_alive} * @param keep_alive The keep alive of the Ollama Object. see {@link OllamaObject#keep_alive}
* @param prompt The system prompt object of the Ollama Object. See {@link OllamaObject#systemPrompt} * @param prompt The system prompt object of the Ollama Object. See {@link OllamaObject#systemPrompt}
*/ */
private OllamaObject(String model, ArrayList<OllamaMessage> messages, ArrayList<Pair<OllamaTool, String>> tools, JSONObject format, Map<String, Object> options, boolean stream, String keep_alive, LLMSystemPrompt prompt) { private OllamaObject(String model, ArrayList<OllamaMessage> messages, ArrayList<Pair<OllamaTool, String>> tools, JSONObject format, Map<String, Object> options, boolean stream, String keep_alive, LLMSystemPrompt prompt, Thinking thinking) {
this.model = model; this.model = model;
this.messages = messages; this.messages = messages;
this.systemPrompt = prompt; this.systemPrompt = prompt;
this.thinking = thinking;
this.stream = stream;
// Reflecting the reflection injection for ALL tools that was added in the OllamaObjectBuilder, etc, etc comment.. its past midnignt and i'm tired. // Reflecting the reflection injection for ALL tools that was added in the OllamaObjectBuilder, etc, etc comment.. its past midnignt and i'm tired.
for(Pair<OllamaTool, String> tool : tools) for(Pair<OllamaTool, String> tool : tools)
@@ -346,6 +352,7 @@ public class OllamaObject {
json.put("options", options); json.put("options", options);
json.put("stream", stream); json.put("stream", stream);
json.put("keep_alive", keep_alive); json.put("keep_alive", keep_alive);
json.put("think", thinking);
return json; return json;
} }
@@ -394,6 +401,10 @@ public class OllamaObject {
* This represents the System Prompt for the LLM * This represents the System Prompt for the LLM
*/ */
LLMSystemPrompt systemPrompt; LLMSystemPrompt systemPrompt;
/**
* Weather the model should think or not (requires supportive model)
*/
Thinking think;
/** /**
* Creates a new instance of {@link OllamaObjectBuilder}. * Creates a new instance of {@link OllamaObjectBuilder}.
@@ -434,10 +445,8 @@ public class OllamaObject {
/** /**
* Sets if the Ollama Object is streamed. * Sets if the Ollama Object is streamed.
* @param stream If the Ollama Object is streamed * @param stream If the Ollama Object is streamed
* @deprecated This should be false due to being broken in the current version of this system
* @return The {@link OllamaObjectBuilder} * @return The {@link OllamaObjectBuilder}
*/ */
@Deprecated
public OllamaObjectBuilder stream(boolean stream) { public OllamaObjectBuilder stream(boolean stream) {
this.stream = stream; this.stream = stream;
return this; return this;
@@ -592,12 +601,114 @@ public class OllamaObject {
return this; return this;
} }
/**
* Sets the thinking tier that should be requested from the model when this
* {@link OllamaObject} is built.
* <p>
* <b>Not all models support this.</b> Whether {@code think} has any effect depends
* entirely on the model being queried — models without native thinking support will
* simply ignore it.
* <p>
* Among models that <i>do</i> support it, the accepted scheme is not consistent:
* <ul>
* <li>Some models only support a boolean on/off toggle (thinking enabled or disabled)</li>
* <li>Others support graded levels, e.g. {@code "low"}, {@code "medium"}, {@code "high"},
* or {@code "max"}</li>
* </ul>
* Callers are responsible for knowing which scheme the target model expects and supplying
* a {@link Thinking} value it can actually interpret — an unsupported value is not
* guaranteed to fail cleanly and may instead be silently ignored by Ollama.
*
* @param think the thinking tier to request, or {@code null}/equivalent to leave
* thinking behavior at the model's default
* @return this builder, for chaining
*/
public OllamaObjectBuilder setThinkingTier(Thinking think)
{
this.think = think;
return this;
}
/** /**
* Builds the {@link OllamaObject} * Builds the {@link OllamaObject}
* @return The {@link OllamaObject} * @return The {@link OllamaObject}
*/ */
public OllamaObject build() { public OllamaObject build() {
return new OllamaObject(model, messages, tools, format, options, stream, keep_alive, systemPrompt); return new OllamaObject(model, messages, tools, format, options, stream, keep_alive, systemPrompt, think);
}
}
/**
* Represents the "thinking" tier that can be requested from an Ollama model via the
* {@code think} request field.
* <p>
* Support for this varies by model, and models that do support it do not agree on a
* single scheme:
* <ul>
* <li>Some models only understand a boolean toggle — {@link #TRUE} / {@link #FALSE}</li>
* <li>Others understand graded levels — {@link #LOW}, {@link #MEDIUM}, {@link #HIGH},
* {@link #MAX}</li>
* </ul>
* All six values live in this single enum for convenience, but mixing schemes on a model
* that doesn't support the one you pick is a caller error — e.g. sending {@link #HIGH} to
* a model that only understands {@link #TRUE}/{@link #FALSE} is not guaranteed to fail
* cleanly and may simply be ignored. Callers must know which scheme their target model
* expects before choosing a value here.
*/
public static enum Thinking {
/** Thinking enabled, for models using the boolean on/off scheme. */
TRUE("true"),
/** Thinking disabled, for models using the boolean on/off scheme. */
FALSE("false"),
/** Lowest thinking effort, for models using the graded-level scheme. */
LOW("low"),
/** Moderate thinking effort, for models using the graded-level scheme. */
MEDIUM("medium"),
/** High thinking effort, for models using the graded-level scheme. */
HIGH("high"),
/** Maximum thinking effort, for models using the graded-level scheme. */
MAX("max");
/** The raw string value sent to Ollama's {@code think} request field. */
final String value;
private Thinking(String value)
{
this.value = value;
}
/**
* Returns the raw string form of this tier, as expected by Ollama's {@code think}
* request field.
*
* @return the raw value, e.g. {@code "true"} or {@code "high"}
*/
public String getValue()
{
return value;
}
/**
* Resolves a {@link Thinking} constant from its raw string value.
* <p>
* Matching is case-insensitive; the input is lowercased before comparison.
*
* @param value the raw value to resolve, e.g. {@code "true"} or {@code "HIGH"}
* @return the matching {@link Thinking} constant
* @throws IllegalArgumentException if no constant matches the given value
*/
public static Thinking fromValue(String value) {
for(Thinking roleRole : values()) {
if(roleRole.value.equals(value.toLowerCase()))
return roleRole;
}
throw new IllegalArgumentException("Invalid value: " + value);
} }
} }
} }