Add LLMSystemPrompt as a unified system-prompt builder for OllamaObject
Replaces ad-hoc system-prompt string construction with a structured builder (Identity, Context, Capabilities, Behavior, Output), plus OllamaObject#setSystemMessage/setSystemPrompt for replacing the system message post-construction instead of only at build time.
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@ plugins {
|
||||
id 'java-library'
|
||||
}
|
||||
|
||||
version = '1.7'
|
||||
version = '1.8.13'
|
||||
|
||||
dependencies {
|
||||
implementation project(":Plugin-API")
|
||||
|
||||
@@ -0,0 +1,593 @@
|
||||
package me.neurodock.core;
|
||||
|
||||
import com.sun.jdi.connect.spi.TransportService;
|
||||
import me.neurodock.ollama.*;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
//# Identity
|
||||
//You are [name], [role/purpose].
|
||||
//
|
||||
//# Context
|
||||
//[Static facts it needs — what NeuroDock is, what world/game state means, etc.]
|
||||
//
|
||||
//# Capabilities / Tools
|
||||
//[What tools it has access to and when to use them]
|
||||
//
|
||||
//# Behavior rules
|
||||
//- [Constraints: tone, length, what not to do]
|
||||
//- [Formatting requirements]
|
||||
//
|
||||
//# Output format
|
||||
//[Exact schema if you need structured output — JSON shape, etc.]
|
||||
|
||||
/**
|
||||
* Compiles a structured system prompt for an LLM out of discrete, reusable pieces —
|
||||
* identity, static context, tool-usage capabilities, behavior rules, and output format —
|
||||
* instead of hand-writing and concatenating prompt strings inline.
|
||||
* <p>
|
||||
* Each populated section is wrapped in its own XML-style tag
|
||||
* ({@code <Identity>}, {@code <Context>}, {@code <Capabilities>}, {@code <Behavior>},
|
||||
* {@code <Output>}) and only included in the final prompt if it was actually set — see
|
||||
* {@link #generateSystemPrompt()}.
|
||||
* <p>
|
||||
* Instances are assembled via {@link #builder()}. Callers who need capability routing
|
||||
* hints compiled from a live set of registered tools must call
|
||||
* {@link #generateCapabilities(OllamaObject)} before {@link #generateSystemPrompt()};
|
||||
* see that method's docs for why this is a separate step rather than being folded into
|
||||
* construction.
|
||||
*
|
||||
* @see Builder
|
||||
* @see Capabilities
|
||||
*/
|
||||
public class LLMSystemPrompt {
|
||||
/**
|
||||
* The persona/role block for this prompt, or {@code null} if not set. See {@link Identity}.
|
||||
*/
|
||||
private Identity identity;
|
||||
/**
|
||||
* The raw, pre-built {@code <Context>} body for this prompt (static facts the model
|
||||
* needs), or {@code null} if not set. Typically produced via {@link Context#build()}.
|
||||
*/
|
||||
private String context;
|
||||
/**
|
||||
* Usage-hint definitions for registered tools, or {@code null} if this prompt doesn't
|
||||
* compile a capabilities section. This holds the *rules* for what to say about each
|
||||
* tool; the actual compiled text lives in {@link #compiledCapabilities} and is only
|
||||
* populated once {@link #generateCapabilities(OllamaObject)} has been called.
|
||||
*/
|
||||
private Capabilities capabilities;
|
||||
/**
|
||||
* The compiled, ready-to-render {@code <Capabilities>} body, built by
|
||||
* {@link #generateCapabilities(OllamaObject)} from {@link #capabilities} and a live
|
||||
* set of registered tools. {@code null} until that method has been called at least
|
||||
* once, or if {@link #capabilities} was never set.
|
||||
*/
|
||||
private String compiledCapabilities;
|
||||
/**
|
||||
* The raw, pre-built {@code <Behavior>} body for this prompt (tone, constraints,
|
||||
* formatting rules), or {@code null} if not set. Usually populated via
|
||||
* {@link Builder#loadBehaviorFromFile(String)}.
|
||||
*/
|
||||
private String behavior;
|
||||
/**
|
||||
* The output-format specification for this prompt (e.g. a JSON schema the model
|
||||
* should reply in), or {@code null} if the model should respond in free text.
|
||||
*/
|
||||
private OutputFormat outputFormat;
|
||||
|
||||
/**
|
||||
* Constructs a system prompt from its component parts. Prefer {@link #builder()}
|
||||
* over calling this directly.
|
||||
*
|
||||
* @param identity the persona/role block, or {@code null} to omit it
|
||||
* @param context the pre-built static-context body, or {@code null} to omit it
|
||||
* @param capabilities the tool usage-hint definitions, or {@code null} to omit the
|
||||
* capabilities section entirely
|
||||
* @param behavior the pre-built behavior/constraints body, or {@code null} to omit it
|
||||
* @param outputFormat the output-format specification, or {@code null} for free-text output
|
||||
*/
|
||||
public LLMSystemPrompt(Identity identity, String context, Capabilities capabilities, String behavior, OutputFormat outputFormat) {
|
||||
this.identity = identity;
|
||||
this.context = context;
|
||||
this.capabilities = capabilities;
|
||||
this.behavior = behavior;
|
||||
this.outputFormat = outputFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles the {@code <Capabilities>} section from the tools currently registered on
|
||||
* the given {@link OllamaObject}, using the usage hints defined in {@link #capabilities}.
|
||||
* <p>
|
||||
* This is intentionally a separate step from construction rather than something the
|
||||
* constructor does automatically: which tools are registered on an {@link OllamaObject}
|
||||
* can change after this prompt is built (tools added/removed at runtime), so the
|
||||
* compiled capabilities text needs to be regenerated on demand against whatever the
|
||||
* current tool set actually is, not frozen at construction time.
|
||||
* <p>
|
||||
* For each registered {@link OllamaFunctionTool}, this looks up a usage hint by the
|
||||
* tool's {@link Class} via {@link Capabilities#getUsageHints()}. If no hint was
|
||||
* registered for that class, the tool is silently omitted from the compiled output —
|
||||
* there is no fallback to {@link OllamaFunctionTool#description()}, since that text
|
||||
* is written to help the model choose a tool mid-conversation, not to explain routing
|
||||
* up front in a system prompt; conflating the two would blur what each is for.
|
||||
* <p>
|
||||
* If {@link #capabilities} is {@code null} (this prompt doesn't use a capabilities
|
||||
* section at all), this clears {@link #compiledCapabilities} to {@code null} and
|
||||
* returns without inspecting {@code ollamaObject}.
|
||||
*
|
||||
* @param ollamaObject the object whose currently-registered tools should be inspected
|
||||
*/
|
||||
public void generateCapabilities(OllamaObject ollamaObject) {
|
||||
if(capabilities == null) {
|
||||
compiledCapabilities = null;
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
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());
|
||||
|
||||
// fall back to description() if no explicit routing hint was set
|
||||
String line = hint != null ? hint : funcTool.description();
|
||||
if (line == null) continue; // nothing usable to say about this tool, skip it
|
||||
|
||||
builder.append(llmName).append(": ").append(line).append(System.lineSeparator());
|
||||
}
|
||||
}
|
||||
compiledCapabilities = builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders this prompt's currently-set sections into a single system-role
|
||||
* {@link OllamaMessage}, ready to be sent to Ollama.
|
||||
* <p>
|
||||
* Each section — {@link #identity}, {@link #context}, {@link #compiledCapabilities},
|
||||
* {@link #behavior}, {@link #outputFormat} — is wrapped in its own XML-style tag and
|
||||
* appended in that fixed order. A section is included only if it is non-null (and, for
|
||||
* capabilities, non-blank); sections that were never set or never compiled are simply
|
||||
* skipped rather than emitted as empty tags.
|
||||
* <p>
|
||||
* Note that {@link #compiledCapabilities} reflects whatever tool set was live the last
|
||||
* time {@link #generateCapabilities(OllamaObject)} was called — call that first if the
|
||||
* registered tools may have changed since.
|
||||
*
|
||||
* @return a new {@link OllamaMessage} with role {@link OllamaMessageRole#SYSTEM}
|
||||
* containing the compiled prompt text
|
||||
*/
|
||||
public OllamaMessage generateSystemPrompt() {
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
if(identity != null) {
|
||||
builder.append("<Identity>").append(System.lineSeparator()).append(identity.build()).append(System.lineSeparator()).append("</Identity>").append(System.lineSeparator());
|
||||
}
|
||||
if(context != null) {
|
||||
builder.append("<Context>").append(System.lineSeparator()).append(context).append(System.lineSeparator()).append("</Context>").append(System.lineSeparator());
|
||||
}
|
||||
if (compiledCapabilities != null && !compiledCapabilities.isBlank()) {
|
||||
builder.append("<Capabilities>").append(System.lineSeparator())
|
||||
.append(compiledCapabilities)
|
||||
.append(System.lineSeparator()).append("</Capabilities>").append(System.lineSeparator());
|
||||
}
|
||||
if(behavior != null) {
|
||||
builder.append("<Behavior>").append(System.lineSeparator()).append(behavior).append(System.lineSeparator()).append("</Behavior>").append(System.lineSeparator());
|
||||
}
|
||||
if(outputFormat != null) {
|
||||
builder.append("<Output>").append(System.lineSeparator()).append(outputFormat.build()).append(System.lineSeparator()).append("</Output>").append(System.lineSeparator());
|
||||
}
|
||||
|
||||
return new OllamaMessage(OllamaMessageRole.SYSTEM, builder.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Builder} for assembling an {@link LLMSystemPrompt}.
|
||||
*
|
||||
* @return a fresh, empty {@link Builder}
|
||||
*/
|
||||
public static Builder builder()
|
||||
{
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* The persona/role block of a system prompt — who the model is and what it's for.
|
||||
* <p>
|
||||
* At least one of {@code name}, {@code role}, or {@code purpose} must be non-blank;
|
||||
* {@link #build()} throws if all three are absent, since an empty identity block is
|
||||
* almost certainly a mistake rather than an intentional choice.
|
||||
*
|
||||
* @param name the model's given name (e.g. {@code "Alice"}), or {@code null}/blank to omit
|
||||
* @param role the model's role (e.g. {@code "Personal assistant"}), or {@code null}/blank to omit
|
||||
* @param purpose the model's purpose/goal, or {@code null}/blank to omit
|
||||
*/
|
||||
public record Identity(String name, String role, String purpose) {
|
||||
/**
|
||||
* Builds the identity sentence from whichever of {@link #name}, {@link #role}, and
|
||||
* {@link #purpose} are present, joining them with commas.
|
||||
*
|
||||
* @return the composed identity sentence, e.g.
|
||||
* {@code "You are Alice, Your role is Personal assistant"}
|
||||
* @throws IllegalArgumentException if {@link #name}, {@link #role}, and
|
||||
* {@link #purpose} are all {@code null} or blank
|
||||
*/
|
||||
public String build()
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if(name != null && !name.isBlank())
|
||||
{
|
||||
builder.append("You are ").append(name);
|
||||
}
|
||||
if(role != null && !role.isBlank())
|
||||
{
|
||||
if(!builder.isEmpty())
|
||||
{
|
||||
builder.append(", ");
|
||||
}
|
||||
builder.append("Your role is ").append(role);
|
||||
}
|
||||
if(purpose != null && !purpose.isBlank())
|
||||
{
|
||||
if(!builder.isEmpty())
|
||||
{
|
||||
builder.append(", ");
|
||||
}
|
||||
builder.append("Your purpose is ").append(purpose);
|
||||
}
|
||||
if(!builder.isEmpty())
|
||||
return builder.toString();
|
||||
throw new IllegalArgumentException("Invalid identity");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for the static-facts {@code <Context>} section of a system prompt —
|
||||
* things the model should always know (e.g. "Always answer in English").
|
||||
* <p>
|
||||
* Facts are appended in the order they're added and joined with newlines by
|
||||
* {@link #build()}. The resulting string is what gets passed to
|
||||
* {@link Builder#context(String)}.
|
||||
*/
|
||||
public static class Context {
|
||||
/**
|
||||
* The ordered list of context facts added so far.
|
||||
*/
|
||||
ArrayList<String> facts = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Appends a single fact to this context block.
|
||||
*
|
||||
* @param fact the fact text to add
|
||||
* @return this {@link Context}, for chaining
|
||||
*/
|
||||
public Context addFact(String fact) {
|
||||
facts.add(fact);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the facts added to this context block so far, in insertion order.
|
||||
*
|
||||
* @return the live list of facts backing this {@link Context}
|
||||
*/
|
||||
public ArrayList<String> getFacts() {
|
||||
return facts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins all added facts into a single newline-separated string.
|
||||
*
|
||||
* @return the compiled context body, or an empty string if no facts were added
|
||||
*/
|
||||
public String build() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for(String fact: facts) {
|
||||
if(!builder.isEmpty()) builder.append("\n");
|
||||
builder.append(fact);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines per-tool usage-routing hints for the {@code <Capabilities>} section of a
|
||||
* system prompt — i.e. "use this tool only in these cases" guidance, distinct from a
|
||||
* tool's own {@link OllamaFunctionTool#description()}.
|
||||
* <p>
|
||||
* Hints are keyed by the tool's {@link Class} rather than by a registered instance or
|
||||
* its LLM-facing name, since NeuroDock's convention is one instance per tool class —
|
||||
* see {@link #use(Class, String)}. This class only holds the hint definitions; the
|
||||
* actual compiled text is produced by
|
||||
* {@link LLMSystemPrompt#generateCapabilities(OllamaObject)}.
|
||||
*/
|
||||
public static class Capabilities {
|
||||
/**
|
||||
* Usage hints keyed by tool class, in the order they were registered.
|
||||
*/
|
||||
private final LinkedHashMap<Class<? extends OllamaFunctionTool>, String> usageHints = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* Registers a usage-routing hint for a tool class — guidance on when the model
|
||||
* should reach for this tool, e.g. {@code "This should only be used when needing
|
||||
* to run CLI tools"}.
|
||||
* <p>
|
||||
* Keyed by class rather than instance because a tool's LLM-facing name (which
|
||||
* depends on its registration source) isn't known until it's actually registered
|
||||
* on an {@link OllamaObject}; class identity is stable and known up front.
|
||||
*
|
||||
* @param toolClass the tool class this hint applies to
|
||||
* @param usageHint the routing guidance text for this tool
|
||||
* @return this {@link Capabilities}, for chaining
|
||||
*/
|
||||
public Capabilities use(Class<? extends OllamaFunctionTool> toolClass, String usageHint) {
|
||||
usageHints.put(toolClass, usageHint);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the usage hints registered so far, keyed by tool class.
|
||||
*
|
||||
* @return the live map backing this {@link Capabilities}
|
||||
*/
|
||||
Map<Class<? extends OllamaFunctionTool>, String> getUsageHints() {
|
||||
return usageHints;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The behavior/constraints section of a system prompt — tone, formatting rules, and
|
||||
* other "how to behave" instructions.
|
||||
* <p>
|
||||
* <b>Not yet implemented</b> — currently unused; {@link LLMSystemPrompt#behavior} is
|
||||
* populated directly as a {@link String}, typically via
|
||||
* {@link Builder#loadBehaviorFromFile(String)}, rather than through this class.
|
||||
*/
|
||||
public static class Behavior {
|
||||
/**
|
||||
* Structured behavioral constraints. Not yet implemented.
|
||||
*/
|
||||
Constraints constraints;
|
||||
/**
|
||||
* Output formatting rules. Not yet implemented.
|
||||
*/
|
||||
String formating;
|
||||
|
||||
// TODO: Implement this
|
||||
|
||||
/**
|
||||
* Structured constraints for a {@link Behavior} block (tone, length limits, things
|
||||
* to avoid, etc.).
|
||||
* <p>
|
||||
* <b>Not yet implemented.</b>
|
||||
*/
|
||||
public static class Constraints
|
||||
{
|
||||
// TODO: Implement this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the expected output format for a system prompt's {@code <Output>} section
|
||||
* — e.g. a JSON schema the model should structure its replies as.
|
||||
*/
|
||||
public static class OutputFormat {
|
||||
/**
|
||||
* The raw format specification, rendered verbatim in the {@code <Output>} section.
|
||||
*/
|
||||
String format;
|
||||
|
||||
/**
|
||||
* Creates an output format from a raw, pre-formatted string.
|
||||
*
|
||||
* @param format the format specification text
|
||||
*/
|
||||
public OutputFormat(String format) {
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an output format from a JSON schema.
|
||||
*
|
||||
* @param format the JSON schema to use as the format specification
|
||||
*/
|
||||
public OutputFormat(JSONObject format) {
|
||||
this.format = format.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the format specification text for rendering into the prompt.
|
||||
*
|
||||
* @return the raw format string
|
||||
*/
|
||||
public String build () {
|
||||
return format;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for assembling an {@link LLMSystemPrompt} from its component sections.
|
||||
* Obtain an instance via {@link LLMSystemPrompt#builder()}.
|
||||
*/
|
||||
public static class Builder {
|
||||
/**
|
||||
* The identity block to use, or {@code null} to omit it. Set via {@link #identity(Identity)}.
|
||||
*/
|
||||
private Identity identity;
|
||||
/**
|
||||
* The pre-built context body to use, or {@code null} to omit it. Set via {@link #context(String)}.
|
||||
*/
|
||||
private String context;
|
||||
/**
|
||||
* The capabilities/usage-hint definitions to use, or {@code null} to omit the
|
||||
* capabilities section. Set via {@link #capabilities(Capabilities)}.
|
||||
*/
|
||||
private Capabilities capabilities;
|
||||
/**
|
||||
* The pre-built behavior body to use, or {@code null} to omit it. Set via
|
||||
* {@link #behavior(String)} or loaded from disk via {@link #loadBehaviorFromFile(String)}.
|
||||
*/
|
||||
private String behavior;
|
||||
/**
|
||||
* The output format specification to use, or {@code null} for free-text output.
|
||||
* Set via {@link #outputFormat(OutputFormat)}.
|
||||
*/
|
||||
private OutputFormat outputFormat;
|
||||
|
||||
/**
|
||||
* Loads {@link #behavior} from a text file, resolved in order against:
|
||||
* <ol>
|
||||
* <li>the classpath</li>
|
||||
* <li>{@link Core#DATA_DIR}</li>
|
||||
* <li>the current runtime/working directory</li>
|
||||
* <li>an exact file path match</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param path the file to look for, resolved as described above
|
||||
* @return this {@link Builder}, for chaining
|
||||
* @throws IllegalStateException if no file could be found at any of the candidate
|
||||
* locations, or if it exists but cannot be read
|
||||
*/
|
||||
public Builder loadBehaviorFromFile(String path) {
|
||||
behavior = BehaviorLoader.load(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the identity/persona block for the prompt being built.
|
||||
*
|
||||
* @param identity the identity to use
|
||||
* @return this {@link Builder}, for chaining
|
||||
*/
|
||||
public Builder identity(Identity identity) {
|
||||
this.identity = identity;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the pre-built static-context body for the prompt being built.
|
||||
*
|
||||
* @param context the context body, typically produced via {@link Context#build()}
|
||||
* @return this {@link Builder}, for chaining
|
||||
*/
|
||||
public Builder context(String context) {
|
||||
this.context = context;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the capability usage-hint definitions for the prompt being built. These are
|
||||
* only compiled into prompt text once
|
||||
* {@link LLMSystemPrompt#generateCapabilities(OllamaObject)} is called on the built
|
||||
* instance.
|
||||
*
|
||||
* @param capabilities the usage-hint definitions to use
|
||||
* @return this {@link Builder}, for chaining
|
||||
*/
|
||||
public Builder capabilities(Capabilities capabilities) {
|
||||
this.capabilities = capabilities;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the pre-built behavior body for the prompt being built directly, bypassing
|
||||
* {@link #loadBehaviorFromFile(String)}.
|
||||
*
|
||||
* @param behavior the behavior/constraints text to use
|
||||
* @return this {@link Builder}, for chaining
|
||||
*/
|
||||
public Builder behavior(String behavior) {
|
||||
this.behavior = behavior;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the output format specification for the prompt being built.
|
||||
*
|
||||
* @param outputFormat the output format to use
|
||||
* @return this {@link Builder}, for chaining
|
||||
*/
|
||||
public Builder outputFormat(OutputFormat outputFormat) {
|
||||
this.outputFormat = outputFormat;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the {@link LLMSystemPrompt} from the sections configured on this builder.
|
||||
*
|
||||
* @return a new {@link LLMSystemPrompt}
|
||||
*/
|
||||
public LLMSystemPrompt build() {
|
||||
return new LLMSystemPrompt(identity, context, capabilities, behavior, outputFormat);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper for resolving and reading a behavior file from one of several
|
||||
* candidate locations. See {@link Builder#loadBehaviorFromFile(String)}.
|
||||
*/
|
||||
private static class BehaviorLoader {
|
||||
/**
|
||||
* Checks whether a classpath resource exists at the given path without leaving it open.
|
||||
*
|
||||
* @param path the classpath-relative resource path to check
|
||||
* @return {@code true} if a resource stream could be opened at that path
|
||||
*/
|
||||
private static boolean classpathResourceExists(String path) {
|
||||
try (InputStream is = BehaviorLoader.class.getResourceAsStream(path)) {
|
||||
return is != null;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves and reads a behavior file as UTF-8 text, checking the classpath,
|
||||
* {@link Core#DATA_DIR}, the working directory, and an exact path match, in that order.
|
||||
*
|
||||
* @param path the file to look for
|
||||
* @return the full file contents, with each line terminated by {@code \n}
|
||||
* @throws IllegalStateException if the resource cannot be found at any candidate
|
||||
* location, or if it exists but cannot be read
|
||||
*/
|
||||
public static String load(String path) {
|
||||
try {
|
||||
InputStream in;
|
||||
if (classpathResourceExists("/"+path)) {
|
||||
in = BehaviorLoader.class.getResourceAsStream("/"+path);
|
||||
} else {
|
||||
Path dataDirPath = Path.of(Core.DATA_DIR.getPath(), path);
|
||||
Path relativePath = Path.of(path);
|
||||
if (Files.exists(dataDirPath)) {
|
||||
in = new FileInputStream(dataDirPath.toFile());
|
||||
} else if (Files.exists(relativePath)) {
|
||||
in = new FileInputStream(relativePath.toFile());
|
||||
} else {
|
||||
throw new IllegalStateException("Could not find resource " + path);
|
||||
}
|
||||
}
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line).append('\n'); // see note below
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Could not load resource " + path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,15 @@ public abstract class OllamaFunctionTool implements OllamaTool {
|
||||
*/
|
||||
protected String source = "";
|
||||
|
||||
/**
|
||||
* returns the source of this tool
|
||||
* @return String source
|
||||
*/
|
||||
public final String getSource()
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
JSONObject ret = new JSONObject();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package me.neurodock.ollama;
|
||||
|
||||
import me.neurodock.core.Core;
|
||||
import me.neurodock.core.LLMSystemPrompt;
|
||||
import me.neurodock.core.LaunchOptions;
|
||||
import me.neurodock.core.Pair;
|
||||
import me.neurodock.core.files.FileHandlerLocation;
|
||||
@@ -66,7 +67,7 @@ public class OllamaObject {
|
||||
* @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}
|
||||
*/
|
||||
private OllamaObject(String model, ArrayList<OllamaMessage> messages, ArrayList<Pair<OllamaTool, String>> tools, JSONObject format, Map<String, Object> options, boolean stream, String keep_alive) {
|
||||
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) {
|
||||
this.model = model;
|
||||
this.messages = messages;
|
||||
|
||||
@@ -121,7 +122,7 @@ public class OllamaObject {
|
||||
} else {
|
||||
message = new OllamaMessageToolCall(OllamaMessageRole.fromRole(obj.getString("role")), obj.getString("content"), obj.getJSONArray("tool_calls"));
|
||||
}
|
||||
messages.add(message);
|
||||
this.messages.add(message);
|
||||
}
|
||||
}catch (Exception e) {
|
||||
System.out.println("Error loading old data");
|
||||
@@ -133,6 +134,36 @@ public class OllamaObject {
|
||||
System.out.println("No old data found, skipping loading old data.");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
System.out.println("No old prompts. Inserting system prompt...");
|
||||
Core.writeLog("No old prompts. Inserting system prompt...");
|
||||
this.messages.add(prompt.generateSystemPrompt());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,13 +284,31 @@ public class OllamaObject {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a system message to the messages.
|
||||
* <p>
|
||||
* Equvalent to {@code OllamaObject#addMessage(new OllamaMessage(OllamaMessageRole.SYSTEM, <message>))}
|
||||
* @param message
|
||||
* Sets the system message, replacing any existing one at the start of {@link #messages},
|
||||
* or inserting one at the start if none exists yet.
|
||||
* @param message the system message text
|
||||
*/
|
||||
public void addSystemMessage(String message) {
|
||||
messages.add(new OllamaMessage(OllamaMessageRole.SYSTEM, message));
|
||||
public void setSystemMessage(String message) {
|
||||
applySystemPrompt(new OllamaMessage(OllamaMessageRole.SYSTEM, message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the current system prompt if one exists at the start of {@link #messages},
|
||||
* or inserts one at the start if none exists yet.
|
||||
*/
|
||||
public void setSystemPrompt(LLMSystemPrompt prompt) {
|
||||
applySystemPrompt(prompt.generateSystemPrompt()); // shared with the constructor
|
||||
}
|
||||
|
||||
private void applySystemPrompt(OllamaMessage message) {
|
||||
if (message == null) return;
|
||||
if (messages.isEmpty()) {
|
||||
messages.add(message);
|
||||
} else if (messages.getFirst().getRole() != OllamaMessageRole.SYSTEM) {
|
||||
messages.addFirst(message);
|
||||
} else {
|
||||
messages.set(0, message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -330,6 +379,10 @@ public class OllamaObject {
|
||||
* The keep alive of the Ollama Object.
|
||||
*/
|
||||
String keep_alive;
|
||||
/**
|
||||
* This represents the System Prompt for the LLM
|
||||
*/
|
||||
LLMSystemPrompt systemPrompt;
|
||||
|
||||
/**
|
||||
* Creates a new instance of {@link OllamaObjectBuilder}.
|
||||
@@ -521,13 +574,19 @@ public class OllamaObject {
|
||||
//throw new IllegalArgumentException("FileHandler is not supported yet!");
|
||||
return addTools(FileHandler.getTools());
|
||||
}
|
||||
|
||||
|
||||
|
||||
public OllamaObjectBuilder setSystemPrompt(LLMSystemPrompt systemPrompt) {
|
||||
this.systemPrompt = systemPrompt;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the {@link OllamaObject}
|
||||
* @return The {@link OllamaObject}
|
||||
*/
|
||||
public OllamaObject build() {
|
||||
return new OllamaObject(model, messages, tools, format, options, stream, keep_alive);
|
||||
return new OllamaObject(model, messages, tools, format, options, stream, keep_alive, systemPrompt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user