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

### Motivation and Context

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

### Description

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

### Contribution Checklist

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

- [ ] The code builds clean without any errors or warnings
- [ ] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [ ] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone 😄
This commit is contained in:
John Oliver
2024-04-19 14:28:34 +01:00
committed by GitHub
parent 6f0561e953
commit 0a7f96ae4b
29 changed files with 5726 additions and 101 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
@@ -61,6 +61,17 @@ public interface KernelHook<T extends KernelHookEvent> extends Predicate<KernelH
}
}
/**
* A hook that accepts {@link PreToolCallEvent}
*/
interface PreToolCallHook extends KernelHook<PreToolCallEvent> {
@Override
default boolean test(KernelHookEvent arguments) {
return PreToolCallEvent.class.isAssignableFrom(arguments.getClass());
}
}
/**
* A hook that accepts {@link PromptRenderedEvent}
*/
@@ -5,6 +5,7 @@ import com.microsoft.semantickernel.hooks.KernelHook.FunctionInvokedHook;
import com.microsoft.semantickernel.hooks.KernelHook.FunctionInvokingHook;
import com.microsoft.semantickernel.hooks.KernelHook.PostChatCompletionHook;
import com.microsoft.semantickernel.hooks.KernelHook.PreChatCompletionHook;
import com.microsoft.semantickernel.hooks.KernelHook.PreToolCallHook;
import com.microsoft.semantickernel.hooks.KernelHook.PromptRenderedHook;
import com.microsoft.semantickernel.hooks.KernelHook.PromptRenderingHook;
import java.util.Collections;
@@ -102,6 +103,17 @@ public class KernelHooks {
return addHook((PreChatCompletionHook) function::apply);
}
/**
* Add a {@link PreToolCallHook} to the collection of hooks.
*
* @param function the function to add
* @return the key of the hook in the collection
*/
public String addPreToolCallHook(
Function<PreToolCallEvent, PreToolCallEvent> function) {
return addHook((PreToolCallHook) function::apply);
}
/**
* Add a {@link PostChatCompletionEvent} to the collection of hooks.
*
@@ -0,0 +1,48 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.hooks;
import com.microsoft.semantickernel.contextvariables.ContextVariableTypes;
import com.microsoft.semantickernel.semanticfunctions.KernelFunction;
import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
* Represents a KernelHookEvent that is raised before a tool call is invoked.
*/
public class PreToolCallEvent implements KernelHookEvent {
private final ContextVariableTypes contextVariableTypes;
private final String functionName;
private final KernelFunctionArguments arguments;
private final KernelFunction<?> function;
/**
* Creates a new instance of the {@link PreToolCallEvent} class.
*
* @param functionName the name of the function
* @param arguments the arguments
* @param function the function
* @param contextVariableTypes the context variable types
*/
@SuppressFBWarnings("EI_EXPOSE_REP2")
public PreToolCallEvent(
String functionName,
KernelFunctionArguments arguments,
KernelFunction<?> function,
ContextVariableTypes contextVariableTypes) {
this.functionName = functionName;
this.arguments = arguments;
this.function = function;
this.contextVariableTypes = contextVariableTypes;
}
@SuppressFBWarnings("EI_EXPOSE_REP")
public KernelFunctionArguments getArguments() {
return arguments;
}
@SuppressFBWarnings("EI_EXPOSE_REP2")
public KernelFunction<?> getFunction() {
return function;
}
}
@@ -13,6 +13,7 @@ import com.microsoft.semantickernel.hooks.KernelHooks.UnmodifiableKernelHooks;
import com.microsoft.semantickernel.semanticfunctions.KernelFunction;
import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.NoSuchElementException;
import java.util.function.BiConsumer;
import javax.annotation.Nullable;
import org.slf4j.Logger;
@@ -109,6 +110,13 @@ public class FunctionInvocation<T> extends Mono<FunctionResult<T>> {
null,
new InvocationContext(context))
.handle(convertToType(variableType))
.onErrorResume(e -> {
if (e instanceof NoSuchElementException) {
return Mono.empty();
} else {
return Mono.error(e);
}
})
.subscribe(coreSubscriber);
}
@@ -89,7 +89,27 @@ public class KernelPluginFactory {
* @return The new plugin.
*/
public static KernelPlugin createFromObject(Object target, String pluginName) {
List<KernelFunction<?>> methods = Arrays.stream(target.getClass().getMethods())
Class<?> clazz = target.getClass();
return KernelPluginFactory.createFromObject(clazz, target, pluginName);
}
/**
* Creates a plugin that wraps the specified target object. Methods decorated with
* {@code {@literal @}DefineSKFunction} will be included in the plugin.
*
* @param clazz The class to be wrapped.
* @param target The instance of the class to be wrapped.
* @param pluginName Name of the plugin for function collection and prompt templates. If the
* value is {@code null}, a plugin name is derived from the type of the
* target.
* @return The new plugin.
*/
public static KernelPlugin createFromObject(Class<?> clazz, Object target, String pluginName) {
if (!clazz.isInstance(target)) {
throw new SKException("Target object is not an instance of the provided class");
}
List<KernelFunction<?>> methods = Arrays.stream(clazz.getMethods())
.filter(method -> method.isAnnotationPresent(DefineKernelFunction.class))
.map(method -> {
DefineKernelFunction annotation = method.getAnnotation(DefineKernelFunction.class);
@@ -119,7 +139,15 @@ public class KernelPluginFactory {
}).collect(ArrayList::new, (list, it) -> list.add(it), (a, b) -> a.addAll(b));
return createFromFunctions(pluginName, methods);
KernelPlugin plugin = createFromFunctions(pluginName, methods);
if (plugin.getFunctions().isEmpty()) {
LOGGER.warn(
"No functions found in class {}. This can be caused by DI frameworks that create proxies, or modules that are not making your methods visible. "
+ "Try using: KernelPluginFactory.createFromObject(Class<?> clazz, Object target, String pluginName).",
clazz.getName());
}
return plugin;
}
private static Class<?> getReturnType(DefineKernelFunction annotation, Method method) {
@@ -270,11 +298,15 @@ public class KernelPluginFactory {
KernelFunctionParameter annotation = parameter.getAnnotation(
KernelFunctionParameter.class);
List<String> enumValues = KernelFunctionFromMethod
.getEnumOptions(annotation.type());
return InputVariable.build(
annotation.name(),
annotation.type(),
annotation.description(),
annotation.defaultValue(),
enumValues,
annotation.required());
}).collect(Collectors.toList());
}
@@ -5,6 +5,8 @@ import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.microsoft.semantickernel.plugin.KernelPluginFactory;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import javax.annotation.Nullable;
@@ -20,6 +22,8 @@ public class InputVariable {
private final String defaultValue;
private final boolean isRequired;
private final String type;
@Nullable
private final List<?> enumValues;
/**
* Creates a new instance of {@link InputVariable}.
@@ -29,6 +33,7 @@ public class InputVariable {
* @param description the description of the input variable
* @param defaultValue the default value of the input variable
* @param isRequired whether the input variable is required
* @param enumValues the enum values of the input variable
*/
@JsonCreator
public InputVariable(
@@ -36,7 +41,8 @@ public class InputVariable {
@JsonProperty("type") String type,
@JsonProperty("description") @Nullable String description,
@JsonProperty("default") @Nullable String defaultValue,
@JsonAlias({ "required", "is_required" }) @JsonProperty("is_required") boolean isRequired) {
@JsonAlias({ "required", "is_required" }) @JsonProperty("is_required") boolean isRequired,
@JsonProperty("enum") @Nullable List<?> enumValues) {
this.name = name;
this.description = description;
this.defaultValue = defaultValue;
@@ -45,6 +51,11 @@ public class InputVariable {
type = "java.lang.String";
}
this.type = type;
if (enumValues != null) {
this.enumValues = Collections.unmodifiableList(enumValues);
} else {
this.enumValues = null;
}
}
/**
@@ -58,6 +69,7 @@ public class InputVariable {
this.description = null;
this.defaultValue = null;
this.isRequired = true;
this.enumValues = null;
}
/**
@@ -75,8 +87,10 @@ public class InputVariable {
Class<?> type,
@Nullable String description,
@Nullable String defaultValue,
@Nullable List<?> enumValues,
boolean required) {
return new InputVariable(name, type.getName(), description, defaultValue, required);
return new InputVariable(name, type.getName(), description, defaultValue, required,
enumValues);
}
/**
@@ -166,4 +180,13 @@ public class InputVariable {
return isRequired == other.isRequired;
}
/**
* Gets the possible enum values of the input variable.
*
* @return the possible enum values of the input variable
*/
@Nullable
public List<?> getEnumValues() {
return enumValues;
}
}
@@ -315,6 +315,13 @@ public class KernelFunctionFromMethod<T> extends KernelFunction<T> {
e);
}
})
.flatMap(it -> {
if (it == null) {
return Mono.empty();
} else {
return Mono.just(it);
}
})
.subscribeOn(Schedulers.boundedElastic()));
}
@@ -557,14 +564,29 @@ public class KernelFunctionFromMethod<T> extends KernelFunction<T> {
type = annotation.type();
}
List<String> enumValues = getEnumOptions(type);
return InputVariable.build(
name,
type,
description,
defaultValue,
enumValues,
isRequired);
}
public static @Nullable List<String> getEnumOptions(Class<?> type) {
List<String> enumValues = null;
if (type.isEnum()) {
enumValues = Arrays.stream(type.getEnumConstants())
.map(it -> {
return it.toString();
})
.collect(Collectors.toList());
}
return enumValues;
}
/**
* A builder for {@link KernelFunction}.
*
@@ -59,7 +59,7 @@ public class KernelFunctionFromMethodTest {
String functionName = "concat";
String description = "concatenate two strings";
List<InputVariable> parameters = Collections.singletonList(InputVariable.build(
"string1", String.class, "first string to concatenate", null, true));
"string1", String.class, "first string to concatenate", null, null, true));
OutputVariable<String> returnParameter = new OutputVariable<>(
"concatenated strings", String.class);
KernelFunction<?> result = KernelFunctionFromMethod.create(method, "test", pluginName,
@@ -81,7 +81,7 @@ public class KernelFunctionFromMethodTest {
String description = "concatenate two strings";
List<InputVariable> parameters = Collections
.singletonList(new InputVariable("string1",
"java.lang.String", "first string to concatenate", null, true));
"java.lang.String", "first string to concatenate", null, true, null));
OutputVariable<?> returnParameter = new OutputVariable<>("java.lang.String",
"concatenated strings");
KernelFunction<String> result = new KernelFunctionFromPrompt.Builder()
@@ -55,13 +55,15 @@ public class KernelFunctionYamlTest {
"java.lang.String",
"The topic of the story.",
null,
true),
true,
null),
new InputVariable(
"length",
"java.lang.String",
"The number of sentences in the story.",
null,
true)))
true,
null)))
.withOutputVariable("The generated story.", "java.lang.String")
.withExecutionSettings(new HashMap() {
{
@@ -19,7 +19,7 @@ public class PromptTemplateConfigTest {
String template = "A template for testing {{plugin.function input}}";
List<InputVariable> inputVariables = Arrays.asList(
new InputVariable("input", "java.lang.String", "a description", "default value",
true));
true, null));
OutputVariable outputVariable = new OutputVariable("java.lang.String",
"a description");
@@ -61,7 +61,7 @@ public class PromptTemplateConfigTest {
String templateFormat = "semantic-kernel";
List<InputVariable> inputVariables = Arrays.asList(
new InputVariable("input", "java.lang.String", "a description", "default value",
true));
true, null));
OutputVariable outputVariable = new OutputVariable("java.lang.String",
"a description");
HashMap<String, PromptExecutionSettings> executionSettings = new HashMap<String, PromptExecutionSettings>() {
@@ -100,7 +100,7 @@ public class PromptTemplateConfigTest {
String templateFormat = "semantic-kernel";
List<InputVariable> inputVariables = Arrays.asList(
new InputVariable("input", "java.lang.String", "a description", "default value",
true));
true, null));
OutputVariable outputVariable = new OutputVariable("java.lang.String",
"a description");
HashMap<String, PromptExecutionSettings> executionSettings = new HashMap<String, PromptExecutionSettings>() {
@@ -41,7 +41,7 @@ public class PromptTemplateFactoryTest {
Method method = PromptTemplateFactoryTest.class.getMethod("function", String.class);
List<InputVariable> parameters = Collections.singletonList(
new InputVariable("input", String.class.getName(), "the input", "borked",
true));
true, null));
OutputVariable<String> returnParameter = new OutputVariable<>(
"the output", String.class);
KernelFunction<?> function = KernelFunctionFromMethod.create(
@@ -72,7 +72,7 @@ public class PromptTemplateFactoryTest {
List<InputVariable> inputVariables = Arrays.asList(
new InputVariable("input", "java.lang.String", "a description",
"input from config",
true));
true, null));
OutputVariable outputVariable = new OutputVariable("java.lang.String",
"a description");
HashMap<String, PromptExecutionSettings> executionSettings = new HashMap<String, PromptExecutionSettings>() {