Advancing Tool Support - Part 4

* Adopted new tool calling logic in OllamaChatModel, while maintaining full API backward compatibility thanks to the LegacyToolCallingManager.
* Improved efficiency and robustness of merging options in prompts for Ollama.
* Update Ollama Autoconfiguration to use the new ToolCallingManager.
* Improved troubleshooting for new tool calling APIs and finalised changes for full backward compatibility.
* Updated Ollama Testcontainers dependency to 0.5.7.

Relates to gh-2049

Signed-off-by: Thomas Vitale <ThomasVitale@users.noreply.github.com>
This commit is contained in:
Thomas Vitale
2025-01-29 23:26:32 +01:00
committed by Christian Tzolov
parent 76ab91fab8
commit b902ca2afb
31 changed files with 1269 additions and 151 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 the original author or authors.
* Copyright 2023-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,15 +18,21 @@ package org.springframework.ai.ollama;
import java.time.Duration;
import java.util.Base64;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import com.fasterxml.jackson.core.type.TypeReference;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.model.tool.LegacyToolCallingManager;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.ai.util.json.JsonParser;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.messages.AssistantMessage;
@@ -82,6 +88,8 @@ import org.springframework.util.StringUtils;
*/
public class OllamaChatModel extends AbstractToolCallSupport implements ChatModel {
private static final Logger logger = LoggerFactory.getLogger(OllamaChatModel.class);
private static final String DONE = "done";
private static final String METADATA_PROMPT_EVAL_COUNT = "prompt-eval-count";
@@ -100,6 +108,8 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode
private static final ChatModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultChatModelObservationConvention();
private static final ToolCallingManager DEFAULT_TOOL_CALLING_MANAGER = ToolCallingManager.builder().build();
private final OllamaApi chatApi;
private final OllamaOptions defaultOptions;
@@ -108,8 +118,11 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode
private final OllamaModelManager modelManager;
private final ToolCallingManager toolCallingManager;
private ChatModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION;
@Deprecated
public OllamaChatModel(OllamaApi ollamaApi, OllamaOptions defaultOptions,
FunctionCallbackResolver functionCallbackResolver, List<FunctionCallback> toolFunctionCallbacks,
ObservationRegistry observationRegistry, ModelManagementOptions modelManagementOptions) {
@@ -120,6 +133,26 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode
Assert.notNull(modelManagementOptions, "modelManagementOptions must not be null");
this.chatApi = ollamaApi;
this.defaultOptions = defaultOptions;
this.toolCallingManager = new LegacyToolCallingManager(functionCallbackResolver, toolFunctionCallbacks);
this.observationRegistry = observationRegistry;
this.modelManager = new OllamaModelManager(this.chatApi, modelManagementOptions);
initializeModel(defaultOptions.getModel(), modelManagementOptions.pullModelStrategy());
logger.warn("This constructor is deprecated and will be removed in the next milestone. "
+ "Please use the new constructor accepting ToolCallingManager instead.");
}
public OllamaChatModel(OllamaApi ollamaApi, OllamaOptions defaultOptions, ToolCallingManager toolCallingManager,
ObservationRegistry observationRegistry, ModelManagementOptions modelManagementOptions) {
super(null, defaultOptions, List.of());
Assert.notNull(ollamaApi, "ollamaApi must not be null");
Assert.notNull(defaultOptions, "defaultOptions must not be null");
Assert.notNull(toolCallingManager, "toolCallingManager must not be null");
Assert.notNull(observationRegistry, "observationRegistry must not be null");
Assert.notNull(modelManagementOptions, "modelManagementOptions must not be null");
this.chatApi = ollamaApi;
this.defaultOptions = defaultOptions;
this.toolCallingManager = toolCallingManager;
this.observationRegistry = observationRegistry;
this.modelManager = new OllamaModelManager(this.chatApi, modelManagementOptions);
initializeModel(defaultOptions.getModel(), modelManagementOptions.pullModelStrategy());
@@ -186,7 +219,10 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode
@Override
public ChatResponse call(Prompt prompt) {
return this.internalCall(prompt, null);
// Before moving any further, build the final request Prompt,
// merging runtime and default options.
Prompt requestPrompt = buildRequestPrompt(prompt);
return this.internalCall(requestPrompt, null);
}
private ChatResponse internalCall(Prompt prompt, ChatResponse previousChatResponse) {
@@ -196,7 +232,7 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
.prompt(prompt)
.provider(OllamaApi.PROVIDER_NAME)
.requestOptions(buildRequestOptions(request))
.requestOptions(prompt.getOptions())
.build();
ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
@@ -233,9 +269,9 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode
});
if (!isProxyToolCalls(prompt, this.defaultOptions) && response != null
&& isToolCall(response, Set.of("stop"))) {
var toolCallConversation = handleToolCalls(prompt, response);
if (ToolCallingChatOptions.isInternalToolExecutionEnabled(prompt.getOptions()) && response != null
&& response.hasToolCalls()) {
var toolCallConversation = this.toolCallingManager.executeToolCalls(prompt, response);
// Recursively call the call method with the tool call message
// conversation that contains the call responses.
return this.internalCall(new Prompt(toolCallConversation, prompt.getOptions()), response);
@@ -246,7 +282,10 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return this.internalStream(prompt, null);
// Before moving any further, build the final request Prompt,
// merging runtime and default options.
Prompt requestPrompt = buildRequestPrompt(prompt);
return this.internalStream(requestPrompt, null);
}
private Flux<ChatResponse> internalStream(Prompt prompt, ChatResponse previousChatResponse) {
@@ -256,7 +295,7 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode
final ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
.prompt(prompt)
.provider(OllamaApi.PROVIDER_NAME)
.requestOptions(buildRequestOptions(request))
.requestOptions(prompt.getOptions())
.build();
Observation observation = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(
@@ -295,8 +334,8 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode
// @formatter:off
Flux<ChatResponse> chatResponseFlux = chatResponse.flatMap(response -> {
if (isToolCall(response, Set.of("stop"))) {
var toolCallConversation = handleToolCalls(prompt, response);
if (ToolCallingChatOptions.isInternalToolExecutionEnabled(prompt.getOptions()) && response.hasToolCalls()) {
var toolCallConversation = this.toolCallingManager.executeToolCalls(prompt, response);
// Recursively call the stream method with the tool call message
// conversation that contains the call responses.
return this.internalStream(new Prompt(toolCallConversation, prompt.getOptions()), response);
@@ -316,6 +355,48 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode
});
}
Prompt buildRequestPrompt(Prompt prompt) {
// Process runtime options
OllamaOptions runtimeOptions = null;
if (prompt.getOptions() != null) {
if (prompt.getOptions() instanceof ToolCallingChatOptions toolCallingChatOptions) {
runtimeOptions = ModelOptionsUtils.copyToTarget(toolCallingChatOptions, ToolCallingChatOptions.class,
OllamaOptions.class);
}
else if (prompt.getOptions() instanceof FunctionCallingOptions functionCallingOptions) {
runtimeOptions = ModelOptionsUtils.copyToTarget(functionCallingOptions, FunctionCallingOptions.class,
OllamaOptions.class);
}
else {
runtimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class,
OllamaOptions.class);
}
}
// Define request options by merging runtime options and default options
OllamaOptions requestOptions = ModelOptionsUtils.merge(runtimeOptions, this.defaultOptions,
OllamaOptions.class);
// Merge tool names and tool callbacks explicitly since they are ignored by
// Jackson, used by ModelOptionsUtils.
if (runtimeOptions != null) {
requestOptions.setTools(
ToolCallingChatOptions.mergeToolNames(runtimeOptions.getTools(), this.defaultOptions.getTools()));
requestOptions.setToolCallbacks(ToolCallingChatOptions.mergeToolCallbacks(runtimeOptions.getToolCallbacks(),
this.defaultOptions.getToolCallbacks()));
}
else {
requestOptions.setTools(this.defaultOptions.getTools());
requestOptions.setToolCallbacks(this.defaultOptions.getToolCallbacks());
}
// Validate request options
if (!StringUtils.hasText(requestOptions.getModel())) {
throw new IllegalArgumentException("model cannot be null or empty");
}
return new Prompt(prompt.getInstructions(), requestOptions);
}
/**
* Package access for testing.
*/
@@ -338,7 +419,8 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode
if (!CollectionUtils.isEmpty(assistantMessage.getToolCalls())) {
toolCalls = assistantMessage.getToolCalls().stream().map(toolCall -> {
var function = new ToolCallFunction(toolCall.name(),
ModelOptionsUtils.jsonToMap(toolCall.arguments()));
JsonParser.fromJson(toolCall.arguments(), new TypeReference<>() {
}));
return new ToolCall(function);
}).toList();
}
@@ -356,49 +438,24 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode
throw new IllegalArgumentException("Unsupported message type: " + message.getMessageType());
}).flatMap(List::stream).toList();
Set<String> functionsForThisRequest = new HashSet<>();
OllamaOptions requestOptions = (OllamaOptions) prompt.getOptions();
// runtime options
OllamaOptions runtimeOptions = null;
if (prompt.getOptions() != null) {
if (prompt.getOptions() instanceof FunctionCallingOptions functionCallingOptions) {
runtimeOptions = ModelOptionsUtils.copyToTarget(functionCallingOptions, FunctionCallingOptions.class,
OllamaOptions.class);
}
else {
runtimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class,
OllamaOptions.class);
}
functionsForThisRequest.addAll(this.runtimeFunctionCallbackConfigurations(runtimeOptions));
}
if (!CollectionUtils.isEmpty(this.defaultOptions.getFunctions())) {
functionsForThisRequest.addAll(this.defaultOptions.getFunctions());
}
OllamaOptions mergedOptions = ModelOptionsUtils.merge(runtimeOptions, this.defaultOptions, OllamaOptions.class);
// Override the model.
if (!StringUtils.hasText(mergedOptions.getModel())) {
throw new IllegalArgumentException("Model is not set!");
}
String model = mergedOptions.getModel();
OllamaApi.ChatRequest.Builder requestBuilder = OllamaApi.ChatRequest.builder(model)
OllamaApi.ChatRequest.Builder requestBuilder = OllamaApi.ChatRequest.builder(requestOptions.getModel())
.stream(stream)
.messages(ollamaMessages)
.options(mergedOptions);
.options(requestOptions);
if (mergedOptions.getFormat() != null) {
requestBuilder.format(mergedOptions.getFormat());
if (requestOptions.getFormat() != null) {
requestBuilder.format(requestOptions.getFormat());
}
if (mergedOptions.getKeepAlive() != null) {
requestBuilder.keepAlive(mergedOptions.getKeepAlive());
if (requestOptions.getKeepAlive() != null) {
requestBuilder.keepAlive(requestOptions.getKeepAlive());
}
// Add the enabled functions definitions to the request's tools parameter.
if (!CollectionUtils.isEmpty(functionsForThisRequest)) {
requestBuilder.tools(this.getFunctionTools(functionsForThisRequest));
List<ToolDefinition> toolDefinitions = this.toolCallingManager.resolveToolDefinitions(requestOptions);
if (!CollectionUtils.isEmpty(toolDefinitions)) {
requestBuilder.tools(this.getTools(toolDefinitions));
}
return requestBuilder.build();
@@ -417,28 +474,14 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode
}
private List<ChatRequest.Tool> getFunctionTools(Set<String> functionNames) {
return this.resolveFunctionCallbacks(functionNames).stream().map(functionCallback -> {
var function = new ChatRequest.Tool.Function(functionCallback.getName(), functionCallback.getDescription(),
functionCallback.getInputTypeSchema());
return new ChatRequest.Tool(function);
private List<ChatRequest.Tool> getTools(List<ToolDefinition> toolDefinitions) {
return toolDefinitions.stream().map(toolDefinition -> {
var tool = new ChatRequest.Tool.Function(toolDefinition.name(), toolDefinition.description(),
toolDefinition.inputSchema());
return new ChatRequest.Tool(tool);
}).toList();
}
private ChatOptions buildRequestOptions(OllamaApi.ChatRequest request) {
var options = ModelOptionsUtils.mapToClass(request.options(), OllamaOptions.class);
return ChatOptions.builder()
.model(request.model())
.frequencyPenalty(options.getFrequencyPenalty())
.maxTokens(options.getMaxTokens())
.presencePenalty(options.getPresencePenalty())
.stopSequences(options.getStopSequences())
.temperature(options.getTemperature())
.topK(options.getTopK())
.topP(options.getTopP())
.build();
}
@Override
public ChatOptions getDefaultOptions() {
return OllamaOptions.fromOptions(this.defaultOptions);
@@ -468,9 +511,11 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode
private OllamaOptions defaultOptions = OllamaOptions.builder().model(OllamaModel.MISTRAL.id()).build();
private ToolCallingManager toolCallingManager;
private FunctionCallbackResolver functionCallbackResolver;
private List<FunctionCallback> toolFunctionCallbacks = List.of();
private List<FunctionCallback> toolFunctionCallbacks;
private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
@@ -489,11 +534,18 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode
return this;
}
public Builder toolCallingManager(ToolCallingManager toolCallingManager) {
this.toolCallingManager = toolCallingManager;
return this;
}
@Deprecated
public Builder functionCallbackResolver(FunctionCallbackResolver functionCallbackResolver) {
this.functionCallbackResolver = functionCallbackResolver;
return this;
}
@Deprecated
public Builder toolFunctionCallbacks(List<FunctionCallback> toolFunctionCallbacks) {
this.toolFunctionCallbacks = toolFunctionCallbacks;
return this;
@@ -510,8 +562,27 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode
}
public OllamaChatModel build() {
return new OllamaChatModel(this.ollamaApi, this.defaultOptions, this.functionCallbackResolver,
this.toolFunctionCallbacks, this.observationRegistry, this.modelManagementOptions);
if (toolCallingManager != null) {
Assert.isNull(functionCallbackResolver,
"functionCallbackResolver must not be set when toolCallingManager is set");
Assert.isNull(toolFunctionCallbacks,
"toolFunctionCallbacks must not be set when toolCallingManager is set");
return new OllamaChatModel(this.ollamaApi, this.defaultOptions, this.toolCallingManager,
this.observationRegistry, this.modelManagementOptions);
}
if (functionCallbackResolver != null) {
Assert.isNull(toolCallingManager,
"toolCallingManager must not be set when functionCallbackResolver is set");
List<FunctionCallback> toolCallbacks = this.toolFunctionCallbacks != null ? this.toolFunctionCallbacks
: List.of();
return new OllamaChatModel(this.ollamaApi, this.defaultOptions, this.functionCallbackResolver,
toolCallbacks, this.observationRegistry, this.modelManagementOptions);
}
return new OllamaChatModel(this.ollamaApi, this.defaultOptions, DEFAULT_TOOL_CALLING_MANAGER,
this.observationRegistry, this.modelManagementOptions);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 the original author or authors.
* Copyright 2023-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
package org.springframework.ai.ollama.api;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -32,7 +33,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.embedding.EmbeddingOptions;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
@@ -48,7 +50,7 @@ import org.springframework.util.Assert;
* @see <a href="https://github.com/ollama/ollama/blob/main/api/types.go">Ollama Types</a>
*/
@JsonInclude(Include.NON_NULL)
public class OllamaOptions implements FunctionCallingOptions, EmbeddingOptions {
public class OllamaOptions implements ToolCallingChatOptions, EmbeddingOptions {
private static final List<String> NON_SUPPORTED_FIELDS = List.of("model", "format", "keep_alive", "truncate");
@@ -305,6 +307,9 @@ public class OllamaOptions implements FunctionCallingOptions, EmbeddingOptions {
@JsonProperty("truncate")
private Boolean truncate;
@JsonIgnore
private Boolean internalToolExecutionEnabled;
/**
* Tool Function Callbacks to register with the ChatModel.
* For Prompt Options the functionCallbacks are automatically enabled for the duration of the prompt execution.
@@ -312,21 +317,18 @@ public class OllamaOptions implements FunctionCallingOptions, EmbeddingOptions {
* from the registry to be used by the ChatModel chat completion requests.
*/
@JsonIgnore
private List<FunctionCallback> functionCallbacks = new ArrayList<>();
private List<FunctionCallback> toolCallbacks = new ArrayList<>();
/**
* List of functions, identified by their names, to configure for function calling in
* the chat completion requests.
* Functions with those names must exist in the functionCallbacks registry.
* The {@link #functionCallbacks} from the PromptOptions are automatically enabled for the duration of the prompt execution.
* The {@link #toolCallbacks} from the PromptOptions are automatically enabled for the duration of the prompt execution.
* Note that function enabled with the default options are enabled for all chat completion requests. This could impact the token count and the billing.
* If the functions is set in a prompt options, then the enabled functions are only active for the duration of this prompt execution.
*/
@JsonIgnore
private Set<String> functions = new HashSet<>();
@JsonIgnore
private Boolean proxyToolCalls;
private Set<String> toolNames = new HashSet<>();
@JsonIgnore
private Map<String, Object> toolContext;
@@ -381,9 +383,9 @@ public class OllamaOptions implements FunctionCallingOptions, EmbeddingOptions {
.mirostatEta(fromOptions.getMirostatEta())
.penalizeNewline(fromOptions.getPenalizeNewline())
.stop(fromOptions.getStop())
.functions(fromOptions.getFunctions())
.proxyToolCalls(fromOptions.getProxyToolCalls())
.functionCallbacks(fromOptions.getFunctionCallbacks())
.tools(fromOptions.getTools())
.internalToolExecutionEnabled(fromOptions.isInternalToolExecutionEnabled())
.toolCallbacks(fromOptions.getToolCallbacks())
.toolContext(fromOptions.getToolContext()).build();
}
@@ -683,23 +685,73 @@ public class OllamaOptions implements FunctionCallingOptions, EmbeddingOptions {
}
@Override
@JsonIgnore
public List<FunctionCallback> getToolCallbacks() {
return this.toolCallbacks;
}
@Override
@JsonIgnore
public void setToolCallbacks(List<FunctionCallback> toolCallbacks) {
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
this.toolCallbacks = toolCallbacks;
}
@Override
@JsonIgnore
public Set<String> getTools() {
return this.toolNames;
}
@Override
@JsonIgnore
public void setTools(Set<String> toolNames) {
Assert.notNull(toolNames, "toolNames cannot be null");
Assert.noNullElements(toolNames, "toolNames cannot contain null elements");
toolNames.forEach(tool -> Assert.hasText(tool, "toolNames cannot contain empty elements"));
this.toolNames = toolNames;
}
@Override
@Nullable
@JsonIgnore
public Boolean isInternalToolExecutionEnabled() {
return internalToolExecutionEnabled;
}
@Override
@JsonIgnore
public void setInternalToolExecutionEnabled(@Nullable Boolean internalToolExecutionEnabled) {
this.internalToolExecutionEnabled = internalToolExecutionEnabled;
}
@Override
@Deprecated
@JsonIgnore
public List<FunctionCallback> getFunctionCallbacks() {
return this.functionCallbacks;
return this.getToolCallbacks();
}
@Override
@Deprecated
@JsonIgnore
public void setFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
this.functionCallbacks = functionCallbacks;
this.setToolCallbacks(functionCallbacks);
}
@Override
@Deprecated
@JsonIgnore
public Set<String> getFunctions() {
return this.functions;
return this.getTools();
}
@Override
@Deprecated
@JsonIgnore
public void setFunctions(Set<String> functions) {
this.functions = functions;
this.setTools(functions);
}
@Override
@@ -709,20 +761,26 @@ public class OllamaOptions implements FunctionCallingOptions, EmbeddingOptions {
}
@Override
@Deprecated
@JsonIgnore
public Boolean getProxyToolCalls() {
return this.proxyToolCalls;
return this.internalToolExecutionEnabled != null ? !this.internalToolExecutionEnabled : null;
}
@Deprecated
@JsonIgnore
public void setProxyToolCalls(Boolean proxyToolCalls) {
this.proxyToolCalls = proxyToolCalls;
this.internalToolExecutionEnabled = proxyToolCalls != null ? !proxyToolCalls : null;
}
@Override
@JsonIgnore
public Map<String, Object> getToolContext() {
return this.toolContext;
}
@Override
@JsonIgnore
public void setToolContext(Map<String, Object> toolContext) {
this.toolContext = toolContext;
}
@@ -769,9 +827,9 @@ public class OllamaOptions implements FunctionCallingOptions, EmbeddingOptions {
&& Objects.equals(this.mirostat, that.mirostat) && Objects.equals(this.mirostatTau, that.mirostatTau)
&& Objects.equals(this.mirostatEta, that.mirostatEta)
&& Objects.equals(this.penalizeNewline, that.penalizeNewline) && Objects.equals(this.stop, that.stop)
&& Objects.equals(this.functionCallbacks, that.functionCallbacks)
&& Objects.equals(this.proxyToolCalls, that.proxyToolCalls)
&& Objects.equals(this.functions, that.functions) && Objects.equals(this.toolContext, that.toolContext);
&& Objects.equals(this.toolCallbacks, that.toolCallbacks)
&& Objects.equals(this.internalToolExecutionEnabled, that.internalToolExecutionEnabled)
&& Objects.equals(this.toolNames, that.toolNames) && Objects.equals(this.toolContext, that.toolContext);
}
@Override
@@ -781,7 +839,7 @@ public class OllamaOptions implements FunctionCallingOptions, EmbeddingOptions {
this.useMMap, this.useMLock, this.numThread, this.numKeep, this.seed, this.numPredict, this.topK,
this.topP, this.tfsZ, this.typicalP, this.repeatLastN, this.temperature, this.repeatPenalty,
this.presencePenalty, this.frequencyPenalty, this.mirostat, this.mirostatTau, this.mirostatEta,
this.penalizeNewline, this.stop, this.functionCallbacks, this.functions, this.proxyToolCalls,
this.penalizeNewline, this.stop, this.toolCallbacks, this.toolNames, this.internalToolExecutionEnabled,
this.toolContext);
}
@@ -959,25 +1017,53 @@ public class OllamaOptions implements FunctionCallingOptions, EmbeddingOptions {
return this;
}
public Builder toolCallbacks(List<FunctionCallback> toolCallbacks) {
this.options.setToolCallbacks(toolCallbacks);
return this;
}
public Builder toolCallbacks(FunctionCallback... toolCallbacks) {
Assert.notNull(toolCallbacks, "toolCallbacks cannot be null");
this.options.toolCallbacks.addAll(Arrays.asList(toolCallbacks));
return this;
}
public Builder tools(Set<String> toolNames) {
this.options.setTools(toolNames);
return this;
}
public Builder tools(String... toolNames) {
Assert.notNull(toolNames, "toolNames cannot be null");
this.options.toolNames.addAll(Set.of(toolNames));
return this;
}
public Builder internalToolExecutionEnabled(@Nullable Boolean internalToolExecutionEnabled) {
this.options.setInternalToolExecutionEnabled(internalToolExecutionEnabled);
return this;
}
@Deprecated
public Builder functionCallbacks(List<FunctionCallback> functionCallbacks) {
this.options.functionCallbacks = functionCallbacks;
return this;
return toolCallbacks(functionCallbacks);
}
@Deprecated
public Builder functions(Set<String> functions) {
Assert.notNull(functions, "Function names must not be null");
this.options.functions = functions;
return this;
return tools(functions);
}
@Deprecated
public Builder function(String functionName) {
Assert.hasText(functionName, "Function name must not be empty");
this.options.functions.add(functionName);
return this;
return tools(functionName);
}
@Deprecated
public Builder proxyToolCalls(Boolean proxyToolCalls) {
this.options.proxyToolCalls = proxyToolCalls;
if (proxyToolCalls != null) {
this.options.setInternalToolExecutionEnabled(!proxyToolCalls);
}
return this;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023-2024 the original author or authors.
* Copyright 2023-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,7 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Christian Tzolov
* @author Thomas Vitale
*/
public class OllamaChatRequestTests {
class OllamaChatRequestTests {
OllamaChatModel chatModel = OllamaChatModel.builder()
.ollamaApi(new OllamaApi())
@@ -37,9 +37,10 @@ public class OllamaChatRequestTests {
.build();
@Test
public void createRequestWithDefaultOptions() {
void createRequestWithDefaultOptions() {
var prompt = this.chatModel.buildRequestPrompt(new Prompt("Test message content"));
var request = this.chatModel.ollamaChatRequest(new Prompt("Test message content"), false);
var request = this.chatModel.ollamaChatRequest(prompt, false);
assertThat(request.messages()).hasSize(1);
assertThat(request.stream()).isFalse();
@@ -52,12 +53,12 @@ public class OllamaChatRequestTests {
}
@Test
public void createRequestWithPromptOllamaOptions() {
void createRequestWithPromptOllamaOptions() {
// Runtime options should override the default options.
OllamaOptions promptOptions = OllamaOptions.builder().temperature(0.8).topP(0.5).numGPU(2).build();
var prompt = this.chatModel.buildRequestPrompt(new Prompt("Test message content", promptOptions));
var request = this.chatModel.ollamaChatRequest(new Prompt("Test message content", promptOptions), true);
var request = this.chatModel.ollamaChatRequest(prompt, true);
assertThat(request.messages()).hasSize(1);
assertThat(request.stream()).isTrue();
@@ -74,11 +75,11 @@ public class OllamaChatRequestTests {
@Test
public void createRequestWithPromptPortableChatOptions() {
// Ollama runtime options.
ChatOptions portablePromptOptions = ChatOptions.builder().temperature(0.9).topK(100).topP(0.6).build();
var prompt = this.chatModel.buildRequestPrompt(new Prompt("Test message content", portablePromptOptions));
var request = this.chatModel.ollamaChatRequest(new Prompt("Test message content", portablePromptOptions), true);
var request = this.chatModel.ollamaChatRequest(prompt, true);
assertThat(request.messages()).hasSize(1);
assertThat(request.stream()).isTrue();
@@ -92,31 +93,33 @@ public class OllamaChatRequestTests {
@Test
public void createRequestWithPromptOptionsModelOverride() {
// Ollama runtime options.
OllamaOptions promptOptions = OllamaOptions.builder().model("PROMPT_MODEL").build();
var prompt = this.chatModel.buildRequestPrompt(new Prompt("Test message content", promptOptions));
var request = this.chatModel.ollamaChatRequest(new Prompt("Test message content", promptOptions), true);
var request = this.chatModel.ollamaChatRequest(prompt, true);
assertThat(request.model()).isEqualTo("PROMPT_MODEL");
}
@Test
public void createRequestWithDefaultOptionsModelOverride() {
OllamaChatModel chatModel = OllamaChatModel.builder()
.ollamaApi(new OllamaApi())
.defaultOptions(OllamaOptions.builder().model("DEFAULT_OPTIONS_MODEL").build())
.build();
var request = chatModel.ollamaChatRequest(new Prompt("Test message content"), true);
var prompt1 = chatModel.buildRequestPrompt(new Prompt("Test message content"));
var request = chatModel.ollamaChatRequest(prompt1, true);
assertThat(request.model()).isEqualTo("DEFAULT_OPTIONS_MODEL");
// Prompt options should override the default options.
OllamaOptions promptOptions = OllamaOptions.builder().model("PROMPT_MODEL").build();
var prompt2 = chatModel.buildRequestPrompt(new Prompt("Test message content", promptOptions));
request = chatModel.ollamaChatRequest(new Prompt("Test message content", promptOptions), true);
request = chatModel.ollamaChatRequest(prompt2, true);
assertThat(request.model()).isEqualTo("PROMPT_MODEL");
}

View File

@@ -76,7 +76,7 @@ class OllamaWithOpenAiChatModelIT {
private static final String DEFAULT_OLLAMA_MODEL = "mistral";
@Container
static OllamaContainer ollamaContainer = new OllamaContainer("ollama/ollama:0.5.1");
static OllamaContainer ollamaContainer = new OllamaContainer("ollama/ollama:0.5.7");
static String baseUrl = "http://localhost:11434";