Java: Fix required tool call being requested multiple times (#5962)

### Motivation and Context

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->

### Description

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [ ] The code builds clean without any errors or warnings
- [ ] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [ ] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone 😄
This commit is contained in:
John Oliver
2024-04-24 11:41:23 +01:00
committed by GitHub
parent fcc56cd909
commit c418266ae3
4 changed files with 160 additions and 21 deletions
@@ -52,6 +52,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -200,15 +201,11 @@ public class OpenAIChatCompletion extends OpenAiService implements ChatCompletio
return performToolCall(kernel, invocationContext, requestMessages,
toolCall);
}
return requestMessages;
})
.flatMap(it -> it)
.flatMap(msgs -> {
ChatRequestMessage m = msgs.get(msgs.size() - 1);
if (m instanceof ChatRequestAssistantMessage &&
((ChatRequestAssistantMessage) m).getToolCalls().size() > 0) {
LOGGER.error("No response");
}
return internalChatMessageContentsAsync(msgs, kernel, functions,
invocationContext, autoInvokeAttempts - 1);
})
@@ -262,9 +259,9 @@ public class OpenAIChatCompletion extends OpenAiService implements ChatCompletio
functionResult.getResult(),
functionToolCall.getId());
ArrayList<ChatRequestMessage> res = new ArrayList<>(
msgs);
ArrayList<ChatRequestMessage> res = new ArrayList<>(msgs);
res.add(requestToolMessage);
return res;
})
.switchIfEmpty(Mono.fromSupplier(
@@ -456,8 +453,11 @@ public class OpenAIChatCompletion extends OpenAiService implements ChatCompletio
.setModel(chatCompletionService.getModelId());
if (invocationContext != null && invocationContext.getToolCallBehavior() != null) {
configureToolCallBehaviorOptions(options, invocationContext.getToolCallBehavior(),
functions, autoInvokeAttempts);
configureToolCallBehaviorOptions(
options,
invocationContext.getToolCallBehavior(),
functions,
chatRequestMessages);
}
PromptExecutionSettings promptExecutionSettings = invocationContext != null
@@ -523,7 +523,7 @@ public class OpenAIChatCompletion extends OpenAiService implements ChatCompletio
ChatCompletionsOptions options,
@Nullable ToolCallBehavior toolCallBehavior,
@Nullable List<OpenAIFunction> functions,
int autoInvokeAttempts) {
List<ChatRequestMessage> chatRequestMessages) {
if (toolCallBehavior == null) {
return;
@@ -537,6 +537,18 @@ public class OpenAIChatCompletion extends OpenAiService implements ChatCompletio
if (toolCallBehavior instanceof ToolCallBehavior.RequiredKernelFunction) {
KernelFunction<?> toolChoice = ((ToolCallBehavior.RequiredKernelFunction) toolCallBehavior)
.getRequiredFunction();
String toolChoiceName = String.format("%s%s%s",
toolChoice.getPluginName(),
OpenAIFunction.getNameSeparator(),
toolChoice.getName());
// If required tool call has already been called dont ask for it again
boolean hasBeenExecuted = hasToolCallBeenExecuted(chatRequestMessages, toolChoiceName);
if (hasBeenExecuted) {
return;
}
List<ChatCompletionsToolDefinition> toolDefinitions = new ArrayList<>();
toolDefinitions.add(new ChatCompletionsFunctionToolDefinition(
@@ -547,10 +559,7 @@ public class OpenAIChatCompletion extends OpenAiService implements ChatCompletio
options.setTools(toolDefinitions);
try {
String json = String.format(
"{\"type\":\"function\",\"function\":{\"name\":\"%s%s%s\"}}",
toolChoice.getPluginName(),
OpenAIFunction.getNameSeparator(),
toolChoice.getName());
"{\"type\":\"function\",\"function\":{\"name\":\"%s\"}}", toolChoiceName);
options.setToolChoice(BinaryData.fromObject(new ObjectMapper().readTree(json)));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
@@ -582,6 +591,39 @@ public class OpenAIChatCompletion extends OpenAiService implements ChatCompletio
options.setToolChoice(BinaryData.fromString("auto"));
}
private static boolean hasToolCallBeenExecuted(List<ChatRequestMessage> chatRequestMessages,
String toolChoiceName) {
return chatRequestMessages
.stream()
.flatMap(message -> {
// Extract tool calls
if (message instanceof ChatRequestAssistantMessage) {
return ((ChatRequestAssistantMessage) message).getToolCalls().stream();
}
return Stream.empty();
})
.filter(toolCall -> {
// Filter if tool call has correct name
if (toolCall instanceof ChatCompletionsFunctionToolCall) {
return ((ChatCompletionsFunctionToolCall) toolCall).getFunction().getName()
.equals(toolChoiceName);
}
return false;
})
.allMatch(toolcall -> {
String id = toolcall.getId();
// True if tool call id has a response message
return chatRequestMessages
.stream()
.filter(
chatRequestMessage -> chatRequestMessage instanceof ChatRequestToolMessage)
.anyMatch(
chatRequestMessage -> ((ChatRequestToolMessage) chatRequestMessage)
.getToolCallId()
.equals(id));
});
}
private static List<ChatRequestMessage> getChatRequestMessages(ChatHistory chatHistory) {
List<ChatMessageContent<?>> messages = chatHistory.getMessages();
if (messages == null || messages.isEmpty()) {
@@ -10,15 +10,19 @@ import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion;
import com.microsoft.semantickernel.aiservices.openai.textcompletion.OpenAITextGenerationService;
import com.microsoft.semantickernel.plugin.KernelPluginFactory;
import com.microsoft.semantickernel.semanticfunctions.KernelFunction;
import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments;
import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig;
import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction;
import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService;
import com.microsoft.semantickernel.services.textcompletion.TextGenerationService;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
@WireMockTest
public class RenderingTest {
@@ -150,6 +154,58 @@ public class RenderingTest {
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("{{$ignore}}"));
}
@DefineKernelFunction(name = "WithEmptyListReturn")
public List<Integer> WithEmptyListReturn() {
return List.of();
}
@DefineKernelFunction(name = "WithListReturn")
public List<Integer> WithListReturn() {
return List.of(1, 2, 3);
}
@DefineKernelFunction(name = "WithListReturn2", returnType = "java.util.List")
public List<Integer> WithListReturn2() {
return List.of(1, 2, 3);
}
@DefineKernelFunction(name = "WithListReturn3", returnType = "java.util.List")
public Mono<List<Integer>> WithListReturn3() {
return Mono.just(List.of(1, 2, 3));
}
@DefineKernelFunction(name = "WithListReturn4", returnType = "java.util.List")
public Mono<List<Integer>> WithListReturn4() {
return Mono.just(List.of());
}
@Test
public void canHandleIterableReturnFromFunction() {
buildChatKernel()
.invokeAsync(
KernelFunction
.createFromPrompt("""
<message role="user">{{RenderingTest-WithEmptyListReturn}}</message>
<message role="user">{{RenderingTest-WithListReturn}}</message>
<message role="user">{{RenderingTest-WithListReturn2}}</message>
<message role="user">{{RenderingTest-WithListReturn3}}</message>
<message role="user">{{RenderingTest-WithListReturn4}}</message>
""")
.withTemplateFormat("handlebars")
.build())
.block();
Assertions.assertTrue(
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().equals(
"{\"messages\":["
+ "{\"role\":\"user\",\"content\":\"[]\"},"
+ "{\"role\":\"user\",\"content\":\"[1, 2, 3]\"},"
+ "{\"role\":\"user\",\"content\":\"[1, 2, 3]\"},"
+ "{\"role\":\"user\",\"content\":\"[1, 2, 3]\"},"
+ "{\"role\":\"user\",\"content\":\"[]\"}"
+ "],\"model\":\"gpt-35-turbo-2\"}"));
}
private Kernel buildTextKernel() {
wm.addStubMapping(
stubFor(
@@ -205,6 +261,7 @@ public class RenderingTest {
Kernel kernel = Kernel.builder()
.withAIService(ChatCompletionService.class, textGenerationService)
.withPlugin(KernelPluginFactory.createFromObject(this, "RenderingTest"))
.build();
return kernel;
}
@@ -277,6 +277,27 @@ public class Example09_FunctionTypes {
.invokeAsync(plugin.get("AsyncWithBoxedPrimativeReturnType"))
.block();
System.out.println(result12.getResult());
var result13 = kernel
.invokeAsync(plugin.get("WithEmptyListReturn"))
.block();
System.out.println(result13.getResult());
var result14 = kernel
.invokeAsync(plugin.get("WithListReturn"))
.block();
System.out.println(result14.getResult());
var result15 = kernel
.invokeAsync(plugin.get("WithListReturn2"))
.block();
System.out.println(result15.getResult());
var result16 = kernel
.invokeAsync(plugin.get("WithListReturn3"))
.block();
System.out.println(result16.getResult());
/*
* TODO: support FunctionResult
* kernel
@@ -514,6 +535,26 @@ public class Example09_FunctionTypes {
return Mono.just(Integer.valueOf(1));
}
@DefineKernelFunction(name = "WithEmptyListReturn")
public List<Integer> WithEmptyListReturn() {
return List.of();
}
@DefineKernelFunction(name = "WithListReturn")
public List<Integer> WithListReturn() {
return List.of(1, 2, 3);
}
@DefineKernelFunction(name = "WithListReturn2", returnType = "java.util.List")
public List<Integer> WithListReturn2() {
return List.of(1, 2, 3);
}
@DefineKernelFunction(name = "WithListReturn3", returnType = "java.util.List")
public Mono<List<Integer>> WithListReturn3() {
return Mono.just(List.of(1, 2, 3));
}
/*
* /// <summary>
* /// Example using a function to return the result of another inner function
@@ -181,13 +181,12 @@ public class KernelFunctionFromMethod<T> extends KernelFunction<T> {
}
return mono
.map(it -> {
if (it instanceof Iterable) {
// Handle return from things like Mono<List<?>>
// from {{function 'input'}} as part of the prompt.
return (T) ((Iterable<?>) it).iterator().next();
} else {
return (T) it;
.flatMap(it -> {
try {
return Mono.just((T) it);
} catch (ClassCastException e) {
return Mono.error(
new SKException("Return type does not match the expected type", e));
}
})
.map(it -> {