mirror of
https://github.com/amithkoujalgi/ollama4j.git
synced 2025-10-14 09:28:58 +02:00

- Introduced a new `javadoc` target in the Makefile to generate Javadocs. - Refactored model classes: renamed `ModelsProcessResponse` to `ModelProcessesResponse` and updated related references. - Updated `OllamaEmbedRequestModel` and `OllamaEmbedResponseModel` to `OllamaEmbedRequest` and `OllamaEmbedResponse`, respectively, across the codebase. - Added new classes for `OllamaEmbedRequest` and `OllamaEmbedResponse` to improve clarity and maintainability.
1539 lines
65 KiB
Java
1539 lines
65 KiB
Java
/*
|
|
* 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.integrationtests;
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
|
|
import io.github.ollama4j.OllamaAPI;
|
|
import io.github.ollama4j.exceptions.OllamaException;
|
|
import io.github.ollama4j.impl.ConsoleOutputChatTokenHandler;
|
|
import io.github.ollama4j.impl.ConsoleOutputGenerateTokenHandler;
|
|
import io.github.ollama4j.models.chat.*;
|
|
import io.github.ollama4j.models.embed.OllamaEmbedRequest;
|
|
import io.github.ollama4j.models.embed.OllamaEmbedResponse;
|
|
import io.github.ollama4j.models.generate.OllamaGenerateRequest;
|
|
import io.github.ollama4j.models.generate.OllamaGenerateRequestBuilder;
|
|
import io.github.ollama4j.models.generate.OllamaGenerateStreamObserver;
|
|
import io.github.ollama4j.models.response.Model;
|
|
import io.github.ollama4j.models.response.ModelDetail;
|
|
import io.github.ollama4j.models.response.OllamaResult;
|
|
import io.github.ollama4j.samples.AnnotatedTool;
|
|
import io.github.ollama4j.tools.OllamaToolCallsFunction;
|
|
import io.github.ollama4j.tools.Tools;
|
|
import io.github.ollama4j.tools.annotations.OllamaToolService;
|
|
import io.github.ollama4j.utils.OptionsBuilder;
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.util.*;
|
|
import java.util.concurrent.CountDownLatch;
|
|
import org.junit.jupiter.api.BeforeAll;
|
|
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
|
|
import org.junit.jupiter.api.Order;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.TestMethodOrder;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.testcontainers.ollama.OllamaContainer;
|
|
|
|
@OllamaToolService(providers = {AnnotatedTool.class})
|
|
@TestMethodOrder(OrderAnnotation.class)
|
|
@SuppressWarnings({"HttpUrlsUsage", "SpellCheckingInspection", "FieldCanBeLocal", "ConstantValue"})
|
|
class OllamaAPIIntegrationTest {
|
|
private static final Logger LOG = LoggerFactory.getLogger(OllamaAPIIntegrationTest.class);
|
|
|
|
private static OllamaContainer ollama;
|
|
private static OllamaAPI api;
|
|
|
|
private static final String EMBEDDING_MODEL = "all-minilm";
|
|
private static final String VISION_MODEL = "moondream:1.8b";
|
|
private static final String THINKING_TOOL_MODEL = "deepseek-r1:1.5b";
|
|
private static final String THINKING_TOOL_MODEL_2 = "qwen3:0.6b";
|
|
private static final String GENERAL_PURPOSE_MODEL = "gemma3:270m";
|
|
private static final String TOOLS_MODEL = "mistral:7b";
|
|
|
|
/**
|
|
* Initializes the OllamaAPI instance for integration tests.
|
|
*
|
|
* <p>This method sets up the OllamaAPI client, either using an external Ollama host (if
|
|
* environment variables are set) or by starting a Testcontainers-based Ollama instance. It also
|
|
* configures request timeout and model pull retry settings.
|
|
*/
|
|
@BeforeAll
|
|
static void setUp() {
|
|
// ... (no javadoc needed for private setup logic)
|
|
int requestTimeoutSeconds = 60;
|
|
int numberOfRetriesForModelPull = 5;
|
|
|
|
try {
|
|
String useExternalOllamaHostEnv = System.getenv("USE_EXTERNAL_OLLAMA_HOST");
|
|
String ollamaHostEnv = System.getenv("OLLAMA_HOST");
|
|
|
|
boolean useExternalOllamaHost;
|
|
String ollamaHost;
|
|
|
|
if (useExternalOllamaHostEnv == null && ollamaHostEnv == null) {
|
|
Properties props = new Properties();
|
|
try {
|
|
props.load(
|
|
OllamaAPIIntegrationTest.class
|
|
.getClassLoader()
|
|
.getResourceAsStream("test-config.properties"));
|
|
} catch (Exception e) {
|
|
throw new RuntimeException(
|
|
"Could not load test-config.properties from classpath", e);
|
|
}
|
|
useExternalOllamaHost =
|
|
Boolean.parseBoolean(
|
|
props.getProperty("USE_EXTERNAL_OLLAMA_HOST", "false"));
|
|
ollamaHost = props.getProperty("OLLAMA_HOST");
|
|
requestTimeoutSeconds =
|
|
Integer.parseInt(props.getProperty("REQUEST_TIMEOUT_SECONDS"));
|
|
numberOfRetriesForModelPull =
|
|
Integer.parseInt(props.getProperty("NUMBER_RETRIES_FOR_MODEL_PULL"));
|
|
} else {
|
|
useExternalOllamaHost = Boolean.parseBoolean(useExternalOllamaHostEnv);
|
|
ollamaHost = ollamaHostEnv;
|
|
}
|
|
|
|
if (useExternalOllamaHost) {
|
|
LOG.info("Using external Ollama host: {}", ollamaHost);
|
|
api = new OllamaAPI(ollamaHost);
|
|
} else {
|
|
throw new RuntimeException(
|
|
"USE_EXTERNAL_OLLAMA_HOST is not set so, we will be using Testcontainers"
|
|
+ " Ollama host for the tests now. If you would like to use an external"
|
|
+ " host, please set the env var to USE_EXTERNAL_OLLAMA_HOST=true and"
|
|
+ " set the env var OLLAMA_HOST=http://localhost:11435 or a different"
|
|
+ " host/port.");
|
|
}
|
|
} catch (Exception e) {
|
|
String ollamaVersion = "0.6.1";
|
|
int internalPort = 11434;
|
|
int mappedPort = 11435;
|
|
ollama = new OllamaContainer("ollama/ollama:" + ollamaVersion);
|
|
ollama.addExposedPort(internalPort);
|
|
List<String> portBindings = new ArrayList<>();
|
|
portBindings.add(mappedPort + ":" + internalPort);
|
|
ollama.setPortBindings(portBindings);
|
|
ollama.start();
|
|
LOG.info("Using Testcontainer Ollama host...");
|
|
api =
|
|
new OllamaAPI(
|
|
"http://"
|
|
+ ollama.getHost()
|
|
+ ":"
|
|
+ ollama.getMappedPort(internalPort));
|
|
}
|
|
api.setRequestTimeoutSeconds(requestTimeoutSeconds);
|
|
api.setNumberOfRetriesForModelPull(numberOfRetriesForModelPull);
|
|
}
|
|
|
|
/**
|
|
* Verifies that a ConnectException is thrown when attempting to connect to a non-existent
|
|
* Ollama endpoint.
|
|
*
|
|
* <p>Scenario: Ensures the API client fails gracefully when the Ollama server is unreachable.
|
|
*/
|
|
@Test
|
|
@Order(1)
|
|
void shouldThrowConnectExceptionForWrongEndpoint() {
|
|
OllamaAPI ollamaAPI = new OllamaAPI("http://wrong-host:11434");
|
|
assertThrows(OllamaException.class, ollamaAPI::listModels);
|
|
}
|
|
|
|
/**
|
|
* Tests retrieval of the Ollama server version.
|
|
*
|
|
* <p>Scenario: Calls the /api/version endpoint and asserts a non-null version string is
|
|
* returned.
|
|
*/
|
|
@Test
|
|
@Order(1)
|
|
void shouldReturnVersionFromVersionAPI() throws OllamaException {
|
|
String version = api.getVersion();
|
|
assertNotNull(version);
|
|
}
|
|
|
|
/**
|
|
* Tests the /api/ping endpoint for server liveness.
|
|
*
|
|
* <p>Scenario: Ensures the Ollama server responds to ping requests.
|
|
*/
|
|
@Test
|
|
@Order(1)
|
|
void shouldPingSuccessfully() throws OllamaException {
|
|
boolean pingResponse = api.ping();
|
|
assertTrue(pingResponse, "Ping should return true");
|
|
}
|
|
|
|
/**
|
|
* Tests listing all available models from the Ollama server.
|
|
*
|
|
* <p>Scenario: Calls /api/tags and verifies the returned list is not null (may be empty).
|
|
*/
|
|
@Test
|
|
@Order(2)
|
|
void shouldListModels() throws OllamaException {
|
|
List<Model> models = api.listModels();
|
|
assertNotNull(models, "Models should not be null");
|
|
assertTrue(models.size() >= 0, "Models list can be empty or contain elements");
|
|
}
|
|
|
|
@Test
|
|
@Order(2)
|
|
void shouldUnloadModel() {
|
|
final String model = GENERAL_PURPOSE_MODEL;
|
|
assertDoesNotThrow(
|
|
() -> api.unloadModel(model), "unloadModel should not throw any exception");
|
|
}
|
|
|
|
/**
|
|
* Tests pulling a model and verifying it appears in the model list.
|
|
*
|
|
* <p>Scenario: Pulls an embedding model, then checks that it is present in the list of models.
|
|
*/
|
|
@Test
|
|
@Order(3)
|
|
void shouldPullModelAndListModels() throws OllamaException {
|
|
api.pullModel(EMBEDDING_MODEL);
|
|
List<Model> models = api.listModels();
|
|
assertNotNull(models, "Models should not be null");
|
|
assertFalse(models.isEmpty(), "Models list should contain elements");
|
|
}
|
|
|
|
/**
|
|
* Tests fetching detailed information for a specific model.
|
|
*
|
|
* <p>Scenario: Pulls a model and retrieves its details, asserting the model file contains the
|
|
* model name.
|
|
*/
|
|
@Test
|
|
@Order(4)
|
|
void shouldGetModelDetails() throws OllamaException {
|
|
api.pullModel(EMBEDDING_MODEL);
|
|
ModelDetail modelDetails = api.getModelDetails(EMBEDDING_MODEL);
|
|
assertNotNull(modelDetails);
|
|
assertTrue(modelDetails.getModelFile().contains(EMBEDDING_MODEL));
|
|
}
|
|
|
|
/**
|
|
* Tests generating embeddings for a batch of input texts.
|
|
*
|
|
* <p>Scenario: Uses the embedding model to generate vector embeddings for two input sentences.
|
|
*/
|
|
@Test
|
|
@Order(5)
|
|
void shouldReturnEmbeddings() throws Exception {
|
|
api.pullModel(EMBEDDING_MODEL);
|
|
OllamaEmbedRequest m = new OllamaEmbedRequest();
|
|
m.setModel(EMBEDDING_MODEL);
|
|
m.setInput(Arrays.asList("Why is the sky blue?", "Why is the grass green?"));
|
|
OllamaEmbedResponse embeddings = api.embed(m);
|
|
assertNotNull(embeddings, "Embeddings should not be null");
|
|
assertFalse(embeddings.getEmbeddings().isEmpty(), "Embeddings should not be empty");
|
|
}
|
|
|
|
/**
|
|
* Tests generating structured output using the 'format' parameter.
|
|
*
|
|
* <p>Scenario: Calls generateWithFormat with a prompt and a JSON schema, expecting a structured
|
|
* response. Usage: generate with format, no thinking, no streaming.
|
|
*/
|
|
@Test
|
|
@Order(6)
|
|
void shouldGenerateWithStructuredOutput() throws OllamaException {
|
|
api.pullModel(TOOLS_MODEL);
|
|
|
|
String prompt =
|
|
"The sun is shining brightly and is directly overhead at the zenith, casting my"
|
|
+ " shadow over my foot, so it must be noon.";
|
|
|
|
Map<String, Object> format = new HashMap<>();
|
|
format.put("type", "object");
|
|
format.put(
|
|
"properties",
|
|
new HashMap<String, Object>() {
|
|
{
|
|
put(
|
|
"isNoon",
|
|
new HashMap<String, Object>() {
|
|
{
|
|
put("type", "boolean");
|
|
}
|
|
});
|
|
}
|
|
});
|
|
format.put("required", List.of("isNoon"));
|
|
|
|
OllamaGenerateRequest request =
|
|
OllamaGenerateRequestBuilder.builder()
|
|
.withModel(TOOLS_MODEL)
|
|
.withPrompt(prompt)
|
|
.withFormat(format)
|
|
.build();
|
|
OllamaGenerateStreamObserver handler = null;
|
|
OllamaResult result = api.generate(request, handler);
|
|
|
|
assertNotNull(result);
|
|
assertNotNull(result.getResponse());
|
|
assertFalse(result.getResponse().isEmpty());
|
|
assertNotNull(result.getStructuredResponse().get("isNoon"));
|
|
}
|
|
|
|
/**
|
|
* Tests basic text generation with default options.
|
|
*
|
|
* <p>Scenario: Calls generate with a general-purpose model, no thinking, no streaming, no
|
|
* format. Usage: generate, raw=false, think=false, no streaming.
|
|
*/
|
|
@Test
|
|
@Order(6)
|
|
void shouldGenerateWithDefaultOptions() throws OllamaException {
|
|
api.pullModel(GENERAL_PURPOSE_MODEL);
|
|
boolean raw = false;
|
|
boolean thinking = false;
|
|
OllamaGenerateRequest request =
|
|
OllamaGenerateRequestBuilder.builder()
|
|
.withModel(GENERAL_PURPOSE_MODEL)
|
|
.withPrompt(
|
|
"What is the capital of France? And what's France's connection with"
|
|
+ " Mona Lisa?")
|
|
.withRaw(raw)
|
|
.withThink(thinking)
|
|
.withOptions(new OptionsBuilder().build())
|
|
.build();
|
|
OllamaGenerateStreamObserver handler = null;
|
|
OllamaResult result = api.generate(request, handler);
|
|
assertNotNull(result);
|
|
assertNotNull(result.getResponse());
|
|
assertFalse(result.getResponse().isEmpty());
|
|
}
|
|
|
|
/**
|
|
* Tests text generation with streaming enabled.
|
|
*
|
|
* <p>Scenario: Calls generate with a general-purpose model, streaming the response tokens.
|
|
* Usage: generate, raw=false, think=false, streaming enabled.
|
|
*/
|
|
@Test
|
|
@Order(7)
|
|
void shouldGenerateWithDefaultOptionsStreamed() throws OllamaException {
|
|
api.pullModel(GENERAL_PURPOSE_MODEL);
|
|
boolean raw = false;
|
|
OllamaGenerateRequest request =
|
|
OllamaGenerateRequestBuilder.builder()
|
|
.withModel(GENERAL_PURPOSE_MODEL)
|
|
.withPrompt(
|
|
"What is the capital of France? And what's France's connection with"
|
|
+ " Mona Lisa?")
|
|
.withRaw(raw)
|
|
.withThink(false)
|
|
.withOptions(new OptionsBuilder().build())
|
|
.build();
|
|
OllamaResult result =
|
|
api.generate(
|
|
request,
|
|
new OllamaGenerateStreamObserver(
|
|
null, new ConsoleOutputGenerateTokenHandler()));
|
|
assertNotNull(result);
|
|
assertNotNull(result.getResponse());
|
|
assertFalse(result.getResponse().isEmpty());
|
|
}
|
|
|
|
/**
|
|
* Tests chat API with custom options (e.g., temperature).
|
|
*
|
|
* <p>Scenario: Builds a chat request with system and user messages, sets a custom temperature,
|
|
* and verifies the response. Usage: chat, no tools, no thinking, no streaming, custom options.
|
|
*/
|
|
@Test
|
|
@Order(8)
|
|
void shouldGenerateWithCustomOptions() throws OllamaException {
|
|
api.pullModel(GENERAL_PURPOSE_MODEL);
|
|
|
|
OllamaChatRequestBuilder builder =
|
|
OllamaChatRequestBuilder.builder().withModel(GENERAL_PURPOSE_MODEL);
|
|
OllamaChatRequest requestModel =
|
|
builder.withMessage(
|
|
OllamaChatMessageRole.SYSTEM,
|
|
"You are a helpful assistant who can generate random person's first"
|
|
+ " and last names in the format [First name, Last name].")
|
|
.build();
|
|
requestModel =
|
|
builder.withMessages(requestModel.getMessages())
|
|
.withMessage(OllamaChatMessageRole.USER, "Give me a cool name")
|
|
.withOptions(new OptionsBuilder().setTemperature(0.5f).build())
|
|
.build();
|
|
OllamaChatResult chatResult = api.chat(requestModel, null);
|
|
|
|
assertNotNull(chatResult);
|
|
assertNotNull(chatResult.getResponseModel());
|
|
assertFalse(chatResult.getResponseModel().getMessage().getResponse().isEmpty());
|
|
}
|
|
|
|
/**
|
|
* Tests chat API with a system prompt and verifies the assistant's response.
|
|
*
|
|
* <p>Scenario: Sends a system prompt instructing the assistant to reply with a specific word,
|
|
* then checks the response. Usage: chat, no tools, no thinking, no streaming, system prompt.
|
|
*/
|
|
@Test
|
|
@Order(9)
|
|
void shouldChatWithSystemPrompt() throws OllamaException {
|
|
api.pullModel(GENERAL_PURPOSE_MODEL);
|
|
|
|
String expectedResponse = "Bhai";
|
|
|
|
OllamaChatRequestBuilder builder =
|
|
OllamaChatRequestBuilder.builder().withModel(GENERAL_PURPOSE_MODEL);
|
|
OllamaChatRequest requestModel =
|
|
builder.withMessage(
|
|
OllamaChatMessageRole.SYSTEM,
|
|
String.format(
|
|
"[INSTRUCTION-START] You are an obidient and helpful bot"
|
|
+ " named %s. You always answer with only one word and"
|
|
+ " that word is your name. [INSTRUCTION-END]",
|
|
expectedResponse))
|
|
.withMessage(OllamaChatMessageRole.USER, "Who are you?")
|
|
.withOptions(new OptionsBuilder().setTemperature(0.0f).build())
|
|
.build();
|
|
|
|
OllamaChatResult chatResult = api.chat(requestModel, null);
|
|
assertNotNull(chatResult);
|
|
assertNotNull(chatResult.getResponseModel());
|
|
assertNotNull(chatResult.getResponseModel().getMessage());
|
|
assertFalse(chatResult.getResponseModel().getMessage().getResponse().isBlank());
|
|
assertTrue(
|
|
chatResult
|
|
.getResponseModel()
|
|
.getMessage()
|
|
.getResponse()
|
|
.contains(expectedResponse));
|
|
assertEquals(3, chatResult.getChatHistory().size());
|
|
}
|
|
|
|
/**
|
|
* Tests chat API with multi-turn conversation (chat history).
|
|
*
|
|
* <p>Scenario: Sends a sequence of user messages, each time including the chat history, and
|
|
* verifies the assistant's responses. Usage: chat, no tools, no thinking, no streaming,
|
|
* multi-turn.
|
|
*/
|
|
@Test
|
|
@Order(10)
|
|
void shouldChatWithHistory() throws Exception {
|
|
api.pullModel(THINKING_TOOL_MODEL);
|
|
OllamaChatRequestBuilder builder =
|
|
OllamaChatRequestBuilder.builder().withModel(THINKING_TOOL_MODEL);
|
|
|
|
OllamaChatRequest requestModel =
|
|
builder.withMessage(
|
|
OllamaChatMessageRole.USER, "What is 1+1? Answer only in numbers.")
|
|
.build();
|
|
|
|
OllamaChatResult chatResult = api.chat(requestModel, null);
|
|
|
|
assertNotNull(chatResult);
|
|
assertNotNull(chatResult.getChatHistory());
|
|
assertNotNull(chatResult.getChatHistory().stream());
|
|
|
|
requestModel =
|
|
builder.withMessages(chatResult.getChatHistory())
|
|
.withMessage(OllamaChatMessageRole.USER, "And what is its squared value?")
|
|
.build();
|
|
|
|
chatResult = api.chat(requestModel, null);
|
|
|
|
assertNotNull(chatResult);
|
|
assertNotNull(chatResult.getChatHistory());
|
|
assertNotNull(chatResult.getChatHistory().stream());
|
|
|
|
requestModel =
|
|
builder.withMessages(chatResult.getChatHistory())
|
|
.withMessage(
|
|
OllamaChatMessageRole.USER,
|
|
"What is the largest value between 2, 4 and 6?")
|
|
.build();
|
|
|
|
chatResult = api.chat(requestModel, null);
|
|
|
|
assertNotNull(chatResult, "Chat result should not be null");
|
|
assertTrue(
|
|
chatResult.getChatHistory().size() > 2,
|
|
"Chat history should contain more than two messages");
|
|
}
|
|
|
|
/**
|
|
* Tests chat API with explicit tool invocation (client does not handle tools).
|
|
*
|
|
* <p>Scenario: Registers a tool, sends a user message that triggers a tool call, and verifies
|
|
* the tool call and arguments. Usage: chat, explicit tool, useTools=false, no thinking, no
|
|
* streaming.
|
|
*/
|
|
@Test
|
|
@Order(11)
|
|
void shouldChatWithExplicitTool() throws OllamaException {
|
|
String theToolModel = TOOLS_MODEL;
|
|
api.pullModel(theToolModel);
|
|
OllamaChatRequestBuilder builder =
|
|
OllamaChatRequestBuilder.builder().withModel(theToolModel);
|
|
|
|
api.registerTool(EmployeeFinderToolSpec.getSpecification());
|
|
|
|
OllamaChatRequest requestModel =
|
|
builder.withMessage(
|
|
OllamaChatMessageRole.USER,
|
|
"Give me the ID and address of the employee Rahul Kumar.")
|
|
.build();
|
|
requestModel.setOptions(new OptionsBuilder().setTemperature(0.9f).build().getOptionsMap());
|
|
requestModel.setUseTools(true);
|
|
OllamaChatResult chatResult = api.chat(requestModel, null);
|
|
|
|
assertNotNull(chatResult, "chatResult should not be null");
|
|
assertNotNull(chatResult.getResponseModel(), "Response model should not be null");
|
|
assertNotNull(
|
|
chatResult.getResponseModel().getMessage(), "Response message should not be null");
|
|
assertEquals(
|
|
OllamaChatMessageRole.ASSISTANT.getRoleName(),
|
|
chatResult.getResponseModel().getMessage().getRole().getRoleName(),
|
|
"Role of the response message should be ASSISTANT");
|
|
List<OllamaChatToolCalls> toolCalls = chatResult.getChatHistory().get(1).getToolCalls();
|
|
assert (!toolCalls.isEmpty());
|
|
OllamaToolCallsFunction function = toolCalls.get(0).getFunction();
|
|
assertEquals(
|
|
"get-employee-details",
|
|
function.getName(),
|
|
"Tool function name should be 'get-employee-details'");
|
|
assertFalse(
|
|
function.getArguments().isEmpty(), "Tool function arguments should not be empty");
|
|
Object employeeName = function.getArguments().get("employee-name");
|
|
assertNotNull(employeeName, "Employee name argument should not be null");
|
|
assertEquals("Rahul Kumar", employeeName, "Employee name argument should be 'Rahul Kumar'");
|
|
assertTrue(
|
|
chatResult.getChatHistory().size() > 2,
|
|
"Chat history should have more than 2 messages");
|
|
List<OllamaChatToolCalls> finalToolCalls =
|
|
chatResult.getResponseModel().getMessage().getToolCalls();
|
|
assertNull(finalToolCalls, "Final tool calls in the response message should be null");
|
|
}
|
|
|
|
/**
|
|
* Tests chat API with explicit tool invocation and useTools=true.
|
|
*
|
|
* <p>Scenario: Registers a tool, enables useTools, sends a user message, and verifies the
|
|
* assistant's tool call. Usage: chat, explicit tool, useTools=true, no thinking, no streaming.
|
|
*/
|
|
@Test
|
|
@Order(13)
|
|
void shouldChatWithExplicitToolAndUseTools() throws OllamaException {
|
|
String theToolModel = TOOLS_MODEL;
|
|
api.pullModel(theToolModel);
|
|
OllamaChatRequestBuilder builder =
|
|
OllamaChatRequestBuilder.builder().withModel(theToolModel);
|
|
|
|
api.registerTool(EmployeeFinderToolSpec.getSpecification());
|
|
|
|
OllamaChatRequest requestModel =
|
|
builder.withMessage(
|
|
OllamaChatMessageRole.USER,
|
|
"Give me the ID and address of the employee Rahul Kumar.")
|
|
.build();
|
|
requestModel.setOptions(new OptionsBuilder().setTemperature(0.9f).build().getOptionsMap());
|
|
requestModel.setUseTools(true);
|
|
OllamaChatResult chatResult = api.chat(requestModel, null);
|
|
|
|
assertNotNull(chatResult, "chatResult should not be null");
|
|
assertNotNull(chatResult.getResponseModel(), "Response model should not be null");
|
|
assertNotNull(
|
|
chatResult.getResponseModel().getMessage(), "Response message should not be null");
|
|
assertEquals(
|
|
OllamaChatMessageRole.ASSISTANT.getRoleName(),
|
|
chatResult.getResponseModel().getMessage().getRole().getRoleName(),
|
|
"Role of the response message should be ASSISTANT");
|
|
|
|
boolean toolCalled = false;
|
|
List<OllamaChatMessage> msgs = chatResult.getChatHistory();
|
|
for (OllamaChatMessage msg : msgs) {
|
|
if (msg.getRole().equals(OllamaChatMessageRole.TOOL)) {
|
|
toolCalled = true;
|
|
}
|
|
}
|
|
assertTrue(toolCalled, "Assistant message should contain tool calls when useTools is true");
|
|
}
|
|
|
|
/**
|
|
* Tests chat API with explicit tool invocation and streaming enabled.
|
|
*
|
|
* <p>Scenario: Registers a tool, sends a user message, and streams the assistant's response
|
|
* (with tool call). Usage: chat, explicit tool, useTools=false, streaming enabled.
|
|
*/
|
|
@Test
|
|
@Order(14)
|
|
void shouldChatWithToolsAndStream() throws OllamaException {
|
|
String theToolModel = TOOLS_MODEL;
|
|
api.pullModel(theToolModel);
|
|
|
|
OllamaChatRequestBuilder builder =
|
|
OllamaChatRequestBuilder.builder().withModel(theToolModel);
|
|
|
|
api.registerTool(EmployeeFinderToolSpec.getSpecification());
|
|
|
|
OllamaChatRequest requestModel =
|
|
builder.withMessage(
|
|
OllamaChatMessageRole.USER,
|
|
"Give me the ID and address of employee Rahul Kumar")
|
|
.withKeepAlive("0m")
|
|
.withOptions(new OptionsBuilder().setTemperature(0.9f).build())
|
|
.build();
|
|
requestModel.setUseTools(true);
|
|
OllamaChatResult chatResult = api.chat(requestModel, new ConsoleOutputChatTokenHandler());
|
|
|
|
assertNotNull(chatResult, "chatResult should not be null");
|
|
assertNotNull(chatResult.getResponseModel(), "Response model should not be null");
|
|
assertNotNull(
|
|
chatResult.getResponseModel().getMessage(), "Response message should not be null");
|
|
assertEquals(
|
|
OllamaChatMessageRole.ASSISTANT.getRoleName(),
|
|
chatResult.getResponseModel().getMessage().getRole().getRoleName(),
|
|
"Role of the response message should be ASSISTANT");
|
|
List<OllamaChatToolCalls> toolCalls = chatResult.getChatHistory().get(1).getToolCalls();
|
|
assertEquals(
|
|
1,
|
|
toolCalls.size(),
|
|
"There should be exactly one tool call in the second chat history message");
|
|
OllamaToolCallsFunction function = toolCalls.get(0).getFunction();
|
|
assertEquals(
|
|
"get-employee-details",
|
|
function.getName(),
|
|
"Tool function name should be 'get-employee-details'");
|
|
assertFalse(
|
|
function.getArguments().isEmpty(), "Tool function arguments should not be empty");
|
|
assertTrue(
|
|
chatResult.getChatHistory().size() > 2,
|
|
"Chat history should have more than 2 messages");
|
|
List<OllamaChatToolCalls> finalToolCalls =
|
|
chatResult.getResponseModel().getMessage().getToolCalls();
|
|
assertNull(finalToolCalls, "Final tool calls in the response message should be null");
|
|
}
|
|
|
|
/**
|
|
* Tests chat API with an annotated tool (single parameter).
|
|
*
|
|
* <p>Scenario: Registers annotated tools, sends a user message that triggers a tool call, and
|
|
* verifies the tool call and arguments. Usage: chat, annotated tool, no thinking, no streaming.
|
|
*/
|
|
@Test
|
|
@Order(12)
|
|
void shouldChatWithAnnotatedToolSingleParam() throws OllamaException {
|
|
String theToolModel = TOOLS_MODEL;
|
|
api.pullModel(theToolModel);
|
|
OllamaChatRequestBuilder builder =
|
|
OllamaChatRequestBuilder.builder().withModel(theToolModel);
|
|
|
|
api.registerAnnotatedTools();
|
|
|
|
OllamaChatRequest requestModel =
|
|
builder.withMessage(
|
|
OllamaChatMessageRole.USER,
|
|
"Compute the most important constant in the world using 5 digits")
|
|
.build();
|
|
requestModel.setUseTools(true);
|
|
OllamaChatResult chatResult = api.chat(requestModel, null);
|
|
assertNotNull(chatResult);
|
|
assertNotNull(chatResult.getResponseModel());
|
|
assertNotNull(chatResult.getResponseModel().getMessage());
|
|
assertEquals(
|
|
OllamaChatMessageRole.ASSISTANT.getRoleName(),
|
|
chatResult.getResponseModel().getMessage().getRole().getRoleName());
|
|
List<OllamaChatToolCalls> toolCalls = chatResult.getChatHistory().get(1).getToolCalls();
|
|
assert (!toolCalls.isEmpty());
|
|
OllamaToolCallsFunction function = toolCalls.get(0).getFunction();
|
|
assertEquals("computeImportantConstant", function.getName());
|
|
assert (!function.getArguments().isEmpty());
|
|
Object noOfDigits = function.getArguments().get("noOfDigits");
|
|
assertNotNull(noOfDigits);
|
|
assertEquals("5", noOfDigits.toString());
|
|
assertTrue(chatResult.getChatHistory().size() > 2);
|
|
List<OllamaChatToolCalls> finalToolCalls =
|
|
chatResult.getResponseModel().getMessage().getToolCalls();
|
|
assertNull(finalToolCalls);
|
|
}
|
|
|
|
/**
|
|
* Tests chat API with an annotated tool (multiple parameters).
|
|
*
|
|
* <p>Scenario: Registers annotated tools, sends a user message that may trigger a tool call
|
|
* with multiple arguments. Usage: chat, annotated tool, no thinking, no streaming, multiple
|
|
* parameters.
|
|
*
|
|
* <p>Note: This test is non-deterministic due to model variability; some assertions are
|
|
* commented out.
|
|
*/
|
|
@Test
|
|
@Order(13)
|
|
void shouldChatWithAnnotatedToolMultipleParams() throws OllamaException {
|
|
String theToolModel = TOOLS_MODEL;
|
|
api.pullModel(theToolModel);
|
|
OllamaChatRequestBuilder builder =
|
|
OllamaChatRequestBuilder.builder().withModel(theToolModel);
|
|
|
|
api.registerAnnotatedTools(new AnnotatedTool());
|
|
|
|
OllamaChatRequest requestModel =
|
|
builder.withMessage(
|
|
OllamaChatMessageRole.USER,
|
|
"Greet Rahul with a lot of hearts and respond to me with count of"
|
|
+ " emojis that have been in used in the greeting")
|
|
.build();
|
|
|
|
OllamaChatResult chatResult = api.chat(requestModel, null);
|
|
assertNotNull(chatResult);
|
|
assertNotNull(chatResult.getResponseModel());
|
|
assertNotNull(chatResult.getResponseModel().getMessage());
|
|
assertEquals(
|
|
OllamaChatMessageRole.ASSISTANT.getRoleName(),
|
|
chatResult.getResponseModel().getMessage().getRole().getRoleName());
|
|
}
|
|
|
|
/**
|
|
* Tests chat API with streaming enabled (no tools, no thinking).
|
|
*
|
|
* <p>Scenario: Sends a user message and streams the assistant's response. Usage: chat, no
|
|
* tools, no thinking, streaming enabled.
|
|
*/
|
|
@Test
|
|
@Order(15)
|
|
void shouldChatWithStream() throws OllamaException {
|
|
api.deregisterTools();
|
|
api.pullModel(GENERAL_PURPOSE_MODEL);
|
|
OllamaChatRequestBuilder builder =
|
|
OllamaChatRequestBuilder.builder().withModel(GENERAL_PURPOSE_MODEL);
|
|
OllamaChatRequest requestModel =
|
|
builder.withMessage(
|
|
OllamaChatMessageRole.USER,
|
|
"What is the capital of France? And what's France's connection with"
|
|
+ " Mona Lisa?")
|
|
.build();
|
|
requestModel.setThink(false);
|
|
|
|
OllamaChatResult chatResult = api.chat(requestModel, new ConsoleOutputChatTokenHandler());
|
|
assertNotNull(chatResult);
|
|
assertNotNull(chatResult.getResponseModel());
|
|
assertNotNull(chatResult.getResponseModel().getMessage());
|
|
assertNotNull(chatResult.getResponseModel().getMessage().getResponse());
|
|
}
|
|
|
|
/**
|
|
* Tests chat API with thinking and streaming enabled.
|
|
*
|
|
* <p>Scenario: Sends a user message with thinking enabled and streams the assistant's response.
|
|
* Usage: chat, no tools, thinking enabled, streaming enabled.
|
|
*/
|
|
@Test
|
|
@Order(15)
|
|
void shouldChatWithThinkingAndStream() throws OllamaException {
|
|
api.pullModel(THINKING_TOOL_MODEL_2);
|
|
OllamaChatRequestBuilder builder =
|
|
OllamaChatRequestBuilder.builder().withModel(THINKING_TOOL_MODEL_2);
|
|
OllamaChatRequest requestModel =
|
|
builder.withMessage(
|
|
OllamaChatMessageRole.USER,
|
|
"What is the capital of France? And what's France's connection with"
|
|
+ " Mona Lisa?")
|
|
.withThinking(true)
|
|
.withKeepAlive("0m")
|
|
.build();
|
|
|
|
OllamaChatResult chatResult = api.chat(requestModel, new ConsoleOutputChatTokenHandler());
|
|
|
|
assertNotNull(chatResult);
|
|
assertNotNull(chatResult.getResponseModel());
|
|
assertNotNull(chatResult.getResponseModel().getMessage());
|
|
assertNotNull(chatResult.getResponseModel().getMessage().getResponse());
|
|
}
|
|
|
|
/**
|
|
* Tests chat API with an image input from a URL.
|
|
*
|
|
* <p>Scenario: Sends a user message with an image URL and verifies the assistant's response.
|
|
* Usage: chat, vision model, image from URL, no tools, no thinking, no streaming.
|
|
*/
|
|
@Test
|
|
@Order(10)
|
|
void shouldChatWithImageFromURL() throws OllamaException, IOException, InterruptedException {
|
|
api.pullModel(VISION_MODEL);
|
|
|
|
OllamaChatRequestBuilder builder =
|
|
OllamaChatRequestBuilder.builder().withModel(VISION_MODEL);
|
|
OllamaChatRequest requestModel =
|
|
builder.withMessage(
|
|
OllamaChatMessageRole.USER,
|
|
"What's in the picture?",
|
|
Collections.emptyList(),
|
|
"https://t3.ftcdn.net/jpg/02/96/63/80/360_F_296638053_0gUVA4WVBKceGsIr7LNqRWSnkusi07dq.jpg")
|
|
.build();
|
|
api.registerAnnotatedTools(new OllamaAPIIntegrationTest());
|
|
|
|
OllamaChatResult chatResult = api.chat(requestModel, null);
|
|
assertNotNull(chatResult);
|
|
}
|
|
|
|
/**
|
|
* Tests chat API with an image input from a file and multi-turn history.
|
|
*
|
|
* <p>Scenario: Sends a user message with an image file, then continues the conversation with
|
|
* chat history. Usage: chat, vision model, image from file, multi-turn, no tools, no thinking,
|
|
* no streaming.
|
|
*/
|
|
@Test
|
|
@Order(10)
|
|
void shouldChatWithImageFromFileAndHistory() throws OllamaException {
|
|
api.pullModel(VISION_MODEL);
|
|
OllamaChatRequestBuilder builder =
|
|
OllamaChatRequestBuilder.builder().withModel(VISION_MODEL);
|
|
OllamaChatRequest requestModel =
|
|
builder.withMessage(
|
|
OllamaChatMessageRole.USER,
|
|
"What's in the picture?",
|
|
Collections.emptyList(),
|
|
List.of(getImageFileFromClasspath("emoji-smile.jpeg")))
|
|
.build();
|
|
|
|
OllamaChatResult chatResult = api.chat(requestModel, null);
|
|
assertNotNull(chatResult);
|
|
assertNotNull(chatResult.getResponseModel());
|
|
builder.reset();
|
|
|
|
requestModel =
|
|
builder.withMessages(chatResult.getChatHistory())
|
|
.withMessage(OllamaChatMessageRole.USER, "What's the color?")
|
|
.build();
|
|
|
|
chatResult = api.chat(requestModel, null);
|
|
assertNotNull(chatResult);
|
|
assertNotNull(chatResult.getResponseModel());
|
|
}
|
|
|
|
/**
|
|
* Tests generateWithImages using an image file as input.
|
|
*
|
|
* <p>Scenario: Calls generateWithImages with a vision model and an image file, expecting a
|
|
* non-empty response. Usage: generateWithImages, image from file, no streaming.
|
|
*/
|
|
@Test
|
|
@Order(18)
|
|
void shouldGenerateWithImageFiles() throws OllamaException {
|
|
api.pullModel(VISION_MODEL);
|
|
try {
|
|
OllamaGenerateRequest request =
|
|
OllamaGenerateRequestBuilder.builder()
|
|
.withModel(VISION_MODEL)
|
|
.withPrompt("What is in this image?")
|
|
.withRaw(false)
|
|
.withThink(false)
|
|
.withOptions(new OptionsBuilder().build())
|
|
.withImages(List.of(getImageFileFromClasspath("roses.jpg")))
|
|
.withFormat(null)
|
|
.withKeepAlive("0m")
|
|
.build();
|
|
OllamaGenerateStreamObserver handler = null;
|
|
OllamaResult result = api.generate(request, handler);
|
|
assertNotNull(result);
|
|
assertNotNull(result.getResponse());
|
|
assertFalse(result.getResponse().isEmpty());
|
|
} catch (OllamaException e) {
|
|
fail(e);
|
|
} catch (IOException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tests generateWithImages with image file input and streaming enabled.
|
|
*
|
|
* <p>Scenario: Calls generateWithImages with a vision model, an image file, and a streaming
|
|
* handler for the response. Usage: generateWithImages, image from file, streaming enabled.
|
|
*/
|
|
@Test
|
|
@Order(20)
|
|
void shouldGenerateWithImageFilesAndResponseStreamed() throws OllamaException, IOException {
|
|
api.pullModel(VISION_MODEL);
|
|
OllamaGenerateRequest request =
|
|
OllamaGenerateRequestBuilder.builder()
|
|
.withModel(VISION_MODEL)
|
|
.withPrompt("What is in this image?")
|
|
.withRaw(false)
|
|
.withThink(false)
|
|
.withOptions(new OptionsBuilder().build())
|
|
.withImages(List.of(getImageFileFromClasspath("roses.jpg")))
|
|
.withFormat(null)
|
|
.withKeepAlive("0m")
|
|
.build();
|
|
OllamaGenerateStreamObserver handler =
|
|
new OllamaGenerateStreamObserver(
|
|
new ConsoleOutputGenerateTokenHandler(),
|
|
new ConsoleOutputGenerateTokenHandler());
|
|
OllamaResult result = api.generate(request, handler);
|
|
assertNotNull(result);
|
|
assertNotNull(result.getResponse());
|
|
assertFalse(result.getResponse().isEmpty());
|
|
}
|
|
|
|
/**
|
|
* Tests generate with thinking enabled (no streaming).
|
|
*
|
|
* <p>Scenario: Calls generate with think=true, expecting both response and thinking fields to
|
|
* be populated. Usage: generate, think=true, no streaming.
|
|
*/
|
|
@Test
|
|
@Order(20)
|
|
void shouldGenerateWithThinking() throws OllamaException {
|
|
api.pullModel(THINKING_TOOL_MODEL);
|
|
|
|
boolean raw = false;
|
|
boolean think = true;
|
|
|
|
OllamaGenerateRequest request =
|
|
OllamaGenerateRequestBuilder.builder()
|
|
.withModel(THINKING_TOOL_MODEL)
|
|
.withPrompt("Who are you?")
|
|
.withRaw(raw)
|
|
.withThink(think)
|
|
.withOptions(new OptionsBuilder().build())
|
|
.withFormat(null)
|
|
.withKeepAlive("0m")
|
|
.build();
|
|
OllamaGenerateStreamObserver handler = new OllamaGenerateStreamObserver(null, null);
|
|
|
|
OllamaResult result = api.generate(request, handler);
|
|
assertNotNull(result);
|
|
assertNotNull(result.getResponse());
|
|
assertNotNull(result.getThinking());
|
|
}
|
|
|
|
/**
|
|
* Tests generate with thinking and streaming enabled.
|
|
*
|
|
* <p>Scenario: Calls generate with think=true and a stream handler for both thinking and
|
|
* response tokens. Usage: generate, think=true, streaming enabled.
|
|
*/
|
|
@Test
|
|
@Order(20)
|
|
void shouldGenerateWithThinkingAndStreamHandler() throws OllamaException {
|
|
api.pullModel(THINKING_TOOL_MODEL);
|
|
boolean raw = false;
|
|
OllamaGenerateRequest request =
|
|
OllamaGenerateRequestBuilder.builder()
|
|
.withModel(THINKING_TOOL_MODEL)
|
|
.withPrompt("Who are you?")
|
|
.withRaw(raw)
|
|
.withThink(true)
|
|
.withOptions(new OptionsBuilder().build())
|
|
.withFormat(null)
|
|
.withKeepAlive("0m")
|
|
.build();
|
|
OllamaGenerateStreamObserver handler =
|
|
new OllamaGenerateStreamObserver(
|
|
thinkingToken -> {
|
|
LOG.info(thinkingToken.toUpperCase());
|
|
},
|
|
resToken -> {
|
|
LOG.info(resToken.toLowerCase());
|
|
});
|
|
|
|
OllamaResult result = api.generate(request, handler);
|
|
assertNotNull(result);
|
|
assertNotNull(result.getResponse());
|
|
assertNotNull(result.getThinking());
|
|
}
|
|
|
|
/**
|
|
* Tests generate with raw=true parameter.
|
|
*
|
|
* <p>Scenario: Calls generate with raw=true, which sends the prompt as-is without any
|
|
* formatting. Usage: generate, raw=true, no thinking, no streaming.
|
|
*/
|
|
@Test
|
|
@Order(21)
|
|
void shouldGenerateWithRawMode() throws OllamaException {
|
|
api.pullModel(GENERAL_PURPOSE_MODEL);
|
|
api.unloadModel(GENERAL_PURPOSE_MODEL);
|
|
boolean raw = true;
|
|
boolean thinking = false;
|
|
OllamaGenerateRequest request =
|
|
OllamaGenerateRequestBuilder.builder()
|
|
.withModel(GENERAL_PURPOSE_MODEL)
|
|
.withPrompt("What is 2+2?")
|
|
.withRaw(raw)
|
|
.withThink(thinking)
|
|
.withOptions(new OptionsBuilder().build())
|
|
.withFormat(null)
|
|
.withKeepAlive("0m")
|
|
.build();
|
|
OllamaGenerateStreamObserver handler = new OllamaGenerateStreamObserver(null, null);
|
|
OllamaResult result = api.generate(request, handler);
|
|
assertNotNull(result);
|
|
assertNotNull(result.getResponse());
|
|
assertFalse(result.getResponse().isEmpty());
|
|
}
|
|
|
|
/**
|
|
* Tests generate with raw=true and streaming enabled.
|
|
*
|
|
* <p>Scenario: Calls generate with raw=true and streams the response. Usage: generate,
|
|
* raw=true, no thinking, streaming enabled.
|
|
*/
|
|
@Test
|
|
@Order(22)
|
|
void shouldGenerateWithRawModeAndStreaming() throws OllamaException {
|
|
api.pullModel(GENERAL_PURPOSE_MODEL);
|
|
boolean raw = true;
|
|
OllamaGenerateRequest request =
|
|
OllamaGenerateRequestBuilder.builder()
|
|
.withModel(GENERAL_PURPOSE_MODEL)
|
|
.withPrompt("What is the largest planet in our solar system?")
|
|
.withRaw(raw)
|
|
.withThink(false)
|
|
.withOptions(new OptionsBuilder().build())
|
|
.withFormat(null)
|
|
.withKeepAlive("0m")
|
|
.build();
|
|
OllamaGenerateStreamObserver handler =
|
|
new OllamaGenerateStreamObserver(null, new ConsoleOutputGenerateTokenHandler());
|
|
OllamaResult result = api.generate(request, handler);
|
|
|
|
assertNotNull(result);
|
|
assertNotNull(result.getResponse());
|
|
assertFalse(result.getResponse().isEmpty());
|
|
}
|
|
|
|
/**
|
|
* Tests generate with all parameters enabled: raw=true, thinking=true, and streaming.
|
|
*
|
|
* <p>Scenario: Calls generate with all possible parameters enabled. Usage: generate, raw=true,
|
|
* thinking enabled, streaming enabled.
|
|
*/
|
|
@Test
|
|
@Order(24)
|
|
void shouldGenerateWithAllParametersEnabled() throws OllamaException {
|
|
api.pullModel(THINKING_TOOL_MODEL);
|
|
// Settinng raw here instructs to keep the response raw. Even if the model generates
|
|
// 'thinking' tokens, they will not be received as separate tokens and will be mised with
|
|
// 'response' tokens
|
|
boolean raw = true;
|
|
OllamaGenerateRequest request =
|
|
OllamaGenerateRequestBuilder.builder()
|
|
.withModel(THINKING_TOOL_MODEL)
|
|
.withPrompt(
|
|
"Count 1 to 5. Just give me the numbers and do not give any other"
|
|
+ " details or information.")
|
|
.withRaw(raw)
|
|
.withThink(true)
|
|
.withOptions(new OptionsBuilder().setTemperature(0.1f).build())
|
|
.withFormat(null)
|
|
.withKeepAlive("0m")
|
|
.build();
|
|
OllamaGenerateStreamObserver handler =
|
|
new OllamaGenerateStreamObserver(
|
|
thinkingToken -> LOG.info("Thinking token: {}", thinkingToken),
|
|
responseToken -> LOG.info("Response token: {}", responseToken));
|
|
OllamaResult result = api.generate(request, handler);
|
|
assertNotNull(result);
|
|
assertNotNull(result.getResponse());
|
|
assertNotNull(result.getThinking());
|
|
}
|
|
|
|
/**
|
|
* Tests generateWithFormat with complex nested JSON schema.
|
|
*
|
|
* <p>Scenario: Uses a more complex JSON schema with nested objects and arrays. Usage:
|
|
* generateWithFormat with complex schema.
|
|
*/
|
|
@Test
|
|
@Order(25)
|
|
void shouldGenerateWithComplexStructuredOutput() throws OllamaException {
|
|
api.pullModel(TOOLS_MODEL);
|
|
|
|
String prompt =
|
|
"Generate information about three major cities: their names, populations, and top"
|
|
+ " attractions.";
|
|
|
|
Map<String, Object> format = new HashMap<>();
|
|
format.put("type", "object");
|
|
Map<String, Object> properties = new HashMap<>();
|
|
|
|
Map<String, Object> citiesProperty = new HashMap<>();
|
|
citiesProperty.put("type", "array");
|
|
|
|
Map<String, Object> cityItem = new HashMap<>();
|
|
cityItem.put("type", "object");
|
|
|
|
Map<String, Object> cityProperties = new HashMap<>();
|
|
cityProperties.put("name", Map.of("type", "string"));
|
|
cityProperties.put("population", Map.of("type", "number"));
|
|
|
|
Map<String, Object> attractionsProperty = new HashMap<>();
|
|
attractionsProperty.put("type", "array");
|
|
attractionsProperty.put("items", Map.of("type", "string"));
|
|
cityProperties.put("attractions", attractionsProperty);
|
|
|
|
cityItem.put("properties", cityProperties);
|
|
cityItem.put("required", List.of("name", "population", "attractions"));
|
|
|
|
citiesProperty.put("items", cityItem);
|
|
properties.put("cities", citiesProperty);
|
|
|
|
format.put("properties", properties);
|
|
format.put("required", List.of("cities"));
|
|
|
|
OllamaGenerateRequest request =
|
|
OllamaGenerateRequestBuilder.builder()
|
|
.withModel(TOOLS_MODEL)
|
|
.withPrompt(prompt)
|
|
.withFormat(format)
|
|
.withKeepAlive("0m")
|
|
.build();
|
|
OllamaGenerateStreamObserver handler = null;
|
|
|
|
OllamaResult result = api.generate(request, handler);
|
|
|
|
assertNotNull(result);
|
|
assertNotNull(result.getResponse());
|
|
assertNotNull(result.getStructuredResponse());
|
|
assertTrue(result.getStructuredResponse().containsKey("cities"));
|
|
}
|
|
|
|
/**
|
|
* Tests chat with thinking enabled but no streaming.
|
|
*
|
|
* <p>Scenario: Enables thinking in chat mode without streaming. Usage: chat, thinking enabled,
|
|
* no streaming, no tools.
|
|
*/
|
|
@Test
|
|
@Order(26)
|
|
void shouldChatWithThinkingNoStream() throws OllamaException {
|
|
api.pullModel(THINKING_TOOL_MODEL);
|
|
OllamaChatRequestBuilder builder =
|
|
OllamaChatRequestBuilder.builder().withModel(THINKING_TOOL_MODEL);
|
|
OllamaChatRequest requestModel =
|
|
builder.withMessage(
|
|
OllamaChatMessageRole.USER,
|
|
"What is the meaning of life? Think deeply about this.")
|
|
.withThinking(true)
|
|
.build();
|
|
|
|
OllamaChatResult chatResult = api.chat(requestModel, null);
|
|
|
|
assertNotNull(chatResult);
|
|
assertNotNull(chatResult.getResponseModel());
|
|
assertNotNull(chatResult.getResponseModel().getMessage());
|
|
assertNotNull(chatResult.getResponseModel().getMessage().getResponse());
|
|
// Note: Thinking content might be in the message or separate field depending on
|
|
// implementation
|
|
}
|
|
|
|
/**
|
|
* Tests chat with custom options and streaming.
|
|
*
|
|
* <p>Scenario: Combines custom options (temperature, top_p, etc.) with streaming. Usage: chat,
|
|
* custom options, streaming enabled, no tools, no thinking.
|
|
*/
|
|
@Test
|
|
@Order(27)
|
|
void shouldChatWithCustomOptionsAndStreaming() throws OllamaException {
|
|
api.pullModel(GENERAL_PURPOSE_MODEL);
|
|
|
|
OllamaChatRequestBuilder builder =
|
|
OllamaChatRequestBuilder.builder().withModel(GENERAL_PURPOSE_MODEL);
|
|
OllamaChatRequest requestModel =
|
|
builder.withMessage(
|
|
OllamaChatMessageRole.USER,
|
|
"Tell me a creative story about a time traveler")
|
|
.withOptions(
|
|
new OptionsBuilder()
|
|
.setTemperature(0.9f)
|
|
.setTopP(0.9f)
|
|
.setTopK(40)
|
|
.build())
|
|
.build();
|
|
|
|
OllamaChatResult chatResult = api.chat(requestModel, new ConsoleOutputChatTokenHandler());
|
|
|
|
assertNotNull(chatResult);
|
|
assertNotNull(chatResult.getResponseModel());
|
|
assertNotNull(chatResult.getResponseModel().getMessage().getResponse());
|
|
assertFalse(chatResult.getResponseModel().getMessage().getResponse().isEmpty());
|
|
}
|
|
|
|
/**
|
|
* Tests chat with tools, thinking, and streaming all enabled.
|
|
*
|
|
* <p>Scenario: The most complex chat scenario with all features enabled. Usage: chat, tools,
|
|
* thinking enabled, streaming enabled.
|
|
*/
|
|
@Test
|
|
@Order(28)
|
|
void shouldChatWithToolsThinkingAndStreaming() throws OllamaException {
|
|
api.pullModel(THINKING_TOOL_MODEL_2);
|
|
|
|
api.registerTool(EmployeeFinderToolSpec.getSpecification());
|
|
|
|
OllamaChatRequestBuilder builder =
|
|
OllamaChatRequestBuilder.builder().withModel(THINKING_TOOL_MODEL_2);
|
|
OllamaChatRequest requestModel =
|
|
builder.withMessage(
|
|
OllamaChatMessageRole.USER,
|
|
"I need to find information about employee John Smith. Think"
|
|
+ " carefully about what details to retrieve.")
|
|
.withThinking(true)
|
|
.withOptions(new OptionsBuilder().setTemperature(0.1f).build())
|
|
.build();
|
|
requestModel.setUseTools(false);
|
|
OllamaChatResult chatResult = api.chat(requestModel, new ConsoleOutputChatTokenHandler());
|
|
|
|
assertNotNull(chatResult);
|
|
assertNotNull(chatResult.getResponseModel());
|
|
// Verify that either tools were called or a response was generated
|
|
assertTrue(chatResult.getChatHistory().size() >= 2);
|
|
}
|
|
|
|
/**
|
|
* Tests chat with multiple images in a single message.
|
|
*
|
|
* <p>Scenario: Sends multiple images in one chat message. Usage: chat, vision model, multiple
|
|
* images, no tools, no thinking, no streaming.
|
|
*/
|
|
@Test
|
|
@Order(31)
|
|
void shouldChatWithMultipleImages() throws OllamaException {
|
|
api.pullModel(VISION_MODEL);
|
|
|
|
List<OllamaChatToolCalls> tools = Collections.emptyList();
|
|
|
|
File image1 = getImageFileFromClasspath("emoji-smile.jpeg");
|
|
File image2 = getImageFileFromClasspath("roses.jpg");
|
|
|
|
OllamaChatRequestBuilder builder =
|
|
OllamaChatRequestBuilder.builder().withModel(VISION_MODEL);
|
|
OllamaChatRequest requestModel =
|
|
builder.withMessage(
|
|
OllamaChatMessageRole.USER,
|
|
"Compare these images and tell me what you see",
|
|
tools,
|
|
Arrays.asList(image1, image2))
|
|
.build();
|
|
requestModel.setUseTools(false);
|
|
OllamaChatResult chatResult = api.chat(requestModel, null);
|
|
|
|
assertNotNull(chatResult);
|
|
assertNotNull(chatResult.getResponseModel());
|
|
assertNotNull(chatResult.getResponseModel().getMessage().getResponse());
|
|
assertFalse(chatResult.getResponseModel().getMessage().getResponse().isEmpty());
|
|
}
|
|
|
|
/**
|
|
* Tests error handling when model doesn't exist.
|
|
*
|
|
* <p>Scenario: Attempts to use a non-existent model and verifies proper error handling.
|
|
*/
|
|
@Test
|
|
@Order(32)
|
|
void shouldHandleNonExistentModel() {
|
|
String nonExistentModel = "this-model-does-not-exist:latest";
|
|
OllamaGenerateRequest request =
|
|
OllamaGenerateRequestBuilder.builder()
|
|
.withModel(nonExistentModel)
|
|
.withPrompt("Hello")
|
|
.withRaw(false)
|
|
.withThink(false)
|
|
.withOptions(new OptionsBuilder().build())
|
|
.withKeepAlive("0m")
|
|
.build();
|
|
OllamaGenerateStreamObserver handler = new OllamaGenerateStreamObserver(null, null);
|
|
assertThrows(
|
|
OllamaException.class,
|
|
() -> {
|
|
api.generate(request, handler);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Tests chat with empty message (edge case).
|
|
*
|
|
* <p>Scenario: Sends an empty or whitespace-only message. Usage: chat, edge case testing.
|
|
*/
|
|
@Test
|
|
@Order(33)
|
|
void shouldHandleEmptyMessage() throws OllamaException {
|
|
api.pullModel(GENERAL_PURPOSE_MODEL);
|
|
|
|
List<OllamaChatToolCalls> tools = Collections.emptyList();
|
|
OllamaChatRequestBuilder builder =
|
|
OllamaChatRequestBuilder.builder().withModel(GENERAL_PURPOSE_MODEL);
|
|
OllamaChatRequest requestModel =
|
|
builder.withMessage(OllamaChatMessageRole.USER, " ", tools) // whitespace only
|
|
.build();
|
|
requestModel.setUseTools(false);
|
|
OllamaChatResult chatResult = api.chat(requestModel, null);
|
|
|
|
assertNotNull(chatResult);
|
|
assertNotNull(chatResult.getResponseModel());
|
|
// Should handle gracefully even with empty input
|
|
}
|
|
|
|
/**
|
|
* Tests generate with very high temperature setting.
|
|
*
|
|
* <p>Scenario: Tests extreme parameter values for robustness. Usage: generate, extreme
|
|
* parameters, edge case testing.
|
|
*/
|
|
@Test
|
|
@Order(34)
|
|
void shouldGenerateWithExtremeParameters() throws OllamaException {
|
|
api.pullModel(GENERAL_PURPOSE_MODEL);
|
|
OllamaGenerateRequest request =
|
|
OllamaGenerateRequestBuilder.builder()
|
|
.withModel(GENERAL_PURPOSE_MODEL)
|
|
.withPrompt("Generate a random word")
|
|
.withRaw(false)
|
|
.withThink(false)
|
|
.withOptions(
|
|
new OptionsBuilder()
|
|
.setTemperature(2.0f) // Very high temperature
|
|
.setTopP(1.0f)
|
|
.setTopK(1)
|
|
.build())
|
|
.withKeepAlive("0m")
|
|
.build();
|
|
OllamaGenerateStreamObserver handler = new OllamaGenerateStreamObserver(null, null);
|
|
OllamaResult result = api.generate(request, handler);
|
|
assertNotNull(result);
|
|
assertNotNull(result.getResponse());
|
|
}
|
|
|
|
/**
|
|
* Tests embeddings with single input string.
|
|
*
|
|
* <p>Scenario: Tests embedding generation with a single string instead of array. Usage: embed,
|
|
* single input.
|
|
*/
|
|
@Test
|
|
@Order(35)
|
|
void shouldReturnEmbeddingsForSingleInput() throws Exception {
|
|
api.pullModel(EMBEDDING_MODEL);
|
|
|
|
OllamaEmbedRequest requestModel = new OllamaEmbedRequest();
|
|
requestModel.setModel(EMBEDDING_MODEL);
|
|
requestModel.setInput(
|
|
Collections.singletonList("This is a single test sentence for embedding."));
|
|
|
|
OllamaEmbedResponse embeddings = api.embed(requestModel);
|
|
|
|
assertNotNull(embeddings);
|
|
assertFalse(embeddings.getEmbeddings().isEmpty());
|
|
assertEquals(1, embeddings.getEmbeddings().size());
|
|
}
|
|
|
|
/**
|
|
* Tests chat with keep-alive parameter.
|
|
*
|
|
* <p>Scenario: Tests the keep-alive parameter which controls model unloading. Usage: chat,
|
|
* keep-alive parameter, model lifecycle management.
|
|
*/
|
|
@Test
|
|
@Order(36)
|
|
void shouldChatWithKeepAlive() throws OllamaException {
|
|
api.pullModel(GENERAL_PURPOSE_MODEL);
|
|
|
|
OllamaChatRequestBuilder builder =
|
|
OllamaChatRequestBuilder.builder().withModel(GENERAL_PURPOSE_MODEL);
|
|
OllamaChatRequest requestModel =
|
|
builder.withMessage(OllamaChatMessageRole.USER, "Hello, how are you?")
|
|
.withKeepAlive("5m") // Keep model loaded for 5 minutes
|
|
.build();
|
|
requestModel.setUseTools(false);
|
|
OllamaChatResult chatResult = api.chat(requestModel, null);
|
|
|
|
assertNotNull(chatResult);
|
|
assertNotNull(chatResult.getResponseModel());
|
|
assertNotNull(chatResult.getResponseModel().getMessage().getResponse());
|
|
}
|
|
|
|
/**
|
|
* Tests generate with custom context window options.
|
|
*
|
|
* <p>Scenario: Tests generation with custom context length and other advanced options. Usage:
|
|
* generate, advanced options, context management.
|
|
*/
|
|
@Test
|
|
@Order(37)
|
|
void shouldGenerateWithAdvancedOptions() throws OllamaException {
|
|
api.pullModel(GENERAL_PURPOSE_MODEL);
|
|
OllamaGenerateRequest request =
|
|
OllamaGenerateRequestBuilder.builder()
|
|
.withModel(GENERAL_PURPOSE_MODEL)
|
|
.withPrompt("Write a detailed explanation of machine learning")
|
|
.withRaw(false)
|
|
.withThink(false)
|
|
.withOptions(
|
|
new OptionsBuilder()
|
|
.setTemperature(0.7f)
|
|
.setTopP(0.9f)
|
|
.setTopK(40)
|
|
.setNumCtx(4096) // Context window size
|
|
.setRepeatPenalty(1.1f)
|
|
.build())
|
|
.withKeepAlive("0m")
|
|
.build();
|
|
OllamaGenerateStreamObserver handler = new OllamaGenerateStreamObserver(null, null);
|
|
OllamaResult result = api.generate(request, handler);
|
|
|
|
assertNotNull(result);
|
|
assertNotNull(result.getResponse());
|
|
assertFalse(result.getResponse().isEmpty());
|
|
}
|
|
|
|
/**
|
|
* Tests concurrent chat requests to verify thread safety.
|
|
*
|
|
* <p>Scenario: Sends multiple chat requests concurrently to test thread safety. Usage: chat,
|
|
* concurrency testing, thread safety.
|
|
*/
|
|
@Test
|
|
@Order(38)
|
|
void shouldHandleConcurrentChatRequests() throws OllamaException, InterruptedException {
|
|
api.pullModel(GENERAL_PURPOSE_MODEL);
|
|
|
|
int numThreads = 3;
|
|
CountDownLatch latch = new CountDownLatch(numThreads);
|
|
List<OllamaChatResult> results = Collections.synchronizedList(new ArrayList<>());
|
|
List<Exception> exceptions = Collections.synchronizedList(new ArrayList<>());
|
|
|
|
for (int i = 0; i < numThreads; i++) {
|
|
final int threadId = i;
|
|
Thread thread =
|
|
new Thread(
|
|
() -> {
|
|
try {
|
|
OllamaChatRequestBuilder builder =
|
|
OllamaChatRequestBuilder.builder()
|
|
.withModel(GENERAL_PURPOSE_MODEL);
|
|
OllamaChatRequest requestModel =
|
|
builder.withMessage(
|
|
OllamaChatMessageRole.USER,
|
|
"Hello from thread "
|
|
+ threadId
|
|
+ ". What is 2+2?")
|
|
.build();
|
|
requestModel.setUseTools(false);
|
|
OllamaChatResult result = api.chat(requestModel, null);
|
|
results.add(result);
|
|
} catch (Exception e) {
|
|
exceptions.add(e);
|
|
} finally {
|
|
latch.countDown();
|
|
}
|
|
});
|
|
thread.start();
|
|
}
|
|
|
|
latch.await(60, java.util.concurrent.TimeUnit.SECONDS);
|
|
|
|
assertTrue(exceptions.isEmpty(), "No exceptions should occur during concurrent requests");
|
|
assertEquals(numThreads, results.size(), "All requests should complete successfully");
|
|
|
|
for (OllamaChatResult result : results) {
|
|
assertNotNull(result);
|
|
assertNotNull(result.getResponseModel());
|
|
assertNotNull(result.getResponseModel().getMessage().getResponse());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Utility method to retrieve an image file from the classpath.
|
|
*
|
|
* <p>
|
|
*
|
|
* @param fileName the name of the image file
|
|
* @return the File object for the image
|
|
*/
|
|
private File getImageFileFromClasspath(String fileName) {
|
|
ClassLoader classLoader = getClass().getClassLoader();
|
|
return new File(Objects.requireNonNull(classLoader.getResource(fileName)).getFile());
|
|
}
|
|
}
|
|
|
|
class EmployeeFinderToolSpec {
|
|
private EmployeeFinderToolSpec() {
|
|
/* empty constructor */
|
|
}
|
|
|
|
public static Tools.Tool getSpecification() {
|
|
return Tools.Tool.builder()
|
|
.toolSpec(
|
|
Tools.ToolSpec.builder()
|
|
.name("get-employee-details")
|
|
.description("Get employee details from the company database")
|
|
.parameters(
|
|
Tools.Parameters.of(
|
|
Map.of(
|
|
"employee-name",
|
|
Tools.Property.builder()
|
|
.type("string")
|
|
.description(
|
|
"The name of the employee.")
|
|
.required(true)
|
|
.build(),
|
|
"employee-address",
|
|
Tools.Property.builder()
|
|
.type("string")
|
|
.description(
|
|
"The address of the"
|
|
+ " employee.")
|
|
.required(true)
|
|
.build(),
|
|
"employee-phone",
|
|
Tools.Property.builder()
|
|
.type("string")
|
|
.description(
|
|
"The phone number of the"
|
|
+ " employee.")
|
|
.required(true)
|
|
.build())))
|
|
.build())
|
|
.toolFunction(
|
|
arguments -> {
|
|
String address = null;
|
|
String employeeName = null;
|
|
try {
|
|
employeeName = arguments.get("employee-name").toString();
|
|
} catch (Exception e) {
|
|
employeeName = "Mr. LLoyd Llama";
|
|
}
|
|
try {
|
|
address = arguments.get("employee-address").toString();
|
|
} catch (Exception e) {
|
|
address = "Somewhere on earth.";
|
|
}
|
|
|
|
Random random = new Random();
|
|
long min = 1_000_000_000L;
|
|
long max = 9_999_999_999L;
|
|
String phone =
|
|
String.valueOf(
|
|
min + ((long) (random.nextDouble() * (max - min))));
|
|
|
|
return String.format(
|
|
"Employee Details {ID: %s, Name: %s, Address: %s, Phone: %s}",
|
|
UUID.randomUUID(), employeeName, address, phone);
|
|
})
|
|
.build();
|
|
}
|
|
}
|