Remove SampleAgent class and associated YAML configuration file, streamlining the project by eliminating example implementations and their dependencies.

This commit is contained in:
amithkoujalgi 2025-10-19 11:22:16 +05:30
parent bec634dd37
commit 866c08f590
No known key found for this signature in database
GPG Key ID: E29A37746AF94B70
5 changed files with 147 additions and 173 deletions

View File

@ -260,11 +260,10 @@
<version>2.20.0</version> <version>2.20.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>tools.jackson.dataformat</groupId> <groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId> <artifactId>jackson-dataformat-yaml</artifactId>
<version>3.0.0</version> <version>2.20.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.datatype</groupId> <groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId> <artifactId>jackson-datatype-jsr310</artifactId>
@ -281,7 +280,6 @@
<artifactId>slf4j-api</artifactId> <artifactId>slf4j-api</artifactId>
<version>2.0.17</version> <version>2.0.17</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId> <artifactId>junit-jupiter-api</artifactId>
@ -300,7 +298,6 @@
<version>20250517</version> <version>20250517</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.testcontainers</groupId> <groupId>org.testcontainers</groupId>
<artifactId>ollama</artifactId> <artifactId>ollama</artifactId>
@ -313,14 +310,12 @@
<version>1.21.3</version> <version>1.21.3</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- Prometheus metrics dependencies --> <!-- Prometheus metrics dependencies -->
<dependency> <dependency>
<groupId>io.prometheus</groupId> <groupId>io.prometheus</groupId>
<artifactId>simpleclient</artifactId> <artifactId>simpleclient</artifactId>
<version>0.16.0</version> <version>0.16.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>
<artifactId>guava</artifactId> <artifactId>guava</artifactId>

View File

@ -8,6 +8,8 @@
*/ */
package io.github.ollama4j.agent; 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.Ollama;
import io.github.ollama4j.exceptions.OllamaException; import io.github.ollama4j.exceptions.OllamaException;
import io.github.ollama4j.impl.ConsoleOutputGenerateTokenHandler; import io.github.ollama4j.impl.ConsoleOutputGenerateTokenHandler;
@ -19,16 +21,50 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Scanner; import java.util.Scanner;
import lombok.*; import lombok.*;
import tools.jackson.dataformat.yaml.YAMLMapper;
/**
* The {@code Agent} class represents an AI assistant capable of interacting with the Ollama API
* server.
*
* <p>It supports the use of tools (interchangeable code components), persistent chat history, and
* interactive as well as pre-scripted chat sessions.
*
* <h2>Usage</h2>
*
* <ul>
* <li>Instantiate an Agent via {@link #fromYaml(String)} for YAML-based configuration.
* <li>Handle conversation turns via {@link #think(String)}.
* <li>Use {@link #runInteractive()} for an interactive console-based session.
* </ul>
*/
public class Agent { public class Agent {
/** The agent's display name */
private final String name; private final String name;
/** List of supported tools for this agent */
private final List<Tools.Tool> tools; private final List<Tools.Tool> tools;
/** Ollama client instance for communication with the API */
private final Ollama ollamaClient; private final Ollama ollamaClient;
/** The model name used for chat completions */
private final String model; private final String model;
/** Persists chat message history across rounds */
private final List<OllamaChatMessage> chatHistory; private final List<OllamaChatMessage> chatHistory;
/** Optional custom system prompt for the agent */
private final String customPrompt; 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( public Agent(
String name, String name,
Ollama ollamaClient, Ollama ollamaClient,
@ -43,17 +79,29 @@ public class Agent {
this.customPrompt = customPrompt; this.customPrompt = customPrompt;
} }
public static Agent fromYaml(String agentYaml) { /**
* Loads and constructs an Agent from a YAML configuration file (classpath or filesystem).
*
* <p>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 { try {
YAMLMapper mapper = new YAMLMapper(); ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
InputStream input = Agent.class.getClassLoader().getResourceAsStream(agentYaml);
InputStream input =
Agent.class.getClassLoader().getResourceAsStream(yamlPathOrResource);
if (input == null) { 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)) { if (java.nio.file.Files.exists(filePath)) {
input = java.nio.file.Files.newInputStream(filePath); input = java.nio.file.Files.newInputStream(filePath);
} else { } else {
throw new RuntimeException( 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); 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(); * Facilitates a single round of chat for the agent:
if (!tools.isEmpty()) { *
for (Tools.Tool t : tools) { * <ul>
String toolName = t.getToolSpec().getName(); * <li>Builds/promotes the system prompt on the first turn if necessary
String toolDescription = t.getToolSpec().getDescription(); * <li>Adds the user's input to chat history
availableToolsDescription.append( * <li>Submits the chat turn to the Ollama model (with tool/function support)
"\nTool name: '" * <li>Updates internal chat history in accordance with the Ollama chat result
+ toolName * </ul>
+ "'. Tool Description: '" *
+ toolDescription * @param userInput The user's message or question for the agent.
+ "'.\n"); * @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()) { if (chatHistory.isEmpty()) {
chatHistory.add( String systemPrompt =
new OllamaChatMessage( String.format(
OllamaChatMessageRole.SYSTEM, "You are a helpful AI assistant named %s. Your actions are limited to"
"You are a helpful assistant named " + " using the available tools. %s%s",
+ name name,
+ ". You only perform tasks using tools available for you. " (customPrompt != null ? customPrompt : ""),
+ customPrompt availableToolsDescription);
+ ". Following are the tools that you have access to and" chatHistory.add(new OllamaChatMessage(OllamaChatMessageRole.SYSTEM, systemPrompt));
+ " you can perform right actions using right tools."
+ availableToolsDescription));
} }
// Add the user input as a message before sending request
chatHistory.add(new OllamaChatMessage(OllamaChatMessageRole.USER, userInput));
OllamaChatRequest request = OllamaChatRequest request =
OllamaChatRequest.builder() OllamaChatRequest.builder()
.withTools(tools) .withTools(tools)
.withUseTools(true) .withUseTools(true)
.withModel(model) .withModel(model)
.withMessages(chatHistory) .withMessages(chatHistory)
.withMessage(OllamaChatMessageRole.USER, userInput)
.build(); .build();
request.withMessage(OllamaChatMessageRole.USER, userInput);
OllamaChatStreamObserver chatTokenHandler = OllamaChatStreamObserver chatTokenHandler =
new OllamaChatStreamObserver( new OllamaChatStreamObserver(
new ConsoleOutputGenerateTokenHandler(), new ConsoleOutputGenerateTokenHandler(),
new ConsoleOutputGenerateTokenHandler()); new ConsoleOutputGenerateTokenHandler());
OllamaChatResult response = ollamaClient.chat(request, chatTokenHandler); OllamaChatResult response = ollamaClient.chat(request, chatTokenHandler);
// Update chat history for continuity
chatHistory.clear(); chatHistory.clear();
chatHistory.addAll(response.getChatHistory()); chatHistory.addAll(response.getChatHistory());
return response.getResponseModel().getMessage().getResponse(); 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.
*
* <p>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 { public void runInteractive() throws OllamaException {
Scanner sc = new Scanner(System.in); Scanner sc = new Scanner(System.in);
while (true) { while (true) {
System.out.print("\n[You]: "); System.out.print("\n[You]: ");
String input = sc.nextLine(); String input = sc.nextLine();
if ("exit".equalsIgnoreCase(input)) break; if ("exit".equalsIgnoreCase(input)) break;
String response = this.think(input); this.interact(input);
} }
} }
/**
* Bean describing an agent as definable from YAML.
*
* <ul>
* <li>{@code name}: Agent display name
* <li>{@code description}: Freeform description
* <li>{@code tools}: List of tools/functions to enable
* <li>{@code host}: Target Ollama host address
* <li>{@code model}: Name of Ollama model to use
* <li>{@code customPrompt}: Agent's custom base prompt
* <li>{@code requestTimeoutSeconds}: Timeout for requests
* </ul>
*/
@Data @Data
public static class AgentSpec { public static class AgentSpec {
private String name; private String name;
@ -166,19 +258,36 @@ public class Agent {
private int requestTimeoutSeconds; private int requestTimeoutSeconds;
} }
/**
* Subclass extension of {@link Tools.ToolSpec}, which allows associating a tool with a function
* implementation (via FQCN).
*/
@Data @Data
@Setter @Setter
@Getter @Getter
private static class AgentToolSpec extends Tools.ToolSpec { private static class AgentToolSpec extends Tools.ToolSpec {
/** Fully qualified class name of the tool's {@link ToolFunction} implementation */
private String toolFunctionFQCN = null; private String toolFunctionFQCN = null;
/** Instance of the {@link ToolFunction} to invoke */
private ToolFunction toolFunctionInstance = null; private ToolFunction toolFunctionInstance = null;
} }
/** Bean for describing a tool function parameter for use in agent YAML definitions. */
@Data @Data
public class AgentToolParameter { public class AgentToolParameter {
/** The parameter's type (e.g., string, number, etc.) */
private String type; private String type;
/** Description of the parameter */
private String description; private String description;
/** Whether this parameter is required */
private boolean required; private boolean required;
/**
* Enum values (if any) that this parameter may take; _enum used because 'enum' is reserved
*/
private List<String> _enum; // `enum` is a reserved keyword, so use _enum or similar private List<String> _enum; // `enum` is a reserved keyword, so use _enum or similar
} }
} }

View File

@ -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<String, Object> 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<String, Object> 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<String, Object> 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);
}
}

View File

@ -11,8 +11,10 @@ package io.github.ollama4j.tools;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -21,8 +23,6 @@ import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import tools.jackson.core.type.TypeReference;
import tools.jackson.dataformat.yaml.YAMLMapper;
public class Tools { public class Tools {
private Tools() {} private Tools() {}
@ -150,7 +150,7 @@ public class Tools {
public static List<Tool> fromYAMLFile(String filePath, Map<String, ToolFunction> functionMap) { public static List<Tool> fromYAMLFile(String filePath, Map<String, ToolFunction> functionMap) {
try { try {
YAMLMapper mapper = new YAMLMapper(); ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
List<Map<String, Object>> rawTools = List<Map<String, Object>> rawTools =
mapper.readValue(new File(filePath), new TypeReference<>() {}); mapper.readValue(new File(filePath), new TypeReference<>() {});
List<Tool> tools = new ArrayList<>(); List<Tool> tools = new ArrayList<>();

View File

@ -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