mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
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:
+146
-47
@@ -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(),
|
||||
|
||||
+34
-6
@@ -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 + "}";
|
||||
}
|
||||
}
|
||||
|
||||
+211
@@ -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;
|
||||
}
|
||||
}
|
||||
+26
-33
@@ -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 """
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
<modules>
|
||||
<module>sample-code</module>
|
||||
<module>semantickernel-openapi-plugin</module>
|
||||
<module>booking-agent-m365</module>
|
||||
</modules>
|
||||
|
||||
|
||||
+2
@@ -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>() {
|
||||
{
|
||||
|
||||
+4
@@ -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>
|
||||
+176
@@ -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);
|
||||
}
|
||||
}
|
||||
+594
@@ -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
|
||||
@@ -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>
|
||||
|
||||
+11
@@ -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}
|
||||
*/
|
||||
|
||||
+12
@@ -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.
|
||||
*
|
||||
|
||||
+48
@@ -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;
|
||||
}
|
||||
}
|
||||
+8
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+34
-2
@@ -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());
|
||||
}
|
||||
|
||||
+25
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -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}.
|
||||
*
|
||||
|
||||
+2
-2
@@ -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()
|
||||
|
||||
+4
-2
@@ -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() {
|
||||
{
|
||||
|
||||
+3
-3
@@ -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>() {
|
||||
|
||||
+2
-2
@@ -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>() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user