diff --git a/pom.xml b/pom.xml index d05cf48..e7c70b6 100644 --- a/pom.xml +++ b/pom.xml @@ -260,11 +260,10 @@ 2.20.0 - tools.jackson.dataformat + com.fasterxml.jackson.dataformat jackson-dataformat-yaml - 3.0.0 + 2.20.0 - com.fasterxml.jackson.datatype jackson-datatype-jsr310 @@ -281,7 +280,6 @@ slf4j-api 2.0.17 - org.junit.jupiter junit-jupiter-api @@ -300,7 +298,6 @@ 20250517 test - org.testcontainers ollama @@ -313,14 +310,12 @@ 1.21.3 test - io.prometheus simpleclient 0.16.0 - com.google.guava guava diff --git a/src/main/java/io/github/ollama4j/agent/Agent.java b/src/main/java/io/github/ollama4j/agent/Agent.java index f7d82be..4e3ac57 100644 --- a/src/main/java/io/github/ollama4j/agent/Agent.java +++ b/src/main/java/io/github/ollama4j/agent/Agent.java @@ -8,6 +8,8 @@ */ package io.github.ollama4j.agent; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import io.github.ollama4j.Ollama; import io.github.ollama4j.exceptions.OllamaException; import io.github.ollama4j.impl.ConsoleOutputGenerateTokenHandler; @@ -19,16 +21,50 @@ import java.util.ArrayList; import java.util.List; import java.util.Scanner; import lombok.*; -import tools.jackson.dataformat.yaml.YAMLMapper; +/** + * The {@code Agent} class represents an AI assistant capable of interacting with the Ollama API + * server. + * + *

It supports the use of tools (interchangeable code components), persistent chat history, and + * interactive as well as pre-scripted chat sessions. + * + *

Usage

+ * + * + */ public class Agent { + /** The agent's display name */ private final String name; + + /** List of supported tools for this agent */ private final List tools; + + /** Ollama client instance for communication with the API */ private final Ollama ollamaClient; + + /** The model name used for chat completions */ private final String model; + + /** Persists chat message history across rounds */ private final List chatHistory; + + /** Optional custom system prompt for the agent */ private final String customPrompt; + /** + * Constructs a new Agent. + * + * @param name The agent's given name. + * @param ollamaClient The Ollama API client instance to use. + * @param model The model name to use for chat completion. + * @param customPrompt A custom prompt to prepend to all conversations (may be null). + * @param tools List of available tools for function calling. + */ public Agent( String name, Ollama ollamaClient, @@ -43,17 +79,29 @@ public class Agent { this.customPrompt = customPrompt; } - public static Agent fromYaml(String agentYaml) { + /** + * Loads and constructs an Agent from a YAML configuration file (classpath or filesystem). + * + *

The YAML should define the agent, the model, and the desired tool functions (using their + * fully qualified class names for auto-discovery). + * + * @param yamlPathOrResource Path or classpath resource name of the YAML file. + * @return New Agent instance loaded according to the YAML definition. + * @throws RuntimeException if the YAML cannot be read or agent cannot be constructed. + */ + public static Agent load(String yamlPathOrResource) { try { - YAMLMapper mapper = new YAMLMapper(); - InputStream input = Agent.class.getClassLoader().getResourceAsStream(agentYaml); + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + + InputStream input = + Agent.class.getClassLoader().getResourceAsStream(yamlPathOrResource); if (input == null) { - java.nio.file.Path filePath = java.nio.file.Paths.get(agentYaml); + java.nio.file.Path filePath = java.nio.file.Paths.get(yamlPathOrResource); if (java.nio.file.Files.exists(filePath)) { input = java.nio.file.Files.newInputStream(filePath); } else { throw new RuntimeException( - agentYaml + " not found in classpath or file system"); + yamlPathOrResource + " not found in classpath or file system"); } } AgentSpec agentSpec = mapper.readValue(input, AgentSpec.class); @@ -100,61 +148,105 @@ public class Agent { } } - public String think(String userInput) throws OllamaException { - StringBuilder availableToolsDescription = new StringBuilder(); - if (!tools.isEmpty()) { - for (Tools.Tool t : tools) { - String toolName = t.getToolSpec().getName(); - String toolDescription = t.getToolSpec().getDescription(); - availableToolsDescription.append( - "\nTool name: '" - + toolName - + "'. Tool Description: '" - + toolDescription - + "'.\n"); - } - } + /** + * Facilitates a single round of chat for the agent: + * + *

    + *
  • Builds/promotes the system prompt on the first turn if necessary + *
  • Adds the user's input to chat history + *
  • Submits the chat turn to the Ollama model (with tool/function support) + *
  • Updates internal chat history in accordance with the Ollama chat result + *
+ * + * @param userInput The user's message or question for the agent. + * @return The model's response as a string. + * @throws OllamaException If there is a problem with the Ollama API. + */ + public String interact(String userInput) throws OllamaException { + // Build a concise and readable description of available tools + String availableToolsDescription = + tools.isEmpty() + ? "" + : tools.stream() + .map( + t -> + String.format( + "- %s: %s", + t.getToolSpec().getName(), + t.getToolSpec().getDescription() != null + ? t.getToolSpec().getDescription() + : "No description")) + .reduce((a, b) -> a + "\n" + b) + .map(desc -> "\nYou have access to the following tools:\n" + desc) + .orElse(""); + + // Add system prompt if chatHistory is empty if (chatHistory.isEmpty()) { - chatHistory.add( - new OllamaChatMessage( - OllamaChatMessageRole.SYSTEM, - "You are a helpful assistant named " - + name - + ". You only perform tasks using tools available for you. " - + customPrompt - + ". Following are the tools that you have access to and" - + " you can perform right actions using right tools." - + availableToolsDescription)); + String systemPrompt = + String.format( + "You are a helpful AI assistant named %s. Your actions are limited to" + + " using the available tools. %s%s", + name, + (customPrompt != null ? customPrompt : ""), + availableToolsDescription); + chatHistory.add(new OllamaChatMessage(OllamaChatMessageRole.SYSTEM, systemPrompt)); } + + // Add the user input as a message before sending request + chatHistory.add(new OllamaChatMessage(OllamaChatMessageRole.USER, userInput)); + OllamaChatRequest request = OllamaChatRequest.builder() .withTools(tools) .withUseTools(true) .withModel(model) .withMessages(chatHistory) - .withMessage(OllamaChatMessageRole.USER, userInput) .build(); - request.withMessage(OllamaChatMessageRole.USER, userInput); + OllamaChatStreamObserver chatTokenHandler = new OllamaChatStreamObserver( new ConsoleOutputGenerateTokenHandler(), new ConsoleOutputGenerateTokenHandler()); OllamaChatResult response = ollamaClient.chat(request, chatTokenHandler); + + // Update chat history for continuity chatHistory.clear(); chatHistory.addAll(response.getChatHistory()); + return response.getResponseModel().getMessage().getResponse(); } + /** + * Launches an endless interactive console session with the agent, echoing user input and the + * agent's response using the provided chat model and tools. + * + *

Type {@code exit} to break the loop and terminate the session. + * + * @throws OllamaException if any errors occur talking to the Ollama API. + */ public void runInteractive() throws OllamaException { Scanner sc = new Scanner(System.in); while (true) { System.out.print("\n[You]: "); String input = sc.nextLine(); if ("exit".equalsIgnoreCase(input)) break; - String response = this.think(input); + this.interact(input); } } + /** + * Bean describing an agent as definable from YAML. + * + *

    + *
  • {@code name}: Agent display name + *
  • {@code description}: Freeform description + *
  • {@code tools}: List of tools/functions to enable + *
  • {@code host}: Target Ollama host address + *
  • {@code model}: Name of Ollama model to use + *
  • {@code customPrompt}: Agent's custom base prompt + *
  • {@code requestTimeoutSeconds}: Timeout for requests + *
+ */ @Data public static class AgentSpec { private String name; @@ -166,19 +258,36 @@ public class Agent { private int requestTimeoutSeconds; } + /** + * Subclass extension of {@link Tools.ToolSpec}, which allows associating a tool with a function + * implementation (via FQCN). + */ @Data @Setter @Getter private static class AgentToolSpec extends Tools.ToolSpec { + /** Fully qualified class name of the tool's {@link ToolFunction} implementation */ private String toolFunctionFQCN = null; + + /** Instance of the {@link ToolFunction} to invoke */ private ToolFunction toolFunctionInstance = null; } + /** Bean for describing a tool function parameter for use in agent YAML definitions. */ @Data public class AgentToolParameter { + /** The parameter's type (e.g., string, number, etc.) */ private String type; + + /** Description of the parameter */ private String description; + + /** Whether this parameter is required */ private boolean required; + + /** + * Enum values (if any) that this parameter may take; _enum used because 'enum' is reserved + */ private List _enum; // `enum` is a reserved keyword, so use _enum or similar } } diff --git a/src/main/java/io/github/ollama4j/agent/SampleAgent.java b/src/main/java/io/github/ollama4j/agent/SampleAgent.java deleted file mode 100644 index bdce2a8..0000000 --- a/src/main/java/io/github/ollama4j/agent/SampleAgent.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Ollama4j - Java library for interacting with Ollama server. - * Copyright (c) 2025 Amith Koujalgi and contributors. - * - * Licensed under the MIT License (the "License"); - * you may not use this file except in compliance with the License. - * -*/ -package io.github.ollama4j.agent; - -import io.github.ollama4j.exceptions.OllamaException; -import io.github.ollama4j.tools.ToolFunction; -import java.util.Map; - -/** Example usage of the Agent API with some dummy tool functions. */ -public class SampleAgent { - public static void main(String[] args) throws OllamaException { - Agent agent = Agent.fromYaml("agent.yaml"); - agent.runInteractive(); - } -} - -/** ToolFunction implementation that returns a dummy weekly weather forecast. */ -class WeatherToolFunction implements ToolFunction { - @Override - public Object apply(Map arguments) { - String response = - "Monday: Pleasant." - + "Tuesday: Sunny." - + "Wednesday: Windy." - + "Thursday: Cloudy." - + "Friday: Rainy." - + "Saturday: Heavy rains." - + "Sunday: Clear."; - return response; - } -} - -/** ToolFunction implementation for basic arithmetic calculations. */ -class CalculatorToolFunction implements ToolFunction { - @Override - public Object apply(Map arguments) { - String operation = (String) arguments.get("operation"); - double a = ((Number) arguments.get("a")).doubleValue(); - double b = ((Number) arguments.get("b")).doubleValue(); - double result; - switch (operation.toLowerCase()) { - case "add": - result = a + b; - break; - case "subtract": - result = a - b; - break; - case "multiply": - result = a * b; - break; - case "divide": - if (b == 0) { - return "Cannot divide by zero."; - } - result = a / b; - break; - default: - return "Unknown operation: " + operation; - } - return "Result: " + result; - } -} - -/** ToolFunction implementation simulating a hotel booking. */ -class HotelBookingToolFunction implements ToolFunction { - @Override - public Object apply(Map arguments) { - String city = (String) arguments.get("city"); - String checkin = (String) arguments.get("checkin_date"); - String checkout = (String) arguments.get("checkout_date"); - int guests = ((Number) arguments.get("guests")).intValue(); - - // Dummy booking confirmation logic - return String.format( - "Booking confirmed! %d guest(s) in %s from %s to %s. (Confirmation #DUMMY1234)", - guests, city, checkin, checkout); - } -} diff --git a/src/main/java/io/github/ollama4j/tools/Tools.java b/src/main/java/io/github/ollama4j/tools/Tools.java index b1b4795..f7f1701 100644 --- a/src/main/java/io/github/ollama4j/tools/Tools.java +++ b/src/main/java/io/github/ollama4j/tools/Tools.java @@ -11,8 +11,10 @@ package io.github.ollama4j.tools; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import java.io.File; import java.util.ArrayList; import java.util.List; @@ -21,8 +23,6 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import tools.jackson.core.type.TypeReference; -import tools.jackson.dataformat.yaml.YAMLMapper; public class Tools { private Tools() {} @@ -150,7 +150,7 @@ public class Tools { public static List fromYAMLFile(String filePath, Map functionMap) { try { - YAMLMapper mapper = new YAMLMapper(); + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); List> rawTools = mapper.readValue(new File(filePath), new TypeReference<>() {}); List tools = new ArrayList<>(); diff --git a/src/main/resources/agent.yaml b/src/main/resources/agent.yaml deleted file mode 100644 index 5d3bb51..0000000 --- a/src/main/resources/agent.yaml +++ /dev/null @@ -1,46 +0,0 @@ -name: Nimma Mitra -host: http://192.168.29.224:11434 -model: mistral:7b -requestTimeoutSeconds: 120 -customPrompt: > - Only use tools and do not use your creativity. - Do not ever tell me to call the tool or how to use the tool or command myself. - You do that for me. That is why you exist. - You call tool on my behalf and give me a response from the tool. -tools: - - name: weather-tool - toolFunctionFQCN: io.github.ollama4j.agent.WeatherToolFunction - description: Gets the current weather for a given location and day. - parameters: - properties: - location: - type: string - description: The location for which to get the weather. - required: true - day: - type: string - description: The day of the week for which to get the weather. - required: true - - - name: calculator-tool - toolFunctionFQCN: io.github.ollama4j.agent.CalculatorToolFunction - description: Performs a simple arithmetic operation between two numbers. - parameters: - properties: - operation: - type: string - description: The arithmetic operation to perform. - enum: - - add - - subtract - - multiply - - divide - required: true - a: - type: number - description: The first operand. - required: true - b: - type: number - description: The second operand. - required: true