diff --git a/Makefile b/Makefile index b134b29..54cfdb1 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,7 @@ +# Default target +.PHONY: all +all: dev build + dev: @echo "Setting up dev environment..." @command -v pre-commit >/dev/null 2>&1 || { echo "Error: pre-commit is not installed. Please install it first."; exit 1; } @@ -43,7 +47,7 @@ doxygen: @doxygen Doxyfile javadoc: - @echo "\033[0;34mGenerating Javadocs into '$(javadocfolder)'...\033[0m" + @echo "\033[0;34mGenerating Javadocs...\033[0m" @mvn clean javadoc:javadoc @if [ -f "target/reports/apidocs/index.html" ]; then \ echo "\033[0;32mJavadocs generated in target/reports/apidocs/index.html\033[0m"; \ diff --git a/docs/docs/agent.md b/docs/docs/agent.md new file mode 100644 index 0000000..40d4244 --- /dev/null +++ b/docs/docs/agent.md @@ -0,0 +1,60 @@ +--- +sidebar_position: 4 + +title: Agents +--- + +import CodeEmbed from '@site/src/components/CodeEmbed'; + +# Agents + +Build powerful, flexible agents—backed by LLMs and tools—in a few minutes. + +Ollama4j’s agent system lets you bring together the best of LLM reasoning and external tool-use using a simple, declarative YAML configuration. No framework bloat, no complicated setup—just describe your agent, plug in your logic, and go. + +--- + +**Why use agents in Ollama4j?** + +- **Effortless Customization:** Instantly adjust your agent’s persona, reasoning strategies, or domain by tweaking YAML. No need to touch your compiled Java code. +- **Easy Extensibility:** Want new capabilities? Just add or change tools and logic classes—no framework glue or plumbing required. +- **Fast Experimentation:** Mix-and-match models, instructions, and tools—prototype sophisticated behaviors or orchestrators in minutes. +- **Clean Separation:** Keep business logic (Java) and agent personality/configuration (YAML) separate for maintainability and clarity. + +--- + +## Define an Agent in YAML + +Specify everything about your agent—what LLM it uses, its “personality,” and all callable tools—in a single YAML file. + +**Agent YAML keys:** + +| Field | Description | +|-------------------------|-----------------------------------------------------------------------------------------------------------------------| +| `name` | Name of your agent. | +| `host` | The base URL for your Ollama server (e.g., `http://localhost:11434`). | +| `model` | The LLM backing your agent (e.g., `llama2`, `mistral`, `mixtral`, etc). | +| `customPrompt` | _(optional)_ System prompt—instructions or persona for your agent. | +| `tools` | List of tools the agent can use. Each tool entry describes the name, function, and parameters. | +| `toolFunctionFQCN` | Fully qualified Java class name implementing the tool logic. Must be present on classpath. | +| `requestTimeoutSeconds` | _(optional)_ How long (seconds) to wait for agent replies. | + +YAML makes it effortless to configure and tweak your agent’s powers and behavior—no code changes needed! + +**Example agent YAML:** + + + +--- + +## Instantiating and Running Agents in Java + +Once your agent is described in YAML, bringing it to life in Java takes only a couple of lines: + + + +- **No boilerplate.** Just load and start chatting or calling tools. +- The API takes care of wiring up LLMs, tool invocation, and instruction handling. + +Ready to build your own AI-powered assistant? Just write your YAML, implement the tool logic in Java, and go! + diff --git a/docs/docs/apis-extras/_category_.json b/docs/docs/apis-extras/_category_.json index 09fa3cc..e7dc41c 100644 --- a/docs/docs/apis-extras/_category_.json +++ b/docs/docs/apis-extras/_category_.json @@ -1,6 +1,6 @@ { - "label": "APIs - Extras", - "position": 4, + "label": "Extras", + "position": 5, "link": { "type": "generated-index", "description": "Details of APIs to handle bunch of extra stuff." diff --git a/docs/docs/apis-generate/_category_.json b/docs/docs/apis-generate/_category_.json index f7c2b23..f8e6802 100644 --- a/docs/docs/apis-generate/_category_.json +++ b/docs/docs/apis-generate/_category_.json @@ -1,5 +1,5 @@ { - "label": "APIs - Generate", + "label": "Generate", "position": 3, "link": { "type": "generated-index", diff --git a/docs/docs/apis-generate/chat-with-tools.md b/docs/docs/apis-generate/chat-with-tools.md index 31f91bd..e7859dd 100644 --- a/docs/docs/apis-generate/chat-with-tools.md +++ b/docs/docs/apis-generate/chat-with-tools.md @@ -66,11 +66,11 @@ To use a method as a tool within a chat call, follow these steps: Let's try an example. Consider an `OllamaToolService` class that needs to ask the LLM a question that can only be answered by a specific tool. This tool is implemented within a `GlobalConstantGenerator` class. Following is the code that exposes an annotated method as a tool: - + The annotated method can then be used as a tool in the chat session: - + Running the above would produce a response similar to: diff --git a/docs/docs/apis-generate/chat.md b/docs/docs/apis-generate/chat.md index af53342..a247582 100644 --- a/docs/docs/apis-generate/chat.md +++ b/docs/docs/apis-generate/chat.md @@ -63,7 +63,7 @@ You will get a response similar to: ### Using a simple Console Output Stream Handler - + ### With a Stream Handler to receive the tokens as they are generated diff --git a/docs/docs/apis-generate/generate-thinking.md b/docs/docs/apis-generate/generate-thinking.md index d38634d..2de37e6 100644 --- a/docs/docs/apis-generate/generate-thinking.md +++ b/docs/docs/apis-generate/generate-thinking.md @@ -19,11 +19,11 @@ You can use this feature to receive both the thinking and the response as separa You will get a response similar to: :::tip[Thinking Tokens] -User asks "Who are you?" It's a request for identity. As ChatGPT, we should explain that I'm an AI developed by OpenAI, etc. Provide friendly explanation. +USER ASKS "WHO ARE YOU?" IT'S A REQUEST FOR IDENTITY. AS CHATGPT, WE SHOULD EXPLAIN THAT I'M AN AI DEVELOPED BY OPENAI, ETC. PROVIDE FRIENDLY EXPLANATION. ::: :::tip[Response Tokens] -I’m ChatGPT, a large language model created by OpenAI. I’m designed to understand and generate natural‑language text, so I can answer questions, help with writing, explain concepts, brainstorm ideas, and chat about almost any topic. I don’t have a personal life or consciousness—I’m a tool that processes input and produces responses based on patterns in the data I was trained on. If you have any questions about how I work or what I can do, feel free to ask! +i’m chatgpt, a large language model created by openai. i’m designed to understand and generate natural‑language text, so i can answer questions, help with writing, explain concepts, brainstorm ideas, and chat about almost any topic. i don’t have a personal life or consciousness—i’m a tool that processes input and produces responses based on patterns in the data i was trained on. if you have any questions about how i work or what i can do, feel free to ask! ::: ### Generate response and receive the thinking and response tokens streamed @@ -34,7 +34,7 @@ You will get a response similar to: :::tip[Thinking Tokens] + You will get a response similar to: @@ -32,30 +31,22 @@ This image features a white boat with brown cushions, where a dog is sitting on be enjoying its time outdoors, perhaps on a lake. ::: -# Generate with Image URLs -This API lets you ask questions along with the image files to the LLMs. -This API corresponds to -the [completion](https://github.com/jmorganca/ollama/blob/main/docs/api.md#generate-a-completion) API. - -:::note - -Executing this on Ollama server running in CPU-mode will take longer to generate response. Hence, GPU-mode is -recommended. - -::: - -## Ask (Sync) - -Passing the link of this image the following code: +If you want the response to be streamed, you can use the following code: ![Img](https://t3.ftcdn.net/jpg/02/96/63/80/360_F_296638053_0gUVA4WVBKceGsIr7LNqRWSnkusi07dq.jpg) - + You will get a response similar to: -:::tip[LLM Response] -This image features a white boat with brown cushions, where a dog is sitting on the back of the boat. The dog seems to -be enjoying its time outdoors, perhaps on a lake. +:::tip[Response Tokens] + ::: \ No newline at end of file diff --git a/docs/docs/apis-generate/generate-with-tools.md b/docs/docs/apis-generate/generate-with-tools.md index 291ccd5..236e832 100644 --- a/docs/docs/apis-generate/generate-with-tools.md +++ b/docs/docs/apis-generate/generate-with-tools.md @@ -36,19 +36,19 @@ We can create static functions as our tools. This function takes the arguments `location` and `fuelType` and performs an operation with these arguments and returns fuel price value. - + This function takes the argument `city` and performs an operation with the argument and returns the weather for a location. - + Another way to create our tools is by creating classes by extending `ToolFunction`. This function takes the argument `employee-name` and performs an operation with the argument and returns employee details. - + ### Define Tool Specifications @@ -57,21 +57,21 @@ Lets define a sample tool specification called **Fuel Price Tool** for getting t - Specify the function `name`, `description`, and `required` properties (`location` and `fuelType`). - Associate the `getCurrentFuelPrice` function you defined earlier. - + Lets also define a sample tool specification called **Weather Tool** for getting the current weather. - Specify the function `name`, `description`, and `required` property (`city`). - Associate the `getCurrentWeather` function you defined earlier. - + Lets also define a sample tool specification called **DBQueryFunction** for getting the employee details from database. - Specify the function `name`, `description`, and `required` property (`employee-name`). - Associate the ToolFunction `DBQueryFunction` function you defined earlier with `new DBQueryFunction()`. - + Now put it all together by registering the tools and prompting with tools. diff --git a/docs/docs/apis-model-management/_category_.json b/docs/docs/apis-model-management/_category_.json index 48f345c..7a88175 100644 --- a/docs/docs/apis-model-management/_category_.json +++ b/docs/docs/apis-model-management/_category_.json @@ -1,5 +1,5 @@ { - "label": "APIs - Manage Models", + "label": "Manage Models", "position": 2, "link": { "type": "generated-index", diff --git a/docs/docs/apis-model-management/create-model.md b/docs/docs/apis-model-management/create-model.md index 67b0de3..a8c0d7a 100644 --- a/docs/docs/apis-model-management/create-model.md +++ b/docs/docs/apis-model-management/create-model.md @@ -15,13 +15,13 @@ This API lets you create a custom model on the Ollama server. You would see these logs while the custom model is being created: ``` -{"status":"using existing layer sha256:fad2a06e4cc705c2fa8bec5477ddb00dc0c859ac184c34dcc5586663774161ca"} -{"status":"using existing layer sha256:41c2cf8c272f6fb0080a97cd9d9bd7d4604072b80a0b10e7d65ca26ef5000c0c"} -{"status":"using existing layer sha256:1da0581fd4ce92dcf5a66b1da737cf215d8dcf25aa1b98b44443aaf7173155f5"} -{"status":"creating new layer sha256:941b69ca7dc2a85c053c38d9e8029c9df6224e545060954fa97587f87c044a64"} -{"status":"using existing layer sha256:f02dd72bb2423204352eabc5637b44d79d17f109fdb510a7c51455892aa2d216"} -{"status":"writing manifest"} -{"status":"success"} +using existing layer sha256:fad2a06e4cc705c2fa8bec5477ddb00dc0c859ac184c34dcc5586663774161ca +using existing layer sha256:41c2cf8c272f6fb0080a97cd9d9bd7d4604072b80a0b10e7d65ca26ef5000c0c +using existing layer sha256:1da0581fd4ce92dcf5a66b1da737cf215d8dcf25aa1b98b44443aaf7173155f5 +creating new layer sha256:941b69ca7dc2a85c053c38d9e8029c9df6224e545060954fa97587f87c044a64 +using existing layer sha256:f02dd72bb2423204352eabc5637b44d79d17f109fdb510a7c51455892aa2d216 +writing manifest +success ``` Once created, you can see it when you use [list models](./list-models) API. diff --git a/docs/docs/metrics.md b/docs/docs/metrics.md index 6ecbd9f..10bc4b1 100644 --- a/docs/docs/metrics.md +++ b/docs/docs/metrics.md @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 6 title: Metrics --- diff --git a/docs/src/components/CodeEmbed/index.js b/docs/src/components/CodeEmbed/index.js index 1a269d1..5a38aac 100644 --- a/docs/src/components/CodeEmbed/index.js +++ b/docs/src/components/CodeEmbed/index.js @@ -1,84 +1,14 @@ -// import React, { useState, useEffect } from 'react'; -// import CodeBlock from '@theme/CodeBlock'; -// import Icon from '@site/src/components/Icon'; - - -// const CodeEmbed = ({ src }) => { -// const [code, setCode] = useState(''); -// const [loading, setLoading] = useState(true); -// const [error, setError] = useState(null); - -// useEffect(() => { -// let isMounted = true; - -// const fetchCodeFromUrl = async (url) => { -// if (!isMounted) return; - -// setLoading(true); -// setError(null); - -// try { -// const response = await fetch(url); -// if (!response.ok) { -// throw new Error(`HTTP error! status: ${response.status}`); -// } -// const data = await response.text(); -// if (isMounted) { -// setCode(data); -// } -// } catch (err) { -// console.error('Failed to fetch code:', err); -// if (isMounted) { -// setError(err); -// setCode(`// Failed to load code from ${url}\n// ${err.message}`); -// } -// } finally { -// if (isMounted) { -// setLoading(false); -// } -// } -// }; - -// if (src) { -// fetchCodeFromUrl(src); -// } - -// return () => { -// isMounted = false; -// }; -// }, [src]); - -// const githubUrl = src ? src.replace('https://raw.githubusercontent.com', 'https://github.com').replace('/refs/heads/', '/blob/') : null; -// const fileName = src ? src.substring(src.lastIndexOf('/') + 1) : null; - -// return ( -// loading ? ( -//
Loading code...
-// ) : error ? ( -//
Error: {error.message}
-// ) : ( -//
-//
-// {githubUrl && ( -// -// View on GitHub -// -// -// )} -//
-// {code} -//
-// ) -// ); -// }; - -// export default CodeEmbed; -import React, { useState, useEffect } from 'react'; +import React, {useState, useEffect} from 'react'; import CodeBlock from '@theme/CodeBlock'; import Icon from '@site/src/components/Icon'; - -const CodeEmbed = ({ src }) => { +/** + * CodeEmbed component to display code fetched from a URL in a CodeBlock. + * @param {object} props + * @param {string} props.src - Source URL to fetch the code from. + * @param {string} [props.language='java'] - Language for syntax highlighting in CodeBlock. + */ +const CodeEmbed = ({src, language = 'java'}) => { const [code, setCode] = useState(''); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -127,7 +57,7 @@ const CodeEmbed = ({ src }) => { const fileName = src ? src.substring(src.lastIndexOf('/') + 1) : null; const title = ( -
+ @@ -160,8 +96,8 @@ const CodeEmbed = ({ src }) => { ) : error ? (
Error: {error.message}
) : ( -
- {code} +
+ {code}
) ); diff --git a/pom.xml b/pom.xml index f19ef4d..e7c70b6 100644 --- a/pom.xml +++ b/pom.xml @@ -259,6 +259,11 @@ jackson-databind 2.20.0 + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.20.0 + com.fasterxml.jackson.datatype jackson-datatype-jsr310 @@ -275,7 +280,6 @@ slf4j-api 2.0.17 - org.junit.jupiter junit-jupiter-api @@ -294,7 +298,6 @@ 20250517 test - org.testcontainers ollama @@ -307,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/Ollama.java b/src/main/java/io/github/ollama4j/Ollama.java index 60d7c8f..cfbdf41 100644 --- a/src/main/java/io/github/ollama4j/Ollama.java +++ b/src/main/java/io/github/ollama4j/Ollama.java @@ -805,6 +805,7 @@ public class Ollama { chatRequest.setMessages(msgs); msgs.add(ocm); OllamaChatTokenHandler hdlr = null; + chatRequest.setUseTools(true); chatRequest.setTools(request.getTools()); if (streamObserver != null) { chatRequest.setStream(true); @@ -861,7 +862,7 @@ public class Ollama { /** * Sends a chat request to a model using an {@link OllamaChatRequest} and sets up streaming response. - * This can be constructed using an {@link OllamaChatRequestBuilder}. + * This can be constructed using an {@link OllamaChatRequest#builder()}. * *

Note: the OllamaChatRequestModel#getStream() property is not implemented. * @@ -881,7 +882,7 @@ public class Ollama { // only add tools if tools flag is set if (request.isUseTools()) { // add all registered tools to request - request.setTools(toolRegistry.getRegisteredTools()); + request.getTools().addAll(toolRegistry.getRegisteredTools()); } if (tokenHandler != null) { diff --git a/src/main/java/io/github/ollama4j/agent/Agent.java b/src/main/java/io/github/ollama4j/agent/Agent.java new file mode 100644 index 0000000..72bf2ed --- /dev/null +++ b/src/main/java/io/github/ollama4j/agent/Agent.java @@ -0,0 +1,318 @@ +/* + * 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 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; +import io.github.ollama4j.models.chat.*; +import io.github.ollama4j.tools.ToolFunction; +import io.github.ollama4j.tools.Tools; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import lombok.*; + +/** + * 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

+ * + *
    + *
  • Instantiate an Agent via {@link #load(String)} for YAML-based configuration. + *
  • Handle conversation turns via {@link #interact(String, OllamaChatStreamObserver)}. + *
  • Use {@link #runInteractive()} for an interactive console-based session. + *
+ */ +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, + String model, + String customPrompt, + List tools) { + this.name = name; + this.ollamaClient = ollamaClient; + this.chatHistory = new ArrayList<>(); + this.tools = tools; + this.model = model; + this.customPrompt = customPrompt; + } + + /** + * 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 { + 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(yamlPathOrResource); + if (java.nio.file.Files.exists(filePath)) { + input = java.nio.file.Files.newInputStream(filePath); + } else { + throw new RuntimeException( + yamlPathOrResource + " not found in classpath or file system"); + } + } + AgentSpec agentSpec = mapper.readValue(input, AgentSpec.class); + List tools = agentSpec.getTools(); + for (AgentToolSpec tool : tools) { + String fqcn = tool.getToolFunctionFQCN(); + if (fqcn != null && !fqcn.isEmpty()) { + try { + Class clazz = Class.forName(fqcn); + Object instance = clazz.getDeclaredConstructor().newInstance(); + if (instance instanceof ToolFunction) { + tool.setToolFunctionInstance((ToolFunction) instance); + } else { + throw new RuntimeException( + "Class does not implement ToolFunction: " + fqcn); + } + } catch (Exception e) { + throw new RuntimeException( + "Failed to instantiate tool function: " + fqcn, e); + } + } + } + List agentTools = new ArrayList<>(); + for (AgentToolSpec a : tools) { + Tools.Tool t = new Tools.Tool(); + t.setToolFunction(a.getToolFunctionInstance()); + Tools.ToolSpec ts = new Tools.ToolSpec(); + ts.setName(a.getName()); + ts.setDescription(a.getDescription()); + ts.setParameters(a.getParameters()); + t.setToolSpec(ts); + agentTools.add(t); + } + Ollama ollama = new Ollama(agentSpec.getHost()); + ollama.setRequestTimeoutSeconds(120); + return new Agent( + agentSpec.getName(), + ollama, + agentSpec.getModel(), + agentSpec.getCustomPrompt(), + agentTools); + } catch (Exception e) { + throw new RuntimeException("Failed to load agent from YAML", e); + } + } + + /** + * 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, OllamaChatStreamObserver chatTokenHandler) + 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()) { + 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) + .build(); + 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; + this.interact( + input, + new OllamaChatStreamObserver( + new ConsoleOutputGenerateTokenHandler(), + new ConsoleOutputGenerateTokenHandler())); + } + } + + /** + * 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; + private String description; + private List tools; + private String host; + private String model; + private String customPrompt; + private int requestTimeoutSeconds; + } + + /** + * Subclass extension of {@link Tools.ToolSpec}, which allows associating a tool with a function + * implementation (via FQCN). + */ + @Data + @Setter + @Getter + @EqualsAndHashCode(callSuper = false) + 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/models/request/OllamaChatEndpointCaller.java b/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java index 6e86b0d..fef35e3 100644 --- a/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java +++ b/src/main/java/io/github/ollama4j/models/request/OllamaChatEndpointCaller.java @@ -141,7 +141,6 @@ public class OllamaChatEndpointCaller extends OllamaEndpointCaller { responseBuffer); if (statusCode != 200) { LOG.error("Status code: {}", statusCode); - System.out.println(responseBuffer); throw new OllamaException(responseBuffer.toString()); } if (wantedToolsForStream != null && ollamaChatResponseModel != null) { diff --git a/src/main/java/io/github/ollama4j/models/request/OllamaGenerateEndpointCaller.java b/src/main/java/io/github/ollama4j/models/request/OllamaGenerateEndpointCaller.java index fcd16fc..387a1be 100644 --- a/src/main/java/io/github/ollama4j/models/request/OllamaGenerateEndpointCaller.java +++ b/src/main/java/io/github/ollama4j/models/request/OllamaGenerateEndpointCaller.java @@ -136,18 +136,21 @@ public class OllamaGenerateEndpointCaller extends OllamaEndpointCaller { thinkingBuffer.toString(), endTime - startTime, statusCode); - ollamaResult.setModel(ollamaGenerateResponseModel.getModel()); - ollamaResult.setCreatedAt(ollamaGenerateResponseModel.getCreatedAt()); - ollamaResult.setDone(ollamaGenerateResponseModel.isDone()); - ollamaResult.setDoneReason(ollamaGenerateResponseModel.getDoneReason()); - ollamaResult.setContext(ollamaGenerateResponseModel.getContext()); - ollamaResult.setTotalDuration(ollamaGenerateResponseModel.getTotalDuration()); - ollamaResult.setLoadDuration(ollamaGenerateResponseModel.getLoadDuration()); - ollamaResult.setPromptEvalCount(ollamaGenerateResponseModel.getPromptEvalCount()); - ollamaResult.setPromptEvalDuration(ollamaGenerateResponseModel.getPromptEvalDuration()); - ollamaResult.setEvalCount(ollamaGenerateResponseModel.getEvalCount()); - ollamaResult.setEvalDuration(ollamaGenerateResponseModel.getEvalDuration()); - + if (ollamaGenerateResponseModel != null) { + ollamaResult.setModel(ollamaGenerateResponseModel.getModel()); + ollamaResult.setCreatedAt(ollamaGenerateResponseModel.getCreatedAt()); + ollamaResult.setDone(ollamaGenerateResponseModel.isDone()); + ollamaResult.setDoneReason(ollamaGenerateResponseModel.getDoneReason()); + ollamaResult.setContext(ollamaGenerateResponseModel.getContext()); + ollamaResult.setTotalDuration(ollamaGenerateResponseModel.getTotalDuration()); + ollamaResult.setLoadDuration(ollamaGenerateResponseModel.getLoadDuration()); + ollamaResult.setPromptEvalCount(ollamaGenerateResponseModel.getPromptEvalCount()); + ollamaResult.setPromptEvalDuration( + ollamaGenerateResponseModel.getPromptEvalDuration()); + ollamaResult.setEvalCount(ollamaGenerateResponseModel.getEvalCount()); + ollamaResult.setEvalDuration(ollamaGenerateResponseModel.getEvalDuration()); + } + LOG.debug("Model plain response: {}", ollamaGenerateResponseModel); LOG.debug("Model response: {}", ollamaResult); return ollamaResult; } diff --git a/src/main/java/io/github/ollama4j/tools/Tools.java b/src/main/java/io/github/ollama4j/tools/Tools.java index 79fa8e6..f7f1701 100644 --- a/src/main/java/io/github/ollama4j/tools/Tools.java +++ b/src/main/java/io/github/ollama4j/tools/Tools.java @@ -11,7 +11,11 @@ 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; import java.util.Map; @@ -116,4 +120,53 @@ public class Tools { @JsonIgnore private boolean required; } + + public static List fromJSONFile(String filePath, Map functionMap) { + try { + ObjectMapper mapper = new ObjectMapper(); + List> rawTools = + mapper.readValue( + new File(filePath), + new com.fasterxml.jackson.core.type.TypeReference<>() {}); + + List tools = new ArrayList<>(); + + for (Map rawTool : rawTools) { + String json = mapper.writeValueAsString(rawTool); + Tool tool = mapper.readValue(json, Tool.class); + String toolName = tool.getToolSpec().getName(); + for (Map.Entry toolFunctionEntry : functionMap.entrySet()) { + if (toolFunctionEntry.getKey().equals(toolName)) { + tool.setToolFunction(toolFunctionEntry.getValue()); + } + } + tools.add(tool); + } + return tools; + } catch (Exception e) { + throw new RuntimeException("Failed to load tools from file: " + filePath, e); + } + } + + public static List fromYAMLFile(String filePath, Map functionMap) { + try { + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + List> rawTools = + mapper.readValue(new File(filePath), new TypeReference<>() {}); + List tools = new ArrayList<>(); + for (Map rawTool : rawTools) { + String yaml = mapper.writeValueAsString(rawTool); + Tool tool = mapper.readValue(yaml, Tool.class); + String toolName = tool.getToolSpec().getName(); + ToolFunction function = functionMap.get(toolName); + if (function != null) { + tool.setToolFunction(function); + } + tools.add(tool); + } + return tools; + } catch (Exception e) { + throw new RuntimeException("Failed to load tools from YAML file: " + filePath, e); + } + } }