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

@@ -6,21 +6,32 @@ import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Map;
import java.math.BigDecimal;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;
import de.extio.lmlib.agent.responsehandler.AgentResponseHandler;
import de.extio.lmlib.agent.responsehandler.TextAgentResponseHandler;
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.ToolCall;
import de.extio.lmlib.client.ToolCallData;
import de.extio.lmlib.client.ToolDefinition;
import de.extio.lmlib.client.ToolParameters;
import de.extio.lmlib.profile.ModelCategory;
class AgentErrorHandlingTest {
@@ -28,7 +39,7 @@ class AgentErrorHandlingTest {
@Test
void parsingErrorsKeepTheirTypeWithoutAnExceptionWhenTheHandlerReturnsFalse() {
final Client client = mock(Client.class);
when(client.conversation(any(ModelCategory.class), any(), anyBoolean())).thenReturn(new Completion("not json", null, null, new CompletionStatistics(1, Duration.ofMillis(1), 1, 0, 1, 0, BigDecimal.ZERO, false)));
when(client.conversation(any(ModelCategory.class), any(), any(ToolCallData.class), anyBoolean())).thenReturn(new Completion("not json", null, null, new CompletionStatistics(1, Duration.ofMillis(1), 1, 0, 1, 0, BigDecimal.ZERO, false)));
final AgentResponseHandler responseHandler = (completion, context) -> false;
final var agent = new Agent("ParseAgent",
@@ -37,6 +48,7 @@ class AgentErrorHandlingTest {
null,
null,
"prompt",
null,
responseHandler,
null,
null,
@@ -63,6 +75,7 @@ class AgentErrorHandlingTest {
null,
null,
null,
null,
context -> {
throw exception;
},
@@ -80,4 +93,109 @@ class AgentErrorHandlingTest {
assertEquals("boom", result.getErrorException().getMessage());
}
}
@Test
void toolCallDataIsRetainedForFollowUpConversationRequests() {
final var client = mock(Client.class);
when(client.supportsToolCalling()).thenReturn(true);
final var requestCounter = new AtomicInteger();
final var firstRequestToolCallData = new AtomicReference<ToolCallData>();
final var secondRequestToolCallData = new AtomicReference<ToolCallData>();
final var weatherTool = new ToolDefinition(
"get_weather",
"Gets the current weather for a given location.",
ToolParameters.create(Map.of("location", "")),
true);
doAnswer(invocation -> {
final var toolCallData = invocation.getArgument(2, ToolCallData.class);
final var currentRequest = requestCounter.incrementAndGet();
if (currentRequest == 1) {
firstRequestToolCallData.set(toolCallData);
return new Completion(
"",
null,
CompletionFinishReason.TOOL_CALLS,
new CompletionStatistics(1, Duration.ofMillis(1), 1, 0, 1, 0, BigDecimal.ZERO, false),
List.of(new ToolCall("call_1", "function", "get_weather", "{\"location\":\"Berlin, Germany\"}")));
}
secondRequestToolCallData.set(toolCallData);
return new Completion("Berlin is rainy and 21C", null, CompletionFinishReason.DONE, new CompletionStatistics(1, Duration.ofMillis(1), 1, 0, 1, 0, BigDecimal.ZERO, false));
}).when(client).conversation(any(ModelCategory.class), any(), any(ToolCallData.class), anyBoolean());
final BaseAgent toolCaller = new BaseAgent() {
@Override
public String name() {
return "ToolCaller";
}
@Override
public AgentType agentType(final AgentContext context) {
return AgentType.START_CONVERSATION;
}
@Override
public ModelCategory modelCategory(final AgentContext context) {
return ModelCategory.MEDIUM;
}
@Override
public String systemPrompt() {
return "You must call the weather tool.";
}
@Override
public String textTemplate() {
return "What is the weather in Berlin, Germany?";
}
@Override
public List<ToolDefinition> toolDefinitions(final AgentContext context) {
return List.of(weatherTool);
}
@Override
public ToolCallData toolCallData(final AgentContext context) {
return ToolCallData.required(List.of(weatherTool));
}
@Override
public AgentResponseHandler responseHandler(final AgentContext context) {
return (completion, agentContext) -> true;
}
@Override
public AgentNext chooseNextAgent(final AgentContext context) {
return new AgentNext("ToolResultResponder", null);
}
};
final var toolResultResponder = new Agent("ToolResultResponder",
AgentType.CONVERSATION,
ModelCategory.MEDIUM,
null,
null,
"Answer using the tool result already present in the conversation.",
null,
new TextAgentResponseHandler("answer"),
context -> {
final var assistantTurn = context.getConversation().getConversation().getLast();
context.getConversation().addTurn(new Conversation.Turn(Conversation.TurnType.TOOL, ToolParameters.create().add("forecast", "Berlin is rainy and 21C").json(), null, assistantTurn.toolCalls().getFirst().id()));
},
null,
null,
AgentNext::end);
final var context = new AgentContext(Map.of(
toolCaller.name(), toolCaller,
toolResultResponder.name(), toolResultResponder));
try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) {
final var firstResult = toolCaller.execute(client, executorService, context).getFirst();
final var secondResult = toolResultResponder.execute(client, executorService, firstResult).getFirst();
assertEquals(2, requestCounter.get());
assertEquals(ToolCallData.required(List.of(weatherTool)), firstRequestToolCallData.get());
assertEquals(ToolCallData.auto(List.of(weatherTool)), secondRequestToolCallData.get());
assertEquals("Berlin is rainy and 21C", secondResult.getStringValue("answer"));
}
}
}

View File

@@ -24,9 +24,15 @@ import org.springframework.context.annotation.PropertySource;
import de.extio.lmlib.agent.responsehandler.JsonAgentResponseHandler;
import de.extio.lmlib.agent.responsehandler.TextAgentResponseHandler;
import de.extio.lmlib.agent.responsehandler.ToolCallingAgentResponseHandler;
import de.extio.lmlib.client.ClientService;
import de.extio.lmlib.grader.Grader;
import de.extio.lmlib.client.Conversation;
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.ModelCategory;
import de.extio.lmlib.profile.ModelProfileService;
/**
* These tests have been verified with Llama 3.1 8B, Gemma2 27B, Gemma3 27B and GPT-OSS models
@@ -45,6 +51,9 @@ public class AgentTest {
@Autowired
private ClientService clientService;
@Autowired
private ModelProfileService modelProfileService;
@Test
void agenticFlow() throws IOException {
@@ -56,6 +65,7 @@ public class AgentTest {
null,
"Generate a summary of the following Java source code.",
"The source code is:\n{{code}}",
null,
new TextAgentResponseHandler("summary"),
null,
null,
@@ -69,6 +79,7 @@ public class AgentTest {
null,
"",
"Generate a list of 5 distinct functional features. In scope is business logic, not code mechanics. Provide no preamble and no explanation. Return the response in JSON format with the following field: { \"features\": [\"Feature 1\", \"Feature 2\", ... ] }",
null,
new JsonAgentResponseHandler(),
null,
context -> context.getStringValues("features").forEach(feature -> LOGGER.info(feature)),
@@ -85,6 +96,7 @@ public class AgentTest {
The source code is:
{{code}}
The feature is: [[features]]""",
null,
new TextAgentResponseHandler("featureDescription"),
null,
null,
@@ -102,6 +114,7 @@ public class AgentTest {
The code analysis:
{{summary}}
{{featureDescription}}""",
null,
new TextAgentResponseHandler("blogPost"),
context -> {
context.setStringValues("topics", List.of(String.join(", ", context.getStringValues("features").stream().toArray(size -> new String[size]))));
@@ -123,7 +136,7 @@ public class AgentTest {
assertEquals(1, resultContext.getContext().get("blogPost").size());
assertEquals(8, resultContext.getRequestStatistic().getRequests().get());
for (final var feature : resultContext.getContext().get("features")) {
assertTrue(Grader.assessScoreBinary("Does the following blog post mention the feature " + feature, resultContext.getStringValue("blogPost"), this.clientService));
assertTrue(Grader2.assessScoreBinary("Does the following blog post mention the feature " + feature, resultContext.getStringValue("blogPost"), ModelCategory.MEDIUM, this.modelProfileService, this.clientService));
}
}
@@ -138,6 +151,7 @@ public class AgentTest {
null,
"Generate a summary of the following Java source code.",
"The source code is:\n{{code}}",
null,
new TextAgentResponseHandler("summary",
null,
null,
@@ -176,6 +190,146 @@ public class AgentTest {
LOGGER.info("### final result");
LOGGER.info(resultContexts.getFirst().toString());
}
@Test
void toolCallingAgenticFlow() {
final var agents = Map.of(
"ToolCaller",
new Agent("ToolCaller",
AgentType.START_CONVERSATION,
ModelCategory.MEDIUM,
null,
"Use the provided tool when the user asks for weather information. After receiving tool results, respond using the forecast from the tool output.",
"What is the weather in Berlin, Germany? Use the tool.",
List.of(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)),
new ToolCallingAgentResponseHandler(
new TextAgentResponseHandler("answer"),
(completion, context, toolCallResults) -> {
if (completion.toolCalls().isEmpty()) {
return false;
}
context.setStringValue("toolName", completion.toolCalls().getFirst().name());
context.setStringValue("toolArguments", completion.toolCalls().getFirst().arguments());
toolCallResults.add(completion.toolCalls().getFirst(), ToolParameters.create().add("forecast", "Berlin is rainy and 21C"));
return true;
}),
null,
null,
null,
AgentNext::end));
final var context = new AgentContext(agents);
assertTrue(agents.get("ToolCaller").supportsToolCalling(context, this.clientService));
final var resultContexts = this.agentExecutor.walk(agents.get("ToolCaller"), context);
final var resultContext = resultContexts.getFirst();
assertEquals(1, resultContexts.size());
assertEquals("get_weather", resultContext.getStringValue("toolName"));
assertTrue(resultContext.getStringValue("toolArguments").contains("Berlin"));
assertTrue(Grader2.assessScoreBinary("Does the text say that Berlin is rainy and 21C?", resultContext.getStringValue("answer"), ModelCategory.MEDIUM, this.modelProfileService, this.clientService));
}
@Test
void toolCallsCanBeHandledByNextAgentConversation() {
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 BaseAgent toolCaller = new BaseAgent() {
@Override
public String name() {
return "ToolCaller";
}
@Override
public AgentType agentType(final AgentContext context) {
return AgentType.START_CONVERSATION;
}
@Override
public ModelCategory modelCategory(final AgentContext context) {
return ModelCategory.MEDIUM;
}
@Override
public String systemPrompt() {
return "You must call the provided tool for weather questions. Do not answer before the tool has been called.";
}
@Override
public String textTemplate() {
return "What is the weather in Berlin, Germany? Use the tool.";
}
@Override
public List<ToolDefinition> toolDefinitions(final AgentContext context) {
return List.of(weatherTool);
}
@Override
public ToolCallData toolCallData(final AgentContext context) {
return ToolCallData.required(List.of(weatherTool));
}
@Override
public boolean supportsToolCalling(final AgentContext context) {
return true;
}
@Override
public AgentNext chooseNextAgent(final AgentContext context) {
return new AgentNext("ToolResultResponder", null);
}
@Override
public de.extio.lmlib.agent.responsehandler.AgentResponseHandler responseHandler(final AgentContext context) {
return (completion, agentContext) -> true;
}
};
final BaseAgent toolResultResponder = new Agent("ToolResultResponder",
AgentType.CONVERSATION,
ModelCategory.MEDIUM,
null,
"Use the existing tool result in the conversation to answer the original weather question.",
"Answer the original question using the tool result already present in the conversation. Do not call any tool.",
null,
new TextAgentResponseHandler("answer"),
context -> {
final var conversation = context.getConversation();
final var assistantTurn = conversation.getConversation().getLast();
context.setStringValue("handoffTurnType", assistantTurn.type().name());
context.setStringValue("handoffToolName", assistantTurn.toolCalls().getFirst().name());
context.setStringValue("handoffToolArguments", assistantTurn.toolCalls().getFirst().arguments());
context.setStringValue("handoffToolCallId", assistantTurn.toolCalls().getFirst().id());
conversation.addTurn(new Conversation.Turn(Conversation.TurnType.TOOL, ToolParameters.create().add("forecast", "Berlin is rainy and 21C").json(), null, assistantTurn.toolCalls().getFirst().id()));
},
null,
null,
AgentNext::end);
final var agents = Map.of(
"ToolCaller", toolCaller,
"ToolResultResponder", toolResultResponder);
final var context = new AgentContext(agents);
final var resultContexts = this.agentExecutor.walk(toolCaller, context);
final var resultContext = resultContexts.getFirst();
assertEquals(1, resultContexts.size());
assertEquals("ASSISTANT", resultContext.getStringValue("handoffTurnType"));
assertEquals("get_weather", resultContext.getStringValue("handoffToolName"));
assertTrue(resultContext.getStringValue("handoffToolArguments").contains("Berlin"));
assertTrue(resultContext.getConversation().getConversation().stream().anyMatch(turn -> turn.type() == Conversation.TurnType.TOOL
&& resultContext.getStringValue("handoffToolCallId").equals(turn.toolCallId())
&& turn.text().contains("Berlin is rainy and 21C")));
assertTrue(Grader2.assessScoreBinary("Does the text say that Berlin is rainy and 21C?", resultContext.getStringValue("answer"), ModelCategory.MEDIUM, this.modelProfileService, this.clientService));
}
@Test
void doubleBranch() {
@@ -187,6 +341,7 @@ public class AgentTest {
null,
"",
"Generate 5 random numbers from 1 to 10. Provide no preamble and no explanation. Return the response in JSON format with the following field: { \"numbers\": [\"Number 1\", \"Number 2\", ... ] }",
null,
new JsonAgentResponseHandler(),
null,
null,
@@ -200,6 +355,7 @@ public class AgentTest {
null,
"",
"Generate 5 random numbers from 1 to 10. The previous number was [[numbers]]. Provide no preamble and no explanation. Return the response in JSON format with the following field: { \"random\": [\"Number 1\", \"Number 2\", ... ] }",
null,
new JsonAgentResponseHandler(),
null,
null,
@@ -213,6 +369,7 @@ public class AgentTest {
null,
"",
"Add these 2 numbers: {{numbers}} + [[random]] ; Provide no preamble and no explanation. Return the response in JSON format with the following field: { \"result\": \"result\" }",
null,
new JsonAgentResponseHandler(),
null,
null,
@@ -226,6 +383,7 @@ public class AgentTest {
null,
"You are a helpful assistant.",
"If you add {{numbers}} marbles to {{random}} marbles, do you get {{result}} marbles as the result? Answer with 'yes' or 'no'",
null,
new TextAgentResponseHandler("grade"),
null,
null,

View File

@@ -50,6 +50,7 @@ public class CachedClientTest {
var completion = this.clientService.getClient(ModelCategory.SMALL).conversation(
ModelCategory.SMALL,
Conversation.create("You are a funny person", "Tell me a cat joke"),
null,
false);
LOGGER.info(completion.toString());
assertFalse(completion.statistics().cached());
@@ -60,6 +61,7 @@ public class CachedClientTest {
completion = this.clientService.getClient(ModelCategory.SMALL).conversation(
ModelCategory.SMALL,
Conversation.create("You are a funny person", "Tell me a cat joke"),
null,
false);
LOGGER.info(completion.toString());
assertTrue(completion.statistics().cached());
@@ -69,6 +71,7 @@ public class CachedClientTest {
completion = this.clientService.getClient(ModelCategory.SMALL).conversation(
ModelCategory.SMALL,
Conversation.create("You are a funny person", "Tell me a dog joke"),
null,
false);
LOGGER.info(completion.toString());
assertFalse(completion.statistics().cached());
@@ -80,14 +83,14 @@ public class CachedClientTest {
void cacheChat() throws Exception {
final var conversation = Conversation.create("You are an assistant who stutters", "Hi there, I would like to know the climate in Munich.");
var completion = this.clientService.getClient(ModelCategory.MEDIUM).conversation(ModelCategory.MEDIUM, conversation, false);
var completion = this.clientService.getClient(ModelCategory.MEDIUM).conversation(ModelCategory.MEDIUM, conversation, null, false);
LOGGER.info(completion.toString());
assertFalse(completion.statistics().cached());
assertEquals(1, completion.statistics().requests());
final var firstResponse = completion.response();
assertFalse(firstResponse.isBlank());
completion = this.clientService.getClient(ModelCategory.MEDIUM).conversation(ModelCategory.MEDIUM, conversation, false);
completion = this.clientService.getClient(ModelCategory.MEDIUM).conversation(ModelCategory.MEDIUM, conversation, null, false);
LOGGER.info(completion.toString());
assertTrue(completion.statistics().cached());
assertEquals(0, completion.statistics().requests());
@@ -96,7 +99,7 @@ public class CachedClientTest {
conversation.addTurn(new Turn(TurnType.ASSISTANT,
"T-t-the climate in B-b-berlin, um, it's, uh, quite, c-c-cool, actually. B-b-berlin has a, um, temperate climate, with, uh, four distinct s-s-seasons. S-s-spring is, um, pretty mild, with, uh, temperatures ranging from, uh, 10 to 20 degrees Celsius. S-s-summer is, um, warm, with, uh, highs around 25 degrees Celsius. F-f-fall is, um, cool, with, uh, temperatures ranging from 5 to 15 degrees Celsius. And, um, w-w-winter is, uh, quite, c-c-cold, with, uh, temperatures ranging from -2 to 5 degrees Celsius."));
conversation.addTurn(new Turn(TurnType.USER, "Great! How is the weather in summer?"));
completion = this.clientService.getClient(ModelCategory.MEDIUM).conversation(ModelCategory.MEDIUM, conversation, false);
completion = this.clientService.getClient(ModelCategory.MEDIUM).conversation(ModelCategory.MEDIUM, conversation, null, false);
LOGGER.info(completion.toString());
assertFalse(completion.statistics().cached());
assertEquals(1, completion.statistics().requests());
@@ -107,19 +110,19 @@ public class CachedClientTest {
void bypassCacheStillStoresFreshResult() throws Exception {
final var conversation = Conversation.create("You are a witty assistant", "Give me a short joke about caches.");
final var initialCompletion = this.clientService.getClient(ModelCategory.MEDIUM).conversation(ModelCategory.MEDIUM, conversation, false);
final var initialCompletion = this.clientService.getClient(ModelCategory.MEDIUM).conversation(ModelCategory.MEDIUM, conversation, null, false);
LOGGER.info(initialCompletion.toString());
assertFalse(initialCompletion.statistics().cached());
assertEquals(1, initialCompletion.statistics().requests());
assertFalse(initialCompletion.response().isBlank());
final var bypassedCompletion = this.clientService.getClient(ModelCategory.MEDIUM).conversation(ModelCategory.MEDIUM, conversation, true);
final var bypassedCompletion = this.clientService.getClient(ModelCategory.MEDIUM).conversation(ModelCategory.MEDIUM, conversation, null, true);
LOGGER.info(bypassedCompletion.toString());
assertFalse(bypassedCompletion.statistics().cached());
assertEquals(1, bypassedCompletion.statistics().requests());
assertFalse(bypassedCompletion.response().isBlank());
final var cachedCompletion = this.clientService.getClient(ModelCategory.MEDIUM).conversation(ModelCategory.MEDIUM, conversation, false);
final var cachedCompletion = this.clientService.getClient(ModelCategory.MEDIUM).conversation(ModelCategory.MEDIUM, conversation, null, false);
LOGGER.info(cachedCompletion.toString());
assertTrue(cachedCompletion.statistics().cached());
assertEquals(0, cachedCompletion.statistics().requests());
@@ -140,7 +143,7 @@ public class CachedClientTest {
sb.append(chunk.content());
System.out.print(chunk.content());
}
}, false);
}, null, false);
assertFalse(completion.statistics().cached());
assertEquals(1, completion.statistics().requests());
assertTrue(!sb.isEmpty());
@@ -158,7 +161,7 @@ public class CachedClientTest {
sb2.append(chunk.content());
System.out.print(chunk.content());
}
}, false);
}, null, false);
assertTrue(completion.statistics().cached());
assertEquals(sb.toString(), sb2.toString());
assertEquals(sb2.toString(), completion.response());

View File

@@ -5,6 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -21,9 +23,13 @@ import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.context.annotation.PropertySource;
import de.extio.lmlib.client.ClientService;
import de.extio.lmlib.client.CompletionFinishReason;
import de.extio.lmlib.client.Conversation;
import de.extio.lmlib.client.Conversation.Turn;
import de.extio.lmlib.client.Conversation.TurnType;
import de.extio.lmlib.client.ToolCallData;
import de.extio.lmlib.client.ToolDefinition;
import de.extio.lmlib.client.ToolParameters;
import de.extio.lmlib.client.oai.completion.chat.ChatCompletionClient;
import de.extio.lmlib.client.oai.completion.text.TextCompletionClient;
import de.extio.lmlib.grader.Grader2;
@@ -57,6 +63,7 @@ public class CompletionClientTest {
final var completion = this.textCompletionClient.conversation(
modelProfile,
Conversation.create("You are a helpful assistant", "Say the color is green"),
null,
false);
LOGGER.info(completion.reasoning());
@@ -71,7 +78,7 @@ public class CompletionClientTest {
final var conversation = Conversation.create("You are a helpful assistant", "I would like to paint my canvas in a solid color.");
conversation.addTurn(new Turn(TurnType.ASSISTANT, "Which color do you want to see?"));
conversation.addTurn(new Turn(TurnType.USER, "You choose!"));
final var completion = this.textCompletionClient.conversation(modelProfile, conversation, false);
final var completion = this.textCompletionClient.conversation(modelProfile, conversation, null, false);
LOGGER.info(completion.reasoning());
LOGGER.info(completion.response());
@@ -94,7 +101,7 @@ public class CompletionClientTest {
System.out.print(chunk.content());
sb.append(chunk.content());
}
}, false);
}, null, false);
if (completion.reasoning() != null) {
// Prompt template does not separate reasoning and response from the stream at the moment
@@ -114,7 +121,7 @@ public class CompletionClientTest {
final var conversation = Conversation.create("You are a helpful assistant", "I would like to paint my canvas in a solid color.");
conversation.addTurn(new Turn(TurnType.ASSISTANT, "Which color do you want to see?"));
conversation.addTurn(new Turn(TurnType.USER, "You choose!"));
final var completion = this.chatCompletionclient.conversation(modelProfile, conversation, false);
final var completion = this.chatCompletionclient.conversation(modelProfile, conversation, null, false);
LOGGER.info(completion.reasoning());
LOGGER.info(completion.response());
@@ -137,12 +144,84 @@ public class CompletionClientTest {
System.out.print(chunk.content());
sb.append(chunk.content());
}
}, false);
}, null, false);
assertEquals(completion.response(), sb.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.modelProfileService.getModelProfile("profile.model.chatcompletion");
final var toolDefinition = 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 tool when the user asks for weather information. After receiving tool results, answer using the forecast from the tool output.",
"What is the weather in Berlin, Germany? Use the tool.");
final var toolCallData = ToolCallData.required(List.of(toolDefinition));
final var completion = this.chatCompletionclient.conversation(
modelProfile,
conversation,
toolCallData,
false);
assertTrue(this.chatCompletionclient.supportsToolCalling());
assertFalse(this.textCompletionClient.supportsToolCalling());
assertTrue(this.clientService.supportsToolCalling(modelProfile));
assertTrue(completion.finishReason() == CompletionFinishReason.TOOL_CALLS);
assertFalse(completion.toolCalls().isEmpty());
assertEquals("get_weather", completion.toolCalls().getFirst().name());
assertTrue(completion.toolCalls().getFirst().arguments().contains("Berlin"));
conversation.addTurn(new Turn(TurnType.ASSISTANT, "", completion.toolCalls(), null));
conversation.addTurn(new Turn(TurnType.TOOL, ToolParameters.create().add("forecast", "Berlin is rainy and 21C").json(), null, completion.toolCalls().getFirst().id()));
final var finalCompletion = this.chatCompletionclient.conversation(modelProfile, conversation, ToolCallData.auto(List.of(toolDefinition)), false);
assertTrue(finalCompletion.finishReason() == CompletionFinishReason.DONE);
assertTrue(Grader2.assessScoreBinary("Does the text say that Berlin is rainy and 21C?", finalCompletion.response(), modelProfile, this.clientService));
}
@Test
void streamToolCallingChatCompletion() throws Exception {
final var modelProfile = this.modelProfileService.getModelProfile("profile.model.chatcompletion");
final var getWeather = 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 getCpu = new ToolDefinition(
"get_cpu",
"Gets the current CPU utilization.",
ToolParameters.create(Map.of("cpu", "CPU identifier, for example CPU0. Use CPU for overall utilization.")),
true);
final var completion = this.chatCompletionclient.streamConversation(
modelProfile,
Conversation.create(
"You are a helpful assistant. Use the provided tool when applicable.",
"How high is the current CPU utilization?"),
chunk -> {
if (chunk.reasoningContent() != null) {
System.out.print(chunk.reasoningContent());
}
if (chunk.content() != null) {
System.out.print(chunk.content());
}
},
ToolCallData.required(List.of(getWeather, getCpu)),
false);
assertTrue(this.chatCompletionclient.supportsToolCalling());
assertFalse(this.textCompletionClient.supportsToolCalling());
assertTrue(this.clientService.supportsToolCalling(modelProfile));
assertTrue(completion.finishReason() == CompletionFinishReason.TOOL_CALLS);
assertFalse(completion.toolCalls().isEmpty());
assertEquals("get_cpu", completion.toolCalls().getFirst().name());
assertTrue(completion.toolCalls().getFirst().arguments().contains("CPU"));
}
@Disabled
@Test
void hugePrompt() throws Exception {
@@ -156,6 +235,7 @@ public class CompletionClientTest {
final var completion = this.textCompletionClient.conversation(
ModelCategory.MEDIUM,
Conversation.create("You are counting machine", "How many numbers do you count?\n" + sb.toString()),
null,
false);
LOGGER.info(completion.response());
}
@@ -204,6 +284,7 @@ public class CompletionClientTest {
final var completion = this.textCompletionClient.conversation(
ModelCategory.MEDIUM,
Conversation.create("You are a helpful assistant", "How do I calculate the annual profit margin?"),
null,
false);
LOGGER.info(completion.response());
}
@@ -215,6 +296,7 @@ public class CompletionClientTest {
final var completion = this.textCompletionClient.conversation(
ModelCategory.MEDIUM,
Conversation.create("You are a helpful assistant", "How do I calculate the operating profit ratio?"),
null,
false);
LOGGER.info(completion.response());
}
@@ -226,6 +308,7 @@ public class CompletionClientTest {
final var completion = this.textCompletionClient.conversation(
ModelCategory.MEDIUM,
Conversation.create("You are a helpful assistant", "How do I calculate the return of investment?"),
null,
false);
LOGGER.info(completion.response());
}
@@ -237,6 +320,7 @@ public class CompletionClientTest {
final var completion = this.textCompletionClient.conversation(
ModelCategory.MEDIUM,
Conversation.create("You are a helpful assistant", "How do I calculate the return on net worth?"),
null,
false);
LOGGER.info(completion.response());
}