Added support for Imagaes, Ollama model thinking, and streaming of the responce

This commit is contained in:
2026-07-25 21:25:41 +02:00
parent 663ab68172
commit 939fdb2211
5 changed files with 150 additions and 23 deletions
+2
View File
@@ -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'
+1 -1
View File
@@ -2,7 +2,7 @@ plugins {
id 'java-library'
}
version = '1.9.6'
version = '1.9.10'
dependencies {
implementation project(":Plugin-API")
+74 -21
View File
@@ -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.
* <p>
* 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<JSONObject> 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.
* <p>
* 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:
* <ul>
* <li>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.</li>
* <li>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.</li>
* </ul>
* 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."
* <p>
* 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<JSONObject> qurryOllama(Consumer<JSONObject> onChunk)
{
public CompletableFuture<JSONObject> qurryOllama(Consumer<JSONObject> 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();
@@ -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.
+53
View File
@@ -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<Image> 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();
}
}