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'
|
id 'java-library'
|
||||||
}
|
}
|
||||||
|
|
||||||
version = '1.7'
|
version = '1.8.13'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":Plugin-API")
|
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 = "";
|
protected String source = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the source of this tool
|
||||||
|
* @return String source
|
||||||
|
*/
|
||||||
|
public final String getSource()
|
||||||
|
{
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
JSONObject ret = new JSONObject();
|
JSONObject ret = new JSONObject();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package me.neurodock.ollama;
|
package me.neurodock.ollama;
|
||||||
|
|
||||||
import me.neurodock.core.Core;
|
import me.neurodock.core.Core;
|
||||||
|
import me.neurodock.core.LLMSystemPrompt;
|
||||||
import me.neurodock.core.LaunchOptions;
|
import me.neurodock.core.LaunchOptions;
|
||||||
import me.neurodock.core.Pair;
|
import me.neurodock.core.Pair;
|
||||||
import me.neurodock.core.files.FileHandlerLocation;
|
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 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 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.model = model;
|
||||||
this.messages = messages;
|
this.messages = messages;
|
||||||
|
|
||||||
@@ -121,7 +122,7 @@ public class OllamaObject {
|
|||||||
} else {
|
} else {
|
||||||
message = new OllamaMessageToolCall(OllamaMessageRole.fromRole(obj.getString("role")), obj.getString("content"), obj.getJSONArray("tool_calls"));
|
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) {
|
}catch (Exception e) {
|
||||||
System.out.println("Error loading old data");
|
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.");
|
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.
|
* Sets the system message, replacing any existing one at the start of {@link #messages},
|
||||||
* <p>
|
* or inserting one at the start if none exists yet.
|
||||||
* Equvalent to {@code OllamaObject#addMessage(new OllamaMessage(OllamaMessageRole.SYSTEM, <message>))}
|
* @param message the system message text
|
||||||
* @param message
|
|
||||||
*/
|
*/
|
||||||
public void addSystemMessage(String message) {
|
public void setSystemMessage(String message) {
|
||||||
messages.add(new OllamaMessage(OllamaMessageRole.SYSTEM, 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
|
@Override
|
||||||
@@ -330,6 +379,10 @@ public class OllamaObject {
|
|||||||
* The keep alive of the Ollama Object.
|
* The keep alive of the Ollama Object.
|
||||||
*/
|
*/
|
||||||
String keep_alive;
|
String keep_alive;
|
||||||
|
/**
|
||||||
|
* This represents the System Prompt for the LLM
|
||||||
|
*/
|
||||||
|
LLMSystemPrompt systemPrompt;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance of {@link OllamaObjectBuilder}.
|
* Creates a new instance of {@link OllamaObjectBuilder}.
|
||||||
@@ -522,12 +575,18 @@ public class OllamaObject {
|
|||||||
return addTools(FileHandler.getTools());
|
return addTools(FileHandler.getTools());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public OllamaObjectBuilder setSystemPrompt(LLMSystemPrompt systemPrompt) {
|
||||||
|
this.systemPrompt = systemPrompt;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the {@link OllamaObject}
|
* Builds the {@link OllamaObject}
|
||||||
* @return The {@link OllamaObject}
|
* @return The {@link OllamaObject}
|
||||||
*/
|
*/
|
||||||
public OllamaObject build() {
|
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