feat: Enhance chat completion with tool calling capabilities

- Added support for tools in ChatCompletionRequest, including tool choice and parallel tool calls.
- Introduced ChatCompletionTool and ChatCompletionFunction classes to define tool specifications.
- Updated ChatMessage to include tool calls and tool call IDs.
- Created ChatToolCall and ChatToolFunction classes for managing tool call details.
- Modified TextCompletionClient to support tool calling.
- Updated Grader and Grader2 classes to handle new tool-related logic.
- Enhanced Agent and AgentTest classes to incorporate tool calling in agent workflows.
- Added tests for tool calling functionality in CachedClientTest and CompletionClientTest.
This commit is contained in:
extio-de
2026-03-22 18:09:10 +01:00
parent 636353334b
commit dce82a98cd
33 changed files with 1321 additions and 116 deletions

View File

@@ -5,6 +5,7 @@ import java.util.function.Consumer;
import java.util.function.Function;
import de.extio.lmlib.agent.responsehandler.AgentResponseHandler;
import de.extio.lmlib.client.ToolDefinition;
import de.extio.lmlib.profile.ModelCategory;
import de.extio.lmlib.profile.ModelProfile;
@@ -14,6 +15,7 @@ public record Agent(String name,
ModelProfile modelProfile,
String systemPrompt,
String textTemplate,
List<ToolDefinition> toolDefinitions,
AgentResponseHandler responseHandler,
Consumer<AgentContext> preProcessor,
Consumer<AgentContext> postProcessor,
@@ -53,6 +55,14 @@ public record Agent(String name,
}
return BaseAgent.super.modelProfile(context);
}
@Override
public List<ToolDefinition> toolDefinitions(final AgentContext context) {
if (toolDefinitions != null) {
return toolDefinitions;
}
return BaseAgent.super.toolDefinitions(context);
}
@Override
public AgentResponseHandler responseHandler(final AgentContext context) {

View File

@@ -13,6 +13,7 @@ import java.util.stream.Collectors;
import de.extio.lmlib.client.Completion;
import de.extio.lmlib.client.Conversation;
import de.extio.lmlib.client.ToolCallData;
public final class AgentContext {
@@ -47,6 +48,8 @@ public final class AgentContext {
private volatile AgentNext nextAgent;
private volatile Completion lastCompletion;
private volatile ToolCallData toolCallData;
private Consumer<AgentContext> agentContextUpdateConsumer;
@@ -79,6 +82,7 @@ public final class AgentContext {
this.nextAgent = other.nextAgent;
this.requestStatistic = other.requestStatistic;
this.lastCompletion = other.lastCompletion;
this.toolCallData = other.toolCallData;
this.agentContextUpdateConsumer = other.agentContextUpdateConsumer;
this.errorType = other.errorType;
this.errorException = other.errorException;
@@ -196,6 +200,14 @@ public final class AgentContext {
void setLastCompletion(final Completion lastCompletion) {
this.lastCompletion = lastCompletion;
}
ToolCallData getToolCallData() {
return this.toolCallData;
}
void setToolCallData(final ToolCallData toolCallData) {
this.toolCallData = toolCallData;
}
public boolean isError() {
return this.errorType != null;

View File

@@ -4,6 +4,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
@@ -18,14 +19,19 @@ import tools.jackson.databind.ObjectMapper;
import de.extio.lmlib.agent.responsehandler.AgentResponseHandler;
import de.extio.lmlib.agent.responsehandler.StreamedAgentResponseHandler;
import de.extio.lmlib.agent.responsehandler.TextAgentResponseHandler;
import de.extio.lmlib.agent.responsehandler.ToolCallingAgentResponseHandler;
import de.extio.lmlib.client.Chunk;
import de.extio.lmlib.client.Client;
import de.extio.lmlib.client.ClientService;
import de.extio.lmlib.client.Completion;
import de.extio.lmlib.client.Conversation;
import de.extio.lmlib.client.ToolCallData;
import de.extio.lmlib.client.ToolDefinition;
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 {
@@ -56,6 +62,38 @@ public interface BaseAgent {
default String textTemplate() {
return null;
}
default List<ToolDefinition> toolDefinitions(final AgentContext context) {
return null;
}
default ToolCallData toolCallData(final AgentContext context) {
final var toolDefinitions = this.toolDefinitions(context);
if (toolDefinitions == null || toolDefinitions.isEmpty()) {
final var rememberedToolCallData = context.getToolCallData();
if (rememberedToolCallData == null || !rememberedToolCallData.hasTools()) {
return null;
}
return ToolCallData.auto(rememberedToolCallData.tools());
}
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);
}
return clientService.supportsToolCalling(this.modelCategory(context));
}
default AgentResponseHandler responseHandler(final AgentContext context) {
return new TextAgentResponseHandler("response");
@@ -115,13 +153,18 @@ public interface BaseAgent {
split.context().getGraph().add(this.name());
if (!skipCompletionInSplit) {
final var completionClient = Objects.requireNonNull(client);
final boolean skipCache = split.context().isSkipCache() || split.context().isAlwaysSkipCache();
split.context().setSkipCache(false);
final var conversation = this.setupConversation(split);
final var toolCallData = completionClient.supportsToolCalling() ? this.toolCallData(split.context()) : null;
this.rememberToolCallData(split.context(), toolCallData);
boolean parseable = false;
Exception parseException = null;
for (int i = 0; i < 2; i++) {
int parseAttempts = 0;
int toolCallRounds = 0;
while (parseAttempts < 2) {
LOGGER.debug("Conversation: {}", conversation);
final var modelProfile = this.modelProfile(split.context());
@@ -141,33 +184,46 @@ public interface BaseAgent {
};
if (modelProfile != null) {
requestStart = System.currentTimeMillis();
completion = client.streamConversation(modelProfile, conversation, chunkConsumer, skipCache);
completion = completionClient.streamConversation(modelProfile, conversation, chunkConsumer, toolCallData, skipCache);
}
else {
requestStart = System.currentTimeMillis();
completion = client.streamConversation(this.modelCategory(split.context()), conversation, chunkConsumer, skipCache);
completion = completionClient.streamConversation(this.modelCategory(split.context()), conversation, chunkConsumer, toolCallData, skipCache);
}
}
else {
if (modelProfile != null) {
requestStart = System.currentTimeMillis();
completion = client.conversation(modelProfile, conversation, skipCache);
completion = completionClient.conversation(modelProfile, conversation, toolCallData, skipCache);
}
else {
requestStart = System.currentTimeMillis();
completion = client.conversation(this.modelCategory(split.context()), conversation, skipCache);
completion = completionClient.conversation(this.modelCategory(split.context()), conversation, toolCallData, skipCache);
}
}
final var durationMs = System.currentTimeMillis() - requestStart;
split.context().getGraph().add("(" + durationMs + " ms)");
split.context().setLastCompletion(completion);
split.context().getRequestStatistic().add(completion);
if (!completion.toolCalls().isEmpty() && responseHandler instanceof final ToolCallingAgentResponseHandler toolCallingAgentResponseHandler) {
if (toolCallRounds >= 4) {
parseException = new IllegalStateException("Too many tool call rounds");
break;
}
final var toolCallResults = new ToolCallingAgentResponseHandler.ToolCallResults();
if (toolCallingAgentResponseHandler.handleToolCalls(completion, split.context(), toolCallResults)) {
toolCallRounds++;
this.appendToolCallResults(conversation, completion, toolCallResults.results());
continue;
}
}
try {
if (!responseHandler.handle(completion, split.context())) {
split.context().getGraph().add("⚠");
LOGGER.warn("{} Cannot parse response: {}", this.name(), completion.response());
split.context().setSkipCache(true);
parseAttempts++;
continue;
}
@@ -179,6 +235,7 @@ public interface BaseAgent {
split.context().getGraph().add("⚠");
LOGGER.warn("{} Cannot parse response: {}", this.name(), completion.response(), e);
split.context().setSkipCache(true);
parseAttempts++;
continue;
}
}
@@ -248,6 +305,13 @@ public interface BaseAgent {
}
return List.of(first);
}
private void appendToolCallResults(final Conversation conversation, final Completion completion, final List<ToolCallingAgentResponseHandler.ToolCallResult> toolCallResults) {
conversation.addTurn(new Conversation.Turn(TurnType.ASSISTANT, completion.response() == null ? "" : completion.response(), completion.toolCalls(), null));
for (final var toolCallResult : toolCallResults) {
conversation.addTurn(new Conversation.Turn(TurnType.TOOL, toolCallResult.output(), null, toolCallResult.toolCallId()));
}
}
private List<Split> applyTemplate(final AgentContext context) {
String text = this.textTemplate() != null ? this.textTemplate() : "";
@@ -358,6 +422,35 @@ public interface BaseAgent {
}
return conversation;
}
private void appendLastCompletionToConversation(final AgentContext context, final boolean skipCompletion) {
if (skipCompletion || context.getConversation() == null || context.getLastCompletion() == null) {
return;
}
final var currentAgent = context.getAgents().get(context.getCurrentAgentName());
if (currentAgent == null || currentAgent.agentType(context) == AgentType.PROCESSING_ONLY) {
return;
}
if (context.getConversation().getConversation().getLast().type() == TurnType.ASSISTANT) {
return;
}
final var completion = context.getLastCompletion();
if ((completion.response() == null || completion.response().isEmpty()) && completion.toolCalls().isEmpty()) {
return;
}
context.getConversation().addTurn(new Conversation.Turn(TurnType.ASSISTANT, completion.response() == null ? "" : completion.response(), completion.toolCalls(), null));
}
private void rememberToolCallData(final AgentContext context, final ToolCallData toolCallData) {
if (toolCallData == null || !toolCallData.hasTools()) {
return;
}
context.setToolCallData(toolCallData);
}
private void clearRememberedToolCallData(final AgentContext context) {
context.setToolCallData(null);
}
private void nextAgent(final AgentContext context, final boolean skipCompletion) {
context.setNextAgent(this.chooseNextAgent(context));
@@ -388,11 +481,13 @@ public interface BaseAgent {
case final BaseAgent a when a.agentType(context) == AgentType.COMPLETION -> {
context.getGraph().add("→");
context.setConversation(null);
this.clearRememberedToolCallData(context);
}
case final BaseAgent a when a.agentType(context) == AgentType.START_CONVERSATION || a.agentType(context) == AgentType.START_CONVERSATION_WITH_SYSTEM_PROMPT -> {
context.getGraph().add("🗨");
context.setConversation(null);
this.clearRememberedToolCallData(context);
}
case final BaseAgent a when a.agentType(context) == AgentType.CONVERSATION || a.agentType(context) == AgentType.CONVERSATION_WITH_SYSTEM_PROMPT -> {
@@ -402,15 +497,12 @@ public interface BaseAgent {
}
else {
context.getGraph().add("↴");
final var currentAgent = context.getAgents().get(context.getCurrentAgentName());
if (! skipCompletion && currentAgent.agentType(context) != AgentType.PROCESSING_ONLY && context.getConversation().getConversation().getLast().type() != TurnType.ASSISTANT) {
context.getConversation().addTurn(new Conversation.Turn(TurnType.ASSISTANT, context.getLastCompletion().response()));
}
this.appendLastCompletionToConversation(context, skipCompletion);
}
}
case final BaseAgent a when a.agentType(context) == AgentType.PROCESSING_ONLY -> {
this.appendLastCompletionToConversation(context, skipCompletion);
}
default -> {

View File

@@ -0,0 +1,65 @@
package de.extio.lmlib.agent.responsehandler;
import java.util.ArrayList;
import java.util.List;
import de.extio.lmlib.agent.AgentContext;
import de.extio.lmlib.client.Completion;
import de.extio.lmlib.client.ToolCall;
import de.extio.lmlib.client.ToolParameters;
public class ToolCallingAgentResponseHandler implements AgentResponseHandler {
@FunctionalInterface
public interface ToolCallHandler {
boolean handle(Completion completion, AgentContext context, ToolCallResults toolCallResults);
}
public record ToolCallResult(String toolCallId, String output) {
}
public static final class ToolCallResults {
private final List<ToolCallResult> toolCallResults = new ArrayList<>();
public ToolCallResults add(final ToolCall toolCall, final String output) {
return this.add(toolCall.id(), output);
}
public ToolCallResults add(final ToolCall toolCall, final ToolParameters output) {
return this.add(toolCall.id(), output.json());
}
public ToolCallResults add(final String toolCallId, final String output) {
this.toolCallResults.add(new ToolCallResult(toolCallId, output));
return this;
}
public List<ToolCallResult> results() {
return List.copyOf(this.toolCallResults);
}
}
private final AgentResponseHandler delegate;
private final ToolCallHandler toolCallHandler;
public ToolCallingAgentResponseHandler(final AgentResponseHandler delegate, final ToolCallHandler toolCallHandler) {
this.delegate = delegate;
this.toolCallHandler = toolCallHandler;
}
@Override
public boolean handle(final Completion completion, final AgentContext context) {
return this.delegate == null || this.delegate.handle(completion, context);
}
public boolean handleToolCalls(final Completion completion, final AgentContext context, final ToolCallResults toolCallResults) {
return this.toolCallHandler.handle(completion, context, toolCallResults);
}
}

View File

@@ -8,13 +8,15 @@ import de.extio.lmlib.profile.ModelProfile.ModelProvider;
public interface Client {
Completion conversation(ModelCategory modelCategory, Conversation conversation, boolean skipCache);
Completion conversation(ModelProfile modelProfile, Conversation conversation, boolean skipCache);
Completion conversation(ModelCategory modelCategory, Conversation conversation, ToolCallData toolCallData, boolean skipCache);
Completion streamConversation(ModelCategory modelCategory, Conversation conversation, Consumer<Chunk> chunkConsumer, boolean skipCache);
Completion conversation(ModelProfile modelProfile, Conversation conversation, ToolCallData toolCallData, boolean skipCache);
Completion streamConversation(ModelCategory modelCategory, Conversation conversation, Consumer<Chunk> chunkConsumer, ToolCallData toolCallData, boolean skipCache);
Completion streamConversation(ModelProfile modelProfile, Conversation conversation, Consumer<Chunk> chunkConsumer, boolean skipCache);
Completion streamConversation(ModelProfile modelProfile, Conversation conversation, Consumer<Chunk> chunkConsumer, ToolCallData toolCallData, boolean skipCache);
ModelProvider getModelProvider();
boolean supportsToolCalling();
}

View File

@@ -31,6 +31,10 @@ public class ClientService {
final var modelProfile = this.modelProfileService.getModelProfile(category.getModelProfile(), category);
return this.getClient(modelProfile);
}
public boolean supportsToolCalling(final ModelCategory category) {
return this.getClient(category).supportsToolCalling();
}
@Cacheable("clients")
public Client getClient(final ModelProfile modelProfile) {
@@ -41,6 +45,10 @@ public class ClientService {
.map(client -> this.completionInterceptors == null || this.completionInterceptors.isEmpty() ? client : new InterceptingClient(client, this.modelProfileService, this.completionInterceptors))
.orElseThrow(() -> new IllegalStateException("No client found for model profile " + modelProfile.modelName()));
}
public boolean supportsToolCalling(final ModelProfile modelProfile) {
return this.getClient(modelProfile).supportsToolCalling();
}
private static class InterceptingClient implements Client {
@@ -57,24 +65,24 @@ public class ClientService {
}
@Override
public Completion conversation(final ModelCategory modelCategory, final Conversation conversation_, final boolean skipCache) {
public Completion conversation(final ModelCategory modelCategory, final Conversation conversation_, final ToolCallData toolCallData, final boolean skipCache) {
final var modelProfile = this.modelProfileService.getModelProfile(modelCategory.getModelProfile(), modelCategory);
return this.conversation(modelProfile, conversation_, skipCache);
return this.conversation(modelProfile, conversation_, toolCallData, skipCache);
}
@Override
public Completion streamConversation(final ModelCategory modelCategory, final Conversation conversation_, final Consumer<Chunk> chunkConsumer, final boolean skipCache) {
public Completion streamConversation(final ModelCategory modelCategory, final Conversation conversation_, final Consumer<Chunk> chunkConsumer, final ToolCallData toolCallData, final boolean skipCache) {
final var modelProfile = this.modelProfileService.getModelProfile(modelCategory.getModelProfile(), modelCategory);
return this.streamConversation(modelProfile, conversation_, chunkConsumer, skipCache);
return this.streamConversation(modelProfile, conversation_, chunkConsumer, toolCallData, skipCache);
}
@Override
public Completion conversation(final ModelProfile modelProfile, final Conversation conversation_, final boolean skipCache) {
public Completion conversation(final ModelProfile modelProfile, final Conversation conversation_, final ToolCallData toolCallData, final boolean skipCache) {
Conversation conversation = conversation_;
for (final var interceptor : this.completionInterceptors) {
conversation = interceptor.before(modelProfile, conversation);
}
var completion = this.client.conversation(modelProfile, conversation, skipCache);
var completion = this.client.conversation(modelProfile, conversation, toolCallData, skipCache);
for (final var interceptor : this.completionInterceptors) {
completion = interceptor.after(modelProfile, conversation, completion);
}
@@ -82,12 +90,12 @@ public class ClientService {
}
@Override
public Completion streamConversation(final ModelProfile modelProfile, final Conversation conversation_, final Consumer<Chunk> chunkConsumer, final boolean skipCache) {
public Completion streamConversation(final ModelProfile modelProfile, final Conversation conversation_, final Consumer<Chunk> chunkConsumer, final ToolCallData toolCallData, final boolean skipCache) {
Conversation conversation = conversation_;
for (final var interceptor : this.completionInterceptors) {
conversation = interceptor.before(modelProfile, conversation);
}
var completion = this.client.streamConversation(modelProfile, conversation, chunkConsumer, skipCache);
var completion = this.client.streamConversation(modelProfile, conversation, chunkConsumer, toolCallData, skipCache);
for (final var interceptor : this.completionInterceptors) {
completion = interceptor.after(modelProfile, conversation, completion);
}
@@ -98,6 +106,11 @@ public class ClientService {
public ModelProvider getModelProvider() {
return this.client.getModelProvider();
}
@Override
public boolean supportsToolCalling() {
return this.client.supportsToolCalling();
}
}

View File

@@ -1,5 +1,15 @@
package de.extio.lmlib.client;
public final record Completion(String response, String reasoning, CompletionFinishReason finishReason, CompletionStatistics statistics) {
import java.util.List;
public final record Completion(String response, String reasoning, CompletionFinishReason finishReason, CompletionStatistics statistics, List<ToolCall> toolCalls) {
public Completion {
toolCalls = toolCalls == null ? List.of() : List.copyOf(toolCalls);
}
public Completion(final String response, final String reasoning, final CompletionFinishReason finishReason, final CompletionStatistics statistics) {
this(response, reasoning, finishReason, statistics, List.of());
}
}

View File

@@ -15,6 +15,11 @@ public enum CompletionFinishReason {
/**
* Completion was stopped because the content was filtered.
*/
CONTENT_FILTERED
CONTENT_FILTERED,
/**
* Completion stopped because the model emitted one or more tool calls.
*/
TOOL_CALLS
}

View File

@@ -80,10 +80,18 @@ public final class Conversation {
}
public enum TurnType {
SYSTEM, USER, ASSISTANT
SYSTEM, USER, ASSISTANT, TOOL
}
public record Turn(TurnType type, String text) {
public record Turn(TurnType type, String text, List<ToolCall> toolCalls, String toolCallId) {
public Turn {
toolCalls = toolCalls == null ? List.of() : List.copyOf(toolCalls);
}
public Turn(final TurnType type, final String text) {
this(type, text, List.of(), null);
}
}
}

View File

@@ -0,0 +1,5 @@
package de.extio.lmlib.client;
public record ToolCall(String id, String type, String name, String arguments) {
}

View File

@@ -0,0 +1,34 @@
package de.extio.lmlib.client;
import java.util.Collection;
import java.util.List;
public record ToolCallData(List<ToolDefinition> tools, String toolChoice, String forcedToolName, Boolean parallelToolCalls) {
public static final String TOOL_CHOICE_AUTO = "auto";
public static final String TOOL_CHOICE_REQUIRED = "required";
public static final String TOOL_CHOICE_NONE = "none";
public ToolCallData {
tools = tools == null ? List.of() : List.copyOf(tools);
}
public static ToolCallData auto(final Collection<ToolDefinition> tools) {
return new ToolCallData(List.copyOf(tools), TOOL_CHOICE_AUTO, null, null);
}
public static ToolCallData required(final Collection<ToolDefinition> tools) {
return new ToolCallData(List.copyOf(tools), TOOL_CHOICE_REQUIRED, null, null);
}
public static ToolCallData force(final Collection<ToolDefinition> tools, final String forcedToolName) {
return new ToolCallData(List.copyOf(tools), null, forcedToolName, null);
}
public boolean hasTools() {
return !this.tools.isEmpty();
}
}

View File

@@ -0,0 +1,26 @@
package de.extio.lmlib.client;
import java.util.Map;
public record ToolDefinition(String name, String description, Map<String, Object> parameters, Boolean strict) {
public ToolDefinition {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Tool name cannot be null or blank");
}
parameters = parameters == null ? Map.of() : Map.copyOf(parameters);
}
public ToolDefinition(final String name, final String description, final Map<String, Object> parameters) {
this(name, description, parameters, null);
}
public ToolDefinition(final String name, final String description, final ToolParameters parameters, final Boolean strict) {
this(name, description, parameters != null ? parameters.map() : null, strict);
}
public ToolDefinition(final String name, final String description, final ToolParameters parameters) {
this(name, description, parameters != null ? parameters.map() : null, null);
}
}

View File

@@ -0,0 +1,83 @@
package de.extio.lmlib.client;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import tools.jackson.databind.ObjectMapper;
public final class ToolParameters {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final LinkedHashMap<String, Object> values = new LinkedHashMap<>();
private ToolParameters() {
}
public static ToolParameters create() {
return new ToolParameters();
}
public static ToolParameters create(final Map<String, String> properties) {
final var schemaProperties = ToolParameters.create();
for (final var entry : properties.entrySet()) {
schemaProperties.add(entry.getKey(), ToolParameters.create()
.add("type", "string")
.add("description", entry.getValue()));
}
return new ToolParameters()
.add("type", "object")
.add("properties", schemaProperties)
.add("required", List.copyOf(properties.keySet()))
.add("additionalProperties", false);
}
public ToolParameters add(final String key, final Object value) {
this.values.put(key, this.normalize(value));
return this;
}
public Map<String, Object> map() {
return Map.copyOf(this.values);
}
public String json() {
try {
return OBJECT_MAPPER.writeValueAsString(this.values);
}
catch (final Exception e) {
throw new IllegalStateException("Cannot serialize tool parameters", e);
}
}
private Object normalize(final Object value) {
return switch (value) {
case null -> null;
case final ToolParameters toolParameters -> toolParameters.map();
case final Map<?, ?> map -> this.normalizeMap(map);
case final Collection<?> collection -> this.normalizeCollection(collection);
default -> value;
};
}
private Map<String, Object> normalizeMap(final Map<?, ?> map) {
final var normalized = new LinkedHashMap<String, Object>();
for (final var entry : map.entrySet()) {
normalized.put(String.valueOf(entry.getKey()), this.normalize(entry.getValue()));
}
return normalized;
}
private List<Object> normalizeCollection(final Collection<?> collection) {
final var normalized = new ArrayList<Object>(collection.size());
for (final var item : collection) {
normalized.add(this.normalize(item));
}
return List.copyOf(normalized);
}
}

View File

@@ -9,18 +9,22 @@ import java.util.HexFormat;
import java.util.function.Consumer;
import java.util.function.Supplier;
import tools.jackson.databind.ObjectMapper;
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.ToolCallData;
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;
public class CachedClient implements Client {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final CachedClientRepository cacheRepository;
@@ -35,18 +39,18 @@ public class CachedClient implements Client {
}
@Override
public Completion conversation(final ModelCategory modelCategory, final Conversation conversation, final boolean skipCache) {
return this.getCachedResponse(modelCategory, null, skipCache, () -> this.client.conversation(modelCategory, conversation, skipCache), conversation.getConversation().toString());
public Completion conversation(final ModelCategory modelCategory, final Conversation conversation, final ToolCallData toolCallData, final boolean skipCache) {
return this.getCachedResponse(modelCategory, null, skipCache, () -> this.client.conversation(modelCategory, conversation, toolCallData, skipCache), conversation.getConversation().toString(), this.serializeToolCallData(toolCallData));
}
@Override
public Completion conversation(final ModelProfile modelProfile, final Conversation conversation, final boolean skipCache) {
return this.getCachedResponse(null, modelProfile, skipCache, () -> this.client.conversation(modelProfile, conversation, skipCache), conversation.getConversation().toString());
public Completion conversation(final ModelProfile modelProfile, final Conversation conversation, final ToolCallData toolCallData, final boolean skipCache) {
return this.getCachedResponse(null, modelProfile, skipCache, () -> this.client.conversation(modelProfile, conversation, toolCallData, skipCache), conversation.getConversation().toString(), this.serializeToolCallData(toolCallData));
}
@Override
public Completion streamConversation(final ModelCategory modelCategory, final Conversation conversation, final Consumer<Chunk> chunkConsumer, final boolean skipCache) {
final var response = this.getCachedResponse(modelCategory, null, skipCache, () -> this.client.streamConversation(modelCategory, conversation, chunkConsumer, skipCache), conversation.getConversation().toString());
public Completion streamConversation(final ModelCategory modelCategory, final Conversation conversation, final Consumer<Chunk> chunkConsumer, final ToolCallData toolCallData, final boolean skipCache) {
final var response = this.getCachedResponse(modelCategory, null, skipCache, () -> this.client.streamConversation(modelCategory, conversation, chunkConsumer, toolCallData, skipCache), conversation.getConversation().toString(), this.serializeToolCallData(toolCallData));
if (response.statistics().cached()) {
chunkConsumer.accept(new Chunk(response.response(), response.reasoning()));
}
@@ -54,8 +58,8 @@ public class CachedClient implements Client {
}
@Override
public Completion streamConversation(final ModelProfile modelProfile, final Conversation conversation, final Consumer<Chunk> chunkConsumer, final boolean skipCache) {
final var response = this.getCachedResponse(null, modelProfile, skipCache, () -> this.client.streamConversation(modelProfile, conversation, chunkConsumer, skipCache), conversation.getConversation().toString());
public Completion streamConversation(final ModelProfile modelProfile, final Conversation conversation, final Consumer<Chunk> chunkConsumer, final ToolCallData toolCallData, final boolean skipCache) {
final var response = this.getCachedResponse(null, modelProfile, skipCache, () -> this.client.streamConversation(modelProfile, conversation, chunkConsumer, toolCallData, skipCache), conversation.getConversation().toString(), this.serializeToolCallData(toolCallData));
if (response.statistics().cached()) {
chunkConsumer.accept(new Chunk(response.response(), response.reasoning()));
}
@@ -66,21 +70,38 @@ public class CachedClient implements Client {
public ModelProvider getModelProvider() {
return this.client.getModelProvider();
}
@Override
public boolean supportsToolCalling() {
return this.client.supportsToolCalling();
}
private Completion getCachedResponse(final ModelCategory modelCategory, final ModelProfile modelProfile, final boolean skipCache, final Supplier<Completion> supplier, final String... keys) {
final var key = this.createCacheKey(modelCategory, modelProfile, keys);
final var cachedCompletion = skipCache ? null : this.cacheRepository.get(key);
if (cachedCompletion != null) {
return new Completion(cachedCompletion.response(), cachedCompletion.reasoning(), CompletionFinishReason.DONE, new CompletionStatistics(0, Duration.ZERO, cachedCompletion.inTokens(), cachedCompletion.cachedInTokens(), cachedCompletion.outTokens(), cachedCompletion.reasoningOutTokens(), BigDecimal.ZERO, true));
return new Completion(cachedCompletion.response(), cachedCompletion.reasoning(), cachedCompletion.finishReason(), new CompletionStatistics(0, Duration.ZERO, cachedCompletion.inTokens(), cachedCompletion.cachedInTokens(), cachedCompletion.outTokens(), cachedCompletion.reasoningOutTokens(), BigDecimal.ZERO, true), cachedCompletion.toolCalls());
}
final var completion = supplier.get();
if (completion != null) {
this.cacheRepository.put(key, new CachedCompletion(completion.response(), completion.reasoning(), completion.finishReason(), completion.statistics().inTokens(), completion.statistics().cachedInTokens(), completion.statistics().outTokens(), completion.statistics().reasoningOutTokens(), OffsetDateTime.now()));
this.cacheRepository.put(key, new CachedCompletion(completion.response(), completion.reasoning(), completion.finishReason(), completion.toolCalls(), completion.statistics().inTokens(), completion.statistics().cachedInTokens(), completion.statistics().outTokens(), completion.statistics().reasoningOutTokens(), OffsetDateTime.now()));
}
return completion;
}
private String serializeToolCallData(final ToolCallData toolCallData) {
if (toolCallData == null) {
return null;
}
try {
return OBJECT_MAPPER.writeValueAsString(toolCallData);
}
catch (final Exception e) {
return toolCallData.toString();
}
}
private String createCacheKey(ModelCategory modelCategory, ModelProfile modelProfile, final String... keys) {
if (modelProfile == null) {

View File

@@ -1,9 +1,11 @@
package de.extio.lmlib.client.cached;
import java.time.OffsetDateTime;
import java.util.List;
import de.extio.lmlib.client.CompletionFinishReason;
import de.extio.lmlib.client.ToolCall;
public record CachedCompletion(String response, String reasoning, CompletionFinishReason finishReason, long inTokens, long cachedInTokens, long outTokens, long reasoningOutTokens, OffsetDateTime created) {
public record CachedCompletion(String response, String reasoning, CompletionFinishReason finishReason, List<ToolCall> toolCalls, long inTokens, long cachedInTokens, long outTokens, long reasoningOutTokens, OffsetDateTime created) {
}

View File

@@ -26,6 +26,7 @@ import de.extio.lmlib.client.Client;
import de.extio.lmlib.client.Completion;
import de.extio.lmlib.client.CompletionFinishReason;
import de.extio.lmlib.client.Conversation;
import de.extio.lmlib.client.ToolCallData;
import de.extio.lmlib.client.oai.ModelNameSupplier;
import de.extio.lmlib.profile.ModelCategory;
import de.extio.lmlib.profile.ModelProfile;
@@ -82,37 +83,38 @@ public abstract class AbstractCompletionClient implements Client, DisposableBean
}
@Override
public Completion conversation(final ModelCategory modelCategory, final Conversation conversation, final boolean skipCache) {
return this.streamConversation(modelCategory, conversation, null, skipCache);
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 boolean skipCache) {
return this.streamConversation(modelProfile, conversation, null, skipCache);
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 boolean skipCache) {
public Completion streamConversation(final ModelCategory modelCategory_, final Conversation conversation, final Consumer<Chunk> chunkConsumer, final ToolCallData toolCallData, final boolean skipCache) {
final ModelCategory modelCategory = (modelCategory_ == null) ? ModelCategory.MEDIUM : modelCategory_;
final var modelProfile = this.modelProfileService.getModelProfile(modelCategory.getModelProfile(), modelCategory);
return this.streamConversation(modelProfile, conversation, chunkConsumer, skipCache);
return this.streamConversation(modelProfile, conversation, chunkConsumer, toolCallData, skipCache);
}
@Override
public Completion streamConversation(final ModelProfile modelProfile, final Conversation conversation, final Consumer<Chunk> chunkConsumer, final boolean skipCache) {
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.OAI_TEXT_COMPLETION && modelProfile.modelProvider() != ModelProvider.OAI_CHAT_COMPLETION)) {
throw new IllegalArgumentException("Invalid ModelProfile");
}
return this.requestCompletion(conversation, modelProfile, chunkConsumer);
return this.requestCompletion(conversation, modelProfile, chunkConsumer, toolCallData);
}
protected abstract Completion requestCompletion(final Conversation conversation, final ModelProfile modelProfile, final Consumer<Chunk> chunkConsumer);
protected abstract Completion requestCompletion(final Conversation conversation, final ModelProfile modelProfile, final Consumer<Chunk> chunkConsumer, final ToolCallData toolCallData);
protected CompletionFinishReason mapFinishReason(final String finishReason) {
return switch (finishReason) {
case FinishReasons.FINISH_REASON_CONTENT_FILTER -> CompletionFinishReason.CONTENT_FILTERED;
case FinishReasons.FINISH_REASON_LENGTH -> CompletionFinishReason.TOKEN_LIMIT_REACHED;
case FinishReasons.FINISH_REASON_TOOL_CALLS -> CompletionFinishReason.TOOL_CALLS;
default -> CompletionFinishReason.DONE;
};
}

View File

@@ -7,5 +7,7 @@ public final class FinishReasons {
public static final String FINISH_REASON_LENGTH = "length";
public static final String FINISH_REASON_CONTENT_FILTER = "content_filter";
public static final String FINISH_REASON_TOOL_CALLS = "tool_calls";
}

View File

@@ -4,23 +4,24 @@ import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.web.client.RestClient;
import tools.jackson.core.JacksonException;
import de.extio.lmlib.client.Chunk;
import de.extio.lmlib.client.Completion;
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.Conversation.Turn;
import de.extio.lmlib.client.oai.completion.AbstractCompletionClient;
import de.extio.lmlib.profile.ModelProfile;
@@ -34,13 +35,18 @@ public class ChatCompletionClient extends AbstractCompletionClient {
super();
}
@Override
public boolean supportsToolCalling() {
return true;
}
@Override
public ModelProvider getModelProvider() {
return ModelProvider.OAI_CHAT_COMPLETION;
}
@Override
protected Completion requestCompletion(final Conversation conversation, final ModelProfile modelProfile, final Consumer<Chunk> chunkConsumer) {
protected Completion requestCompletion(final Conversation conversation, final ModelProfile modelProfile, final Consumer<Chunk> chunkConsumer, final ToolCallData toolCallData) {
final var chat = createChats(conversation, modelProfile);
final var request = new ChatCompletionRequest();
@@ -76,6 +82,11 @@ public class ChatCompletionClient extends AbstractCompletionClient {
throw new IllegalArgumentException("Invalid reasoning summary details in the model profile: " + modelProfile.reasoningSummaryDetails(), e);
}
}
if (toolCallData != null && toolCallData.hasTools()) {
request.setTools(this.createTools(toolCallData.tools()));
request.setToolChoice(this.createToolChoice(toolCallData));
request.setParallelToolCalls(toolCallData.parallelToolCalls());
}
String requestBody;
try {
@@ -112,6 +123,7 @@ public class ChatCompletionClient extends AbstractCompletionClient {
chatCompletionResponse.setChoices(List.of(new ChatChoice()));
final StringBuilder contentStringBuilder = new StringBuilder();
final StringBuilder reasoningStringBuilder = new StringBuilder();
final List<ChatToolCall> toolCalls = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(clientResponse.getBody(), StandardCharsets.UTF_8))) {
String line;
@@ -162,6 +174,9 @@ public class ChatCompletionClient extends AbstractCompletionClient {
if (content != null || reasoning != null) {
chunkConsumer.accept(new Chunk(content, reasoning));
}
if (streamResponse.getChoices().getFirst().getMessage().getToolCalls() != null && !streamResponse.getChoices().getFirst().getMessage().getToolCalls().isEmpty()) {
this.mergeToolCalls(toolCalls, streamResponse.getChoices().getFirst().getMessage().getToolCalls());
}
}
}
}
@@ -171,6 +186,7 @@ public class ChatCompletionClient extends AbstractCompletionClient {
}
}
chatCompletionResponse.getChoices().getFirst().setMessage(new ChatMessage("assistant", contentStringBuilder.toString(), reasoningStringBuilder.toString()));
chatCompletionResponse.getChoices().getFirst().getMessage().setToolCalls(List.copyOf(toolCalls));
return chatCompletionResponse;
}
});
@@ -181,65 +197,118 @@ public class ChatCompletionClient extends AbstractCompletionClient {
final var choice = response.getChoices().getFirst();
final var content = response.getChoices().getFirst().getMessage().getContent();
final var reasoning = response.getChoices().getFirst().getMessage().getReasoningContent();
final var toolCalls = this.toToolCalls(response.getChoices().getFirst().getMessage().getToolCalls());
if ((content == null || content.isBlank()) && toolCalls.isEmpty()) {
throw new IllegalStateException("No parseable response content from server");
}
final var finishReason = this.mapFinishReason(choice.getFinishReason());
final var statistics = createCompletionStatistics(modelProfile, start, response.getUsage(), response.getTimings(), null, content, reasoning);
return new Completion(content, reasoning, finishReason, statistics);
return new Completion(content, reasoning, finishReason, statistics, toolCalls);
}
private List<ChatCompletionRequest.ChatCompletionTool> createTools(final List<ToolDefinition> toolDefinitions) {
return toolDefinitions.stream().map(toolDefinition -> {
final var function = new ChatCompletionRequest.ChatCompletionFunction();
function.setName(toolDefinition.name());
function.setDescription(toolDefinition.description());
function.setParameters(toolDefinition.parameters());
function.setStrict(toolDefinition.strict());
final var tool = new ChatCompletionRequest.ChatCompletionTool();
tool.setType("function");
tool.setFunction(function);
return tool;
}).toList();
}
private Object createToolChoice(final ToolCallData toolCallData) {
if (toolCallData == null) {
return null;
}
if (toolCallData.forcedToolName() != null && !toolCallData.forcedToolName().isBlank()) {
final var function = new ChatCompletionRequest.ChatCompletionNamedToolChoiceFunction();
function.setName(toolCallData.forcedToolName());
final var namedToolChoice = new ChatCompletionRequest.ChatCompletionNamedToolChoice();
namedToolChoice.setType("function");
namedToolChoice.setFunction(function);
return namedToolChoice;
}
return toolCallData.toolChoice();
}
private void mergeToolCalls(final List<ChatToolCall> currentToolCalls, final List<ChatToolCall> deltaToolCalls) {
for (final var deltaToolCall : deltaToolCalls) {
final int index = deltaToolCall.getIndex() != null ? deltaToolCall.getIndex() : currentToolCalls.size();
while (currentToolCalls.size() <= index) {
currentToolCalls.add(new ChatToolCall());
}
final var currentToolCall = currentToolCalls.get(index);
if (deltaToolCall.getId() != null && !deltaToolCall.getId().isBlank()) {
currentToolCall.setId(deltaToolCall.getId());
}
if (deltaToolCall.getType() != null && !deltaToolCall.getType().isBlank()) {
currentToolCall.setType(deltaToolCall.getType());
}
if (deltaToolCall.getFunction() != null) {
if (currentToolCall.getFunction() == null) {
currentToolCall.setFunction(new ChatToolFunction());
}
if (deltaToolCall.getFunction().getName() != null && !deltaToolCall.getFunction().getName().isBlank()) {
currentToolCall.getFunction().setName(deltaToolCall.getFunction().getName());
}
if (deltaToolCall.getFunction().getArguments() != null && !deltaToolCall.getFunction().getArguments().isEmpty()) {
final var existingArguments = currentToolCall.getFunction().getArguments();
currentToolCall.getFunction().setArguments((existingArguments == null ? "" : existingArguments) + deltaToolCall.getFunction().getArguments());
}
}
}
}
private List<ToolCall> toToolCalls(final List<ChatToolCall> chatToolCalls) {
if (chatToolCalls == null || chatToolCalls.isEmpty()) {
return List.of();
}
return chatToolCalls.stream()
.filter(Objects::nonNull)
.map(toolCall -> new ToolCall(toolCall.getId(), toolCall.getType(), toolCall.getFunction() != null ? toolCall.getFunction().getName() : null, toolCall.getFunction() != null ? toolCall.getFunction().getArguments() : null))
.toList();
}
private List<ChatMessage> createChats(final Conversation conversation, final ModelProfile modelProfile) {
final List<ChatMessage> chat = new ArrayList<>();
final Deque<Turn> turns = new ArrayDeque<>(conversation.getConversation());
Turn systemTurn = null;
Turn userTurn = null;
Turn assistantTurn = null;
boolean first = true;
final AtomicInteger tokens = new AtomicInteger();
while (!turns.isEmpty()) {
final Turn turn = turns.pop();
for (final Turn turn : conversation.getConversation()) {
switch (turn.type()) {
case SYSTEM:
systemTurn = turn;
chat.add(this.createChatMessage("system", turn));
break;
case USER:
userTurn = turn;
if (this.addChatRequestMessage("user", turn, tokens, chat, modelProfile)) {
return chat;
}
break;
case ASSISTANT:
assistantTurn = turn;
if (this.addChatRequestMessage("assistant", turn, tokens, chat, modelProfile)) {
return chat;
}
break;
case TOOL:
if (this.addChatRequestMessage("tool", turn, tokens, chat, modelProfile)) {
return chat;
}
break;
default:
throw new IllegalArgumentException("Unknown turn type: " + turn.type());
}
if (first && userTurn != null) {
if (systemTurn != null) {
chat.add(new ChatMessage("system", systemTurn.text(), null));
}
if (this.addChatRequestMessage("user", userTurn.text(), tokens, chat, modelProfile)) {
break;
}
userTurn = null;
first = false;
}
else if (!first && assistantTurn != null && userTurn != null) {
if (this.addChatRequestMessage("assistant", assistantTurn.text(), tokens, chat, modelProfile)) {
break;
}
if (this.addChatRequestMessage("user", userTurn.text(), tokens, chat, modelProfile)) {
break;
}
userTurn = null;
assistantTurn = null;
}
}
return chat;
}
private boolean addChatRequestMessage(final String role, final String text, final AtomicInteger tokens, final List<ChatMessage> chat, final ModelProfile modelProfile) {
var prompt = text;
private boolean addChatRequestMessage(final String role, final Turn turn, final AtomicInteger tokens, final List<ChatMessage> chat, final ModelProfile modelProfile) {
var prompt = turn.text() != null ? turn.text() : "";
var trimmed = false;
final var textTokens = this.tokenizer.count(prompt, modelProfile);
@@ -252,9 +321,30 @@ public class ChatCompletionClient extends AbstractCompletionClient {
trimmed = true;
}
chat.add(new ChatMessage(role, prompt, null));
final var message = this.createChatMessage(role, turn);
message.setContent(prompt);
chat.add(message);
return trimmed;
}
private ChatMessage createChatMessage(final String role, final Turn turn) {
final var message = new ChatMessage(role, turn.text() != null ? turn.text() : "", null);
if (!turn.toolCalls().isEmpty()) {
message.setToolCalls(turn.toolCalls().stream().map(toolCall -> {
final var function = new ChatToolFunction();
function.setName(toolCall.name());
function.setArguments(toolCall.arguments());
final var chatToolCall = new ChatToolCall();
chatToolCall.setId(toolCall.id());
chatToolCall.setType(toolCall.type());
chatToolCall.setFunction(function);
return chatToolCall;
}).toList());
}
message.setToolCallId(turn.toolCallId());
return message;
}
}

View File

@@ -1,6 +1,7 @@
package de.extio.lmlib.client.oai.completion.chat;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.*;
@@ -13,6 +14,12 @@ final class ChatCompletionRequest extends AbstractCompletionRequest {
private Integer maxCompletionTokens;
private ChatCompletionsRequestReasoning reasoning;
private List<ChatCompletionTool> tools;
private Object toolChoice;
private Boolean parallelToolCalls;
@JsonProperty("messages")
public List<ChatMessage> getMessages() {
@@ -41,6 +48,33 @@ final class ChatCompletionRequest extends AbstractCompletionRequest {
this.reasoning = reasoning;
}
@JsonProperty("tools")
public List<ChatCompletionTool> getTools() {
return this.tools;
}
public void setTools(final List<ChatCompletionTool> tools) {
this.tools = tools;
}
@JsonProperty("tool_choice")
public Object getToolChoice() {
return this.toolChoice;
}
public void setToolChoice(final Object toolChoice) {
this.toolChoice = toolChoice;
}
@JsonProperty("parallel_tool_calls")
public Boolean getParallelToolCalls() {
return this.parallelToolCalls;
}
public void setParallelToolCalls(final Boolean parallelToolCalls) {
this.parallelToolCalls = parallelToolCalls;
}
final static class ChatCompletionsRequestReasoning {
private ChatCompletionsRequestReasoningEffort effort;
@@ -75,4 +109,119 @@ final class ChatCompletionRequest extends AbstractCompletionRequest {
auto, concise, detailed
}
static final class ChatCompletionTool {
private String type;
private ChatCompletionFunction function;
@JsonProperty("type")
public String getType() {
return this.type;
}
public void setType(final String type) {
this.type = type;
}
@JsonProperty("function")
public ChatCompletionFunction getFunction() {
return this.function;
}
public void setFunction(final ChatCompletionFunction function) {
this.function = function;
}
}
static final class ChatCompletionFunction {
private String name;
private String description;
private Map<String, Object> parameters;
private Boolean strict;
@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;
}
@JsonProperty("strict")
public Boolean getStrict() {
return this.strict;
}
public void setStrict(final Boolean strict) {
this.strict = strict;
}
}
static final class ChatCompletionNamedToolChoice {
private String type;
private ChatCompletionNamedToolChoiceFunction function;
@JsonProperty("type")
public String getType() {
return this.type;
}
public void setType(final String type) {
this.type = type;
}
@JsonProperty("function")
public ChatCompletionNamedToolChoiceFunction getFunction() {
return this.function;
}
public void setFunction(final ChatCompletionNamedToolChoiceFunction function) {
this.function = function;
}
}
static final class ChatCompletionNamedToolChoiceFunction {
private String name;
@JsonProperty("name")
public String getName() {
return this.name;
}
public void setName(final String name) {
this.name = name;
}
}
}

View File

@@ -1,5 +1,7 @@
package de.extio.lmlib.client.oai.completion.chat;
import java.util.List;
import com.fasterxml.jackson.annotation.*;
final class ChatMessage {
@@ -9,6 +11,10 @@ final class ChatMessage {
private String content;
private String reasoningContent;
private List<ChatToolCall> toolCalls;
private String toolCallId;
public ChatMessage() {
}
@@ -45,4 +51,22 @@ final class ChatMessage {
public void setReasoningContent(final String reasoningContent) {
this.reasoningContent = reasoningContent;
}
@JsonProperty("tool_calls")
public List<ChatToolCall> getToolCalls() {
return this.toolCalls;
}
public void setToolCalls(final List<ChatToolCall> toolCalls) {
this.toolCalls = toolCalls;
}
@JsonProperty("tool_call_id")
public String getToolCallId() {
return this.toolCallId;
}
public void setToolCallId(final String toolCallId) {
this.toolCallId = toolCallId;
}
}

View File

@@ -0,0 +1,51 @@
package de.extio.lmlib.client.oai.completion.chat;
import com.fasterxml.jackson.annotation.JsonProperty;
final class ChatToolCall {
private Integer index;
private String id;
private String type;
private ChatToolFunction function;
@JsonProperty("index")
public Integer getIndex() {
return this.index;
}
public void setIndex(final Integer index) {
this.index = index;
}
@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 ChatToolFunction getFunction() {
return this.function;
}
public void setFunction(final ChatToolFunction function) {
this.function = function;
}
}

View File

@@ -0,0 +1,29 @@
package de.extio.lmlib.client.oai.completion.chat;
import com.fasterxml.jackson.annotation.JsonProperty;
final class ChatToolFunction {
private String name;
private String arguments;
@JsonProperty("name")
public String getName() {
return this.name;
}
public void setName(final String name) {
this.name = name;
}
@JsonProperty("arguments")
public String getArguments() {
return this.arguments;
}
public void setArguments(final String arguments) {
this.arguments = arguments;
}
}

View File

@@ -13,7 +13,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.web.client.RestClient;
import tools.jackson.core.JacksonException;
@@ -21,6 +20,7 @@ import de.extio.lmlib.client.Chunk;
import de.extio.lmlib.client.Completion;
import de.extio.lmlib.client.CompletionFinishReason;
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.ModelProfile;
@@ -41,7 +41,12 @@ public class TextCompletionClient extends AbstractCompletionClient {
}
@Override
protected Completion requestCompletion(final Conversation conversation, final ModelProfile modelProfile, final Consumer<Chunk> chunkConsumer) {
public boolean supportsToolCalling() {
return false;
}
@Override
protected Completion requestCompletion(final Conversation conversation, final ModelProfile modelProfile, final Consumer<Chunk> chunkConsumer, final ToolCallData toolCallData) {
final var promptStrategy = this.promptStrategyFactory.getStrategy(modelProfile.prompt());
if (promptStrategy == null) {
throw new IllegalArgumentException("Prompt strategy not found: " + modelProfile.prompt());

View File

@@ -36,6 +36,7 @@ public class Grader {
Only provide the response as a single word and no preamble and no explanation.""",
"Here is the text:\n" + text + "\n\n" +
"Here is the user question: " + question),
null,
true);
LOGGER.info(completion.response());
score += Boolean.parseBoolean(completion.response().strip()) ? 1 : -1;

View File

@@ -50,6 +50,7 @@ public class Grader2 {
- 'false': The text does not provide a truthful answer.
Only provide the response as a single word. Do not include any preamble or explanation.
""".formatted(question, text)),
null,
true);
LOGGER.info(completion.response());
score += Boolean.parseBoolean(completion.response().strip()) ? 1 : -1;

View File

@@ -30,6 +30,7 @@ public class AnswerGrader {
The question is: {{questions}}
The answer is: {{answer}}
""",
null,
new JsonAgentResponseHandler(),
context -> {
context.setSkipCache(true);

View File

@@ -42,6 +42,7 @@ public class AnswerGrader2 {
### Output Format
{{qaOutputFormat}}
""",
null,
new JsonAgentResponseHandler(),
context -> {
final Boolean outputDescription = context.getValue("qaOutputDescription", Boolean.class);

View File

@@ -33,6 +33,7 @@ public class AnswerGraderBinary {
Output Format:
{{qaOutputFormat}}
""",
null,
new JsonAgentResponseHandler(),
context -> {
final Boolean outputDescription = context.getValue("qaOutputDescription", Boolean.class);