Added support for Imagaes, Ollama model thinking, and streaming of the responce
This commit is contained in:
@@ -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
@@ -2,7 +2,7 @@ plugins {
|
||||
id 'java-library'
|
||||
}
|
||||
|
||||
version = '1.9.6'
|
||||
version = '1.9.10'
|
||||
|
||||
dependencies {
|
||||
implementation project(":Plugin-API")
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user