Compare commits
3
Commits
4cd4367a19
...
939fdb2211
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
939fdb2211
|
||
|
|
663ab68172
|
||
|
|
330d7df389
|
@@ -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.0'
|
||||
version = '1.9.10'
|
||||
|
||||
dependencies {
|
||||
implementation project(":Plugin-API")
|
||||
|
||||
@@ -19,10 +19,9 @@ import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
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;
|
||||
|
||||
@@ -68,6 +67,14 @@ public class Core {
|
||||
* The URL of the Ollama API.
|
||||
*/
|
||||
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.
|
||||
@@ -346,119 +353,13 @@ 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.
|
||||
*/
|
||||
private void confirmOllama()
|
||||
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");
|
||||
@@ -469,14 +370,14 @@ public class Core {
|
||||
|
||||
if (responseCode != HttpURLConnection.HTTP_OK) {
|
||||
new RuntimeException("Ollama is un-responsive on url: " + url.toString()).printStackTrace();
|
||||
System.exit(1);
|
||||
return false;
|
||||
}
|
||||
}catch (IOException ex)
|
||||
{
|
||||
ex.printStackTrace();
|
||||
System.out.println("Can not reach Ollama!");
|
||||
System.exit(1);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -593,23 +494,78 @@ public class Core {
|
||||
logWriter.flush();
|
||||
}catch (IOException e) {}
|
||||
}
|
||||
|
||||
|
||||
public void cancel()
|
||||
{
|
||||
cancelled.set(true);
|
||||
HttpURLConnection conn = activeConnection;
|
||||
if (conn != null) {
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the OllamaObject to Ollama
|
||||
* @return The response from Ollama
|
||||
* 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(_ -> {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
HttpURLConnection connection = null;
|
||||
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.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())) {
|
||||
@@ -618,43 +574,64 @@ public class Core {
|
||||
}
|
||||
|
||||
int responseCode = connection.getResponseCode();
|
||||
boolean isStreaming = ollamaObject.isStream(); // whatever the real accessor is
|
||||
JSONObject last = null;
|
||||
StringBuilder rawErrorOrDump = new StringBuilder();
|
||||
StringBuilder messageContent = new StringBuilder();
|
||||
|
||||
// HTTP_OK or 200 response code generally means that the server ran successfully without any errors
|
||||
StringBuilder response = new StringBuilder();
|
||||
|
||||
// Read response content
|
||||
// connection.getInputStream() purpose is to obtain an input stream for reading the server's response.
|
||||
try (
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
|
||||
String line;
|
||||
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");
|
||||
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 (Exception ex) {
|
||||
// If the server returns an error, we read the error stream instead
|
||||
} 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) {
|
||||
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) {
|
||||
|
||||
|
||||
connection.disconnect();
|
||||
return new JSONObject(response.toString());
|
||||
return last;
|
||||
} else {
|
||||
connection.disconnect();
|
||||
printMessageHandler.printErrorMessage(new OllamaMessage(OllamaMessageRole.SYSTEM,"Error: HTTP Response code - " + responseCode + "\n" + response.toString()));
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ public class LLMSystemPrompt {
|
||||
for (Pair<OllamaTool, String> tool : ollamaObject.getTools()) {
|
||||
if(tool.getKey() instanceof OllamaFunctionTool funcTool) {
|
||||
String llmName = funcTool.name() + "_" + (funcTool.getSource() != null ? funcTool.getSource() : "");
|
||||
String hint = capabilities.getUsageHints().get(tool.getClass());
|
||||
String hint = capabilities.getUsageHints().get(funcTool.getClass());
|
||||
|
||||
// fall back to description() if no explicit routing hint was set
|
||||
String line = hint != null ? hint : funcTool.description();
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
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.
|
||||
*/
|
||||
@@ -14,6 +24,11 @@ public class OllamaMessage {
|
||||
* 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<String> images;
|
||||
|
||||
/**
|
||||
* Creates a new instance of OllamaMessage.
|
||||
@@ -25,6 +40,25 @@ public class OllamaMessage {
|
||||
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}
|
||||
*/
|
||||
@@ -43,6 +77,10 @@ public class OllamaMessage {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,14 @@ public class OllamaObject {
|
||||
* 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.
|
||||
@@ -66,10 +74,14 @@ public class OllamaObject {
|
||||
* @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<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.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<OllamaTool, String> tool : tools)
|
||||
@@ -135,34 +147,39 @@ public class OllamaObject {
|
||||
}
|
||||
}
|
||||
|
||||
if(prompt == null) {
|
||||
// Do nothing
|
||||
}
|
||||
else 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<OllamaMessage> 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);
|
||||
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<OllamaMessage> 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());
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,6 +352,7 @@ public class OllamaObject {
|
||||
json.put("options", options);
|
||||
json.put("stream", stream);
|
||||
json.put("keep_alive", keep_alive);
|
||||
thinking.putInto(json, "think");
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -383,6 +401,10 @@ public class OllamaObject {
|
||||
* 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}.
|
||||
@@ -423,10 +445,8 @@ public class OllamaObject {
|
||||
/**
|
||||
* Sets 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}
|
||||
*/
|
||||
@Deprecated
|
||||
public OllamaObjectBuilder stream(boolean stream) {
|
||||
this.stream = stream;
|
||||
return this;
|
||||
@@ -581,12 +601,133 @@ public class OllamaObject {
|
||||
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}
|
||||
* @return The {@link OllamaObject}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/** 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.
|
||||
* <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
# This is used in sub-project that dosent explicitly require the API sub-project to only include if thay shuld be included project wide.
|
||||
useAPI = true
|
||||
javaVersion = 26
|
||||
javaVersion = 25
|
||||
Reference in New Issue
Block a user