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:
@@ -31,7 +31,6 @@ import de.extio.lmlib.client.Conversation.Turn;
|
||||
import de.extio.lmlib.client.Conversation.TurnType;
|
||||
import de.extio.lmlib.profile.ModelCategory;
|
||||
import de.extio.lmlib.profile.ModelProfile;
|
||||
import de.extio.lmlib.profile.ModelProfile.ModelProvider;
|
||||
|
||||
public interface BaseAgent {
|
||||
|
||||
@@ -83,15 +82,7 @@ public interface BaseAgent {
|
||||
return ToolCallData.auto(toolDefinitions);
|
||||
}
|
||||
|
||||
default boolean supportsToolCalling(final AgentContext context) {
|
||||
final var modelProfile = this.modelProfile(context);
|
||||
return modelProfile != null && modelProfile.modelProvider() == ModelProvider.OAI_CHAT_COMPLETION;
|
||||
}
|
||||
|
||||
default boolean supportsToolCalling(final AgentContext context, final ClientService clientService) {
|
||||
if (clientService == null) {
|
||||
return this.supportsToolCalling(context);
|
||||
}
|
||||
final var modelProfile = this.modelProfile(context);
|
||||
if (modelProfile != null) {
|
||||
return clientService.supportsToolCalling(modelProfile);
|
||||
@@ -161,7 +152,9 @@ public interface BaseAgent {
|
||||
final boolean skipCache = split.context().isSkipCache() || split.context().isAlwaysSkipCache();
|
||||
split.context().setSkipCache(false);
|
||||
final var conversation = this.setupConversation(split);
|
||||
var toolCallData = completionClient.supportsToolCalling() ? this.toolCallData(split.context()) : null;
|
||||
final var requestedModelProfile = this.modelProfile(split.context());
|
||||
final var supportsToolCalling = requestedModelProfile != null ? completionClient.supportsToolCalling(requestedModelProfile) : completionClient.supportsToolCalling(this.modelCategory(split.context()));
|
||||
var toolCallData = supportsToolCalling ? this.toolCallData(split.context()) : null;
|
||||
this.rememberToolCallData(split.context(), toolCallData);
|
||||
|
||||
boolean parseable = false;
|
||||
|
||||
@@ -21,5 +21,7 @@ public interface Client {
|
||||
|
||||
ModelProvider getModelProvider();
|
||||
|
||||
boolean supportsToolCalling();
|
||||
boolean supportsToolCalling(ModelCategory modelCategory);
|
||||
|
||||
boolean supportsToolCalling(ModelProfile modelProfile);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import de.extio.lmlib.client.ollama.OllamaClient;
|
||||
import de.extio.lmlib.client.oai.completion.chat.ChatCompletionClient;
|
||||
import de.extio.lmlib.client.oai.completion.text.TextCompletionClient;
|
||||
import de.extio.lmlib.profile.ProfileAutoConfiguration;
|
||||
@@ -33,5 +34,11 @@ public class ClientAutoConfiguration {
|
||||
ChatCompletionClient chatCompletionClient() {
|
||||
return new ChatCompletionClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
OllamaClient ollamaClient() {
|
||||
return new OllamaClient();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public class ClientService {
|
||||
}
|
||||
|
||||
public boolean supportsToolCalling(final ModelCategory category) {
|
||||
return this.getClient(category).supportsToolCalling();
|
||||
return this.getClient(category).supportsToolCalling(category);
|
||||
}
|
||||
|
||||
@Cacheable("clients")
|
||||
@@ -56,7 +56,7 @@ public class ClientService {
|
||||
}
|
||||
|
||||
public boolean supportsToolCalling(final ModelProfile modelProfile) {
|
||||
return this.getClient(modelProfile).supportsToolCalling();
|
||||
return this.getClient(modelProfile).supportsToolCalling(modelProfile);
|
||||
}
|
||||
|
||||
private static class InterceptingClient implements Client {
|
||||
@@ -122,8 +122,13 @@ public class ClientService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling() {
|
||||
return this.client.supportsToolCalling();
|
||||
public boolean supportsToolCalling(final ModelCategory modelCategory) {
|
||||
return this.client.supportsToolCalling(modelCategory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling(final ModelProfile modelProfile) {
|
||||
return this.client.supportsToolCalling(modelProfile);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -61,7 +61,8 @@ public final class Conversation {
|
||||
if (completion == null) {
|
||||
throw new IllegalArgumentException("completion must not be null");
|
||||
}
|
||||
this.appendToolCallRound(completion.response(), completion.toolCalls(), toolResults);
|
||||
final var assistantText = completion.toolCalls().isEmpty() ? completion.response() : "";
|
||||
this.appendToolCallRound(assistantText, completion.toolCalls(), toolResults);
|
||||
}
|
||||
|
||||
public void appendToolCallRound(final String assistantText, final List<ToolCall> toolCalls, final List<ToolResult> toolResults) {
|
||||
|
||||
@@ -78,8 +78,13 @@ public class CachedClient implements Client {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling() {
|
||||
return this.client.supportsToolCalling();
|
||||
public boolean supportsToolCalling(final ModelCategory modelCategory) {
|
||||
return this.client.supportsToolCalling(modelCategory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling(final ModelProfile modelProfile) {
|
||||
return this.client.supportsToolCalling(modelProfile);
|
||||
}
|
||||
|
||||
private Completion getCachedResponse(final ModelCategory modelCategory, final ModelProfile modelProfile, final boolean skipCache, final Supplier<Completion> supplier, final String... keys) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import de.extio.lmlib.client.ToolCallData;
|
||||
import de.extio.lmlib.client.ToolDefinition;
|
||||
import de.extio.lmlib.client.Conversation.Turn;
|
||||
import de.extio.lmlib.client.oai.completion.AbstractCompletionClient;
|
||||
import de.extio.lmlib.profile.ModelCategory;
|
||||
import de.extio.lmlib.profile.ModelProfile;
|
||||
import de.extio.lmlib.profile.ModelProfile.ModelProvider;
|
||||
|
||||
@@ -36,8 +37,14 @@ public class ChatCompletionClient extends AbstractCompletionClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling() {
|
||||
return true;
|
||||
public boolean supportsToolCalling(final ModelCategory modelCategory) {
|
||||
final var modelProfile = this.modelProfileService.getModelProfile(modelCategory.getModelProfile(), modelCategory);
|
||||
return this.supportsToolCalling(modelProfile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling(final ModelProfile modelProfile) {
|
||||
return modelProfile != null && modelProfile.modelProvider() == ModelProvider.OAI_CHAT_COMPLETION;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,6 +23,7 @@ import de.extio.lmlib.client.Conversation;
|
||||
import de.extio.lmlib.client.ToolCallData;
|
||||
import de.extio.lmlib.client.Conversation.Turn;
|
||||
import de.extio.lmlib.client.oai.completion.AbstractCompletionClient;
|
||||
import de.extio.lmlib.profile.ModelCategory;
|
||||
import de.extio.lmlib.profile.ModelProfile;
|
||||
import de.extio.lmlib.profile.ModelProfile.ModelProvider;
|
||||
import de.extio.lmlib.prompt.PromptStrategy;
|
||||
@@ -41,7 +42,12 @@ public class TextCompletionClient extends AbstractCompletionClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling() {
|
||||
public boolean supportsToolCalling(final ModelCategory modelCategory) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling(final ModelProfile modelProfile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaChatRequest {
|
||||
|
||||
private String model;
|
||||
|
||||
private List<OllamaMessage> messages;
|
||||
|
||||
private List<OllamaToolDefinition> tools;
|
||||
|
||||
private Map<String, Object> options;
|
||||
|
||||
private Boolean stream;
|
||||
|
||||
private Object think;
|
||||
|
||||
@JsonProperty("model")
|
||||
public String getModel() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
public void setModel(final String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@JsonProperty("messages")
|
||||
public List<OllamaMessage> getMessages() {
|
||||
return this.messages;
|
||||
}
|
||||
|
||||
public void setMessages(final List<OllamaMessage> messages) {
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
@JsonProperty("tools")
|
||||
public List<OllamaToolDefinition> getTools() {
|
||||
return this.tools;
|
||||
}
|
||||
|
||||
public void setTools(final List<OllamaToolDefinition> tools) {
|
||||
this.tools = tools;
|
||||
}
|
||||
|
||||
@JsonProperty("options")
|
||||
public Map<String, Object> getOptions() {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
public void setOptions(final Map<String, Object> options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
@JsonProperty("stream")
|
||||
public Boolean getStream() {
|
||||
return this.stream;
|
||||
}
|
||||
|
||||
public void setStream(final Boolean stream) {
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
@JsonProperty("think")
|
||||
public Object getThink() {
|
||||
return this.think;
|
||||
}
|
||||
|
||||
public void setThink(final Object think) {
|
||||
this.think = think;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaChatResponse {
|
||||
|
||||
private String model;
|
||||
|
||||
private String createdAt;
|
||||
|
||||
private OllamaMessage message;
|
||||
|
||||
private Boolean done;
|
||||
|
||||
private String doneReason;
|
||||
|
||||
private Long totalDuration;
|
||||
|
||||
private Long loadDuration;
|
||||
|
||||
private Integer promptEvalCount;
|
||||
|
||||
private Long promptEvalDuration;
|
||||
|
||||
private Integer evalCount;
|
||||
|
||||
private Long evalDuration;
|
||||
|
||||
private String error;
|
||||
|
||||
@JsonProperty("model")
|
||||
public String getModel() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
public void setModel(final String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@JsonProperty("created_at")
|
||||
public String getCreatedAt() {
|
||||
return this.createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(final String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
@JsonProperty("message")
|
||||
public OllamaMessage getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
public void setMessage(final OllamaMessage message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@JsonProperty("done")
|
||||
public Boolean getDone() {
|
||||
return this.done;
|
||||
}
|
||||
|
||||
public void setDone(final Boolean done) {
|
||||
this.done = done;
|
||||
}
|
||||
|
||||
@JsonProperty("done_reason")
|
||||
public String getDoneReason() {
|
||||
return this.doneReason;
|
||||
}
|
||||
|
||||
public void setDoneReason(final String doneReason) {
|
||||
this.doneReason = doneReason;
|
||||
}
|
||||
|
||||
@JsonProperty("total_duration")
|
||||
public Long getTotalDuration() {
|
||||
return this.totalDuration;
|
||||
}
|
||||
|
||||
public void setTotalDuration(final Long totalDuration) {
|
||||
this.totalDuration = totalDuration;
|
||||
}
|
||||
|
||||
@JsonProperty("load_duration")
|
||||
public Long getLoadDuration() {
|
||||
return this.loadDuration;
|
||||
}
|
||||
|
||||
public void setLoadDuration(final Long loadDuration) {
|
||||
this.loadDuration = loadDuration;
|
||||
}
|
||||
|
||||
@JsonProperty("prompt_eval_count")
|
||||
public Integer getPromptEvalCount() {
|
||||
return this.promptEvalCount;
|
||||
}
|
||||
|
||||
public void setPromptEvalCount(final Integer promptEvalCount) {
|
||||
this.promptEvalCount = promptEvalCount;
|
||||
}
|
||||
|
||||
@JsonProperty("prompt_eval_duration")
|
||||
public Long getPromptEvalDuration() {
|
||||
return this.promptEvalDuration;
|
||||
}
|
||||
|
||||
public void setPromptEvalDuration(final Long promptEvalDuration) {
|
||||
this.promptEvalDuration = promptEvalDuration;
|
||||
}
|
||||
|
||||
@JsonProperty("eval_count")
|
||||
public Integer getEvalCount() {
|
||||
return this.evalCount;
|
||||
}
|
||||
|
||||
public void setEvalCount(final Integer evalCount) {
|
||||
this.evalCount = evalCount;
|
||||
}
|
||||
|
||||
@JsonProperty("eval_duration")
|
||||
public Long getEvalDuration() {
|
||||
return this.evalDuration;
|
||||
}
|
||||
|
||||
public void setEvalDuration(final Long evalDuration) {
|
||||
this.evalDuration = evalDuration;
|
||||
}
|
||||
|
||||
@JsonProperty("error")
|
||||
public String getError() {
|
||||
return this.error;
|
||||
}
|
||||
|
||||
public void setError(final String error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
void copyFrom(final OllamaChatResponse other) {
|
||||
if (other.model != null && !other.model.isBlank()) {
|
||||
this.model = other.model;
|
||||
}
|
||||
if (other.createdAt != null && !other.createdAt.isBlank()) {
|
||||
this.createdAt = other.createdAt;
|
||||
}
|
||||
if (other.done != null) {
|
||||
this.done = other.done;
|
||||
}
|
||||
if (other.doneReason != null && !other.doneReason.isBlank()) {
|
||||
this.doneReason = other.doneReason;
|
||||
}
|
||||
if (other.totalDuration != null) {
|
||||
this.totalDuration = other.totalDuration;
|
||||
}
|
||||
if (other.loadDuration != null) {
|
||||
this.loadDuration = other.loadDuration;
|
||||
}
|
||||
if (other.promptEvalCount != null) {
|
||||
this.promptEvalCount = other.promptEvalCount;
|
||||
}
|
||||
if (other.promptEvalDuration != null) {
|
||||
this.promptEvalDuration = other.promptEvalDuration;
|
||||
}
|
||||
if (other.evalCount != null) {
|
||||
this.evalCount = other.evalCount;
|
||||
}
|
||||
if (other.evalDuration != null) {
|
||||
this.evalDuration = other.evalDuration;
|
||||
}
|
||||
if (other.error != null && !other.error.isBlank()) {
|
||||
this.error = other.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
577
src/main/java/de/extio/lmlib/client/ollama/OllamaClient.java
Normal file
577
src/main/java/de/extio/lmlib/client/ollama/OllamaClient.java
Normal file
@@ -0,0 +1,577 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestClientResponseException;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
import de.extio.lmlib.client.Chunk;
|
||||
import de.extio.lmlib.client.Client;
|
||||
import de.extio.lmlib.client.Completion;
|
||||
import de.extio.lmlib.client.CompletionFinishReason;
|
||||
import de.extio.lmlib.client.CompletionStatistics;
|
||||
import de.extio.lmlib.client.Conversation;
|
||||
import de.extio.lmlib.client.Conversation.Turn;
|
||||
import de.extio.lmlib.client.ToolCall;
|
||||
import de.extio.lmlib.client.ToolCallData;
|
||||
import de.extio.lmlib.client.ToolDefinition;
|
||||
import de.extio.lmlib.client.oai.completion.FinishReasons;
|
||||
import de.extio.lmlib.profile.ModelCategory;
|
||||
import de.extio.lmlib.profile.ModelProfile;
|
||||
import de.extio.lmlib.profile.ModelProfile.ModelProvider;
|
||||
import de.extio.lmlib.profile.ModelProfileService;
|
||||
import de.extio.lmlib.token.TokenizerResolver;
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.DeserializationFeature;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
public class OllamaClient implements Client {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(OllamaClient.class);
|
||||
|
||||
private static final OllamaDialect DEFAULT_OLLAMA_DIALECT = new OllamaDialect() {
|
||||
};
|
||||
|
||||
private final Map<String, List<String>> resolvedModelNames = new ConcurrentHashMap<>();
|
||||
|
||||
private final Map<String, OllamaShowResponse> resolvedModelDetails = new ConcurrentHashMap<>();
|
||||
|
||||
private final ObjectMapper objectMapper = JsonMapper.builder()
|
||||
.changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.NON_NULL))
|
||||
.changeDefaultPropertyInclusion(incl -> incl.withContentInclusion(JsonInclude.Include.NON_NULL))
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
.build();
|
||||
|
||||
@Autowired
|
||||
@Qualifier("lmLibRestClientBuilder")
|
||||
private RestClient.Builder restClientBuilder;
|
||||
|
||||
@Autowired
|
||||
private TokenizerResolver tokenizerResolver;
|
||||
|
||||
@Autowired
|
||||
private ModelProfileService modelProfileService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private OllamaDialect ollamaDialect;
|
||||
|
||||
@Override
|
||||
public Completion conversation(final ModelCategory modelCategory, final Conversation conversation, final ToolCallData toolCallData, final boolean skipCache) {
|
||||
return this.streamConversation(modelCategory, conversation, null, toolCallData, skipCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completion conversation(final ModelProfile modelProfile, final Conversation conversation, final ToolCallData toolCallData, final boolean skipCache) {
|
||||
return this.streamConversation(modelProfile, conversation, null, toolCallData, skipCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completion streamConversation(final ModelCategory modelCategory_, final Conversation conversation, final Consumer<Chunk> chunkConsumer, final ToolCallData toolCallData, final boolean skipCache) {
|
||||
final var modelCategory = modelCategory_ == null ? ModelCategory.MEDIUM : modelCategory_;
|
||||
final var modelProfile = this.modelProfileService.getModelProfile(modelCategory.getModelProfile(), modelCategory);
|
||||
return this.streamConversation(modelProfile, conversation, chunkConsumer, toolCallData, skipCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completion streamConversation(final ModelProfile modelProfile, final Conversation conversation, final Consumer<Chunk> chunkConsumer, final ToolCallData toolCallData, final boolean skipCache) {
|
||||
if (modelProfile == null || modelProfile.modelProvider() != ModelProvider.OLLAMA) {
|
||||
throw new IllegalArgumentException("Invalid ModelProfile");
|
||||
}
|
||||
|
||||
final var request = this.createRequest(conversation, modelProfile, chunkConsumer != null, toolCallData);
|
||||
final String requestBody;
|
||||
try {
|
||||
requestBody = this.objectMapper.writeValueAsString(request);
|
||||
}
|
||||
catch (final JacksonException e) {
|
||||
throw new IllegalStateException("Cannot serialize request body", e);
|
||||
}
|
||||
|
||||
LOGGER.debug("Requesting Ollama chat at {}", modelProfile.url());
|
||||
final var start = LocalDateTime.now();
|
||||
final var restClient = this.restClientBuilder.baseUrl(modelProfile.url()).build();
|
||||
|
||||
var requestSpec = restClient
|
||||
.method(HttpMethod.POST)
|
||||
.uri("/api/chat")
|
||||
.header("Content-Type", "application/json");
|
||||
if (modelProfile.apiKey() != null && !modelProfile.apiKey().isBlank()) {
|
||||
requestSpec = requestSpec.header("Authorization", "Bearer " + modelProfile.apiKey());
|
||||
}
|
||||
|
||||
final var response = requestSpec
|
||||
.body(requestBody)
|
||||
.exchange((clientRequest, clientResponse) -> {
|
||||
if (clientResponse.getStatusCode().isError()) {
|
||||
this.logErrorResponse(clientResponse.getStatusCode(), clientResponse.getBody());
|
||||
throw new IllegalStateException("Error response from server: " + clientResponse.getStatusCode());
|
||||
}
|
||||
if (chunkConsumer == null) {
|
||||
final var ollamaResponse = this.objectMapper.readValue(clientResponse.getBody(), OllamaChatResponse.class);
|
||||
this.throwIfEmbeddedError(ollamaResponse);
|
||||
return ollamaResponse;
|
||||
}
|
||||
return this.readStream(clientResponse.getBody(), chunkConsumer);
|
||||
});
|
||||
|
||||
this.throwIfEmbeddedError(response);
|
||||
final var content = response.getMessage() != null ? response.getMessage().getContent() : null;
|
||||
final var reasoning = response.getMessage() != null ? response.getMessage().getThinking() : null;
|
||||
final var toolCalls = this.toToolCalls(response.getMessage() != null ? response.getMessage().getToolCalls() : List.of());
|
||||
final var finishReason = this.mapFinishReason(response.getDoneReason(), toolCalls);
|
||||
final var statistics = this.createCompletionStatistics(modelProfile, start, response, conversation, content, reasoning);
|
||||
return new Completion(content, reasoning, finishReason, statistics, toolCalls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getModelNames(final ModelProfile modelProfile, final boolean forceReload) {
|
||||
if (modelProfile == null || modelProfile.modelProvider() != ModelProvider.OLLAMA) {
|
||||
return List.of();
|
||||
}
|
||||
if (modelProfile.modelName() != null && !modelProfile.modelName().isBlank()) {
|
||||
return List.of(modelProfile.modelName());
|
||||
}
|
||||
if (modelProfile.url() == null || modelProfile.url().isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
final var cacheKey = modelProfile.category() + "|" + modelProfile.url() + "|" + modelProfile.apiKey();
|
||||
if (forceReload) {
|
||||
final var modelNames = this.loadModelNames(modelProfile);
|
||||
this.resolvedModelNames.put(cacheKey, modelNames);
|
||||
return modelNames;
|
||||
}
|
||||
return this.resolvedModelNames.computeIfAbsent(cacheKey, key -> this.loadModelNames(modelProfile));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelProvider getModelProvider() {
|
||||
return ModelProvider.OLLAMA;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling(final ModelCategory modelCategory) {
|
||||
final var modelProfile = this.modelProfileService.getModelProfile(modelCategory.getModelProfile(), modelCategory);
|
||||
return this.supportsToolCalling(modelProfile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsToolCalling(final ModelProfile modelProfile) {
|
||||
if (modelProfile == null || modelProfile.modelProvider() != ModelProvider.OLLAMA) {
|
||||
return false;
|
||||
}
|
||||
final var modelName = this.getPrimaryModelName(modelProfile);
|
||||
if (modelName == null || modelName.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
return this.supportsToolCapability(this.getModelDetails(modelProfile, modelName));
|
||||
}
|
||||
|
||||
private OllamaChatRequest createRequest(final Conversation conversation, final ModelProfile modelProfile, final boolean streaming, final ToolCallData toolCallData) {
|
||||
final var modelName = this.getPrimaryModelName(modelProfile);
|
||||
if (modelName == null || modelName.isBlank()) {
|
||||
throw new IllegalStateException("No model name resolved for Ollama profile " + modelProfile.category());
|
||||
}
|
||||
final var modelDetails = this.getModelDetails(modelProfile, modelName);
|
||||
final var request = new OllamaChatRequest();
|
||||
request.setModel(modelName);
|
||||
request.setMessages(this.createMessages(conversation));
|
||||
request.setStream(streaming);
|
||||
final var think = this.createThinkRequestValue(modelProfile, modelDetails);
|
||||
if (think != null) {
|
||||
request.setThink(think);
|
||||
}
|
||||
request.setOptions(this.createOptions(modelProfile));
|
||||
if (toolCallData != null && toolCallData.hasTools()) {
|
||||
if (this.supportsToolCapability(modelDetails)) {
|
||||
request.setTools(this.createTools(toolCallData.tools()));
|
||||
}
|
||||
else {
|
||||
LOGGER.debug("Ollama model {} does not advertise tool capability. Tools will be omitted from the request.", modelName);
|
||||
}
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private Object createThinkRequestValue(final ModelProfile modelProfile, final OllamaShowResponse modelDetails) {
|
||||
final var thinkMode = this.getOllamaDialect().think(modelProfile);
|
||||
if (thinkMode == null || !thinkMode.isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
if (!this.supportsThinkingCapability(modelDetails)) {
|
||||
LOGGER.debug("Ollama model {} does not advertise thinking capability. Think will be omitted from the request.", this.getPrimaryModelName(modelProfile));
|
||||
return null;
|
||||
}
|
||||
return thinkMode;
|
||||
}
|
||||
|
||||
private List<OllamaMessage> createMessages(final Conversation conversation) {
|
||||
final List<OllamaMessage> messages = new ArrayList<>();
|
||||
final Map<String, String> toolCallNamesById = new LinkedHashMap<>();
|
||||
for (final Turn turn : conversation.getConversation()) {
|
||||
final var message = new OllamaMessage();
|
||||
message.setRole(this.mapRole(turn));
|
||||
message.setContent(turn.text() != null ? turn.text() : "");
|
||||
if (!turn.toolCalls().isEmpty()) {
|
||||
message.setToolCalls(turn.toolCalls().stream().map(toolCall -> {
|
||||
final var function = new OllamaFunctionCall();
|
||||
function.setName(toolCall.name());
|
||||
function.setArguments(this.parseJsonOrText(toolCall.arguments()));
|
||||
final var ollamaToolCall = new OllamaToolCall();
|
||||
ollamaToolCall.setId(toolCall.id());
|
||||
ollamaToolCall.setType(toolCall.type());
|
||||
ollamaToolCall.setFunction(function);
|
||||
if (toolCall.id() != null && !toolCall.id().isBlank() && toolCall.name() != null && !toolCall.name().isBlank()) {
|
||||
toolCallNamesById.put(toolCall.id(), toolCall.name());
|
||||
}
|
||||
return ollamaToolCall;
|
||||
}).toList());
|
||||
}
|
||||
message.setToolCallId(turn.toolCallId());
|
||||
if (turn.type() == Conversation.TurnType.TOOL && turn.toolCallId() != null && !turn.toolCallId().isBlank()) {
|
||||
message.setToolName(toolCallNamesById.get(turn.toolCallId()));
|
||||
}
|
||||
messages.add(message);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
private String mapRole(final Turn turn) {
|
||||
return switch (turn.type()) {
|
||||
case SYSTEM -> "system";
|
||||
case USER -> "user";
|
||||
case ASSISTANT -> "assistant";
|
||||
case TOOL -> "tool";
|
||||
};
|
||||
}
|
||||
|
||||
private Map<String, Object> createOptions(final ModelProfile modelProfile) {
|
||||
final var options = new LinkedHashMap<String, Object>();
|
||||
options.put("num_predict", modelProfile.maxTokens());
|
||||
options.put("temperature", modelProfile.temperature());
|
||||
options.put("top_p", modelProfile.topP());
|
||||
return options;
|
||||
}
|
||||
|
||||
private List<OllamaToolDefinition> createTools(final List<ToolDefinition> toolDefinitions) {
|
||||
return toolDefinitions.stream().map(toolDefinition -> {
|
||||
final var function = new OllamaToolFunction();
|
||||
function.setName(toolDefinition.name());
|
||||
function.setDescription(toolDefinition.description());
|
||||
function.setParameters(toolDefinition.parameters());
|
||||
final var tool = new OllamaToolDefinition();
|
||||
tool.setType("function");
|
||||
tool.setFunction(function);
|
||||
return tool;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private OllamaChatResponse readStream(final InputStream inputStream, final Consumer<Chunk> chunkConsumer) throws IOException {
|
||||
final var response = new OllamaChatResponse();
|
||||
final var contentBuilder = new StringBuilder();
|
||||
final var reasoningBuilder = new StringBuilder();
|
||||
final List<ToolCall> toolCalls = new ArrayList<>();
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
final var streamResponse = this.objectMapper.readValue(line, OllamaChatResponse.class);
|
||||
this.throwIfEmbeddedError(streamResponse);
|
||||
response.copyFrom(streamResponse);
|
||||
if (streamResponse.getMessage() != null) {
|
||||
String content = null;
|
||||
String reasoning = null;
|
||||
if (streamResponse.getMessage().getContent() != null && !streamResponse.getMessage().getContent().isEmpty()) {
|
||||
content = streamResponse.getMessage().getContent();
|
||||
contentBuilder.append(content);
|
||||
}
|
||||
if (streamResponse.getMessage().getThinking() != null && !streamResponse.getMessage().getThinking().isEmpty()) {
|
||||
reasoning = streamResponse.getMessage().getThinking();
|
||||
reasoningBuilder.append(reasoning);
|
||||
}
|
||||
if (content != null || reasoning != null) {
|
||||
chunkConsumer.accept(new Chunk(content, reasoning));
|
||||
}
|
||||
if (streamResponse.getMessage().getToolCalls() != null && !streamResponse.getMessage().getToolCalls().isEmpty()) {
|
||||
toolCalls.clear();
|
||||
toolCalls.addAll(this.toToolCalls(streamResponse.getMessage().getToolCalls()));
|
||||
}
|
||||
}
|
||||
if (Boolean.TRUE.equals(streamResponse.getDone())) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
final var message = new OllamaMessage();
|
||||
message.setRole("assistant");
|
||||
message.setContent(contentBuilder.toString());
|
||||
message.setThinking(reasoningBuilder.toString());
|
||||
message.setToolCalls(this.toOllamaToolCalls(toolCalls));
|
||||
response.setMessage(message);
|
||||
return response;
|
||||
}
|
||||
|
||||
private List<ToolCall> toToolCalls(final List<OllamaToolCall> ollamaToolCalls) {
|
||||
if (ollamaToolCalls == null || ollamaToolCalls.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
final List<ToolCall> toolCalls = new ArrayList<>();
|
||||
for (int i = 0; i < ollamaToolCalls.size(); i++) {
|
||||
final var toolCall = ollamaToolCalls.get(i);
|
||||
if (toolCall == null || toolCall.getFunction() == null || toolCall.getFunction().getName() == null || toolCall.getFunction().getName().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
toolCalls.add(new ToolCall(
|
||||
toolCall.getId() != null && !toolCall.getId().isBlank() ? toolCall.getId() : "ollama-tool-call-" + (i + 1),
|
||||
toolCall.getType() != null && !toolCall.getType().isBlank() ? toolCall.getType() : "function",
|
||||
toolCall.getFunction().getName(),
|
||||
this.serializeArguments(toolCall.getFunction().getArguments())));
|
||||
}
|
||||
return List.copyOf(toolCalls);
|
||||
}
|
||||
|
||||
private List<OllamaToolCall> toOllamaToolCalls(final List<ToolCall> toolCalls) {
|
||||
return toolCalls.stream().map(toolCall -> {
|
||||
final var function = new OllamaFunctionCall();
|
||||
function.setName(toolCall.name());
|
||||
function.setArguments(this.parseJsonOrText(toolCall.arguments()));
|
||||
final var ollamaToolCall = new OllamaToolCall();
|
||||
ollamaToolCall.setId(toolCall.id());
|
||||
ollamaToolCall.setType(toolCall.type());
|
||||
ollamaToolCall.setFunction(function);
|
||||
return ollamaToolCall;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private CompletionFinishReason mapFinishReason(final String finishReason, final List<ToolCall> toolCalls) {
|
||||
if (toolCalls != null && !toolCalls.isEmpty()) {
|
||||
return CompletionFinishReason.TOOL_CALLS;
|
||||
}
|
||||
return switch (finishReason) {
|
||||
case FinishReasons.FINISH_REASON_LENGTH -> CompletionFinishReason.TOKEN_LIMIT_REACHED;
|
||||
case FinishReasons.FINISH_REASON_CONTENT_FILTER -> CompletionFinishReason.CONTENT_FILTERED;
|
||||
case FinishReasons.FINISH_REASON_TOOL_CALLS -> CompletionFinishReason.TOOL_CALLS;
|
||||
default -> CompletionFinishReason.DONE;
|
||||
};
|
||||
}
|
||||
|
||||
private CompletionStatistics createCompletionStatistics(final ModelProfile modelProfile, final LocalDateTime start, final OllamaChatResponse response, final Conversation conversation, final String content, final String reasoning) {
|
||||
int inTokens = 0;
|
||||
int outTokens = 0;
|
||||
int reasoningOutTokens = 0;
|
||||
if (response.getPromptEvalCount() != null && response.getPromptEvalCount() > 0) {
|
||||
inTokens = response.getPromptEvalCount();
|
||||
}
|
||||
if (response.getEvalCount() != null && response.getEvalCount() > 0) {
|
||||
outTokens = response.getEvalCount();
|
||||
}
|
||||
if (reasoning != null && !reasoning.isBlank()) {
|
||||
reasoningOutTokens = this.tokenizerResolver.count(reasoning, modelProfile);
|
||||
outTokens = Math.max(0, outTokens - reasoningOutTokens);
|
||||
}
|
||||
if (inTokens <= 0) {
|
||||
inTokens = this.tokenizerResolver.count(this.createPromptForCounting(conversation), modelProfile);
|
||||
}
|
||||
if (outTokens <= 0 && content != null && !content.isBlank()) {
|
||||
outTokens = this.tokenizerResolver.count(content, modelProfile);
|
||||
}
|
||||
final var duration = response.getTotalDuration() != null && response.getTotalDuration() > 0
|
||||
? Duration.ofNanos(response.getTotalDuration())
|
||||
: Duration.between(start, LocalDateTime.now());
|
||||
var cost = BigDecimal.ZERO;
|
||||
cost = cost.add(BigDecimal.valueOf(inTokens).multiply(modelProfile.costPerInToken()));
|
||||
cost = cost.add(BigDecimal.valueOf(outTokens).multiply(modelProfile.costPerOutToken()));
|
||||
cost = cost.add(BigDecimal.valueOf(reasoningOutTokens).multiply(modelProfile.costPerReasoningOutToken()));
|
||||
return new CompletionStatistics(1, duration, inTokens, 0, outTokens, reasoningOutTokens, cost, false);
|
||||
}
|
||||
|
||||
private String createPromptForCounting(final Conversation conversation) {
|
||||
final var builder = new StringBuilder();
|
||||
for (final var turn : conversation.getConversation()) {
|
||||
builder.append(turn.type().name());
|
||||
builder.append(':');
|
||||
builder.append(turn.text() != null ? turn.text() : "");
|
||||
builder.append('\n');
|
||||
for (final var toolCall : turn.toolCalls()) {
|
||||
builder.append(toolCall.name());
|
||||
builder.append(':');
|
||||
builder.append(toolCall.arguments());
|
||||
builder.append('\n');
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String getPrimaryModelName(final ModelProfile modelProfile) {
|
||||
return this.getModelNames(modelProfile, false).stream().findFirst().orElse("");
|
||||
}
|
||||
|
||||
private List<String> loadModelNames(final ModelProfile modelProfile) {
|
||||
try {
|
||||
var requestSpec = this.restClientBuilder.baseUrl(modelProfile.url()).build()
|
||||
.method(HttpMethod.GET)
|
||||
.uri("/api/tags");
|
||||
if (modelProfile.apiKey() != null && !modelProfile.apiKey().isBlank()) {
|
||||
requestSpec = requestSpec.header("Authorization", "Bearer " + modelProfile.apiKey());
|
||||
}
|
||||
final var response = requestSpec.retrieve().body(OllamaTagsResponse.class);
|
||||
return response == null || response.getModels() == null ? List.of()
|
||||
: response.getModels().stream()
|
||||
.map(model -> model.getName() != null && !model.getName().isBlank() ? model.getName() : model.getModel())
|
||||
.filter(Objects::nonNull)
|
||||
.filter(modelName -> !modelName.isBlank())
|
||||
.toList();
|
||||
}
|
||||
catch (final RestClientResponseException e) {
|
||||
LOGGER.warn("Failed to load Ollama model names: {} {}", e.getStatusCode(), e.getStatusText());
|
||||
return List.of();
|
||||
}
|
||||
catch (final RestClientException e) {
|
||||
LOGGER.warn("Failed to load Ollama model names: {}", e.getMessage());
|
||||
return List.of();
|
||||
}
|
||||
catch (final RuntimeException e) {
|
||||
LOGGER.warn("Failed to load Ollama model names", e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private OllamaShowResponse getModelDetails(final ModelProfile modelProfile, final String modelName) {
|
||||
final var cacheKey = this.cacheKey(modelProfile, modelName);
|
||||
return this.resolvedModelDetails.computeIfAbsent(cacheKey, key -> this.loadModelDetails(modelProfile, modelName));
|
||||
}
|
||||
|
||||
private OllamaShowResponse loadModelDetails(final ModelProfile modelProfile, final String modelName) {
|
||||
try {
|
||||
final var request = new OllamaShowRequest();
|
||||
request.setModel(modelName);
|
||||
request.setVerbose(false);
|
||||
var requestSpec = this.restClientBuilder.baseUrl(modelProfile.url()).build()
|
||||
.method(HttpMethod.POST)
|
||||
.uri("/api/show")
|
||||
.header("Content-Type", "application/json");
|
||||
if (modelProfile.apiKey() != null && !modelProfile.apiKey().isBlank()) {
|
||||
requestSpec = requestSpec.header("Authorization", "Bearer " + modelProfile.apiKey());
|
||||
}
|
||||
final var requestBody = this.objectMapper.writeValueAsString(request);
|
||||
final var response = requestSpec.body(requestBody).retrieve().body(OllamaShowResponse.class);
|
||||
return response == null ? new OllamaShowResponse() : response;
|
||||
}
|
||||
catch (final RestClientResponseException e) {
|
||||
LOGGER.warn("Failed to load Ollama model details for {}: {} {}", modelName, e.getStatusCode(), e.getStatusText());
|
||||
return new OllamaShowResponse();
|
||||
}
|
||||
catch (final RestClientException e) {
|
||||
LOGGER.warn("Failed to load Ollama model details for {}: {}", modelName, e.getMessage());
|
||||
return new OllamaShowResponse();
|
||||
}
|
||||
catch (final JacksonException e) {
|
||||
LOGGER.warn("Failed to serialize Ollama show request for {}", modelName, e);
|
||||
return new OllamaShowResponse();
|
||||
}
|
||||
catch (final RuntimeException e) {
|
||||
LOGGER.warn("Failed to load Ollama model details for {}", modelName, e);
|
||||
return new OllamaShowResponse();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean supportsThinkingCapability(final OllamaShowResponse modelDetails) {
|
||||
return this.hasCapability(modelDetails, "thinking");
|
||||
}
|
||||
|
||||
private boolean supportsToolCapability(final OllamaShowResponse modelDetails) {
|
||||
return this.hasCapability(modelDetails, "tools")
|
||||
|| this.hasCapability(modelDetails, "tool")
|
||||
|| this.hasCapability(modelDetails, "tool-calling")
|
||||
|| this.hasCapability(modelDetails, "tool_calling");
|
||||
}
|
||||
|
||||
private boolean hasCapability(final OllamaShowResponse modelDetails, final String expectedCapability) {
|
||||
if (modelDetails == null || modelDetails.getCapabilities() == null || modelDetails.getCapabilities().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return modelDetails.getCapabilities().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(capability -> capability.toLowerCase(Locale.ROOT).trim())
|
||||
.anyMatch(capability -> capability.equals(expectedCapability) || capability.contains(expectedCapability));
|
||||
}
|
||||
|
||||
private String cacheKey(final ModelProfile modelProfile, final String modelName) {
|
||||
return modelProfile.category() + "|" + modelProfile.url() + "|" + modelProfile.apiKey() + "|" + modelName;
|
||||
}
|
||||
|
||||
private OllamaDialect getOllamaDialect() {
|
||||
return this.ollamaDialect == null ? DEFAULT_OLLAMA_DIALECT : this.ollamaDialect;
|
||||
}
|
||||
|
||||
private void throwIfEmbeddedError(final OllamaChatResponse response) {
|
||||
if (response != null && response.getError() != null && !response.getError().isBlank()) {
|
||||
LOGGER.warn("Error response from Ollama: {}", response.getError());
|
||||
throw new IllegalStateException(response.getError());
|
||||
}
|
||||
}
|
||||
|
||||
private void logErrorResponse(final HttpStatusCode statusCode, final InputStream responseBody) {
|
||||
try {
|
||||
LOGGER.warn("Error response from server: {} {}", statusCode, new String(responseBody.readAllBytes(), StandardCharsets.UTF_8));
|
||||
}
|
||||
catch (final IOException e) {
|
||||
LOGGER.warn("Error response from server: {} <failed to read response body: {}>", statusCode, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Object parseJsonOrText(final String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
return this.objectMapper.readValue(value, Object.class);
|
||||
}
|
||||
catch (final Exception e) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
private String serializeArguments(final Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof final String string) {
|
||||
return string;
|
||||
}
|
||||
try {
|
||||
return this.objectMapper.writeValueAsString(value);
|
||||
}
|
||||
catch (final JacksonException e) {
|
||||
throw new IllegalStateException("Cannot serialize tool call arguments", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
import de.extio.lmlib.profile.ModelProfile;
|
||||
|
||||
public interface OllamaDialect {
|
||||
|
||||
default ThinkMode think(final ModelProfile modelProfile) {
|
||||
return ThinkMode.ENABLED;
|
||||
}
|
||||
|
||||
enum ThinkMode {
|
||||
DISABLED(Boolean.FALSE),
|
||||
ENABLED(Boolean.TRUE),
|
||||
LOW("low"),
|
||||
MEDIUM("medium"),
|
||||
HIGH("high");
|
||||
|
||||
private final Object wireValue;
|
||||
|
||||
ThinkMode(final Object wireValue) {
|
||||
this.wireValue = wireValue;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return (this.wireValue instanceof final Boolean val && val) || (this.wireValue instanceof final String str && !str.isBlank());
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public Object getWireValue() {
|
||||
return this.wireValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaFunctionCall {
|
||||
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
private Object arguments;
|
||||
|
||||
@JsonProperty("name")
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@JsonProperty("description")
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
public void setDescription(final String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@JsonProperty("arguments")
|
||||
public Object getArguments() {
|
||||
return this.arguments;
|
||||
}
|
||||
|
||||
public void setArguments(final Object arguments) {
|
||||
this.arguments = arguments;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaListedModel {
|
||||
|
||||
private String name;
|
||||
|
||||
private String model;
|
||||
|
||||
@JsonProperty("name")
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@JsonProperty("model")
|
||||
public String getModel() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
public void setModel(final String model) {
|
||||
this.model = model;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaMessage {
|
||||
|
||||
private String role;
|
||||
|
||||
private String content;
|
||||
|
||||
private String thinking;
|
||||
|
||||
private List<OllamaToolCall> toolCalls;
|
||||
|
||||
private String toolCallId;
|
||||
|
||||
private String toolName;
|
||||
|
||||
@JsonProperty("role")
|
||||
public String getRole() {
|
||||
return this.role;
|
||||
}
|
||||
|
||||
public void setRole(final String role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
@JsonProperty("content")
|
||||
public String getContent() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
public void setContent(final String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
@JsonProperty("thinking")
|
||||
public String getThinking() {
|
||||
return this.thinking;
|
||||
}
|
||||
|
||||
public void setThinking(final String thinking) {
|
||||
this.thinking = thinking;
|
||||
}
|
||||
|
||||
@JsonProperty("tool_calls")
|
||||
public List<OllamaToolCall> getToolCalls() {
|
||||
return this.toolCalls;
|
||||
}
|
||||
|
||||
public void setToolCalls(final List<OllamaToolCall> toolCalls) {
|
||||
this.toolCalls = toolCalls;
|
||||
}
|
||||
|
||||
@JsonProperty("tool_call_id")
|
||||
public String getToolCallId() {
|
||||
return this.toolCallId;
|
||||
}
|
||||
|
||||
public void setToolCallId(final String toolCallId) {
|
||||
this.toolCallId = toolCallId;
|
||||
}
|
||||
|
||||
@JsonProperty("tool_name")
|
||||
public String getToolName() {
|
||||
return this.toolName;
|
||||
}
|
||||
|
||||
public void setToolName(final String toolName) {
|
||||
this.toolName = toolName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaShowRequest {
|
||||
|
||||
private String model;
|
||||
|
||||
private Boolean verbose;
|
||||
|
||||
@JsonProperty("model")
|
||||
public String getModel() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
public void setModel(final String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@JsonProperty("verbose")
|
||||
public Boolean getVerbose() {
|
||||
return this.verbose;
|
||||
}
|
||||
|
||||
public void setVerbose(final Boolean verbose) {
|
||||
this.verbose = verbose;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaShowResponse {
|
||||
|
||||
private List<String> capabilities;
|
||||
|
||||
@JsonProperty("capabilities")
|
||||
public List<String> getCapabilities() {
|
||||
return this.capabilities;
|
||||
}
|
||||
|
||||
public void setCapabilities(final List<String> capabilities) {
|
||||
this.capabilities = capabilities;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaTagsResponse {
|
||||
|
||||
private List<OllamaListedModel> models;
|
||||
|
||||
@JsonProperty("models")
|
||||
public List<OllamaListedModel> getModels() {
|
||||
return this.models;
|
||||
}
|
||||
|
||||
public void setModels(final List<OllamaListedModel> models) {
|
||||
this.models = models;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaToolCall {
|
||||
|
||||
private String id;
|
||||
|
||||
private String type;
|
||||
|
||||
private OllamaFunctionCall function;
|
||||
|
||||
@JsonProperty("id")
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(final String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@JsonProperty("type")
|
||||
public String getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public void setType(final String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@JsonProperty("function")
|
||||
public OllamaFunctionCall getFunction() {
|
||||
return this.function;
|
||||
}
|
||||
|
||||
public void setFunction(final OllamaFunctionCall function) {
|
||||
this.function = function;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaToolDefinition {
|
||||
|
||||
private String type;
|
||||
|
||||
private OllamaToolFunction function;
|
||||
|
||||
@JsonProperty("type")
|
||||
public String getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public void setType(final String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@JsonProperty("function")
|
||||
public OllamaToolFunction getFunction() {
|
||||
return this.function;
|
||||
}
|
||||
|
||||
public void setFunction(final OllamaToolFunction function) {
|
||||
this.function = function;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package de.extio.lmlib.client.ollama;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
final class OllamaToolFunction {
|
||||
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
private Map<String, Object> parameters;
|
||||
|
||||
@JsonProperty("name")
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@JsonProperty("description")
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
public void setDescription(final String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@JsonProperty("parameters")
|
||||
public Map<String, Object> getParameters() {
|
||||
return this.parameters;
|
||||
}
|
||||
|
||||
public void setParameters(final Map<String, Object> parameters) {
|
||||
this.parameters = parameters;
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,6 @@ public record ModelProfile(
|
||||
}
|
||||
|
||||
public static enum ModelProvider {
|
||||
OAI_TEXT_COMPLETION, OAI_CHAT_COMPLETION;
|
||||
OAI_TEXT_COMPLETION, OAI_CHAT_COMPLETION, OLLAMA;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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