Java: Add OpenApi plugin importer and upgrade tool calling to cover more usecases (#5829)

### 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-19 14:28:34 +01:00
committed by GitHub
parent 6f0561e953
commit 0a7f96ae4b
29 changed files with 5726 additions and 101 deletions
@@ -22,6 +22,7 @@ import com.azure.core.util.BinaryData;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ContainerNode;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.aiservices.openai.implementation.OpenAIRequestSettings;
import com.microsoft.semantickernel.contextvariables.ContextVariable;
@@ -29,9 +30,11 @@ import com.microsoft.semantickernel.contextvariables.ContextVariableTypes;
import com.microsoft.semantickernel.exceptions.AIException;
import com.microsoft.semantickernel.exceptions.AIException.ErrorCodes;
import com.microsoft.semantickernel.exceptions.SKException;
import com.microsoft.semantickernel.hooks.KernelHookEvent;
import com.microsoft.semantickernel.hooks.KernelHooks;
import com.microsoft.semantickernel.hooks.PostChatCompletionEvent;
import com.microsoft.semantickernel.hooks.PreChatCompletionEvent;
import com.microsoft.semantickernel.hooks.PreToolCallEvent;
import com.microsoft.semantickernel.orchestration.FunctionResult;
import com.microsoft.semantickernel.orchestration.FunctionResultMetadata;
import com.microsoft.semantickernel.orchestration.InvocationContext;
@@ -147,15 +150,15 @@ public class OpenAIChatCompletion implements ChatCompletionService {
@Nullable InvocationContext invocationContext,
int autoInvokeAttempts) {
KernelHooks kernelHooks = invocationContext != null
&& invocationContext.getKernelHooks() != null
? invocationContext.getKernelHooks()
: new KernelHooks();
ChatCompletionsOptions options = kernelHooks
.executeHooks(new PreChatCompletionEvent(
getCompletionsOptions(this, messages,
functions, invocationContext, autoInvokeAttempts)))
ChatCompletionsOptions options = executeHook(
invocationContext,
new PreChatCompletionEvent(
getCompletionsOptions(
this,
messages,
functions,
invocationContext,
autoInvokeAttempts)))
.getOptions();
Mono<List<? extends ChatMessageContent>> result = client
@@ -177,7 +180,7 @@ public class OpenAIChatCompletion implements ChatCompletionService {
.collect(Collectors.toList());
// execute post chat completion hook
kernelHooks.executeHooks(new PostChatCompletionEvent(completions));
executeHook(invocationContext, new PostChatCompletionEvent(completions));
// Just return the result:
// If we don't want to attempt to invoke any functions
@@ -206,53 +209,122 @@ public class OpenAIChatCompletion implements ChatCompletionService {
Mono.just(messages),
(requestMessages, toolCall) -> {
if (toolCall instanceof ChatCompletionsFunctionToolCall) {
return requestMessages
.flatMap(msgs -> {
// OpenAI only supports function tool call at the moment
ChatCompletionsFunctionToolCall functionToolCall = (ChatCompletionsFunctionToolCall) toolCall;
if (kernel == null) {
return Mono
.error(new SKException(
"A tool call was requested, but no kernel was provided to the invocation, this is a unsupported configuration"));
}
ContextVariableTypes contextVariableTypes = invocationContext == null
? new ContextVariableTypes()
: invocationContext.getContextVariableTypes();
return invokeFunctionTool(
kernel,
functionToolCall,
contextVariableTypes)
.map(functionResult -> {
// Add chat request tool message to the chat options
ChatRequestMessage requestToolMessage = new ChatRequestToolMessage(
functionResult.getResult(),
functionToolCall.getId());
ArrayList<ChatRequestMessage> res = new ArrayList<>(
msgs);
res.add(requestToolMessage);
return res;
});
});
return performToolCall(kernel, invocationContext, requestMessages,
toolCall);
}
return requestMessages;
})
.flatMap(it -> it)
.flatMap(
msgs -> {
return internalChatMessageContentsAsync(msgs, kernel, functions,
.flatMap(msgs -> {
return internalChatMessageContentsAsync(msgs, kernel, functions,
invocationContext, autoInvokeAttempts - 1);
})
.onErrorResume(e -> {
// If FunctionInvocationError occurred and there are still attempts left, retry, else exit
if (autoInvokeAttempts > 0) {
List<ChatRequestMessage> currentMessages = messages;
if (e instanceof FunctionInvocationError) {
currentMessages = ((FunctionInvocationError) e).getMessages();
}
return internalChatMessageContentsAsync(currentMessages, kernel,
functions,
invocationContext, autoInvokeAttempts - 1);
});
} else {
return Mono.error(e);
}
});
});
return result.map(op -> (List<ChatMessageContent<?>>) op);
}
private Mono<List<ChatRequestMessage>> performToolCall(
@Nullable Kernel kernel,
@Nullable InvocationContext invocationContext,
Mono<List<ChatRequestMessage>> requestMessages,
ChatCompletionsToolCall toolCall) {
return requestMessages
.flatMap(msgs -> {
try {
// OpenAI only supports function tool call at the moment
ChatCompletionsFunctionToolCall functionToolCall = (ChatCompletionsFunctionToolCall) toolCall;
if (kernel == null) {
return Mono
.error(new SKException(
"A tool call was requested, but no kernel was provided to the invocation, this is a unsupported configuration"));
}
ContextVariableTypes contextVariableTypes = invocationContext == null
? new ContextVariableTypes()
: invocationContext.getContextVariableTypes();
return invokeFunctionTool(
kernel,
invocationContext,
functionToolCall,
contextVariableTypes)
.map(functionResult -> {
// Add chat request tool message to the chat options
ChatRequestMessage requestToolMessage = new ChatRequestToolMessage(
functionResult.getResult(),
functionToolCall.getId());
ArrayList<ChatRequestMessage> res = new ArrayList<>(
msgs);
res.add(requestToolMessage);
return res;
})
.switchIfEmpty(Mono.fromSupplier(
() -> {
ChatRequestMessage requestToolMessage = new ChatRequestToolMessage(
"Completed successfully with no return value",
functionToolCall.getId());
ArrayList<ChatRequestMessage> res = new ArrayList<>(msgs);
res.add(requestToolMessage);
return res;
}))
.onErrorResume(e -> emitError(toolCall, msgs, e));
} catch (Exception e) {
return emitError(toolCall, msgs, e);
}
});
}
private Mono<ArrayList<ChatRequestMessage>> emitError(
ChatCompletionsToolCall toolCall,
List<ChatRequestMessage> msgs,
Throwable e) {
msgs.add(new ChatRequestToolMessage(
"Call failed: " + e.getMessage(),
toolCall.getId()));
return Mono.error(
new FunctionInvocationError(e, msgs));
}
/**
* Exception to be thrown when a function invocation fails.
*/
private static class FunctionInvocationError extends SKException {
private final List<ChatRequestMessage> messages;
public FunctionInvocationError(Throwable e, List<ChatRequestMessage> msgs) {
super(e.getMessage(), e);
this.messages = msgs;
}
public List<ChatRequestMessage> getMessages() {
return messages;
}
}
@SuppressWarnings("StringSplitter")
private Mono<FunctionResult<String>> invokeFunctionTool(
Kernel kernel,
@Nullable InvocationContext invocationContext,
ChatCompletionsFunctionToolCall toolCall,
ContextVariableTypes contextVariableTypes) {
@@ -268,15 +340,35 @@ public class OpenAIChatCompletion implements ChatCompletionService {
pluginName,
openAIFunctionToolCall.getFunctionName());
PreToolCallEvent hookResult = executeHook(invocationContext, new PreToolCallEvent(
openAIFunctionToolCall.getFunctionName(),
openAIFunctionToolCall.getArguments(),
function,
contextVariableTypes));
function = hookResult.getFunction();
KernelFunctionArguments arguments = hookResult.getArguments();
return function
.invokeAsync(kernel)
.withArguments(openAIFunctionToolCall.getArguments())
.withArguments(arguments)
.withResultType(contextVariableTypes.getVariableTypeForClass(String.class));
} catch (JsonProcessingException e) {
return Mono.error(new SKException("Failed to parse tool arguments"));
}
}
private static <T extends KernelHookEvent> T executeHook(
@Nullable InvocationContext invocationContext,
T event) {
KernelHooks kernelHooks = invocationContext != null
&& invocationContext.getKernelHooks() != null
? invocationContext.getKernelHooks()
: new KernelHooks();
return kernelHooks.executeHooks(event);
}
@SuppressWarnings("StringSplitter")
private OpenAIFunctionToolCall extractOpenAIFunctionToolCall(
ChatCompletionsFunctionToolCall toolCall) throws JsonProcessingException {
@@ -293,8 +385,15 @@ public class OpenAIChatCompletion implements ChatCompletionService {
JsonNode jsonToolCallArguments = mapper.readTree(toolCall.getFunction().getArguments());
jsonToolCallArguments.fields().forEachRemaining(
entry -> arguments.put(entry.getKey(),
ContextVariable.of(entry.getValue().asText())));
entry -> {
if (entry.getValue() instanceof ContainerNode) {
arguments.put(entry.getKey(),
ContextVariable.of(entry.getValue().toPrettyString()));
} else {
arguments.put(entry.getKey(),
ContextVariable.of(entry.getValue().asText()));
}
});
return new OpenAIFunctionToolCall(
toolCall.getId(),
@@ -14,6 +14,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -87,8 +88,8 @@ class OpenAIFunction {
try {
ObjectMapper objectMapper = new ObjectMapper();
for (InputVariable parameter : metadata.getParameters()) {
String parameterJsonSchema = getSchemaForFunctionParameter(
parameter.getDescription());
String parameterJsonSchema = getSchemaForFunctionParameter(parameter);
properties.put(parameter.getName(), objectMapper.readTree(parameterJsonSchema));
if (parameter.isRequired()) {
@@ -145,10 +146,37 @@ class OpenAIFunction {
}
}
private static String getSchemaForFunctionParameter(@Nullable String description) {
if (description == null) {
return "{\"type\":\"string\"}";
private static String getSchemaForFunctionParameter(@Nullable InputVariable parameter) {
List<String> entries = new ArrayList<>();
entries.add("\"type\":\"string\"");
// Add description if present
if (parameter != null && parameter.getDescription() != null && !parameter.getDescription()
.isEmpty()) {
String description = parameter.getDescription();
description = description.replace("\n", "");
description = description.replace("\"", "\\\"");
description = String.format("\"description\":\"%s\"", description);
entries.add(description);
}
return String.format("{\"type\":\"string\", \"description\":\"%s\"}", description);
// Add enum options if parameter is an enum
if (parameter != null && parameter.getEnumValues() != null && !parameter.getEnumValues()
.isEmpty()) {
String enumEntry = parameter
.getEnumValues()
.stream()
.map(Object::toString)
.map(it -> "\"" + it + "\"")
.collect(Collectors.joining(","));
entries.add("\"enum\":[ " + enumEntry + " ]");
}
String schema = String.join(",", entries);
return "{" + schema + "}";
}
}
@@ -0,0 +1,211 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.tests;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import com.azure.ai.openai.OpenAIAsyncClient;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.github.tomakehurst.wiremock.WireMockServer;
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.semanticfunctions.KernelFunction;
import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments;
import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig;
import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService;
import com.microsoft.semantickernel.services.textcompletion.TextGenerationService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@WireMockTest
public class RenderingTest {
private WireMockServer wm;
@BeforeEach
public void before() {
wm = new WireMockServer();
}
@AfterEach
public void after() {
wm.stop();
}
@Test
public void textSemanticKernelTemplate() {
buildTextKernel()
.invokeAsync(
KernelFunction
.createFromPrompt("""
Value: {{$value}}
""")
.withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT)
.build())
.withArguments(KernelFunctionArguments
.builder()
.withVariable("value", "{{$ignore}}")
.withVariable("ignore", "dont show")
.build())
.block();
Assertions.assertFalse(
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("dont show"));
Assertions.assertTrue(
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("{{$ignore}}"));
}
@Test
public void textHandleBarsTemplate() {
buildTextKernel()
.invokeAsync(
KernelFunction
.createFromPrompt("""
Value: {{value}}
""")
.withTemplateFormat("handlebars")
.build())
.withArguments(KernelFunctionArguments
.builder()
.withVariable("value", "{{ignore}}")
.withVariable("ignore", "dont show")
.build())
.block();
Assertions.assertFalse(
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("dont show"));
Assertions.assertTrue(
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("{{ignore}}"));
}
@Test
public void chatSemanticKernelTemplate() {
buildChatKernel()
.invokeAsync(
KernelFunction
.createFromPrompt("""
Value: {{$value}}
""")
.withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT)
.build())
.withArguments(KernelFunctionArguments
.builder()
.withVariable("value", "{{$ignore}}")
.withVariable("ignore", "dont show")
.build())
.block();
Assertions.assertFalse(
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("dont show"));
Assertions.assertTrue(
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("{{$ignore}}"));
}
@Test
public void chatHandleBarsTemplate() {
buildChatKernel()
.invokeAsync(
KernelFunction
.createFromPrompt("""
Value: {{value}}
""")
.withTemplateFormat("handlebars")
.build())
.withArguments(KernelFunctionArguments
.builder()
.withVariable("value", "{{ignore}}")
.withVariable("ignore", "dont show")
.build())
.block();
Assertions.assertFalse(
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("dont show"));
Assertions.assertTrue(
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("{{ignore}}"));
}
@Test
public void chatSemanticKernelTemplate2() {
buildChatKernel()
.invokeAsync(
KernelFunction
.createFromPrompt("""
<message role="user">Value: {{$value}}</message>
""")
.withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT)
.build())
.withArguments(KernelFunctionArguments
.builder()
.withVariable("value", "{{$ignore}}")
.withVariable("ignore", "dont show")
.build())
.block();
Assertions.assertFalse(
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("dont show"));
Assertions.assertTrue(
wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("{{$ignore}}"));
}
private Kernel buildTextKernel() {
wm.addStubMapping(
stubFor(
ToolCallBehaviourTest.buildTextResponse(" ", """
"choices": [
{
"text": "Value: bar"
}
]
""")));
wm.start();
final OpenAIAsyncClient client = new OpenAIClientBuilder()
.endpoint("http://localhost:" + wm.port() + "/")
.buildAsyncClient();
TextGenerationService textGenerationService = OpenAITextGenerationService.builder()
.withOpenAIAsyncClient(client)
.withModelId("gpt-35-turbo-2")
.build();
Kernel kernel = Kernel.builder()
.withAIService(TextGenerationService.class, textGenerationService)
.build();
return kernel;
}
private Kernel buildChatKernel() {
wm.addStubMapping(
stubFor(
ToolCallBehaviourTest.buildResponse(" ", """
"choices" : [
{
"finish_reason" : "stop",
"index" : 0,
"message" : {
"content" : "done",
"role" : "assistant"
}
}
]
""")));
wm.start();
final OpenAIAsyncClient client = new OpenAIClientBuilder()
.endpoint("http://localhost:" + wm.port() + "/")
.buildAsyncClient();
ChatCompletionService textGenerationService = OpenAIChatCompletion.builder()
.withOpenAIAsyncClient(client)
.withModelId("gpt-35-turbo-2")
.build();
Kernel kernel = Kernel.builder()
.withAIService(ChatCompletionService.class, textGenerationService)
.build();
return kernel;
}
}
@@ -15,7 +15,6 @@ 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;
@@ -49,37 +48,6 @@ public class ToolCallBehaviourTest {
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();
@@ -251,7 +219,18 @@ public class ToolCallBehaviourTest {
.build();
}
private static MappingBuilder buildResponse(String bodyMatcher, String responseBody) {
public static MappingBuilder buildTextResponse(String bodyMatcher, String responseBody) {
return post(urlEqualTo(
"//openai/deployments/gpt-35-turbo-2/completions?api-version=2024-02-15-preview"))
.withRequestBody(new ContainsPattern(bodyMatcher))
.willReturn(
aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody(formTextResponse(responseBody)));
}
public static MappingBuilder buildResponse(String bodyMatcher, String responseBody) {
return post(urlEqualTo(
"//openai/deployments/gpt-35-turbo-2/chat/completions?api-version=2024-02-15-preview"))
.withRequestBody(new ContainsPattern(bodyMatcher))
@@ -269,6 +248,20 @@ public class ToolCallBehaviourTest {
}
}
private static String formTextResponse(String chatChoices) {
return """
{
"id": "cmpl-3QJ9z1J9z1J9z1J9z1J9z1J9",
"usage": {
"total_tokens": 4,
"completion_tokens": 1,
"prompt_tokens": 1
},
%s
}
""".formatted(chatChoices);
}
private static String formResponse(String chatChoices) {
return """
{
+1
View File
@@ -16,6 +16,7 @@
<modules>
<module>sample-code</module>
<module>semantickernel-openapi-plugin</module>
<module>booking-agent-m365</module>
</modules>
@@ -74,12 +74,14 @@ public class ConfiguringPrompts {
String.class,
"The history of the conversation.",
null,
null,
false))
.addInputVariable(InputVariable.build(
"request",
String.class,
"The user's request.",
null,
null,
true))
.withExecutionSettings(new HashMap<String, PromptExecutionSettings>() {
{
@@ -37,6 +37,10 @@ public class Example17_ChatGPT {
.buildAsyncClient();
}
client = new OpenAIClientBuilder()
.endpoint("http://localhost:8123")
.buildAsyncClient();
ChatCompletionService chatGPT = OpenAIChatCompletion.builder()
.withModelId(MODEL_ID)
.withOpenAIAsyncClient(client)
@@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.microsoft.semantic-kernel</groupId>
<artifactId>semantickernel-samples-parent</artifactId>
<version>1.0.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>semantickernel-openapi-plugin</artifactId>
<name>semantickernel-openapi-plugin</name>
<packaging>jar</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.microsoft.semantic-kernel</groupId>
<artifactId>semantickernel-bom</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.microsoft.semantic-kernel</groupId>
<artifactId>semantickernel-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
</dependency>
<dependency>
<groupId>io.swagger.parser.v3</groupId>
<artifactId>swagger-parser</artifactId>
<version>2.1.22</version>
</dependency>
<dependency>
<groupId>com.microsoft.semantic-kernel</groupId>
<artifactId>semantickernel-aiservices-openai</artifactId>
</dependency>
</dependencies>
<profiles>
<profile>
<id>bug-check</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<build>
<plugins>
<!-- Ignore linting on samples -->
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<executions>
<execution>
<id>run-sample</id>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>com.microsoft.semantickernel.samples.syntaxexamples.${sample}</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,176 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.samples.openapi;
import com.azure.core.http.ContentType;
import com.azure.core.http.HttpClient;
import com.azure.core.http.HttpHeaderName;
import com.azure.core.http.HttpHeaders;
import com.azure.core.http.HttpMethod;
import com.azure.core.http.HttpRequest;
import com.azure.core.http.HttpResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.microsoft.semantickernel.contextvariables.ContextVariable;
import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.PathParameter;
import io.swagger.v3.oas.models.parameters.QueryParameter;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
/**
* Plugin for making HTTP requests specifically to endpoints discovered via OpenAPI.
*/
class OpenAPIHttpRequestPlugin {
private static final Logger LOGGER = LoggerFactory.getLogger(OpenAPIHttpRequestPlugin.class);
private final String serverUrl;
private final String path;
private final PathItem pathItem;
private final HttpClient client;
private final HttpMethod method;
private final Operation operation;
public OpenAPIHttpRequestPlugin(
HttpMethod method,
String serverUrl,
String path,
PathItem pathItem,
HttpClient client,
Operation operation) {
this.method = method;
this.serverUrl = serverUrl;
this.path = path;
this.pathItem = pathItem;
this.client = client;
this.operation = operation;
}
/**
* Executes the HTTP request and return the body of the response.
*
* @param arguments The arguments to the http request.
* @return The body of the response.
*/
public Mono<String> execute(KernelFunctionArguments arguments) {
String body = getBody(arguments);
String query = buildQueryString(arguments);
String path = buildQueryPath(arguments);
String url;
if (!query.isEmpty()) {
url = serverUrl + path + "?" + query;
} else {
url = serverUrl + path;
}
HttpRequest request = new HttpRequest(method, url);
if (body != null) {
request = request
.setHeaders(new HttpHeaders()
.set(HttpHeaderName.CONTENT_TYPE, ContentType.APPLICATION_JSON))
.setBody(body);
}
LOGGER.debug("Executing {} {}", method.name(), url);
if (body != null) {
LOGGER.debug("Body: {}", body);
}
return client
.send(request)
.flatMap(response -> {
if (response.getStatusCode() >= 400) {
return Mono.error(new RuntimeException(
"Request failed with status code: " + response.getStatusCode()));
} else {
return Mono.just(response);
}
})
.flatMap(HttpResponse::getBodyAsString)
.doOnNext(response -> LOGGER.debug("Request response: {}", response));
}
private static @Nullable String getBody(KernelFunctionArguments arguments) {
String body = null;
if (arguments.containsKey("requestbody")) {
ContextVariable<?> requestBody = arguments.get("requestbody");
if (requestBody != null) {
try {
JsonNode tree = new ObjectMapper().readTree(requestBody.getValue(String.class));
body = tree.toPrettyString();
} catch (JsonProcessingException e) {
body = requestBody.getValue(String.class);
}
}
arguments.remove("requestbody");
}
return body;
}
private String buildQueryPath(KernelFunctionArguments arguments) {
return getParameterStreamOfArguments(arguments)
.filter(p -> p instanceof PathParameter)
.reduce(path, (path, parameter) -> {
String name = parameter.getName();
String rendered = getRenderedParameter(arguments, name);
return path.replaceAll("\\{" + name + "}", rendered);
}, (a, b) -> a + b);
}
private static String getRenderedParameter(
KernelFunctionArguments arguments, String name) {
ContextVariable<?> value = arguments.get(name);
if (value == null) {
throw new IllegalArgumentException("Missing value for path parameter: " + name);
}
String rendered = value.getValue(String.class);
if (rendered == null) {
throw new IllegalArgumentException("Path parameter value is null: " + name);
}
return URLEncoder.encode(rendered, StandardCharsets.US_ASCII);
}
private String buildQueryString(KernelFunctionArguments arguments) {
return getParameterStreamOfArguments(arguments)
.filter(p -> p instanceof QueryParameter)
.map(parameter -> {
String name = parameter.getName();
String rendered = getRenderedParameter(arguments, name);
return name + "=" + rendered;
})
.collect(Collectors.joining("&"));
}
private Stream<Parameter> getParameterStreamOfArguments(
KernelFunctionArguments arguments) {
if (operation.getParameters() == null) {
return Stream.empty();
}
return arguments
.keySet()
.stream()
.map(contextVariable -> pathItem
.getGet()
.getParameters()
.stream()
.filter(param -> param.getName().equals(contextVariable)).findFirst())
.filter(Optional::isPresent)
.map(Optional::get);
}
}
@@ -0,0 +1,594 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.samples.openapi;
import com.azure.core.http.ContentType;
import com.azure.core.http.HttpClient;
import com.azure.core.http.HttpHeaders;
import com.azure.core.http.HttpMethod;
import com.azure.core.util.Header;
import com.azure.core.util.HttpClientOptions;
import com.fasterxml.jackson.core.JsonParser.Feature;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.microsoft.semantickernel.exceptions.SKException;
import com.microsoft.semantickernel.plugin.KernelPlugin;
import com.microsoft.semantickernel.semanticfunctions.InputVariable;
import com.microsoft.semantickernel.semanticfunctions.KernelFunction;
import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments;
import com.microsoft.semantickernel.semanticfunctions.OutputVariable;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.BooleanSchema;
import io.swagger.v3.oas.models.media.IntegerSchema;
import io.swagger.v3.oas.models.media.MapSchema;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.NumberSchema;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.parser.OpenAPIV3Parser;
import io.swagger.v3.parser.core.models.ParseOptions;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.commons.text.StringEscapeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SemanticKernelOpenAPIImporter {
private static final Logger LOGGER = LoggerFactory.getLogger(
SemanticKernelOpenAPIImporter.class);
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String pluginName;
private String schema;
private HttpHeaders httpHeaders;
private HttpClient client;
private Function<List<Server>, String> serverSelector;
public Builder withPluginName(String pluginName) {
this.pluginName = pluginName;
return this;
}
public Builder withSchema(String schema) {
this.schema = schema;
return this;
}
public Builder withHttpHeaders(HttpHeaders httpHeaders) {
this.httpHeaders = httpHeaders;
return this;
}
public Builder withClient(HttpClient client) {
this.client = client;
return this;
}
public Builder withServerSelector(Function<List<Server>, String> serverSelector) {
this.serverSelector = serverSelector;
return this;
}
public Builder withServer(String serverUrl) {
this.serverSelector = (ignore) -> serverUrl;
return this;
}
public KernelPlugin build() {
return SemanticKernelOpenAPIImporter.fromSchema(pluginName, schema, httpHeaders, client,
serverSelector);
}
}
public static KernelPlugin fromSchema(
String pluginName,
String schema,
@Nullable HttpHeaders httpHeaders,
@Nullable HttpClient client,
@Nullable Function<List<Server>, String> serverSelector) {
ParseOptions parseOptions = new ParseOptions();
parseOptions.setResolve(true);
parseOptions.setResolveFully(true);
OpenAPI openAPI = new OpenAPIV3Parser().readContents(schema, null, parseOptions)
.getOpenAPI();
client = getHttpClient(httpHeaders, client);
Paths paths = openAPI.getPaths();
String serverUrl;
if (serverSelector != null) {
serverUrl = serverSelector.apply(openAPI.getServers());
} else {
serverUrl = openAPI.getServers().get(0).getUrl();
if (openAPI.getServers().size() > 1) {
LOGGER.warn("Multiple servers found, using the first one: {}", serverUrl);
}
}
Map<String, KernelFunction<?>> functions = getFunctions(
client,
pluginName,
paths,
serverUrl);
return new KernelPlugin(
pluginName,
openAPI.getInfo().getDescription(),
functions);
}
private static HttpClient getHttpClient(
@Nullable HttpHeaders httpHeaders,
@Nullable HttpClient client) {
if (httpHeaders != null && client != null) {
throw new IllegalArgumentException(
"Both httpHeaders and client cannot be provided at the same time");
}
HttpClientOptions options = new HttpClientOptions();
if (httpHeaders != null) {
options.setHeaders(httpHeaders.stream()
.map(header -> new Header(header.getName(), header.getValuesList()))
.toList());
}
if (client == null) {
client = HttpClient.createDefault(options);
}
return client;
}
private static Map<String, KernelFunction<?>> getFunctions(
HttpClient client,
String pluginName,
Paths paths,
String serverUrl) {
return paths
.entrySet()
.stream()
.flatMap((entry) -> {
return formGetOperation(
client,
pluginName,
serverUrl,
entry.getKey(),
entry.getValue())
.stream();
})
.collect(Collectors.toMap(KernelFunction::getName, (func) -> func));
}
private static List<KernelFunction<?>> formGetOperation(
HttpClient client,
String pluginName,
String serverUrl,
String path,
PathItem pathItem) {
List<KernelFunction<?>> plugins = new ArrayList<>();
if (pathItem.getGet() != null) {
var function = getKernelFunctionFromRequest(
client,
pluginName,
serverUrl,
path,
pathItem,
pathItem.getGet(),
HttpMethod.GET);
plugins.add(function);
}
if (pathItem.getDelete() != null) {
var function = getKernelFunctionFromRequest(
client,
pluginName,
serverUrl,
path,
pathItem,
pathItem.getDelete(),
HttpMethod.DELETE);
plugins.add(function);
}
if (pathItem.getPost() != null) {
var function = getKernelFunctionFromRequest(
client,
pluginName,
serverUrl,
path,
pathItem,
pathItem.getPost(),
HttpMethod.POST);
plugins.add(function);
}
if (pathItem.getPut() != null) {
var function = getKernelFunctionFromRequest(
client,
pluginName,
serverUrl,
path,
pathItem,
pathItem.getPut(),
HttpMethod.PUT);
plugins.add(function);
}
if (pathItem.getPatch() != null) {
var function = getKernelFunctionFromRequest(
client,
pluginName,
serverUrl,
path,
pathItem,
pathItem.getPatch(),
HttpMethod.PATCH);
plugins.add(function);
}
if (pathItem.getOptions() != null) {
var function = getKernelFunctionFromRequest(
client,
pluginName,
serverUrl,
path,
pathItem,
pathItem.getOptions(),
HttpMethod.OPTIONS);
plugins.add(function);
}
if (pathItem.getHead() != null) {
var function = getKernelFunctionFromRequest(
client,
pluginName,
serverUrl,
path,
pathItem,
pathItem.getHead(),
HttpMethod.HEAD);
plugins.add(function);
}
if (pathItem.getTrace() != null) {
var function = getKernelFunctionFromRequest(
client,
pluginName,
serverUrl,
path,
pathItem,
pathItem.getTrace(),
HttpMethod.TRACE);
plugins.add(function);
}
return plugins;
}
private static @Nullable KernelFunction<Object> getKernelFunctionFromRequest(HttpClient client,
String pluginName, String serverUrl, String path, PathItem pathItem,
Operation operation,
HttpMethod method) {
List<InputVariable> variableList = getInputVariables(operation);
OutputVariable<String> ov = getOutputVariable(operation);
String description = getDescription(operation.getDescription());
OpenAPIHttpRequestPlugin plugin = new OpenAPIHttpRequestPlugin(method, serverUrl, path,
pathItem, client,
operation);
RequestBody requestBody = operation.getRequestBody();
if (requestBody != null) {
if (requestBody.getContent().get(ContentType.APPLICATION_JSON) != null) {
MediaType mediaType = requestBody.getContent().get(ContentType.APPLICATION_JSON);
Schema<?> schema = mediaType.getSchema();
String example = renderJsonExample(schema);
try {
JsonNode parsed = new ObjectMapper()
.enable(Feature.ALLOW_TRAILING_COMMA)
.readTree(example);
example = parsed.get("root").toPrettyString();
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to parse example JSON", e);
}
List<String> enums = getEnumValues(schema);
variableList = new ArrayList<>(variableList);
variableList.add(InputVariable.build(
"requestBody",
String.class,
"Example request body:\n" + example,
example,
enums,
true));
} else {
LOGGER.warn("No content type found for operation {}", operation.getOperationId());
}
}
return buildKernelFunction(pluginName, plugin, operation, description, variableList, ov);
}
private static String renderJsonExample(Schema<?> schema) {
StringBuilder stringBuilder = new StringBuilder();
renderJsonExample(stringBuilder, "root", schema);
return "{" + stringBuilder + "}";
}
private static void renderJsonExample(
StringBuilder stringBuilder,
String valueKey,
Schema<?> schema) {
if (schema.getExample() != null) {
if (valueKey != null) {
stringBuilder.append("\"").append(valueKey).append("\": ");
}
if (schema instanceof StringSchema) {
stringBuilder.append("\"").append(schema.getExample().toString()).append("\",");
} else {
stringBuilder.append(schema.getExample().toString()).append(",");
}
return;
}
if (schema instanceof IntegerSchema ||
schema instanceof StringSchema ||
schema instanceof NumberSchema ||
schema instanceof BooleanSchema) {
String value;
if (schema instanceof IntegerSchema) {
value = "1";
} else if (schema instanceof StringSchema) {
if (schema.getEnum() != null && !schema.getEnum().isEmpty()) {
value = "\"" + schema.getEnum().get(0).toString() + "\"";
} else {
value = "\"string\"";
}
} else if (schema instanceof NumberSchema) {
value = "1.0";
} else {
value = "true";
}
if (valueKey != null) {
stringBuilder.append("\"").append(valueKey).append("\": ");
}
stringBuilder.append(value).append(",");
} else if (schema instanceof ObjectSchema objectSchema) {
if (valueKey != null) {
stringBuilder.append("\"").append(valueKey).append("\": ");
}
stringBuilder.append("{");
objectSchema.getProperties().forEach((key, value) -> {
renderJsonExample(stringBuilder, key, value);
});
stringBuilder.append("},");
} else if (schema instanceof ArraySchema arraySchema) {
if (valueKey != null) {
stringBuilder.append("\"").append(valueKey).append("\": ");
}
stringBuilder.append("[");
renderJsonExample(stringBuilder, null,
arraySchema.getItems());
stringBuilder.append("],");
} else if (schema instanceof MapSchema) {
throw new SKException("Not yet supported");
} else {
LOGGER.warn("Unsupported schema type {}", schema.getClass().getName());
}
}
private static String renderXmlExample(Schema<?> schema) {
StringBuilder stringBuilder = new StringBuilder();
renderXmlExample(stringBuilder, null, schema);
return stringBuilder.toString();
}
private static void renderXmlExample(StringBuilder stringBuilder, String valueKey,
Schema<?> schema) {
if (schema.getExample() != null) {
stringBuilder.append("<").append(valueKey).append(">");
stringBuilder.append(schema.getExample().toString());
stringBuilder.append("</").append(valueKey).append(">");
return;
}
if (schema instanceof IntegerSchema ||
schema instanceof StringSchema ||
schema instanceof NumberSchema ||
schema instanceof BooleanSchema) {
String value;
if (schema instanceof IntegerSchema) {
value = "1";
} else if (schema instanceof StringSchema) {
if (schema.getEnum() != null && !schema.getEnum().isEmpty()) {
value = schema.getEnum().get(0).toString();
} else {
value = "string";
}
} else if (schema instanceof NumberSchema) {
value = "1.0";
} else {
value = "true";
}
stringBuilder.append("<").append(valueKey).append(">");
stringBuilder.append(value);
stringBuilder.append("</").append(valueKey).append(">");
} else if (schema instanceof ObjectSchema objectSchema) {
String nameTag = "<" + schema.getXml().getName() + ">";
String closingNameTag = "</" + schema.getXml().getName() + ">";
stringBuilder.append(nameTag);
objectSchema.getProperties().forEach((key, value) -> {
renderXmlExample(stringBuilder, key, value);
});
stringBuilder.append(closingNameTag);
} else if (schema instanceof ArraySchema arraySchema) {
String nameTag = "<" + valueKey + ">";
String closingNameTag = "</" + valueKey + ">";
stringBuilder.append(nameTag);
renderXmlExample(stringBuilder, arraySchema.getItems().getXml().getName(),
arraySchema.getItems());
stringBuilder.append(closingNameTag);
} else if (schema instanceof MapSchema) {
throw new SKException("Not yet supported");
} else {
LOGGER.warn("Unsupported schema type {}", schema.getClass().getName());
}
}
private static KernelFunction<Object> buildKernelFunction(
String pluginName,
OpenAPIHttpRequestPlugin plugin,
Operation operation,
String description,
List<InputVariable> variableList,
OutputVariable<String> ov) {
try {
Method method = OpenAPIHttpRequestPlugin.class.getMethod("execute",
KernelFunctionArguments.class);
return KernelFunction
.createFromMethod(method, plugin)
.withFunctionName(operation.getOperationId())
.withPluginName(pluginName)
.withDescription(description)
.withParameters(variableList)
.withReturnParameter(ov)
.build();
} catch (NoSuchMethodException e) {
return null;
}
}
private static OutputVariable<String> getOutputVariable(Operation get) {
return new OutputVariable<>(
get.getDescription(),
String.class);
}
private static @Nullable String getDescription(String get) {
String description = get;
if (description != null) {
description = StringEscapeUtils.escapeXml11(description);
description = description.replaceAll("\\n", "");
}
return description;
}
private static List<InputVariable> getInputVariables(Operation get) {
List<InputVariable> variableList = List.of();
if (get.getParameters() != null) {
variableList = get
.getParameters()
.stream()
.map(parameter -> {
Class<?> type = getType(parameter);
Object def = parameter.getSchema().getDefault();
String description = getDescription(parameter.getDescription());
List<String> enums = getEnumValues(parameter.getSchema());
return InputVariable.build(
parameter.getName(),
type,
description,
def != null ? def.toString() : null,
enums,
parameter.getRequired());
})
.collect(Collectors.toList());
}
return variableList;
}
private static List<String> getEnumValues(Schema<?> parameter) {
if (parameter.getEnum() == null) {
return null;
}
return parameter.getEnum()
.stream()
.map(Object::toString)
.toList();
}
private static Class<?> getType(Parameter parameter) {
Class<?> type = String.class;
String t = parameter.getSchema().getType();
if (t != null) {
switch (t) {
case "integer":
type = Integer.class;
break;
case "number":
type = Double.class;
break;
case "boolean":
type = Boolean.class;
break;
case "array":
type = List.class;
break;
// Not sure if we can support this
case "null":
break;
case "object":
break;
default:
break;
}
}
return type;
}
}
@@ -0,0 +1,69 @@
// Copyright (c) Microsoft. All rights reserved.
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.implementation.EmbeddedResourceLoader;
import com.microsoft.semantickernel.orchestration.FunctionResult;
import com.microsoft.semantickernel.orchestration.ToolCallBehavior;
import com.microsoft.semantickernel.plugin.KernelPlugin;
import com.microsoft.semantickernel.samples.openapi.SemanticKernelOpenAPIImporter;
import com.microsoft.semantickernel.semanticfunctions.KernelFunction;
import java.io.FileNotFoundException;
public class ExampleAdoptiumImporter {
public static void main(String[] args) throws FileNotFoundException {
runJson();
runYaml();
}
private static void runJson() throws FileNotFoundException {
String json = EmbeddedResourceLoader.readFile("adoptium.json",
ExampleAdoptiumImporter.class);
makeRequests(json);
}
private static void runYaml() throws FileNotFoundException {
String yaml = EmbeddedResourceLoader.readFile("adoptium.yaml",
ExampleAdoptiumImporter.class);
makeRequests(yaml);
}
private static void makeRequests(String schema) {
KernelPlugin plugin = SemanticKernelOpenAPIImporter
.builder()
.withPluginName("adoptium")
.withSchema(schema)
.build();
Kernel kernel = ExampleOpenAPIParent.kernelBuilder()
.withPlugin(plugin)
.build();
performRequest(kernel,
"""
Parse the java version 11.0.22+9?
""");
performRequest(kernel,
"""
What are the names of the GA versions of Java available between 8 and 9 inclusive?
""");
performRequest(kernel,
"""
What is the version of the latest GA release of Java between 11 and 12?
""");
}
private static void performRequest(Kernel kernel, String request) {
KernelFunction<String> function = KernelFunction.<String>createFromPrompt(request)
.build();
FunctionResult<String> result = kernel.invokeAsync(function)
.withResultType(String.class)
.withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true))
.block();
System.out.println(result.getResult());
}
}
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft. All rights reserved.
import com.azure.ai.openai.OpenAIAsyncClient;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.core.credential.AzureKeyCredential;
import com.azure.core.credential.KeyCredential;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.Kernel.Builder;
import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService;
public class ExampleOpenAPIParent {
private static final String PLUGIN_DIR = System.getenv("PLUGIN_DIR") == null ? "."
: System.getenv("PLUGIN_DIR");
private static final String CLIENT_KEY = System.getenv("CLIENT_KEY");
private static final String AZURE_CLIENT_KEY = System.getenv("AZURE_CLIENT_KEY");
// Only required if AZURE_CLIENT_KEY is set
private static final String CLIENT_ENDPOINT = System.getenv("CLIENT_ENDPOINT");
private static final String MODEL_ID = System.getenv()
.getOrDefault("MODEL_ID", "gpt-35-turbo-2");
public static Builder kernelBuilder() {
OpenAIAsyncClient client;
if (AZURE_CLIENT_KEY != null) {
client = new OpenAIClientBuilder()
.credential(new AzureKeyCredential(AZURE_CLIENT_KEY))
.endpoint(CLIENT_ENDPOINT)
.buildAsyncClient();
} else {
client = new OpenAIClientBuilder()
.credential(new KeyCredential(CLIENT_KEY))
.buildAsyncClient();
}
ChatCompletionService openAIChatCompletion = ChatCompletionService.builder()
.withOpenAIAsyncClient(client)
.withModelId(MODEL_ID)
.build();
return Kernel.builder()
.withAIService(ChatCompletionService.class, openAIChatCompletion);
}
}
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft. All rights reserved.
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.implementation.EmbeddedResourceLoader;
import com.microsoft.semantickernel.orchestration.FunctionResult;
import com.microsoft.semantickernel.orchestration.ToolCallBehavior;
import com.microsoft.semantickernel.plugin.KernelPlugin;
import com.microsoft.semantickernel.samples.openapi.SemanticKernelOpenAPIImporter;
import com.microsoft.semantickernel.semanticfunctions.KernelFunction;
import java.io.FileNotFoundException;
public class ExamplePetstoreImporter {
public static void main(String[] args) throws FileNotFoundException {
String yaml = EmbeddedResourceLoader.readFile("petstore.yaml",
ExamplePetstoreImporter.class);
KernelPlugin plugin = SemanticKernelOpenAPIImporter
.builder()
.withPluginName("petstore")
.withSchema(yaml)
.withServer("http://localhost:8090/api/v3")
.build();
Kernel kernel = ExampleOpenAPIParent.kernelBuilder()
.withPlugin(plugin)
.build();
performRequest(kernel,
"""
Create a user with the following details:
username: john_doe
first name: John
last name: Doe
email: [email protected]
password: password123
phone: 1234567890
user status: 1""");
performRequest(kernel, "Add a new cat called Sandy to the store.");
performRequest(kernel, "List all available pets.");
}
private static void performRequest(Kernel kernel, String request) {
KernelFunction<String> function = KernelFunction.<String>createFromPrompt(request)
.build();
FunctionResult<String> result = kernel.invokeAsync(function)
.withResultType(String.class)
.withToolCallBehavior(ToolCallBehavior.allowAllKernelFunctions(true))
.block();
System.out.println(result.getResult());
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,822 @@
# Sample from: Swagger - https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml
# Under Apache 2.0 License
---
openapi: 3.0.2
servers:
- url: /v3
info:
description: |-
This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about
Swagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!
You can now help us improve the API whether it's by making changes to the definition itself or to the code.
That way, with time, we can improve the API in general, and expose some of the new features in OAS3.
Some useful links:
- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)
- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)
version: 1.0.20-SNAPSHOT
title: Swagger Petstore - OpenAPI 3.0
termsOfService: 'http://swagger.io/terms/'
contact:
email: [email protected]
license:
name: Apache 2.0
url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
tags:
- name: pet
description: Everything about your Pets
externalDocs:
description: Find out more
url: 'http://swagger.io'
- name: store
description: Access to Petstore orders
externalDocs:
description: Find out more about our store
url: 'http://swagger.io'
- name: user
description: Operations about user
paths:
/pet:
post:
tags:
- pet
summary: Add a new pet to the store
description: Add a new pet to the store
operationId: addPet
responses:
'200':
description: Successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/json:
schema:
$ref: '#/components/schemas/Pet'
'405':
description: Invalid input
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
requestBody:
description: Create a new pet in the store
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/Pet'
put:
tags:
- pet
summary: Update an existing pet
description: Update an existing pet by Id
operationId: updatePet
responses:
'200':
description: Successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/json:
schema:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid ID supplied
'404':
description: Pet not found
'405':
description: Validation exception
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
requestBody:
description: Update an existent pet in the store
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/Pet'
/pet/findByStatus:
get:
tags:
- pet
summary: Finds Pets by status
description: Multiple status values can be provided with comma separated strings
operationId: findPetsByStatus
parameters:
- name: status
in: query
description: Status values that need to be considered for filter
required: false
explode: true
schema:
type: string
enum:
- available
- pending
- sold
default: available
responses:
'200':
description: successful operation
content:
application/xml:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid status value
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
/pet/findByTags:
get:
tags:
- pet
summary: Finds Pets by tags
description: >-
Multiple tags can be provided with comma separated strings. Use tag1,
tag2, tag3 for testing.
operationId: findPetsByTags
parameters:
- name: tags
in: query
description: Tags to filter by
required: false
explode: true
schema:
type: array
items:
type: string
responses:
'200':
description: successful operation
content:
application/xml:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid tag value
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
'/pet/{petId}':
get:
tags:
- pet
summary: Find pet by ID
description: Returns a single pet
operationId: getPetById
parameters:
- name: petId
in: path
description: ID of pet to return
required: true
schema:
type: integer
format: int64
responses:
'200':
description: successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/json:
schema:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid ID supplied
'404':
description: Pet not found
security:
- api_key: []
- petstore_auth:
- 'write:pets'
- 'read:pets'
post:
tags:
- pet
summary: Updates a pet in the store with form data
description: ''
operationId: updatePetWithForm
parameters:
- name: petId
in: path
description: ID of pet that needs to be updated
required: true
schema:
type: integer
format: int64
- name: name
in: query
description: Name of pet that needs to be updated
schema:
type: string
- name: status
in: query
description: Status of pet that needs to be updated
schema:
type: string
responses:
'405':
description: Invalid input
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
delete:
tags:
- pet
summary: Deletes a pet
description: ''
operationId: deletePet
parameters:
- name: api_key
in: header
description: ''
required: false
schema:
type: string
- name: petId
in: path
description: Pet id to delete
required: true
schema:
type: integer
format: int64
responses:
'400':
description: Invalid pet value
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
'/pet/{petId}/uploadImage':
post:
tags:
- pet
summary: uploads an image
description: ''
operationId: uploadFile
parameters:
- name: petId
in: path
description: ID of pet to update
required: true
schema:
type: integer
format: int64
- name: additionalMetadata
in: query
description: Additional Metadata
required: false
schema:
type: string
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
requestBody:
content:
application/octet-stream:
schema:
type: string
format: binary
/store/inventory:
get:
tags:
- store
summary: Returns pet inventories by status
description: Returns a map of status codes to quantities
operationId: getInventory
x-swagger-router-controller: OrderController
responses:
'200':
description: successful operation
content:
application/json:
schema:
type: object
additionalProperties:
type: integer
format: int32
security:
- api_key: []
/store/order:
post:
tags:
- store
summary: Place an order for a pet
description: Place a new order in the store
operationId: placeOrder
x-swagger-router-controller: OrderController
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
'405':
description: Invalid input
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
application/xml:
schema:
$ref: '#/components/schemas/Order'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/Order'
'/store/order/{orderId}':
get:
tags:
- store
summary: Find purchase order by ID
x-swagger-router-controller: OrderController
description: >-
For valid response try integer IDs with value <= 5 or > 10. Other values
will generate exceptions.
operationId: getOrderById
parameters:
- name: orderId
in: path
description: ID of order that needs to be fetched
required: true
schema:
type: integer
format: int64
responses:
'200':
description: successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/Order'
application/json:
schema:
$ref: '#/components/schemas/Order'
'400':
description: Invalid ID supplied
'404':
description: Order not found
delete:
tags:
- store
summary: Delete purchase order by ID
x-swagger-router-controller: OrderController
description: >-
For valid response try integer IDs with value < 1000. Anything above
1000 or nonintegers will generate API errors
operationId: deleteOrder
parameters:
- name: orderId
in: path
description: ID of the order that needs to be deleted
required: true
schema:
type: integer
format: int64
responses:
'400':
description: Invalid ID supplied
'404':
description: Order not found
/user:
post:
tags:
- user
summary: Create user
description: This can only be done by the logged in user.
operationId: createUser
responses:
default:
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/User'
application/xml:
schema:
$ref: '#/components/schemas/User'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
application/xml:
schema:
$ref: '#/components/schemas/User'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/User'
description: Created user object
/user/createWithList:
post:
tags:
- user
summary: Creates list of users with given input array
description: 'Creates list of users with given input array'
x-swagger-router-controller: UserController
operationId: createUsersWithListInput
responses:
'200':
description: Successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/User'
application/json:
schema:
$ref: '#/components/schemas/User'
default:
description: successful operation
requestBody:
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
/user/login:
get:
tags:
- user
summary: Logs user into the system
description: ''
operationId: loginUser
parameters:
- name: username
in: query
description: The user name for login
required: false
schema:
type: string
- name: password
in: query
description: The password for login in clear text
required: false
schema:
type: string
responses:
'200':
description: successful operation
headers:
X-Rate-Limit:
description: calls per hour allowed by the user
schema:
type: integer
format: int32
X-Expires-After:
description: date in UTC when token expires
schema:
type: string
format: date-time
content:
application/xml:
schema:
type: string
application/json:
schema:
type: string
'400':
description: Invalid username/password supplied
/user/logout:
get:
tags:
- user
summary: Logs out current logged in user session
description: ''
operationId: logoutUser
parameters: []
responses:
default:
description: successful operation
'/user/{username}':
get:
tags:
- user
summary: Get user by user name
description: ''
operationId: getUserByName
parameters:
- name: username
in: path
description: 'The name that needs to be fetched. Use user1 for testing. '
required: true
schema:
type: string
responses:
'200':
description: successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/User'
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
description: Invalid username supplied
'404':
description: User not found
put:
tags:
- user
summary: Update user
x-swagger-router-controller: UserController
description: This can only be done by the logged in user.
operationId: updateUser
parameters:
- name: username
in: path
description: name that needs to be updated
required: true
schema:
type: string
responses:
default:
description: successful operation
requestBody:
description: Update an existent user in the store
content:
application/json:
schema:
$ref: '#/components/schemas/User'
application/xml:
schema:
$ref: '#/components/schemas/User'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/User'
delete:
tags:
- user
summary: Delete user
description: This can only be done by the logged in user.
operationId: deleteUser
parameters:
- name: username
in: path
description: The name that needs to be deleted
required: true
schema:
type: string
responses:
'400':
description: Invalid username supplied
'404':
description: User not found
externalDocs:
description: Find out more about Swagger
url: 'http://swagger.io'
components:
schemas:
Order:
x-swagger-router-model: io.swagger.petstore.model.Order
properties:
id:
type: integer
format: int64
example: 10
petId:
type: integer
format: int64
example: 198772
quantity:
type: integer
format: int32
example: 7
shipDate:
type: string
format: date-time
status:
type: string
description: Order Status
enum:
- placed
- approved
- delivered
example: approved
complete:
type: boolean
xml:
name: order
type: object
Customer:
properties:
id:
type: integer
format: int64
example: 100000
username:
type: string
example: fehguy
address:
type: array
items:
$ref: '#/components/schemas/Address'
xml:
wrapped: true
name: addresses
xml:
name: customer
type: object
Address:
properties:
street:
type: string
example: 437 Lytton
city:
type: string
example: Palo Alto
state:
type: string
example: CA
zip:
type: string
example: 94301
xml:
name: address
type: object
Category:
x-swagger-router-model: io.swagger.petstore.model.Category
properties:
id:
type: integer
format: int64
example: 1
name:
type: string
example: Dogs
xml:
name: category
type: object
User:
x-swagger-router-model: io.swagger.petstore.model.User
properties:
id:
type: integer
format: int64
example: 10
username:
type: string
example: theUser
firstName:
type: string
example: John
lastName:
type: string
example: James
email:
type: string
example: [email protected]
password:
type: string
example: 12345
phone:
type: string
example: 12345
userStatus:
type: integer
format: int32
example: 1
description: User Status
xml:
name: user
type: object
Tag:
x-swagger-router-model: io.swagger.petstore.model.Tag
properties:
id:
type: integer
format: int64
name:
type: string
xml:
name: tag
type: object
Pet:
x-swagger-router-model: io.swagger.petstore.model.Pet
required:
- name
- photoUrls
properties:
id:
type: integer
format: int64
example: 10
name:
type: string
example: doggie
category:
$ref: '#/components/schemas/Category'
photoUrls:
type: array
xml:
wrapped: true
items:
type: string
xml:
name: photoUrl
tags:
type: array
xml:
wrapped: true
items:
$ref: '#/components/schemas/Tag'
xml:
name: tag
status:
type: string
description: pet status in the store
enum:
- available
- pending
- sold
xml:
name: pet
type: object
ApiResponse:
properties:
code:
type: integer
format: int32
type:
type: string
message:
type: string
xml:
name: '##default'
type: object
requestBodies:
Pet:
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
application/xml:
schema:
$ref: '#/components/schemas/Pet'
description: Pet object that needs to be added to the store
UserArray:
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
description: List of user object
securitySchemes:
petstore_auth:
type: oauth2
flows:
implicit:
authorizationUrl: 'http://localhost:8123/oauth/authorize'
scopes:
'write:pets': modify pets in your account
'read:pets': read your pets
api_key:
type: apiKey
name: api_key
in: header
+2 -1
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
@@ -61,6 +61,17 @@ public interface KernelHook<T extends KernelHookEvent> extends Predicate<KernelH
}
}
/**
* A hook that accepts {@link PreToolCallEvent}
*/
interface PreToolCallHook extends KernelHook<PreToolCallEvent> {
@Override
default boolean test(KernelHookEvent arguments) {
return PreToolCallEvent.class.isAssignableFrom(arguments.getClass());
}
}
/**
* A hook that accepts {@link PromptRenderedEvent}
*/
@@ -5,6 +5,7 @@ import com.microsoft.semantickernel.hooks.KernelHook.FunctionInvokedHook;
import com.microsoft.semantickernel.hooks.KernelHook.FunctionInvokingHook;
import com.microsoft.semantickernel.hooks.KernelHook.PostChatCompletionHook;
import com.microsoft.semantickernel.hooks.KernelHook.PreChatCompletionHook;
import com.microsoft.semantickernel.hooks.KernelHook.PreToolCallHook;
import com.microsoft.semantickernel.hooks.KernelHook.PromptRenderedHook;
import com.microsoft.semantickernel.hooks.KernelHook.PromptRenderingHook;
import java.util.Collections;
@@ -102,6 +103,17 @@ public class KernelHooks {
return addHook((PreChatCompletionHook) function::apply);
}
/**
* Add a {@link PreToolCallHook} to the collection of hooks.
*
* @param function the function to add
* @return the key of the hook in the collection
*/
public String addPreToolCallHook(
Function<PreToolCallEvent, PreToolCallEvent> function) {
return addHook((PreToolCallHook) function::apply);
}
/**
* Add a {@link PostChatCompletionEvent} to the collection of hooks.
*
@@ -0,0 +1,48 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.hooks;
import com.microsoft.semantickernel.contextvariables.ContextVariableTypes;
import com.microsoft.semantickernel.semanticfunctions.KernelFunction;
import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
* Represents a KernelHookEvent that is raised before a tool call is invoked.
*/
public class PreToolCallEvent implements KernelHookEvent {
private final ContextVariableTypes contextVariableTypes;
private final String functionName;
private final KernelFunctionArguments arguments;
private final KernelFunction<?> function;
/**
* Creates a new instance of the {@link PreToolCallEvent} class.
*
* @param functionName the name of the function
* @param arguments the arguments
* @param function the function
* @param contextVariableTypes the context variable types
*/
@SuppressFBWarnings("EI_EXPOSE_REP2")
public PreToolCallEvent(
String functionName,
KernelFunctionArguments arguments,
KernelFunction<?> function,
ContextVariableTypes contextVariableTypes) {
this.functionName = functionName;
this.arguments = arguments;
this.function = function;
this.contextVariableTypes = contextVariableTypes;
}
@SuppressFBWarnings("EI_EXPOSE_REP")
public KernelFunctionArguments getArguments() {
return arguments;
}
@SuppressFBWarnings("EI_EXPOSE_REP2")
public KernelFunction<?> getFunction() {
return function;
}
}
@@ -13,6 +13,7 @@ import com.microsoft.semantickernel.hooks.KernelHooks.UnmodifiableKernelHooks;
import com.microsoft.semantickernel.semanticfunctions.KernelFunction;
import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.NoSuchElementException;
import java.util.function.BiConsumer;
import javax.annotation.Nullable;
import org.slf4j.Logger;
@@ -109,6 +110,13 @@ public class FunctionInvocation<T> extends Mono<FunctionResult<T>> {
null,
new InvocationContext(context))
.handle(convertToType(variableType))
.onErrorResume(e -> {
if (e instanceof NoSuchElementException) {
return Mono.empty();
} else {
return Mono.error(e);
}
})
.subscribe(coreSubscriber);
}
@@ -89,7 +89,27 @@ public class KernelPluginFactory {
* @return The new plugin.
*/
public static KernelPlugin createFromObject(Object target, String pluginName) {
List<KernelFunction<?>> methods = Arrays.stream(target.getClass().getMethods())
Class<?> clazz = target.getClass();
return KernelPluginFactory.createFromObject(clazz, target, pluginName);
}
/**
* Creates a plugin that wraps the specified target object. Methods decorated with
* {@code {@literal @}DefineSKFunction} will be included in the plugin.
*
* @param clazz The class to be wrapped.
* @param target The instance of the class to be wrapped.
* @param pluginName Name of the plugin for function collection and prompt templates. If the
* value is {@code null}, a plugin name is derived from the type of the
* target.
* @return The new plugin.
*/
public static KernelPlugin createFromObject(Class<?> clazz, Object target, String pluginName) {
if (!clazz.isInstance(target)) {
throw new SKException("Target object is not an instance of the provided class");
}
List<KernelFunction<?>> methods = Arrays.stream(clazz.getMethods())
.filter(method -> method.isAnnotationPresent(DefineKernelFunction.class))
.map(method -> {
DefineKernelFunction annotation = method.getAnnotation(DefineKernelFunction.class);
@@ -119,7 +139,15 @@ public class KernelPluginFactory {
}).collect(ArrayList::new, (list, it) -> list.add(it), (a, b) -> a.addAll(b));
return createFromFunctions(pluginName, methods);
KernelPlugin plugin = createFromFunctions(pluginName, methods);
if (plugin.getFunctions().isEmpty()) {
LOGGER.warn(
"No functions found in class {}. This can be caused by DI frameworks that create proxies, or modules that are not making your methods visible. "
+ "Try using: KernelPluginFactory.createFromObject(Class<?> clazz, Object target, String pluginName).",
clazz.getName());
}
return plugin;
}
private static Class<?> getReturnType(DefineKernelFunction annotation, Method method) {
@@ -270,11 +298,15 @@ public class KernelPluginFactory {
KernelFunctionParameter annotation = parameter.getAnnotation(
KernelFunctionParameter.class);
List<String> enumValues = KernelFunctionFromMethod
.getEnumOptions(annotation.type());
return InputVariable.build(
annotation.name(),
annotation.type(),
annotation.description(),
annotation.defaultValue(),
enumValues,
annotation.required());
}).collect(Collectors.toList());
}
@@ -5,6 +5,8 @@ import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.microsoft.semantickernel.plugin.KernelPluginFactory;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import javax.annotation.Nullable;
@@ -20,6 +22,8 @@ public class InputVariable {
private final String defaultValue;
private final boolean isRequired;
private final String type;
@Nullable
private final List<?> enumValues;
/**
* Creates a new instance of {@link InputVariable}.
@@ -29,6 +33,7 @@ public class InputVariable {
* @param description the description of the input variable
* @param defaultValue the default value of the input variable
* @param isRequired whether the input variable is required
* @param enumValues the enum values of the input variable
*/
@JsonCreator
public InputVariable(
@@ -36,7 +41,8 @@ public class InputVariable {
@JsonProperty("type") String type,
@JsonProperty("description") @Nullable String description,
@JsonProperty("default") @Nullable String defaultValue,
@JsonAlias({ "required", "is_required" }) @JsonProperty("is_required") boolean isRequired) {
@JsonAlias({ "required", "is_required" }) @JsonProperty("is_required") boolean isRequired,
@JsonProperty("enum") @Nullable List<?> enumValues) {
this.name = name;
this.description = description;
this.defaultValue = defaultValue;
@@ -45,6 +51,11 @@ public class InputVariable {
type = "java.lang.String";
}
this.type = type;
if (enumValues != null) {
this.enumValues = Collections.unmodifiableList(enumValues);
} else {
this.enumValues = null;
}
}
/**
@@ -58,6 +69,7 @@ public class InputVariable {
this.description = null;
this.defaultValue = null;
this.isRequired = true;
this.enumValues = null;
}
/**
@@ -75,8 +87,10 @@ public class InputVariable {
Class<?> type,
@Nullable String description,
@Nullable String defaultValue,
@Nullable List<?> enumValues,
boolean required) {
return new InputVariable(name, type.getName(), description, defaultValue, required);
return new InputVariable(name, type.getName(), description, defaultValue, required,
enumValues);
}
/**
@@ -166,4 +180,13 @@ public class InputVariable {
return isRequired == other.isRequired;
}
/**
* Gets the possible enum values of the input variable.
*
* @return the possible enum values of the input variable
*/
@Nullable
public List<?> getEnumValues() {
return enumValues;
}
}
@@ -315,6 +315,13 @@ public class KernelFunctionFromMethod<T> extends KernelFunction<T> {
e);
}
})
.flatMap(it -> {
if (it == null) {
return Mono.empty();
} else {
return Mono.just(it);
}
})
.subscribeOn(Schedulers.boundedElastic()));
}
@@ -557,14 +564,29 @@ public class KernelFunctionFromMethod<T> extends KernelFunction<T> {
type = annotation.type();
}
List<String> enumValues = getEnumOptions(type);
return InputVariable.build(
name,
type,
description,
defaultValue,
enumValues,
isRequired);
}
public static @Nullable List<String> getEnumOptions(Class<?> type) {
List<String> enumValues = null;
if (type.isEnum()) {
enumValues = Arrays.stream(type.getEnumConstants())
.map(it -> {
return it.toString();
})
.collect(Collectors.toList());
}
return enumValues;
}
/**
* A builder for {@link KernelFunction}.
*
@@ -59,7 +59,7 @@ public class KernelFunctionFromMethodTest {
String functionName = "concat";
String description = "concatenate two strings";
List<InputVariable> parameters = Collections.singletonList(InputVariable.build(
"string1", String.class, "first string to concatenate", null, true));
"string1", String.class, "first string to concatenate", null, null, true));
OutputVariable<String> returnParameter = new OutputVariable<>(
"concatenated strings", String.class);
KernelFunction<?> result = KernelFunctionFromMethod.create(method, "test", pluginName,
@@ -81,7 +81,7 @@ public class KernelFunctionFromMethodTest {
String description = "concatenate two strings";
List<InputVariable> parameters = Collections
.singletonList(new InputVariable("string1",
"java.lang.String", "first string to concatenate", null, true));
"java.lang.String", "first string to concatenate", null, true, null));
OutputVariable<?> returnParameter = new OutputVariable<>("java.lang.String",
"concatenated strings");
KernelFunction<String> result = new KernelFunctionFromPrompt.Builder()
@@ -55,13 +55,15 @@ public class KernelFunctionYamlTest {
"java.lang.String",
"The topic of the story.",
null,
true),
true,
null),
new InputVariable(
"length",
"java.lang.String",
"The number of sentences in the story.",
null,
true)))
true,
null)))
.withOutputVariable("The generated story.", "java.lang.String")
.withExecutionSettings(new HashMap() {
{
@@ -19,7 +19,7 @@ public class PromptTemplateConfigTest {
String template = "A template for testing {{plugin.function input}}";
List<InputVariable> inputVariables = Arrays.asList(
new InputVariable("input", "java.lang.String", "a description", "default value",
true));
true, null));
OutputVariable outputVariable = new OutputVariable("java.lang.String",
"a description");
@@ -61,7 +61,7 @@ public class PromptTemplateConfigTest {
String templateFormat = "semantic-kernel";
List<InputVariable> inputVariables = Arrays.asList(
new InputVariable("input", "java.lang.String", "a description", "default value",
true));
true, null));
OutputVariable outputVariable = new OutputVariable("java.lang.String",
"a description");
HashMap<String, PromptExecutionSettings> executionSettings = new HashMap<String, PromptExecutionSettings>() {
@@ -100,7 +100,7 @@ public class PromptTemplateConfigTest {
String templateFormat = "semantic-kernel";
List<InputVariable> inputVariables = Arrays.asList(
new InputVariable("input", "java.lang.String", "a description", "default value",
true));
true, null));
OutputVariable outputVariable = new OutputVariable("java.lang.String",
"a description");
HashMap<String, PromptExecutionSettings> executionSettings = new HashMap<String, PromptExecutionSettings>() {
@@ -41,7 +41,7 @@ public class PromptTemplateFactoryTest {
Method method = PromptTemplateFactoryTest.class.getMethod("function", String.class);
List<InputVariable> parameters = Collections.singletonList(
new InputVariable("input", String.class.getName(), "the input", "borked",
true));
true, null));
OutputVariable<String> returnParameter = new OutputVariable<>(
"the output", String.class);
KernelFunction<?> function = KernelFunctionFromMethod.create(
@@ -72,7 +72,7 @@ public class PromptTemplateFactoryTest {
List<InputVariable> inputVariables = Arrays.asList(
new InputVariable("input", "java.lang.String", "a description",
"input from config",
true));
true, null));
OutputVariable outputVariable = new OutputVariable("java.lang.String",
"a description");
HashMap<String, PromptExecutionSettings> executionSettings = new HashMap<String, PromptExecutionSettings>() {
+2 -1
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.microsoft.semantic-kernel</groupId>