Java: Add example of adding headers to OpenAPI requests (#5980)

### 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-24 12:04:58 +01:00
committed by GitHub
parent c418266ae3
commit f12d8c8667
8 changed files with 205 additions and 19 deletions
@@ -210,6 +210,9 @@ public class OpenAIChatCompletion extends OpenAiService implements ChatCompletio
invocationContext, autoInvokeAttempts - 1);
})
.onErrorResume(e -> {
LOGGER.warn("Tool invocation attempt failed: ", e);
// If FunctionInvocationError occurred and there are still attempts left, retry, else exit
if (autoInvokeAttempts > 0) {
List<ChatRequestMessage> currentMessages = messages;
@@ -69,6 +69,21 @@
<groupId>com.microsoft.semantic-kernel</groupId>
<artifactId>semantickernel-aiservices-openai</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
@@ -31,7 +31,7 @@ import reactor.core.publisher.Mono;
/**
* Plugin for making HTTP requests specifically to endpoints discovered via OpenAPI.
*/
class OpenAPIHttpRequestPlugin {
public class OpenAPIHttpRequestPlugin {
private static final Logger LOGGER = LoggerFactory.getLogger(OpenAPIHttpRequestPlugin.class);
@@ -41,6 +41,7 @@ class OpenAPIHttpRequestPlugin {
private final HttpClient client;
private final HttpMethod method;
private final Operation operation;
private final HttpHeaders httpHeaders;
public OpenAPIHttpRequestPlugin(
HttpMethod method,
@@ -48,12 +49,14 @@ class OpenAPIHttpRequestPlugin {
String path,
PathItem pathItem,
HttpClient client,
HttpHeaders httpHeaders,
Operation operation) {
this.method = method;
this.serverUrl = serverUrl;
this.path = path;
this.pathItem = pathItem;
this.client = client;
this.httpHeaders = httpHeaders;
this.operation = operation;
}
@@ -77,11 +80,18 @@ class OpenAPIHttpRequestPlugin {
HttpRequest request = new HttpRequest(method, url);
HttpHeaders headers = new HttpHeaders();
if (httpHeaders != null) {
headers = headers.setAllHttpHeaders(httpHeaders);
}
if (body != null) {
request = request
.setHeaders(new HttpHeaders()
.set(HttpHeaderName.CONTENT_TYPE, ContentType.APPLICATION_JSON))
.setBody(body);
headers = headers.add(HttpHeaderName.CONTENT_TYPE, ContentType.APPLICATION_JSON);
request = request.setBody(body);
}
if (headers.getSize() > 0) {
request.setHeaders(headers);
}
LOGGER.debug("Executing {} {}", method.name(), url);
@@ -131,7 +131,8 @@ public class SemanticKernelOpenAPIImporter {
client,
pluginName,
paths,
serverUrl);
serverUrl,
httpHeaders);
return new KernelPlugin(
pluginName,
@@ -143,6 +144,8 @@ public class SemanticKernelOpenAPIImporter {
@Nullable HttpHeaders httpHeaders,
@Nullable HttpClient client) {
// Currently this does not apply as the Netty client does not obay the headers provided in the
// HttpClientOptions, however they may do one day
if (httpHeaders != null && client != null) {
throw new IllegalArgumentException(
"Both httpHeaders and client cannot be provided at the same time");
@@ -165,28 +168,31 @@ public class SemanticKernelOpenAPIImporter {
HttpClient client,
String pluginName,
Paths paths,
String serverUrl) {
String serverUrl,
HttpHeaders headers) {
return paths
.entrySet()
.stream()
.flatMap((entry) -> {
return formGetOperation(
return formOperation(
client,
pluginName,
serverUrl,
entry.getKey(),
entry.getValue())
entry.getValue(),
headers)
.stream();
})
.collect(Collectors.toMap(KernelFunction::getName, (func) -> func));
}
private static List<KernelFunction<?>> formGetOperation(
private static List<KernelFunction<?>> formOperation(
HttpClient client,
String pluginName,
String serverUrl,
String path,
PathItem pathItem) {
PathItem pathItem,
HttpHeaders headers) {
List<KernelFunction<?>> plugins = new ArrayList<>();
@@ -198,6 +204,7 @@ public class SemanticKernelOpenAPIImporter {
path,
pathItem,
pathItem.getGet(),
headers,
HttpMethod.GET);
plugins.add(function);
@@ -210,6 +217,7 @@ public class SemanticKernelOpenAPIImporter {
path,
pathItem,
pathItem.getDelete(),
headers,
HttpMethod.DELETE);
plugins.add(function);
@@ -222,6 +230,7 @@ public class SemanticKernelOpenAPIImporter {
path,
pathItem,
pathItem.getPost(),
headers,
HttpMethod.POST);
plugins.add(function);
@@ -234,6 +243,7 @@ public class SemanticKernelOpenAPIImporter {
path,
pathItem,
pathItem.getPut(),
headers,
HttpMethod.PUT);
plugins.add(function);
@@ -246,6 +256,7 @@ public class SemanticKernelOpenAPIImporter {
path,
pathItem,
pathItem.getPatch(),
headers,
HttpMethod.PATCH);
plugins.add(function);
@@ -258,6 +269,7 @@ public class SemanticKernelOpenAPIImporter {
path,
pathItem,
pathItem.getOptions(),
headers,
HttpMethod.OPTIONS);
plugins.add(function);
@@ -270,6 +282,7 @@ public class SemanticKernelOpenAPIImporter {
path,
pathItem,
pathItem.getHead(),
headers,
HttpMethod.HEAD);
plugins.add(function);
@@ -282,6 +295,7 @@ public class SemanticKernelOpenAPIImporter {
path,
pathItem,
pathItem.getTrace(),
headers,
HttpMethod.TRACE);
plugins.add(function);
@@ -290,17 +304,27 @@ public class SemanticKernelOpenAPIImporter {
return plugins;
}
private static @Nullable KernelFunction<Object> getKernelFunctionFromRequest(HttpClient client,
String pluginName, String serverUrl, String path, PathItem pathItem,
private static @Nullable KernelFunction<Object> getKernelFunctionFromRequest(
HttpClient client,
String pluginName,
String serverUrl,
String path,
PathItem pathItem,
Operation operation,
HttpHeaders headers,
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,
OpenAPIHttpRequestPlugin plugin = new OpenAPIHttpRequestPlugin(
method,
serverUrl,
path,
pathItem,
client,
headers,
operation);
RequestBody requestBody = operation.getRequestBody();
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft. All rights reserved.
import com.azure.core.http.HttpHeader;
import com.azure.core.http.HttpHeaders;
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 com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.junit.jupiter.api.Assertions;
public class ExampleAuthenticatedRequestImporter {
public static void main(String[] args) throws IOException {
String yaml = EmbeddedResourceLoader.readFile("authenticatedRequest.yaml",
ExampleAuthenticatedRequestImporter.class);
KernelPlugin plugin = SemanticKernelOpenAPIImporter
.builder()
.withPluginName("authenticatedRequest")
.withSchema(yaml)
.withServer("http://127.0.0.1:8890")
.withHttpHeaders(new HttpHeaders(List.of(
new HttpHeader("Authorization", "Bearer 1234"))))
.build();
Kernel kernel = ExampleOpenAPIParent.kernelBuilder()
.withPlugin(plugin)
.build();
HttpServer httpServer = null;
try {
httpServer = HttpServer.create(new InetSocketAddress("127.0.0.1", 8890), 0);
httpServer.createContext("/state", exchange -> {
Assertions.assertTrue(
exchange.getRequestHeaders().containsKey("Authorization"),
"Authorization header not found");
exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0);
exchange.getResponseBody().write(
"""
{
"state": "running"
}
"""
.stripIndent()
.getBytes(StandardCharsets.UTF_8));
exchange.close();
});
httpServer.start();
performRequest(kernel,
"""
What is the current state of the system?
""".stripIndent());
} finally {
httpServer.stop(0);
}
}
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,25 @@
openapi: 3.0.2
servers:
- url: /
info:
description: |-
Example of an authenticated api
version: 1.0.1-SNAPSHOT
title: Auth Example
paths:
/state:
get:
summary: Get the latest state of the system
description: The current state the system
operationId: getState
responses:
'200':
description: The current state of the system
security:
- BearerAuth:
- 'read'
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss} %-5level %c{1.} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
@@ -25,6 +25,7 @@ import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -174,10 +175,15 @@ public class KernelFunctionFromMethod<T> extends KernelFunction<T> {
.collect(Collectors.toList());
Mono<?> mono;
if (method.getReturnType().isAssignableFrom(Mono.class)) {
mono = (Mono<?>) method.invoke(instance, args.toArray());
} else {
mono = invokeAsyncFunction(method, instance, args);
try {
if (method.getReturnType().isAssignableFrom(Mono.class)) {
mono = (Mono<?>) method.invoke(instance, args.toArray());
} else {
mono = invokeAsyncFunction(method, instance, args);
}
} catch (Exception e) {
return Mono.error(
new SKException("Function threw an exception: " + method.getName(), e));
}
return mono
@@ -333,6 +339,8 @@ public class KernelFunctionFromMethod<T> extends KernelFunction<T> {
return parameter -> {
if (KernelFunctionArguments.class.isAssignableFrom(parameter.getType())) {
return context;
} else if (Kernel.class.isAssignableFrom(parameter.getType())) {
return kernel;
} else {
return getArgumentValue(method, context, parameter, kernel, invocationContext);
}
@@ -542,6 +550,7 @@ public class KernelFunctionFromMethod<T> extends KernelFunction<T> {
return Arrays.stream(method
.getParameters())
.map(KernelFunctionFromMethod::toKernelParameterMetadata)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
@@ -555,6 +564,10 @@ public class KernelFunctionFromMethod<T> extends KernelFunction<T> {
boolean isRequired = true;
Class<?> type = parameter.getType();
if (Kernel.class.isAssignableFrom(type) || KernelFunctionArguments.class.isAssignableFrom(
type)) {
return null;
}
if (annotation != null) {
name = annotation.name();
description = annotation.description();