Java: Add KernelPluginFactoryTest (#5498)

### 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-03-15 15:30:03 +00:00
committed by GitHub
parent c38b236c20
commit a9a2f7bf9f
8 changed files with 249 additions and 40 deletions
+22 -7
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>
@@ -66,11 +67,25 @@
<version>3.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.11.0</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.11.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<reuseForks>false</reuseForks>
<forkCount>1</forkCount>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.implementation;
import com.microsoft.semantickernel.exceptions.SKException;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
@@ -14,12 +15,16 @@ import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utility class for loading resources from the classpath or filesystem.
*/
public class EmbeddedResourceLoader {
private static final Logger LOGGER = LoggerFactory.getLogger(EmbeddedResourceLoader.class);
/**
* Loads a file to a string from the classpath using getResourceAsStream
*
@@ -41,7 +46,8 @@ public class EmbeddedResourceLoader {
* @return File content
* @throws FileNotFoundException Error in case the file doesn't exist
*/
public static String readFile(String fileName, Class<?> clazz, ResourceLocation... locations)
public static String readFile(String fileName, @Nullable Class<?> clazz,
ResourceLocation... locations)
throws FileNotFoundException {
List<ResourceLocation> locationsList = Arrays.stream(locations)
@@ -52,6 +58,10 @@ public class EmbeddedResourceLoader {
type -> {
switch (type) {
case CLASSPATH:
if (clazz == null) {
throw new SKException(
"Resource location CLASSPATH requires a class");
}
return getResourceAsStream(fileName, clazz);
case CLASSPATH_ROOT:
try (InputStream inputStream = Thread.currentThread()
@@ -116,6 +126,7 @@ public class EmbeddedResourceLoader {
return bf.lines().collect(Collectors.joining("\n"));
} catch (IOException e) {
// IGNORE
LOGGER.trace("Failed to load file: " + fileName, e);
}
return null;
}
@@ -6,11 +6,12 @@ import com.microsoft.semantickernel.contextvariables.CaseInsensitiveMap;
import com.microsoft.semantickernel.exceptions.SKException;
import com.microsoft.semantickernel.implementation.EmbeddedResourceLoader;
import com.microsoft.semantickernel.implementation.EmbeddedResourceLoader.ResourceLocation;
import com.microsoft.semantickernel.semanticfunctions.KernelFunction;
import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromPrompt;
import com.microsoft.semantickernel.semanticfunctions.InputVariable;
import com.microsoft.semantickernel.semanticfunctions.OutputVariable;
import com.microsoft.semantickernel.semanticfunctions.KernelFunction;
import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromMethod;
import com.microsoft.semantickernel.semanticfunctions.KernelFunctionFromPrompt;
import com.microsoft.semantickernel.semanticfunctions.KernelPromptTemplateFactory;
import com.microsoft.semantickernel.semanticfunctions.OutputVariable;
import com.microsoft.semantickernel.semanticfunctions.PromptTemplate;
import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig;
import com.microsoft.semantickernel.semanticfunctions.PromptTemplateFactory;
@@ -97,21 +98,32 @@ public class KernelPluginFactory {
annotation.returnDescription(),
returnType);
return KernelFunction
KernelFunctionFromMethod.Builder<Object> builder = KernelFunction
.createFromMethod(method, target)
.withPluginName(pluginName)
.withFunctionName(annotation.name())
.withDescription(annotation.description())
.withParameters(getParameters(method))
.withReturnParameter(kernelReturnParameterMetadata)
.build();
.withReturnParameter(kernelReturnParameterMetadata);
if (pluginName != null && !pluginName.isEmpty()) {
builder = builder.withPluginName(pluginName);
}
if (annotation.name() != null && !annotation.name().isEmpty()) {
builder = builder.withFunctionName(annotation.name());
}
if (annotation.description() != null && !annotation.description().isEmpty()) {
builder = builder.withDescription(annotation.description());
}
return builder.build();
}).collect(ArrayList::new, (list, it) -> list.add(it), (a, b) -> a.addAll(b));
return createFromFunctions(pluginName, methods);
}
private static Class<?> getReturnType(DefineKernelFunction annotation, Method method) {
Class<?> returnType;
Class<?> returnType = null;
if (annotation.returnType().isEmpty()) {
returnType = method.getReturnType();
@@ -126,17 +138,22 @@ public class KernelPluginFactory {
// primarily meant to handle void
return method.getReturnType();
}
try {
returnType = Thread.currentThread().getContextClassLoader()
.loadClass(annotation.returnType());
} catch (ClassNotFoundException e) {
returnType = getCommonTypeAlias(method, className);
// ignore
}
if (returnType == null) {
throw new SKException("Could not find return type " + annotation.returnType()
+ " is not found on method " + method.getDeclaringClass().getName() + "."
+ method.getName());
}
if (returnType == null) {
returnType = getCommonTypeAlias(method, className);
}
if (returnType == null) {
throw new SKException("Could not find return type " + annotation.returnType()
+ " is not found on method " + method.getDeclaringClass().getName() + "."
+ method.getName());
}
if (!Publisher.class.isAssignableFrom(method.getReturnType())
@@ -145,7 +162,6 @@ public class KernelPluginFactory {
"Return type " + returnType.getName() + " is not assignable from "
+ method.getReturnType());
}
}
return returnType;
@@ -167,6 +183,10 @@ public class KernelPluginFactory {
returnType = COMMON_CLASS_NAMES.get(className);
}
if (returnType != null && Publisher.class.isAssignableFrom(method.getReturnType())) {
return returnType;
}
if (returnType != null && !returnType.isAssignableFrom(method.getReturnType())) {
returnType = BOXED_FROM_PRIMATIVE.get(returnType);
}
@@ -183,6 +203,10 @@ public class KernelPluginFactory {
public static Class<?> getTypeForName(String className) {
Class<?> clazz = PRIMATIVE_CLASS_NAMES.get(className);
if (clazz == null) {
clazz = COMMON_CLASS_NAMES.get(className);
}
if (clazz != null) {
return clazz;
}
@@ -265,8 +289,10 @@ public class KernelPluginFactory {
* @param promptTemplateFactory The factory to use for creating prompt templates.
* @return The imported plugin.
*/
public static KernelPlugin importPluginFromDirectory(Path parentDirectory,
String pluginDirectoryName, PromptTemplateFactory promptTemplateFactory) {
public static KernelPlugin importPluginFromDirectory(
Path parentDirectory,
String pluginDirectoryName,
@Nullable PromptTemplateFactory promptTemplateFactory) {
// Verify.ValidSkillName(pluginDirectoryName);
File pluginDir = new File(parentDirectory.toFile(), pluginDirectoryName);
@@ -312,7 +338,7 @@ public class KernelPluginFactory {
}
private static KernelFunction<?> getKernelFunction(
PromptTemplateFactory promptTemplateFactory,
@Nullable PromptTemplateFactory promptTemplateFactory,
File configPath,
File promptPath)
throws IOException {
@@ -334,7 +360,7 @@ public class KernelPluginFactory {
@SuppressWarnings("unchecked")
private static <T> KernelFunction<T> getKernelFunction(
PromptTemplateFactory promptTemplateFactory,
@Nullable PromptTemplateFactory promptTemplateFactory,
PromptTemplateConfig config,
String template) {
PromptTemplate promptTemplate;
@@ -360,6 +386,26 @@ public class KernelPluginFactory {
.build();
}
/**
* @param parentDirectory The parent directory containing the plugin directories.
* @param pluginDirectoryName The name of the plugin directory.
* @param functionName The name of the function to import.
* @param promptTemplateFactory The factory to use for creating prompt templates.
* @return The imported plugin.
* @see KernelPluginFactory#importPluginFromResourcesDirectory(String, String, String,
* PromptTemplateFactory, Class)
*/
@Nullable
public static KernelPlugin importPluginFromResourcesDirectory(
String parentDirectory,
String pluginDirectoryName,
String functionName,
@Nullable PromptTemplateFactory promptTemplateFactory) {
return importPluginFromResourcesDirectory(parentDirectory, pluginDirectoryName,
functionName,
promptTemplateFactory, null);
}
/**
* Imports a plugin from a resource directory, which may be on the classpath or filesystem. The
* directory should contain subdirectories, each of which contains a prompt template and a
@@ -370,7 +416,8 @@ public class KernelPluginFactory {
* @param pluginDirectoryName The name of the plugin directory.
* @param functionName The name of the function to import.
* @param promptTemplateFactory The factory to use for creating prompt templates.
* @param clazz The class to use for loading resources.
* @param clazz The class to use for loading resources. If null, the classloader
* will be used.
* @return The imported plugin.
*/
@Nullable
@@ -378,8 +425,8 @@ public class KernelPluginFactory {
String parentDirectory,
String pluginDirectoryName,
String functionName,
PromptTemplateFactory promptTemplateFactory,
Class<?> clazz) {
@Nullable PromptTemplateFactory promptTemplateFactory,
@Nullable Class<?> clazz) {
String template = getTemplatePrompt(parentDirectory, pluginDirectoryName, functionName,
clazz);
@@ -409,7 +456,7 @@ public class KernelPluginFactory {
String pluginDirectory,
String pluginName,
String functionName,
Class<?> clazz) {
@Nullable Class<?> clazz) {
String promptFileName = pluginDirectory + File.separator + pluginName + File.separator
+ functionName
+ File.separator + PROMPT_FILE;
@@ -423,8 +470,13 @@ public class KernelPluginFactory {
}
}
private static String getFileContents(String file, Class<?> clazz)
private static String getFileContents(String file, @Nullable Class<?> clazz)
throws FileNotFoundException {
if (clazz == null) {
return EmbeddedResourceLoader.readFile(file, null, ResourceLocation.CLASSPATH_ROOT,
ResourceLocation.FILESYSTEM);
}
return EmbeddedResourceLoader.readFile(file, clazz, ResourceLocation.CLASSPATH_ROOT,
ResourceLocation.CLASSPATH, ResourceLocation.FILESYSTEM);
}
@@ -433,7 +485,7 @@ public class KernelPluginFactory {
private static PromptTemplateConfig getPromptTemplateConfig(
String pluginDirectory,
String pluginName, String functionName,
Class<?> clazz) {
@Nullable Class<?> clazz) {
String configFileName = pluginDirectory + File.separator + pluginName + File.separator
+ functionName
+ File.separator + CONFIG_FILE;
@@ -91,7 +91,7 @@ public class KernelFunctionFromMethod<T> extends KernelFunction<T> {
MethodDetails methodDetails = getMethodDetails(functionName, method, target);
if (description == null) {
if (description == null || description.isEmpty()) {
description = methodDetails.getDescription();
}
@@ -123,11 +123,15 @@ public class KernelFunctionFromMethod<T> extends KernelFunction<T> {
String description = null;
String returnDescription = null;
if (annotation != null) {
description = annotation.description();
returnDescription = annotation.returnDescription();
if (!annotation.description().isEmpty()) {
description = annotation.description();
}
if (!annotation.returnDescription().isEmpty()) {
returnDescription = annotation.returnDescription();
}
}
if (functionName == null) {
if (functionName == null || functionName.isEmpty()) {
functionName = method.getName();
}
@@ -33,7 +33,8 @@ public @interface DefineKernelFunction {
/**
* The fully qualified class name of the return value of the function, for example,
* "java.lang.String". If this parameter is not provided, the model will attempt to infer the
* return type from the method signature.
* return type from the method signature. For async methods provide the return from the async
* method, i.e. if the method returns a Mono&lt;String&gt; then the returnType should be "java.lang.String".
*
* @return the fully qualified class name of the return value of the function
*/
@@ -0,0 +1,101 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.plugin;
import com.microsoft.semantickernel.semanticfunctions.KernelFunction;
import com.microsoft.semantickernel.semanticfunctions.annotations.DefineKernelFunction;
import com.microsoft.semantickernel.semanticfunctions.annotations.KernelFunctionParameter;
import java.nio.file.Paths;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
public class KernelPluginFactoryTest {
public static class TestPlugin {
@DefineKernelFunction
public String testFunction(
@KernelFunctionParameter(name = "input") String input) {
return "test" + input;
}
@DefineKernelFunction(returnType = "int")
public Mono<Integer> asyncTestFunction(
@KernelFunctionParameter(name = "input") String input) {
return Mono.just(1);
}
}
@Test
public void createFromObjectTest() {
KernelPlugin plugin = KernelPluginFactory.createFromObject(
new TestPlugin(),
"test");
Assertions.assertNotNull(plugin);
Assertions.assertEquals(plugin.getName(), "test");
Assertions.assertEquals(plugin.getFunctions().size(), 2);
KernelFunction<?> testFunction = plugin.getFunctions()
.get("testFunction");
Assertions.assertNotNull(testFunction);
Assertions.assertNotNull(testFunction.getMetadata().getOutputVariableType());
Assertions.assertEquals(testFunction.getMetadata().getOutputVariableType().getType(),
String.class);
Assertions.assertEquals(testFunction.getMetadata().getParameters().size(), 1);
Assertions.assertEquals(testFunction.getMetadata().getParameters().get(0).getType(),
String.class.getName());
}
@Test
public void importPluginFromDirectoryTest() {
KernelPlugin plugin = KernelPluginFactory.importPluginFromDirectory(
Paths.get("./src/test/resources/Plugins"),
"ExamplePlugins",
null);
Assertions.assertNotNull(plugin);
Assertions.assertEquals(plugin.getName(), "ExamplePlugins");
Assertions.assertEquals(plugin.getFunctions().size(), 1);
KernelFunction<?> testFunction = plugin.getFunctions()
.get("examplefunction");
Assertions.assertNotNull(testFunction);
Assertions.assertNotNull(testFunction.getMetadata().getOutputVariableType());
Assertions.assertEquals(testFunction.getMetadata().getOutputVariableType().getType(),
String.class);
Assertions.assertEquals(testFunction.getMetadata().getParameters().size(), 1);
Assertions.assertEquals(testFunction.getMetadata().getParameters().get(0).getTypeClass(),
String.class);
}
@Test
public void importPluginFromResourcesDirectoryTest() {
KernelPlugin plugin = KernelPluginFactory.importPluginFromResourcesDirectory(
"src/test/resources/Plugins",
"ExamplePlugins",
"ExampleFunction",
null);
Assertions.assertNotNull(plugin);
Assertions.assertEquals(plugin.getName(), "ExamplePlugins");
Assertions.assertEquals(plugin.getFunctions().size(), 1);
KernelFunction<?> testFunction = plugin.getFunctions()
.get("examplefunction");
Assertions.assertNotNull(testFunction);
Assertions.assertNotNull(testFunction.getMetadata().getOutputVariableType());
Assertions.assertEquals(testFunction.getMetadata().getOutputVariableType().getType(),
String.class);
Assertions.assertEquals(testFunction.getMetadata().getParameters().size(), 1);
Assertions.assertEquals(testFunction.getMetadata().getParameters().get(0).getTypeClass(),
String.class);
}
}
@@ -0,0 +1,24 @@
{
"schema": 1,
"description": "Sample plugin loaded via resources",
"execution_settings": {
"default": {
"max_tokens": 150,
"temperature": 0.9,
"top_p": 0.0,
"presence_penalty": 0.6,
"frequency_penalty": 0.0
}
},
"input_variables": [
{
"name": "prompt",
"type": "string",
"description": "The prompt to generate text from"
}
],
"output_variables": {
"type": "string",
"description": "The generated text"
}
}