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:
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user