diff --git a/API/build.gradle b/API/build.gradle
index 4d6f181..03d6a86 100644
--- a/API/build.gradle
+++ b/API/build.gradle
@@ -14,6 +14,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:4.1.0-M4'
implementation 'org.springframework.boot:spring-boot-starter-webflux:4.1.0-M4'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.0-M1'
+ testImplementation 'org.springframework.boot:spring-boot-starter-test:4.1.0-M4'
+
//implementation 'org.springframework.boot:spring-boot-starter-actuator'
testImplementation 'org.springframework.boot:spring-boot-starter-test:4.1.0-M4'
diff --git a/Core/build.gradle b/Core/build.gradle
index 3a5ae9f..6bfde9c 100644
--- a/Core/build.gradle
+++ b/Core/build.gradle
@@ -2,7 +2,7 @@ plugins {
id 'java-library'
}
-version = '1.9.6'
+version = '1.9.10'
dependencies {
implementation project(":Plugin-API")
diff --git a/Core/src/main/java/me/neurodock/core/Core.java b/Core/src/main/java/me/neurodock/core/Core.java
index 65560a1..5a4cb54 100644
--- a/Core/src/main/java/me/neurodock/core/Core.java
+++ b/Core/src/main/java/me/neurodock/core/Core.java
@@ -359,7 +359,7 @@ public class Core {
private boolean confirmOllama()
{
try {
- URL url = new URL("http://" + ollamaIP + ":" + ollamaPort + "/api/version");
+ URL url = URI.create("http://" + ollamaIP + ":" + ollamaPort + "/api/version").toURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Content-Type", "application/json");
@@ -504,17 +504,52 @@ public class Core {
}
}
+ /**
+ * 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(_ -> {});
}
-
+
/**
- * Sends the OllamaObject to Ollama
- * @return The response from Ollama
+ * 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:
+ *
+ * - If {@code stream} is {@code true}, Ollama emits one JSON object per line as
+ * generation progresses; {@code onChunk} fires once per line, in order, as each
+ * arrives.
+ * - If {@code stream} is {@code false}, Ollama emits the entire response as a single
+ * object once generation is complete; {@code onChunk} fires exactly once with that
+ * full object.
+ *
+ * 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)
- {
+ public CompletableFuture qurryOllama(Consumer onChunk) {
return CompletableFuture.supplyAsync(() -> {
HttpURLConnection connection = null;
try {
@@ -539,42 +574,60 @@ public class Core {
}
int responseCode = connection.getResponseCode();
- StringBuilder response = new StringBuilder();
+ 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 (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);
}
- response.append(line);
}
} catch (IOException ex) {
- if (cancelled.get()) {
- throw new CancellationException("Query cancelled mid-response");
- }
+ 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) {
- response.append(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 new JSONObject(response.toString());
+ return last;
} else {
printMessageHandler.printErrorMessage(new OllamaMessage(OllamaMessageRole.SYSTEM,
- "Error: HTTP Response code - " + responseCode + "\n" + response));
+ "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");
- }
+ if (cancelled.get()) throw new CancellationException("Query cancelled");
throw new RuntimeException(e);
} finally {
if (connection != null) connection.disconnect();
diff --git a/Core/src/main/java/me/neurodock/ollama/OllamaObject.java b/Core/src/main/java/me/neurodock/ollama/OllamaObject.java
index d1785fe..ada1538 100644
--- a/Core/src/main/java/me/neurodock/ollama/OllamaObject.java
+++ b/Core/src/main/java/me/neurodock/ollama/OllamaObject.java
@@ -352,7 +352,7 @@ public class OllamaObject {
json.put("options", options);
json.put("stream", stream);
json.put("keep_alive", keep_alive);
- json.put("think", thinking);
+ thinking.putInto(json, "think");
return json;
}
@@ -683,6 +683,25 @@ public class OllamaObject {
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.
diff --git a/Core/src/test/java/OllamaImages.java b/Core/src/test/java/OllamaImages.java
new file mode 100644
index 0000000..6c34604
--- /dev/null
+++ b/Core/src/test/java/OllamaImages.java
@@ -0,0 +1,53 @@
+import me.neurodock.core.Core;
+import me.neurodock.core.LaunchOptions;
+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.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);
+ Core.setDataDirectory("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();
+
+ }
+}