feat: Support OLLAMA provider
- Added OllamaClient to handle conversations with the Ollama API. - Introduced OllamaDialect for managing thinking modes. - Created data classes: OllamaFunctionCall, OllamaListedModel, OllamaMessage, OllamaShowRequest, OllamaShowResponse, OllamaTagsResponse, OllamaToolCall, OllamaToolDefinition, and OllamaToolFunction for structured API communication. - Developed unit tests for OllamaClient to validate model listing, chat completion, streaming responses, and tool calling capabilities. - Configured test properties for local Ollama instance.
This commit is contained in:
@@ -34,6 +34,7 @@ import de.extio.lmlib.client.ToolCallData;
|
||||
import de.extio.lmlib.client.ToolDefinition;
|
||||
import de.extio.lmlib.client.ToolParameters;
|
||||
import de.extio.lmlib.profile.ModelCategory;
|
||||
import de.extio.lmlib.profile.ModelProfile;
|
||||
|
||||
class AgentErrorHandlingTest {
|
||||
|
||||
@@ -98,7 +99,8 @@ class AgentErrorHandlingTest {
|
||||
@Test
|
||||
void toolCallDataIsRetainedForFollowUpConversationRequests() {
|
||||
final var client = mock(Client.class);
|
||||
when(client.supportsToolCalling()).thenReturn(true);
|
||||
when(client.supportsToolCalling(any(ModelProfile.class))).thenReturn(true);
|
||||
when(client.supportsToolCalling(any(ModelCategory.class))).thenReturn(true);
|
||||
final var requestCounter = new AtomicInteger();
|
||||
final var firstRequestToolCallData = new AtomicReference<ToolCallData>();
|
||||
final var secondRequestToolCallData = new AtomicReference<ToolCallData>();
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import de.extio.lmlib.agent.responsehandler.JsonAgentResponseHandler;
|
||||
import de.extio.lmlib.agent.responsehandler.TextAgentResponseHandler;
|
||||
@@ -43,7 +43,7 @@ import de.extio.lmlib.profile.ModelProfileService;
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
@PropertySource("classpath:/application-test.properties")
|
||||
@TestPropertySource("classpath:/application-test.properties")
|
||||
public class AgentTest {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AgentTest.class);
|
||||
@@ -367,7 +367,7 @@ public class AgentTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling(final AgentContext context) {
|
||||
public boolean supportsToolCalling(final AgentContext context, final ClientService clientService) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import de.extio.lmlib.client.ClientService;
|
||||
import de.extio.lmlib.client.Conversation;
|
||||
@@ -29,7 +29,7 @@ import de.extio.lmlib.profile.ModelCategory;
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
@Import({ TestCachedClientRepository.Config.class })
|
||||
@PropertySource("classpath:/application-test.properties")
|
||||
@TestPropertySource("classpath:/application-test.properties")
|
||||
public class CachedClientTest {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CachedClientTest.class);
|
||||
|
||||
@@ -20,7 +20,7 @@ import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import de.extio.lmlib.client.ClientService;
|
||||
import de.extio.lmlib.client.CompletionFinishReason;
|
||||
@@ -41,7 +41,7 @@ import de.extio.lmlib.profile.ModelProfileService;
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
@PropertySource("classpath:/application-test.properties")
|
||||
@TestPropertySource("classpath:/application-test.properties")
|
||||
public class CompletionClientTest {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CompletionClientTest.class);
|
||||
@@ -174,8 +174,8 @@ public class CompletionClientTest {
|
||||
toolCallData,
|
||||
false);
|
||||
|
||||
assertTrue(this.chatCompletionclient.supportsToolCalling());
|
||||
assertFalse(this.textCompletionClient.supportsToolCalling());
|
||||
assertTrue(this.chatCompletionclient.supportsToolCalling(modelProfile));
|
||||
assertFalse(this.textCompletionClient.supportsToolCalling(this.modelProfileService.getModelProfile("profile.model.textcompletion")));
|
||||
assertTrue(this.clientService.supportsToolCalling(modelProfile));
|
||||
assertTrue(completion.finishReason() == CompletionFinishReason.TOOL_CALLS);
|
||||
assertEquals(2, completion.toolCalls().size());
|
||||
@@ -226,8 +226,8 @@ public class CompletionClientTest {
|
||||
ToolCallData.required(List.of(getWeather, getCpu)).withParallelToolCalls(true),
|
||||
false);
|
||||
|
||||
assertTrue(this.chatCompletionclient.supportsToolCalling());
|
||||
assertFalse(this.textCompletionClient.supportsToolCalling());
|
||||
assertTrue(this.chatCompletionclient.supportsToolCalling(modelProfile));
|
||||
assertFalse(this.textCompletionClient.supportsToolCalling(this.modelProfileService.getModelProfile("profile.model.textcompletion")));
|
||||
assertTrue(this.clientService.supportsToolCalling(modelProfile));
|
||||
assertTrue(completion.finishReason() == CompletionFinishReason.TOOL_CALLS);
|
||||
assertEquals(2, completion.toolCalls().size());
|
||||
|
||||
152
src/test/java/de/extio/lmlib/client/ollama/OllamaClientTest.java
Normal file
152
src/test/java/de/extio/lmlib/client/ollama/OllamaClientTest.java
Normal file
@@ -0,0 +1,152 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import de.extio.lmlib.client.ClientService;
|
||||
import de.extio.lmlib.client.CompletionFinishReason;
|
||||
import de.extio.lmlib.client.Conversation;
|
||||
import de.extio.lmlib.client.ToolCall;
|
||||
import de.extio.lmlib.client.ToolCallData;
|
||||
import de.extio.lmlib.client.ToolDefinition;
|
||||
import de.extio.lmlib.client.ToolParameters;
|
||||
import de.extio.lmlib.grader.Grader2;
|
||||
import de.extio.lmlib.profile.ModelProfile;
|
||||
import de.extio.lmlib.profile.ModelProfileService;
|
||||
|
||||
@Disabled("This test requires a running Ollama instance")
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
@TestPropertySource("classpath:/application-test.properties")
|
||||
public class OllamaClientTest {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(OllamaClientTest.class);
|
||||
|
||||
@Autowired
|
||||
private OllamaClient ollamaClient;
|
||||
|
||||
@Autowired
|
||||
private ModelProfileService modelProfileService;
|
||||
|
||||
@Autowired
|
||||
private ClientService clientService;
|
||||
|
||||
@Test
|
||||
void listModels() throws Exception {
|
||||
final var modelProfile = this.modelProfile();
|
||||
final var modelNames = this.ollamaClient.getModelNames(modelProfile, true);
|
||||
LOGGER.info("Models: {}", modelNames);
|
||||
assertFalse(modelNames.isEmpty());
|
||||
assertFalse(modelNames.getFirst().isBlank());
|
||||
assertEquals(ModelProfile.ModelProvider.OLLAMA, this.clientService.getClient(modelProfile).getModelProvider());
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatCompletion() throws Exception {
|
||||
final var modelProfile = this.modelProfile();
|
||||
final var completion = this.ollamaClient.conversation(
|
||||
modelProfile,
|
||||
Conversation.create("You are a helpful assistant", "Say the color is green"),
|
||||
null,
|
||||
false);
|
||||
|
||||
LOGGER.info(completion.reasoning());
|
||||
LOGGER.info(completion.response());
|
||||
|
||||
assertTrue(Grader2.assessScoreBinary("Does the text mention the color green?", completion.response(), modelProfile, this.clientService));
|
||||
}
|
||||
|
||||
@Test
|
||||
void streamChatCompletion() throws Exception {
|
||||
final var modelProfile = this.modelProfile();
|
||||
final var conversation = Conversation.create("You are a helpful assistant", "I would like to play a game of chess.");
|
||||
conversation.addTurn(new Conversation.Turn(Conversation.TurnType.ASSISTANT, "Which opening do you choose?"));
|
||||
conversation.addTurn(new Conversation.Turn(Conversation.TurnType.USER, "You choose, but explain the opening to me."));
|
||||
final var responseBuilder = new StringBuilder();
|
||||
final var reasoningBuilder = new StringBuilder();
|
||||
final var completion = this.ollamaClient.streamConversation(modelProfile, conversation, chunk -> {
|
||||
if (chunk.reasoningContent() != null) {
|
||||
System.out.print(chunk.reasoningContent());
|
||||
reasoningBuilder.append(chunk.reasoningContent());
|
||||
}
|
||||
if (chunk.content() != null) {
|
||||
System.out.print(chunk.content());
|
||||
responseBuilder.append(chunk.content());
|
||||
}
|
||||
}, null, false);
|
||||
|
||||
assertEquals(completion.response(), responseBuilder.toString());
|
||||
if (completion.reasoning() != null) {
|
||||
assertEquals(completion.reasoning(), reasoningBuilder.toString());
|
||||
}
|
||||
assertTrue(Grader2.assessScoreBinary("Does the text explain a chess opening?", completion.response(), modelProfile, this.clientService));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toolCallingChatCompletion() throws Exception {
|
||||
final var modelProfile = this.modelProfile();
|
||||
final var weatherTool = new ToolDefinition(
|
||||
"get_weather",
|
||||
"Gets the current weather for a given location.",
|
||||
ToolParameters.create(Map.of("location", "City and country, for example Berlin, Germany")),
|
||||
true);
|
||||
final var conversation = Conversation.create(
|
||||
"You are a helpful assistant. Use the provided tools when the user asks for location information. After receiving tool results, answer the question from the tool output.",
|
||||
"What is the weather in Berlin, Germany? Use the tool.");
|
||||
final var completion = this.ollamaClient.conversation(
|
||||
modelProfile,
|
||||
conversation,
|
||||
ToolCallData.required(List.of(weatherTool)).withParallelToolCalls(true),
|
||||
false);
|
||||
|
||||
LOGGER.info(completion.reasoning());
|
||||
LOGGER.info(completion.response());
|
||||
LOGGER.info("Tool calls: {}", completion.toolCalls());
|
||||
|
||||
assertEquals(CompletionFinishReason.TOOL_CALLS, completion.finishReason());
|
||||
assertFalse(completion.toolCalls().isEmpty());
|
||||
final var weatherToolCall = this.findToolCall(completion.toolCalls(), "get_weather");
|
||||
assertTrue(weatherToolCall.arguments().contains("Berlin"));
|
||||
|
||||
conversation.appendToolCallRound(completion, List.of(
|
||||
new Conversation.ToolResult(ToolParameters.create()
|
||||
.add("forecast", "Berlin is rainy and 21C")
|
||||
.json(), weatherToolCall.id())));
|
||||
LOGGER.info("Conversation after tool call: {}", conversation.getConversation());
|
||||
|
||||
final var finalCompletion = this.ollamaClient.conversation(modelProfile, conversation, ToolCallData.auto(List.of(weatherTool)), false);
|
||||
LOGGER.info(finalCompletion.reasoning());
|
||||
LOGGER.info(finalCompletion.response());
|
||||
LOGGER.info("Tool calls: {}", finalCompletion.toolCalls());
|
||||
assertEquals(CompletionFinishReason.DONE, finalCompletion.finishReason());
|
||||
assertTrue(Grader2.assessScoreBinary("Does the text say that Berlin is rainy and 21C?", finalCompletion.response(), modelProfile, this.clientService));
|
||||
}
|
||||
|
||||
private ModelProfile modelProfile() {
|
||||
return this.modelProfileService.getModelProfile("profile.model.ollama");
|
||||
}
|
||||
|
||||
private ToolCall findToolCall(final List<ToolCall> toolCalls, final String toolName) {
|
||||
return toolCalls.stream()
|
||||
.filter(toolCall -> toolName.equals(toolCall.name()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("Missing tool call: " + toolName));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,7 +28,7 @@ import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.junit.jupiter.api.TestInstance.Lifecycle;
|
||||
|
||||
@@ -45,7 +45,7 @@ import de.extio.lmlib.profile.ModelCategory;
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
@PropertySource("classpath:/application-test.properties")
|
||||
@TestPropertySource("classpath:/application-test.properties")
|
||||
@TestInstance(Lifecycle.PER_CLASS)
|
||||
public class GraderTest {
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package de.extio.lmlib.token;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -13,7 +10,7 @@ import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import de.extio.lmlib.profile.ModelCategory;
|
||||
@@ -22,7 +19,7 @@ import de.extio.lmlib.profile.ModelProfileService;
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
@PropertySource("classpath:/application-test.properties")
|
||||
@TestPropertySource("classpath:/application-test.properties")
|
||||
public class TokenizerTest {
|
||||
|
||||
private static final String TEXT = "Hello this is a text! How are you doing? The weather is great today! Super sunny shiny!!!";
|
||||
|
||||
@@ -6,8 +6,12 @@ profile.model.cold=local-chatcompletion
|
||||
|
||||
profile.model.textcompletion=local-completion
|
||||
profile.model.chatcompletion=local-chatcompletion
|
||||
profile.model.ollama=local-ollama-chatcompletion
|
||||
#profile.model.azure=llama3.3-70b-azure
|
||||
|
||||
agent.threads=2
|
||||
|
||||
client.collectStatistics=true
|
||||
|
||||
logging.level.root=INFO
|
||||
logging.level.de.extio.lmlib=DEBUG
|
||||
|
||||
16
src/test/resources/local-ollama-chatcompletion.properties
Normal file
16
src/test/resources/local-ollama-chatcompletion.properties
Normal file
@@ -0,0 +1,16 @@
|
||||
category=OLLAMA
|
||||
prompts=
|
||||
tokenizer=fallback
|
||||
tokenEncoding=cl100k_base
|
||||
maxTokens=2500
|
||||
maxContextLength=32768
|
||||
temperature=0.3
|
||||
topP=0.95
|
||||
modelProvider=OLLAMA
|
||||
modelName=
|
||||
url=http://localhost:11434
|
||||
apiKey=
|
||||
cost1MInTokens=0
|
||||
cost1MCachedInTokens=0
|
||||
cost1MOutTokens=0
|
||||
cost1MReasoningOutTokens=0
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<include resource="org/springframework/boot/logging/logback/base.xml" />
|
||||
<logger name="de.extio" level="INFO"/>
|
||||
<logger name="de.extio" level="DEBUG"/>
|
||||
</configuration>
|
||||
|
||||
Reference in New Issue
Block a user