diff --git a/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java b/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java index 8c728b8832..0c2e11780a 100644 --- a/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java +++ b/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java @@ -198,8 +198,10 @@ public class OpenAIChatCompletion implements ChatCompletionService { functionResult.getResult(), functionToolCall.getId()); - msgs.add(requestToolMessage); - return msgs; + ArrayList res = new ArrayList<>( + msgs); + res.add(requestToolMessage); + return res; }); }); } @@ -207,8 +209,10 @@ public class OpenAIChatCompletion implements ChatCompletionService { }) .flatMap(it -> it) .flatMap( - msgs -> internalChatMessageContentsAsync(msgs, kernel, functions, - invocationContext, autoInvokeAttempts - 1)); + msgs -> { + return internalChatMessageContentsAsync(msgs, kernel, functions, + invocationContext, autoInvokeAttempts - 1); + }); }); return result.map(op -> (List>) op); @@ -218,36 +222,50 @@ public class OpenAIChatCompletion implements ChatCompletionService { private Mono> invokeFunctionTool( Kernel kernel, ChatCompletionsFunctionToolCall toolCall) { + + try { + OpenAIFunctionToolCall openAIFunctionToolCall = extractOpenAIFunctionToolCall(toolCall); + KernelFunction function = kernel.getFunction( + openAIFunctionToolCall.getPluginName(), + openAIFunctionToolCall.getFunctionName()); + + return function + .invokeAsync(kernel) + .withArguments(openAIFunctionToolCall.getArguments()) + .withResultType(ContextVariableTypes.getGlobalVariableTypeForClass( + String.class)); + } catch (JsonProcessingException e) { + return Mono.error(new SKException("Failed to parse tool arguments")); + } + } + + private OpenAIFunctionToolCall extractOpenAIFunctionToolCall( + ChatCompletionsFunctionToolCall toolCall) throws JsonProcessingException { + // Split the full name of a function into plugin and function name String name = toolCall.getFunction().getName(); String[] parts = name.split(OpenAIFunction.getNameSeparator()); String pluginName = parts.length > 1 ? parts[0] : ""; String fnName = parts.length > 1 ? parts[1] : parts[0]; - KernelFunction function = kernel.getFunction(pluginName, fnName); - KernelFunctionArguments arguments = KernelFunctionArguments.builder().build(); - try { - ObjectMapper mapper = new ObjectMapper(); - JsonNode jsonToolCallArguments = mapper.readTree(toolCall.getFunction().getArguments()); + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonToolCallArguments = mapper.readTree(toolCall.getFunction().getArguments()); - jsonToolCallArguments.fields().forEachRemaining( - entry -> arguments.put(entry.getKey(), - ContextVariable.of(entry.getValue().asText()))); - } catch (JsonProcessingException e) { - LOGGER.error("Failed to parse json", e); - return Mono.empty(); - } + jsonToolCallArguments.fields().forEachRemaining( + entry -> arguments.put(entry.getKey(), + ContextVariable.of(entry.getValue().asText()))); + + return new OpenAIFunctionToolCall( + toolCall.getId(), + pluginName, + fnName, + arguments); - return function - .invokeAsync(kernel) - .withArguments(arguments) - .withResultType(ContextVariableTypes.getGlobalVariableTypeForClass( - String.class)); } - private Mono> getChatMessageContentsAsync( + private Mono> getChatMessageContentsAsync( ChatCompletions completions) { FunctionResultMetadata completionMetadata = FunctionResultMetadata.build( completions.getId(), @@ -262,16 +280,40 @@ public class OpenAIChatCompletion implements ChatCompletionService { .collect(Collectors.toList()); return Flux.fromIterable(responseMessages) - .map(response -> new ChatMessageContent( + .map(response -> new OpenAIChatMessageContent( AuthorRole.ASSISTANT, response.getContent(), this.getModelId(), null, null, - completionMetadata)) + completionMetadata, + formOpenAiToolCalls(response))) .collectList(); } + private List formOpenAiToolCalls(ChatResponseMessage response) { + if (response.getToolCalls() == null || response.getToolCalls().isEmpty()) { + return new ArrayList<>(); + } + return response + .getToolCalls() + .stream() + .map(call -> { + if (call instanceof ChatCompletionsFunctionToolCall) { + try { + return extractOpenAIFunctionToolCall( + (ChatCompletionsFunctionToolCall) call); + } catch (JsonProcessingException e) { + throw new SKException("Failed to parse tool arguments", e); + } + } else { + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + private static ChatCompletionsOptions getCompletionsOptions( ChatCompletionService chatCompletionService, List chatRequestMessages, diff --git a/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatMessageContent.java b/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatMessageContent.java new file mode 100644 index 0000000000..c862905f05 --- /dev/null +++ b/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatMessageContent.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.aiservices.openai.chatcompletion; + +import com.microsoft.semantickernel.orchestration.FunctionResultMetadata; +import com.microsoft.semantickernel.services.chatcompletion.AuthorRole; +import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; + +public class OpenAIChatMessageContent extends ChatMessageContent { + + @Nullable + private final List toolCall; + + public OpenAIChatMessageContent(AuthorRole authorRole, String content, @Nullable String modelId, + @Nullable T innerContent, @Nullable Charset encoding, + @Nullable FunctionResultMetadata metadata) { + super(authorRole, content, modelId, innerContent, encoding, metadata); + toolCall = null; + } + + public OpenAIChatMessageContent( + AuthorRole authorRole, + String content, + @Nullable String modelId, + @Nullable T innerContent, + @Nullable Charset encoding, + @Nullable FunctionResultMetadata metadata, + @Nullable List toolCall) { + super(authorRole, content, modelId, innerContent, encoding, metadata); + + if (toolCall == null) { + this.toolCall = null; + } else { + this.toolCall = Collections.unmodifiableList(toolCall); + } + } + + @Nullable + public List getToolCall() { + return toolCall; + } +} diff --git a/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIFunctionToolCall.java b/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIFunctionToolCall.java new file mode 100644 index 0000000000..3486cf5098 --- /dev/null +++ b/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIFunctionToolCall.java @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.aiservices.openai.chatcompletion; + +import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import javax.annotation.Nullable; + +public class OpenAIFunctionToolCall { + + /// Gets the ID of the tool call. + @Nullable + private final String id; + + /// Gets the name of the plugin with which this function is associated, if any. + + @Nullable + private final String pluginName; + + /// Gets the name of the function. + private final String functionName; + + /// Gets a name/value collection of the arguments to the function, if any. + @Nullable + private final KernelFunctionArguments arguments; + + public OpenAIFunctionToolCall( + @Nullable String id, + @Nullable String pluginName, + String functionName, + @Nullable KernelFunctionArguments arguments) { + this.id = id; + this.pluginName = pluginName; + this.functionName = functionName; + this.arguments = arguments; + } + + @Nullable + public String getId() { + return id; + } + + @Nullable + public String getPluginName() { + return pluginName; + } + + public String getFunctionName() { + return functionName; + } + + @Nullable + public KernelFunctionArguments getArguments() { + return arguments; + } +} diff --git a/java/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ToolCallBehaviourTest.java b/java/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ToolCallBehaviourTest.java new file mode 100644 index 0000000000..c49be212cf --- /dev/null +++ b/java/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/ToolCallBehaviourTest.java @@ -0,0 +1,311 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.tests; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; + +import com.azure.ai.openai.OpenAIAsyncClient; +import com.azure.ai.openai.OpenAIClientBuilder; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.MappingBuilder; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import com.github.tomakehurst.wiremock.matching.ContainsPattern; +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatMessageContent; +import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIFunctionToolCall; +import com.microsoft.semantickernel.exceptions.SKException; +import com.microsoft.semantickernel.orchestration.InvocationContext; +import com.microsoft.semantickernel.orchestration.ToolCallBehavior; +import com.microsoft.semantickernel.plugin.KernelPlugin; +import com.microsoft.semantickernel.semanticfunctions.KernelFunction; +import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromMethod; +import com.microsoft.semantickernel.services.chatcompletion.AuthorRole; +import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; +import com.microsoft.semantickernel.services.chatcompletion.ChatHistory; +import com.microsoft.semantickernel.services.chatcompletion.ChatMessageContent; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +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 org.mockito.Mockito; + +@WireMockTest +public class ToolCallBehaviourTest { + + private WireMockServer wm; + + @BeforeEach + public void before() { + wm = new WireMockServer(); + } + + @AfterEach + public void after() { + wm.stop(); + } + + @Test + public void toolThatDoesNotExistThrows() { + ChatCompletionService chatCompletionService = getChatCompletionService(); + + Kernel kernel = Kernel.builder() + .withAIService(ChatCompletionService.class, chatCompletionService) + .build(); + + ChatHistory messages = new ChatHistory(); + messages.addMessage( + new ChatMessageContent<>( + AuthorRole.USER, + "Call A function")); + + try { + chatCompletionService + .getChatMessageContentsAsync( + messages, + kernel, + InvocationContext.builder() + .withToolCallBehavior( + ToolCallBehavior.allowAllKernelFunctions(true)) + .build()) + .block(); + Assertions.fail("Expected an exception to be thrown"); + } catch (SKException e) { + Assertions.assertTrue(e.getMessage() + .contains("Failed to find plugin apluginname")); + } + } + + @Test + public void nonAutoInvokedIsNotCalled() throws NoSuchMethodException { + ChatCompletionService chatCompletionService = getChatCompletionService(); + + TestPlugin testPlugin = Mockito.spy(new TestPlugin()); + KernelFunction method = KernelFunctionFromMethod.builder() + .withFunctionName("doIt") + .withMethod(TestPlugin.class.getMethod("doIt")) + .withTarget(testPlugin) + .withPluginName("apluginname") + .build(); + + Kernel kernel = Kernel.builder() + .withAIService(ChatCompletionService.class, chatCompletionService) + .withPlugin( + new KernelPlugin( + "apluginname", + "A plugin description", + Map.of("doIt", method))) + .build(); + + ChatHistory messages = new ChatHistory(); + messages.addMessage( + new ChatMessageContent<>( + AuthorRole.USER, + "Call A function")); + + List> result = chatCompletionService + .getChatMessageContentsAsync( + messages, + kernel, + InvocationContext.builder() + .withToolCallBehavior( + ToolCallBehavior.allowOnlyKernelFunctions(false, method)) + .build()) + .block(); + + List toolCalls = ((OpenAIChatMessageContent) result.get(0)) + .getToolCall(); + + Assertions.assertNotNull(toolCalls); + Assertions.assertEquals(1, toolCalls.size()); + Assertions.assertEquals("apluginname", toolCalls.get(0).getPluginName()); + Assertions.assertEquals("doIt", toolCalls.get(0).getFunctionName()); + Assertions.assertEquals("call_abc123", toolCalls.get(0).getId()); + + Mockito.verify(testPlugin, Mockito.times(0)).doIt(); + } + + @Test + public void toolIsInvoked() throws NoSuchMethodException { + ChatCompletionService chatCompletionService = getChatCompletionService(); + + TestPlugin testPlugin = Mockito.spy(new TestPlugin()); + KernelFunction method = KernelFunctionFromMethod.builder() + .withFunctionName("doIt") + .withMethod(TestPlugin.class.getMethod("doIt")) + .withTarget(testPlugin) + .withPluginName("apluginname") + .build(); + + Kernel kernel = Kernel.builder() + .withAIService(ChatCompletionService.class, chatCompletionService) + .withPlugin( + new KernelPlugin( + "apluginname", + "A plugin description", + Map.of("doIt", method))) + .build(); + + ChatHistory messages = new ChatHistory(); + messages.addMessage( + new ChatMessageContent<>( + AuthorRole.USER, + "Call A function")); + + List> result = chatCompletionService + .getChatMessageContentsAsync( + messages, + kernel, + InvocationContext.builder() + .withToolCallBehavior( + ToolCallBehavior.allowAllKernelFunctions(true)) + .build()) + .block(); + + Assertions.assertTrue(result.get(0).getContent().contains("tool call done")); + Mockito.verify(testPlugin, Mockito.times(1)).doIt(); + + result = chatCompletionService + .getChatMessageContentsAsync( + messages, + kernel, + InvocationContext.builder() + .withToolCallBehavior( + ToolCallBehavior.allowOnlyKernelFunctions(true, Arrays.asList(method))) + .build()) + .block(); + + Assertions.assertTrue(result.get(0).getContent().contains("tool call done")); + Mockito.verify(testPlugin, Mockito.times(2)).doIt(); + + result = chatCompletionService + .getChatMessageContentsAsync( + messages, + kernel, + InvocationContext.builder() + .withToolCallBehavior( + ToolCallBehavior.allowOnlyKernelFunctions(true, method)) + .build()) + .block(); + + Assertions.assertTrue(result.get(0).getContent().contains("tool call done")); + Mockito.verify(testPlugin, Mockito.times(3)).doIt(); + } + + private ChatCompletionService getChatCompletionService() { + wm + .addStubMapping( + stubFor( + buildResponse("Call A function", """ + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_abc123", + "type": "function", + "function": { + "name": "apluginname-doIt", + "arguments": "{}" + } + } + ] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ] + """))); + + wm.addStubMapping( + stubFor( + buildResponse("Tool call performed", """ + "choices" : [ + { + "finish_reason" : "stop", + "index" : 0, + "message" : { + "content" : "tool call done", + "role" : "assistant" + } + } + ] + """))); + + wm.start(); + + final OpenAIAsyncClient client = new OpenAIClientBuilder() + .endpoint("http://localhost:" + wm.port() + "/") + .buildAsyncClient(); + + return ChatCompletionService.builder() + .withOpenAIAsyncClient(client) + .withModelId("gpt-35-turbo-2") + .build(); + } + + private static MappingBuilder buildResponse(String bodyMatcher, String responseBody) { + return post(urlEqualTo( + "//openai/deployments/gpt-35-turbo-2/chat/completions?api-version=2023-12-01-preview")) + .withRequestBody(new ContainsPattern(bodyMatcher)) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(formResponse(responseBody))); + } + + public static class TestPlugin { + + public String doIt() { + return "Tool call performed"; + } + } + + private static String formResponse(String chatChoices) { + return """ + { + %s, + "created" : 1707253061, + "id" : "chatcmpl-xxx", + "model" : "gpt-35-turbo", + "object" : "chat.completion", + "prompt_filter_results" : [ + { + "content_filter_results" : { + "hate" : { + "filtered" : false, + "severity" : "safe" + }, + "self_harm" : { + "filtered" : false, + "severity" : "safe" + }, + "sexual" : { + "filtered" : false, + "severity" : "safe" + }, + "violence" : { + "filtered" : false, + "severity" : "safe" + } + }, + "prompt_index" : 0 + } + ], + "usage" : { + "completion_tokens" : 67, + "prompt_tokens" : 17, + "total_tokens" : 84 + } + } + """.formatted(chatChoices); + } +} diff --git a/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example57_KernelHooks.java b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example57_KernelHooks.java index 309cbd6d90..0313edb38a 100644 --- a/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example57_KernelHooks.java +++ b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example57_KernelHooks.java @@ -225,7 +225,8 @@ public class Example57_KernelHooks { event.getArguments(), new FunctionResult<>( ContextVariable.of(result), - event.getResult().getMetadata())); + event.getResult().getMetadata(), + result)); }; kernel.getGlobalKernelHooks().addHook(hook); diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/KernelPluginCollection.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/KernelPluginCollection.java index 3348e447b8..1b682e4c34 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/KernelPluginCollection.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/KernelPluginCollection.java @@ -2,6 +2,7 @@ package com.microsoft.semantickernel; import com.microsoft.semantickernel.contextvariables.CaseInsensitiveMap; +import com.microsoft.semantickernel.exceptions.SKException; import com.microsoft.semantickernel.plugin.KernelPlugin; import com.microsoft.semantickernel.semanticfunctions.KernelFunction; import com.microsoft.semantickernel.semanticfunctions.KernelFunctionMetadata; @@ -49,12 +50,12 @@ class KernelPluginCollection { KernelFunction getFunction(String pluginName, String functionName) { KernelPlugin plugin = plugins.get(pluginName); if (plugin == null) { - throw new IllegalArgumentException("Failed to find plugin " + pluginName); + throw new SKException("Failed to find plugin " + pluginName); } KernelFunction function = plugin.get(functionName); if (function == null) { - throw new IllegalArgumentException( + throw new SKException( "Function '" + functionName + "' not found in plugin '" + pluginName + "'"); } return function; diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/CharacterVariableContextVariableTypeConverter.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/CharacterVariableContextVariableTypeConverter.java index c6787f9087..2629c0e4a7 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/CharacterVariableContextVariableTypeConverter.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/CharacterVariableContextVariableTypeConverter.java @@ -5,6 +5,7 @@ import static com.microsoft.semantickernel.contextvariables.ContextVariableTypes import com.microsoft.semantickernel.contextvariables.ContextVariableTypeConverter; import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; +import com.microsoft.semantickernel.exceptions.SKException; /** * A {@link ContextVariableTypeConverter} for {@code java.lang.Character} variables. Use @@ -33,7 +34,7 @@ public class CharacterVariableContextVariableTypeConverter extends */ static Character charFromPromptString(String s) { if (s.length() != 1) { - throw new IllegalArgumentException("Cannot convert string to character: " + s); + throw new SKException("Cannot convert string to character: " + s); } return s.charAt(0); } diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/CodeBlock.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/CodeBlock.java index a3cf44ee51..f209477c55 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/CodeBlock.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/CodeBlock.java @@ -174,7 +174,7 @@ public final class CodeBlock extends Block implements CodeRendering { // Check if the function has parameters to be set if (functionMetadata.getParameters().isEmpty()) { - throw new IllegalArgumentException( + throw new SKException( "Function " + fBlock.getPluginName() + "." + fBlock.getFunctionName() + " does not take any arguments but it is being called in the template with {this._tokens.Count - 1} arguments."); } diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionInvocation.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionInvocation.java index 8d1312a139..a659373d3f 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionInvocation.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionInvocation.java @@ -117,7 +117,8 @@ public class FunctionInvocation extends Mono> { try { sink.next(new FunctionResult<>( ContextVariable.convert(result.getResult(), variableType), - result.getMetadata())); + result.getMetadata(), + result.getUnconvertedResult())); } catch (Exception e) { sink.error(new SKException( "Failed to convert result to requested type: " diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionResult.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionResult.java index 480121e455..82bf3d591e 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionResult.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionResult.java @@ -17,6 +17,8 @@ public class FunctionResult { private final ContextVariable result; private final FunctionResultMetadata metadata; + @Nullable + private final Object unconvertedResult; /** * Create a new instance of FunctionResult. @@ -26,9 +28,11 @@ public class FunctionResult { */ public FunctionResult( ContextVariable result, - @Nullable FunctionResultMetadata metadata) { + @Nullable FunctionResultMetadata metadata, + @Nullable Object unconvertedResult) { this.result = result; this.metadata = metadata == null ? FunctionResultMetadata.empty() : metadata; + this.unconvertedResult = unconvertedResult; } /** @@ -36,8 +40,9 @@ public class FunctionResult { * * @param of The result of the function invocation. */ - public FunctionResult(ContextVariable of) { - this(of, FunctionResultMetadata.empty()); + public FunctionResult(ContextVariable of, + @Nullable Object unconvertedResult) { + this(of, FunctionResultMetadata.empty(), unconvertedResult); } /** @@ -71,4 +76,9 @@ public class FunctionResult { public FunctionResultMetadata getMetadata() { return metadata; } + + @Nullable + public Object getUnconvertedResult() { + return unconvertedResult; + } } diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/ToolCallBehavior.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/ToolCallBehavior.java index e9fe8bed01..8148eb16c2 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/ToolCallBehavior.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/ToolCallBehavior.java @@ -1,16 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.orchestration; +import com.microsoft.semantickernel.semanticfunctions.KernelFunction; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; - import javax.annotation.Nullable; -import com.microsoft.semantickernel.semanticfunctions.KernelFunction; - /** * Defines the behavior of a tool call. Currently, the only tool available is function calling. */ diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionArguments.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionArguments.java index 0a072b4ba5..95bea8b9b3 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionArguments.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionArguments.java @@ -119,7 +119,7 @@ public class KernelFunctionArguments implements Map> return (ContextVariable) value; } - throw new IllegalArgumentException( + throw new SKException( String.format( "Variable %s is of type %s, but requested type is %s", key, value.getType().getClazz(), clazz)); diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromMethod.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromMethod.java index 7693d6d892..e7991ee311 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromMethod.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromMethod.java @@ -177,7 +177,7 @@ public class KernelFunctionFromMethod extends KernelFunction { mono = invokeAsyncFunction(method, instance, args); } - Mono r = mono + return mono .map(it -> { if (it instanceof Iterable) { // Handle return from things like Mono> @@ -186,9 +186,7 @@ public class KernelFunctionFromMethod extends KernelFunction { } else { return (T) it; } - }); - - return r + }) .map(it -> { // If given a variable type, use it. // If it's wrong, then it's a programming error on the part of the caller. @@ -200,7 +198,7 @@ public class KernelFunctionFromMethod extends KernelFunction { it.getClass().getName())); } return new FunctionResult<>( - new ContextVariable<>(variableType, it)); + new ContextVariable<>(variableType, it), it); } Class returnParameterType = function @@ -221,7 +219,8 @@ public class KernelFunctionFromMethod extends KernelFunction { if (contextVariableType != null) { return new FunctionResult<>( - new ContextVariable<>(contextVariableType, it)); + new ContextVariable<>(contextVariableType, it), + it); } // If we get here, then either the returnParameterType doesn't match T diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromPrompt.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromPrompt.java index fe2c5cb649..5192c642c8 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromPrompt.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/semanticfunctions/KernelFunctionFromPrompt.java @@ -164,7 +164,8 @@ public class KernelFunctionFromPrompt extends KernelFunction { return Flux.just( new FunctionResult<>( new ContextVariable<>(variableType, value), - chatMessageContent.getMetadata())); + chatMessageContent.getMetadata(), + chatMessageContent)); } return Flux.empty(); }) @@ -175,7 +176,8 @@ public class KernelFunctionFromPrompt extends KernelFunction { it.getResult() != null ? variableType.of(it.getResult()).getValue() : null), - it.getMetadata()); + it.getMetadata(), + it.getUnconvertedResult()); }); } else if (client instanceof TextGenerationService) { result = ((TextGenerationService) client) @@ -200,7 +202,8 @@ public class KernelFunctionFromPrompt extends KernelFunction { new ContextVariable<>( variableType, value), - textContent.getMetadata())); + textContent.getMetadata(), + textContent)); }); } else { return Flux.error(new IllegalStateException("Unknown service type"));