Java: non-streaming side of OpenAIChatCompletion (#4588)

Fill in non-streaming API in OpenAIChatCompletion.

---------

Co-authored-by: John Oliver <[email protected]>
This commit is contained in:
David Grieve
2024-01-18 15:23:29 +00:00
committed by GitHub
co-authored by John Oliver
parent 3e0cfce676
commit aca09de4f7
17 changed files with 445 additions and 277 deletions
@@ -1,5 +1,17 @@
package com.microsoft.semantickernel.aiservices.openai.chatcompletion;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.azure.ai.openai.OpenAIAsyncClient;
import com.azure.ai.openai.models.ChatChoice;
import com.azure.ai.openai.models.ChatCompletions;
@@ -16,23 +28,22 @@ import com.azure.ai.openai.models.ChatResponseMessage;
import com.azure.ai.openai.models.ChatRole;
import com.azure.ai.openai.models.FunctionCall;
import com.azure.ai.openai.models.FunctionDefinition;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.chatcompletion.AuthorRole;
import com.microsoft.semantickernel.chatcompletion.ChatCompletionService;
import com.microsoft.semantickernel.chatcompletion.ChatHistory;
import com.microsoft.semantickernel.chatcompletion.ChatMessageContent;
import com.microsoft.semantickernel.chatcompletion.StreamingChatMessageContent;
import com.microsoft.semantickernel.orchestration.KernelFunction;
import com.microsoft.semantickernel.orchestration.PromptExecutionSettings;
import com.microsoft.semantickernel.orchestration.contextvariables.ContextVariable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.microsoft.semantickernel.orchestration.contextvariables.ContextVariableType;
import com.microsoft.semantickernel.orchestration.contextvariables.ContextVariableTypes;
import com.microsoft.semantickernel.orchestration.contextvariables.KernelArguments;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -63,30 +74,28 @@ public class OpenAIChatCompletion implements ChatCompletionService {
List<ChatRequestMessage> chatRequestMessages = getChatRequestMessages(chatHistory);
List<FunctionDefinition> functions = Collections.emptyList();
return internalChatMessageContentsAsync(chatRequestMessages, functions,
promptExecutionSettings);
return internalChatMessageContentsAsync(chatRequestMessages, functions, promptExecutionSettings, kernel);
}
@Override
public Flux<StreamingChatMessageContent> getStreamingChatMessageContentsAsync(
ChatHistory chatHistory,
PromptExecutionSettings promptExecutionSettings, Kernel kernel) {
return internalStreamingChatMessageContentsAsync(
getChatRequestMessages(chatHistory),
Collections.emptyList(),
promptExecutionSettings);
public Flux<StreamingChatMessageContent> getStreamingChatMessageContentsAsync(ChatHistory chatHistory,
PromptExecutionSettings promptExecutionSettings, Kernel kernel)
{
List<ChatRequestMessage> chatRequestMessages = getChatRequestMessages(chatHistory);
List<FunctionDefinition> functions = Collections.emptyList();
return internalStreamingChatMessageContentsAsync(chatRequestMessages, functions, promptExecutionSettings, kernel);
}
@Override
public Mono<List<ChatMessageContent>> getChatMessageContentsAsync(String prompt,
PromptExecutionSettings promptExecutionSettings, Kernel kernel) {
ParsedPrompt parsedPrompt = XMLPromptParser.parse(prompt);
PromptExecutionSettings promptExecutionSettings, Kernel kernel) {
ParsedPrompt parsedPrompt = XMLPromptParser.parse(prompt);
return internalChatMessageContentsAsync(
parsedPrompt.getChatRequestMessages(),
parsedPrompt.getFunctions(),
promptExecutionSettings);
promptExecutionSettings,
kernel);
}
@Override
@@ -100,151 +109,196 @@ public class OpenAIChatCompletion implements ChatCompletionService {
return internalStreamingChatMessageContentsAsync(
parsedPrompt.getChatRequestMessages(),
parsedPrompt.getFunctions(),
promptExecutionSettings);
}
promptExecutionSettings,
kernel);
private interface ContentBuffer<T> {
void append(T content);
StreamingChatMessageContent toStreamingChatMessageContent();
}
private static class AssistantContentBuffer implements ContentBuffer<String> {
private final StringBuilder sb = new StringBuilder();
@Override
public void append(String content) {
sb.append(content);
}
@Override
public StreamingChatMessageContent toStreamingChatMessageContent() {
return new StreamingChatMessageContent(AuthorRole.ASSISTANT, sb.toString());
}
}
private static class ToolContentBuffer implements ContentBuffer<FunctionCall> {
private String name = null;
private List<String> arguments = new ArrayList<>();
@Override
public void append(FunctionCall functionCall) {
String fnName = functionCall.getName();
String fnArguments = functionCall.getArguments();
if (this.name == null && fnName != null && !fnName.isEmpty()) {
this.name = fnName;
} else if (fnArguments != null && !fnArguments.isEmpty()) {
this.arguments.add(fnArguments);
}
}
@Override
public StreamingChatMessageContent toStreamingChatMessageContent() {
StringBuilder sb = new StringBuilder("{\"type\":\"function\", \"function\": ");
sb.append(String.format("{\"name\":\"%s\", \"parameters\": ", name));
boolean first = true;
// when concatentated, args should be valid json
for (String argument : arguments) {
sb.append(argument);
}
// close off function, and type
sb.append("}}");
assert isBalanced(sb.toString());
return new StreamingChatMessageContent(AuthorRole.TOOL, sb.toString());
}
private boolean isBalanced(String str) {
int openParens = 0;
int closeParens = 0;
boolean inString = false;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (!inString && c == '(') {
openParens++;
} else if (!inString && c == ')') {
closeParens++;
} else if (c == '"') {
inString = !inString;
}
}
return openParens == closeParens;
}
}
private static class ChatResponseCollector {
private final Map<AuthorRole, ContentBuffer<?>> roleToContent = new HashMap<>();
private ContentBuffer<?> collectorFor(AuthorRole role) {
return roleToContent.computeIfAbsent(role, k -> {
if (k == AuthorRole.TOOL) {
return new ToolContentBuffer();
}
return new AssistantContentBuffer();
});
}
@SuppressWarnings("unchecked")
private <T> void append(AuthorRole role, T content) {
ContentBuffer<T> contentBuffer = (ContentBuffer<T>) collectorFor(role);
contentBuffer.append(content);
}
private StreamingChatMessageContent toStreamingChatMessageContent() {
assert roleToContent.size() == 1;
ContentBuffer<?> contentBuffer = roleToContent.values().iterator().next();
return contentBuffer.toStreamingChatMessageContent();
}
}
private Flux<StreamingChatMessageContent> internalStreamingChatMessageContentsAsync(
List<ChatRequestMessage> chatRequestMessages,
List<FunctionDefinition> functions,
PromptExecutionSettings promptExecutionSettings) {
ChatCompletionsOptions options = getCompletionsOptions(this, chatRequestMessages, functions,
promptExecutionSettings);
PromptExecutionSettings promptExecutionSettings,
Kernel kernel)
{
ChatCompletionsOptions options = getCompletionsOptions(this, chatRequestMessages, functions, promptExecutionSettings);
Flux<List<StreamingChatMessageContent>> results =
internalStreamingChatMessageContentsAsync(options, kernel).buffer();
return results.flatMap(list -> {
boolean makeSecondCall = false;
for (StreamingChatMessageContent messageContent : list) {
if (messageContent.getRole() == AuthorRole.TOOL) {
makeSecondCall = true;
String content = messageContent.getContent();
String id = messageContent.getModelId();
ChatRequestToolMessage toolMessage = new ChatRequestToolMessage(content, id);
chatRequestMessages.add(toolMessage);
}
}
if (makeSecondCall) {
return internalStreamingChatMessageContentsAsync(options, kernel);
}
return Flux.fromIterable(list);
});
}
private Flux<StreamingChatMessageContent> internalStreamingChatMessageContentsAsync(ChatCompletionsOptions options, Kernel kernel) {
return client
.getChatCompletionsStream(getModelId(), options)
.filter(chatCompletion -> chatCompletion != null)
.map(ChatCompletions::getChoices)
.mapNotNull(ChatCompletions::getChoices)
.filter(choices -> choices != null && !choices.isEmpty())
.reduceWith(ChatResponseCollector::new, (accumulator, choices) -> {
choices.stream()
.map(choice -> choice.getDelta())
.filter(chatResponseMessage -> chatResponseMessage != null)
.forEach(chatResponseMessage -> {
if (chatResponseMessage.getContent() != null) {
accumulator.append(AuthorRole.ASSISTANT,
chatResponseMessage.getContent());
} else if (chatResponseMessage.getToolCalls() != null) {
List<ChatCompletionsToolCall> toolCalls = chatResponseMessage.getToolCalls();
toolCalls.forEach(toolCall -> {
if (toolCall instanceof ChatCompletionsFunctionToolCall) {
FunctionCall functionCall = ((ChatCompletionsFunctionToolCall) toolCall).getFunction();
accumulator.append(AuthorRole.TOOL, functionCall);
}
});
}
});
return accumulator;
})
.reduceWith(ChatResponseCollector::new, (accumulator, choices) -> accumulateResponsesFromStream(accumulator, choices))
.map(ChatResponseCollector::toStreamingChatMessageContent)
.map(streamingChatMessageContent -> {
if (streamingChatMessageContent.getRole() == AuthorRole.TOOL) {
return invokeTool(kernel, streamingChatMessageContent.getContent()).block();
}
return streamingChatMessageContent;
})
.flux();
}
private Mono<List<ChatMessageContent>> internalChatMessageContentsAsync(
List<ChatRequestMessage> chatRequestMessages,
List<FunctionDefinition> functions,
PromptExecutionSettings promptExecutionSettings) {
ChatCompletionsOptions options = getCompletionsOptions(this, chatRequestMessages, functions,
promptExecutionSettings);
PromptExecutionSettings promptExecutionSettings,
Kernel kernel)
{
ChatCompletionsOptions options = getCompletionsOptions(this, chatRequestMessages, functions, promptExecutionSettings);
Mono<List<ChatMessageContent>> results =
internalChatMessageContentsAsync(options, kernel);
return results.flatMap(list -> {
boolean makeSecondCall = false;
for (ChatMessageContent messageContent : list) {
if (messageContent.getAuthorRole() == AuthorRole.TOOL) {
makeSecondCall = true;
String content = messageContent.getContent();
String id = messageContent.getModelId();
ChatRequestToolMessage toolMessage = new ChatRequestToolMessage(content, id);
chatRequestMessages.add(toolMessage);
}
}
if (makeSecondCall) {
return internalChatMessageContentsAsync(options, kernel);
}
return Mono.just(list);
});
}
private Mono<List<ChatMessageContent>> internalChatMessageContentsAsync(
ChatCompletionsOptions options,
Kernel kernel) {
return client
.getChatCompletions(getModelId(), options)
.flatMap(chatCompletions -> Mono.just(toChatMessageContents(chatCompletions)));
.mapNotNull(ChatCompletions::getChoices)
.filter(choices -> choices != null && !choices.isEmpty())
.map(this::accumulateResponses)
.map(responses ->
responses.stream()
.map(ChatResponseCollector::toChatMessageContent)
.collect(Collectors.toList())
);
}
// streaming case
private ChatResponseCollector accumulateResponsesFromStream(ChatResponseCollector collector, List<ChatChoice> choices) {
choices.stream()
.map(choice -> choice.getDelta())
.filter(chatResponseMessage -> chatResponseMessage != null)
.forEach(chatResponseMessage -> accumulateResponse(collector, chatResponseMessage));
return collector;
}
// non-streaming case
private List<ChatResponseCollector> accumulateResponses(List<ChatChoice> choices) {
List<ChatResponseCollector> collectors = new ArrayList<>();
choices.stream()
.map(choice -> choice.getDelta())
.filter(chatResponseMessage -> chatResponseMessage != null)
.map(this::accumulateResponse)
.filter(chatResponseMessage -> chatResponseMessage != null)
.forEach(collectors::add);
return collectors;
}
private ChatResponseCollector accumulateResponse(ChatResponseMessage response) {
return accumulateResponse(null, response);
}
private ChatResponseCollector accumulateResponse(ChatResponseCollector collector, ChatResponseMessage response) {
// collector is null for the non-streaming case and not null for the streaming case
if (response.getContent() != null) {
if (collector == null) collector = new ChatResponseCollector();
collector.append(AuthorRole.ASSISTANT, response.getContent());
} else if (response.getToolCalls() != null) {
List<ChatCompletionsToolCall> toolCalls = response.getToolCalls();
// TODO: This assumes one tool call per response, which is _definitely_ a bad assumption.
for( ChatCompletionsToolCall toolCall : toolCalls) {
if (collector == null) collector = new ChatResponseCollector();
if (toolCall instanceof ChatCompletionsFunctionToolCall) {
collector.append(AuthorRole.TOOL,(ChatCompletionsFunctionToolCall)toolCall);
}
}
}
return collector;
}
/*
* Given a json string, invoke the tool specified in the json string.
* At this time, the only tool we have is 'function'.
* The json string should be of the form:
* {"type":"function", "function": {"name":"search-search", "parameters": {"query":"Banksy"}}}
* where 'name' is <plugin name '-' function name>.
*/
private Mono<StreamingChatMessageContent> invokeTool(Kernel kernel, String json) {
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(json);
String id = jsonNode.get("id").asText("");
jsonNode = jsonNode.get("function");
if (jsonNode != null) {
// function is the only tool we have right now.
Mono<ContextVariable<String>> result = invokeFunction(kernel, jsonNode);
if (result != null) {
return result.map(contextVariable -> {
String content = contextVariable.getValue();
return new StreamingChatMessageContent(AuthorRole.TOOL, content).setModelId(id);
});
}
}
} catch (JsonProcessingException e) {
LOGGER.error("Failed to parse json", e);
}
return Mono.empty();
}
/*
* The jsonNode should represent: {"name":"search-search", "parameters": {"query":"Banksy"}}}
*/
private Mono<ContextVariable<String>> invokeFunction(Kernel kernel, JsonNode jsonNode) {
String name = jsonNode.get("name").asText();
String[] parts = name.split("-");
String pluginName = parts.length > 0 ? parts[0] : "";
String fnName = parts.length > 1 ? parts[1] : "";
JsonNode parameters = jsonNode.get("parameters");
KernelFunction kernelFunction = kernel.getPlugins().getFunction(pluginName, fnName);
if (kernelFunction == null) return Mono.empty();
KernelArguments arguments = null;
if (parameters != null) {
Map<String, ContextVariable<?>> variables = new HashMap<>();
parameters.fields().forEachRemaining(entry -> {
String paramName = entry.getKey();
String paramValue = entry.getValue().asText();
ContextVariable<?> contextVariable = ContextVariable.of(paramValue);
variables.put(paramName, contextVariable);
});
arguments = KernelArguments.builder().withVariables(variables).build();
}
ContextVariableType<String> variableType = ContextVariableTypes.getDefaultVariableTypeForClass(String.class);
return kernelFunction.invokeAsync(kernel, arguments, variableType);
}
private static ChatCompletionsOptions getCompletionsOptions(
@@ -299,7 +353,19 @@ public class OpenAIChatCompletion implements ChatCompletionService {
})
.collect(Collectors.toList());
}
private static ChatRequestMessage getChatRequestMessage(
String role,
String content)
{
try {
AuthorRole authorRole = AuthorRole.valueOf(role.toUpperCase());
return getChatRequestMessage(authorRole, content);
} catch (IllegalArgumentException e) {
LOGGER.debug("Unknown author role: " + role);
return null;
}
}
static ChatRequestMessage getChatRequestMessage(
AuthorRole authorRole,
@@ -345,19 +411,143 @@ public class OpenAIChatCompletion implements ChatCompletionService {
if (chatRole == ChatRole.ASSISTANT) {
return AuthorRole.ASSISTANT;
}
if (chatRole == ChatRole.SYSTEM) {
if(chatRole == ChatRole.SYSTEM) {
return AuthorRole.SYSTEM;
}
if (chatRole == ChatRole.USER) {
if(chatRole == ChatRole.USER) {
return AuthorRole.USER;
}
if (chatRole == ChatRole.TOOL) {
if(chatRole == ChatRole.TOOL) {
return AuthorRole.TOOL;
}
throw new IllegalArgumentException("Unknown chat role: " + chatRole);
}
private interface ContentBuffer<T> {
void append(T content);
StreamingChatMessageContent toStreamingChatMessageContent();
ChatMessageContent toChatMessageContent();
}
private static class AssistantContentBuffer implements ContentBuffer<String> {
private final StringBuilder sb = new StringBuilder();
@Override
public void append(String content) {
sb.append(content);
}
@Override
public StreamingChatMessageContent toStreamingChatMessageContent() {
return new StreamingChatMessageContent(AuthorRole.ASSISTANT, sb.toString());
}
@Override
public ChatMessageContent toChatMessageContent() {
return new ChatMessageContent(AuthorRole.ASSISTANT, sb.toString());
}
}
private static class ToolContentBuffer implements ContentBuffer<ChatCompletionsFunctionToolCall> {
private String id = null;
private String name = null;
private List<String> arguments = new ArrayList<>();
@Override
public void append(ChatCompletionsFunctionToolCall toolCall) {
FunctionCall function = toolCall.getFunction();
String toolCallId = toolCall.getId();
String fnName = function.getName();
String fnArguments = function.getArguments();
if (this.id == null && toolCallId != null && !toolCallId.isEmpty()) {
this.id = toolCallId;
}
if (this.name == null && fnName != null && !fnName.isEmpty()) {
this.name = fnName;
}
if (fnArguments != null && !fnArguments.isEmpty()) {
this.arguments.add(fnArguments);
}
}
@Override
public StreamingChatMessageContent toStreamingChatMessageContent() {
return new StreamingChatMessageContent(AuthorRole.TOOL, toJsonString());
}
@Override
public ChatMessageContent toChatMessageContent() {
return new ChatMessageContent(AuthorRole.TOOL, toJsonString());
}
private String toJsonString() {
StringBuilder sb = new StringBuilder(
String.format("{\"type\":\"function\", \"id\":\"%s\", \"function\": ", id));
sb.append(String.format("{\"name\":\"%s\", \"parameters\": ", name));
// when concatentated, args should be valid json
for(String argument : arguments) {
sb.append(argument);
}
// close off function, and type
sb.append("}}");
assert isBalanced(sb.toString());
return sb.toString();
}
// used to check that the json string is balanced
private boolean isBalanced(String str) {
int openParens = 0;
int closeParens = 0;
boolean inString = false;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (!inString && c == '(') {
openParens++;
} else if (!inString && c == ')') {
closeParens++;
} else if (c == '"') {
inString = !inString;
}
}
return openParens == closeParens;
}
}
// For streaming,
private static class ChatResponseCollector {
private final Map<AuthorRole,ContentBuffer<?>> roleToContent = new HashMap<>();
private ContentBuffer<?> collectorFor(AuthorRole role) {
return roleToContent.computeIfAbsent(role, k -> {
if (k == AuthorRole.TOOL) return new ToolContentBuffer();
return new AssistantContentBuffer();
});
}
@SuppressWarnings("unchecked")
private <T> void append(AuthorRole role, T content) {
ContentBuffer<T> contentBuffer = (ContentBuffer<T>)collectorFor(role);
contentBuffer.append(content);
}
private StreamingChatMessageContent toStreamingChatMessageContent() {
assert roleToContent.size() == 1;
ContentBuffer<?> contentBuffer = roleToContent.values().iterator().next();
return contentBuffer.toStreamingChatMessageContent();
}
private ChatMessageContent toChatMessageContent() {
assert roleToContent.size() == 1;
ContentBuffer<?> contentBuffer = roleToContent.values().iterator().next();
return contentBuffer.toChatMessageContent();
}
}
public static class Builder extends ChatCompletionService.Builder {
@Override
public OpenAIChatCompletion build() {
return new OpenAIChatCompletion(client, modelId);
@@ -13,11 +13,11 @@ template_format: handlebars
description: A function that uses the chat history to respond to the user.
input_variables:
- name: messages
type: ChatHistory
type: com.microsoft.semantickernel.chatcompletion.ChatHistory
description: The history of the chat.
is_required: true
output_variable:
type: string
type: java.lang.String
description: The response from the assistant.
execution_settings:
gpt-3.5-turbo:
@@ -14,16 +14,16 @@ template_format: handlebars
description: A function that uses the chat history to respond to the user.
input_variables:
- name: persona
type: string
type: java.lang.String
description: The persona of the assistant.
default: You are a helpful assistant.
is_required: false
- name: messages
type: ChatHistory
type: com.microsoft.semantickernel.chatcompletion.ChatHistory
description: The history of the chat.
is_required: true
output_variable:
type: string
type: java.lang.String
description: The response from the assistant.
execution_settings:
gpt-3.5-turbo:
@@ -19,16 +19,16 @@ template_format: handlebars
description: A function that gets uses the chat history to respond to the user.
input_variables:
- name: persona
type: string
type: java.lang.String
description: The persona of the assistant.
default: You are a helpful assistant.
is_required: false
- name: messages
type: ChatHistory
type: com.microsoft.semantickernel.chatcompletion.ChatHistory
description: The history of the chat.
is_required: true
output_variable:
type: string
type: java.lang.String
description: The response from the assistant.
execution_settings:
gpt-3.5-turbo:
@@ -31,16 +31,16 @@ template_format: handlebars
description: A function that gets uses the chat history to respond to the user.
input_variables:
- name: persona
type: string
type: java.lang.String
description: The persona of the assistant.
default: You are a helpful assistant.
is_required: false
- name: messages
type: ChatHistory
type: com.microsoft.semantickernel.chatcompletion.ChatHistory
description: The history of the chat.
is_required: true
output_variable:
type: string
type: java.lang.String
description: The response from the assistant.
execution_settings:
gpt-3.5-turbo:
@@ -108,11 +108,11 @@ template_format: handlebars
description: A function that determines the next action the assistant should take.
input_variables:
- name: messages
type: ChatHistory
type: com.microsoft.semantickernel.chatcompletion.ChatHistory
description: The history of the chat.
is_required: true
output_variable:
type: string
type: java.lang.String
description: The action the assistant should take.
execution_settings:
gpt-3.5-turbo:
@@ -34,11 +34,11 @@ template_format: handlebars
description: A function that generates the required information to solve a math problem
input_variables:
- name: messages
type: ChatHistory
type: com.microsoft.semantickernel.chatcompletion.ChatHistory
description: The history of the chat.
is_required: true
output_variable:
type: string
type: java.lang.String
description: The summary, inputs, and steps required to solve the problem.
execution_settings:
gpt-3.5-turbo:
@@ -17,16 +17,16 @@ template_format: handlebars
description: A function that gets uses the chat history to respond to the user.
input_variables:
- name: persona
type: string
type: java.lang.String
description: The persona of the assistant.
default: You are a helpful assistant.
is_required: false
- name: messages
type: ChatHistory
type: com.microsoft.semantickernel.chatcompletion.ChatHistory
description: The history of the chat.
is_required: true
output_variable:
type: string
type: java.lang.String
description: The response from the assistant.
execution_settings:
gpt-4:
@@ -11,7 +11,7 @@ template_format: handlebars
description: A function that guesses what word should replace [MASK].
input_variables:
output_variable:
type: string
type: java.lang.String
description: The most likely word for [MASK].
execution_settings:
bert:
@@ -12,7 +12,7 @@ template_format: handlebars
description: A function that generates a description of an image.
input_variables:
output_variable:
type: string
type: java.lang.String
description: The text description of the image.
execution_settings:
gpt-4:
@@ -11,7 +11,7 @@ template_format: handlebars
description: A function that answers a question using context.
input_variables:
output_variable:
type: string
type: java.lang.String
description: The answer.
execution_settings:
deepset:
@@ -11,7 +11,7 @@ template_format: handlebars
description: A function that summarizes some content.
input_variables:
output_variable:
type: string
type: java.lang.String
description: The summary.
execution_settings:
facebook:
@@ -28,11 +28,11 @@ template_format: handlebars
description: A function that determines the best search query to perform on behalf of the user.
input_variables:
- name: messages
type: ChatHistory
type: com.microsoft.semantickernel.chatcompletion.ChatHistory
description: The history of the chat.
is_required: true
output_variable:
type: string
type: java.lang.String
description: The Bing search to perform on behalf of the user.
execution_settings:
gpt-3.5-turbo:
@@ -1,5 +1,15 @@
package com.microsoft.semantickernel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.microsoft.semantickernel.chatcompletion.ChatCompletionService;
import com.microsoft.semantickernel.orchestration.KernelFunction;
import com.microsoft.semantickernel.orchestration.contextvariables.ContextVariable;
@@ -11,13 +21,7 @@ import com.microsoft.semantickernel.plugin.KernelPlugin;
import com.microsoft.semantickernel.plugin.KernelPluginCollection;
import com.microsoft.semantickernel.semanticfunctions.PromptTemplate;
import com.microsoft.semantickernel.textcompletion.TextGenerationService;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -2,16 +2,50 @@ package com.microsoft.semantickernel;
import com.microsoft.semantickernel.orchestration.contextvariables.ContextVariable;
import java.util.Map;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
public class KernelContent {
public class KernelContent<T extends KernelContent<T>> {
@Nullable
private Object innerContent;
@Nullable
private String modelId;
@Nullable
private Map<String, ContextVariable<?>> metadata;
public KernelContent(
@Nullable
Object innerContent,
@Nullable
String modelId,
@Nullable
Map<String, ContextVariable<?>> metadata
) {}
) {
this.innerContent = innerContent;
this.modelId = modelId;
this.metadata = metadata;
}
T setInnerContent(@Nullable Object innerContent) {
this.innerContent = innerContent;
return (T) this;
}
@CheckForNull
public Object getInnerContent() {
return innerContent;
}
@CheckForNull
public String getModelId() {
return modelId;
}
@CheckForNull
public Map<String, ContextVariable<?>> getMetadata() {
return metadata;
}
}
@@ -1,17 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.chatcompletion;
import com.microsoft.semantickernel.StreamingKernelContent;
import com.microsoft.semantickernel.orchestration.contextvariables.ContextVariable;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import com.microsoft.semantickernel.StreamingKernelContent;
import com.microsoft.semantickernel.orchestration.contextvariables.ContextVariable;
public class StreamingChatMessageContent extends StreamingKernelContent {
private String content;
private AuthorRole role;
private final String content;
private final AuthorRole role;
private Charset encoding;
private String modelId;
public StreamingChatMessageContent(
AuthorRole role,
@@ -47,24 +49,26 @@ public class StreamingChatMessageContent extends StreamingKernelContent {
return content;
}
public void setContent(String content) {
this.content = content;
}
public AuthorRole getRole() {
return role;
}
public void setRole(AuthorRole role) {
this.role = role;
}
public Charset getEncoding() {
return encoding;
}
public void setEncoding(Charset encoding) {
public StreamingChatMessageContent setEncoding(Charset encoding) {
this.encoding = encoding;
return this;
}
public StreamingChatMessageContent setModelId(String modelId) {
this.modelId = modelId;
return this;
}
public String getModelId() {
return modelId;
}
}
@@ -1,12 +1,18 @@
package com.microsoft.semantickernel.semanticfunctions;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.azure.core.exception.HttpResponseException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.microsoft.semantickernel.AIService;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.TextAIService;
import com.microsoft.semantickernel.chatcompletion.AuthorRole;
import com.microsoft.semantickernel.chatcompletion.ChatCompletionService;
import com.microsoft.semantickernel.orchestration.DefaultKernelFunction;
import com.microsoft.semantickernel.orchestration.KernelFunction;
@@ -15,15 +21,9 @@ import com.microsoft.semantickernel.orchestration.PromptExecutionSettings;
import com.microsoft.semantickernel.orchestration.StreamingContent;
import com.microsoft.semantickernel.orchestration.contextvariables.ContextVariable;
import com.microsoft.semantickernel.orchestration.contextvariables.ContextVariableType;
import com.microsoft.semantickernel.orchestration.contextvariables.ContextVariableTypes;
import com.microsoft.semantickernel.orchestration.contextvariables.KernelArguments;
import com.microsoft.semantickernel.textcompletion.TextGenerationService;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -82,24 +82,11 @@ public class KernelFunctionFromPrompt extends DefaultKernelFunction {
executionSettings,
kernel)
.concatMap(streamingChatMessageContent -> {
if (streamingChatMessageContent.getRole() == AuthorRole.ASSISTANT) {
T value = variableType
.getConverter()
.fromPromptString(
streamingChatMessageContent.getContent());
return Flux.just(new StreamingContent<>(value));
} else if (streamingChatMessageContent.getRole() == AuthorRole.TOOL) {
Mono<ContextVariable<String>> toolResult = invokeTool(kernel,
T value = variableType
.getConverter()
.fromPromptString(
streamingChatMessageContent.getContent());
return toolResult.flatMapMany(contextVariable -> {
T value = variableType
.getConverter()
.fromPromptString(
contextVariable.getValue());
return Flux.just(new StreamingContent<>(value));
});
}
return Flux.empty();
return Flux.just(new StreamingContent<>(value));
});
return result;
@@ -168,57 +155,6 @@ public class KernelFunctionFromPrompt extends DefaultKernelFunction {
});
}
/*
* Given a json string, invoke the tool specified in the json string.
* At this time, the only tool we have is 'function'.
* The json string should be of the form:
* {"type":"function", "function": {"name":"search-search", "parameters": {"query":"Banksy"}}}
* where 'name' is <plugin name '-' function name>.
*/
private Mono<ContextVariable<String>> invokeTool(Kernel kernel, String json) {
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(json);
jsonNode = jsonNode.get("function");
if (jsonNode != null) {
return invokeFunction(kernel, jsonNode);
}
} catch (Exception e) {
LOGGER.error("Failed to parse json", e);
}
return Mono.empty();
}
/*
* The jsonNode should represent: {"name":"search-search", "parameters": {"query":"Banksy"}}}
*/
private Mono<ContextVariable<String>> invokeFunction(Kernel kernel, JsonNode jsonNode) {
String name = jsonNode.get("name").asText();
String[] parts = name.split("-");
String pluginName = parts.length > 0 ? parts[0] : "";
String fnName = parts.length > 1 ? parts[1] : "";
JsonNode parameters = jsonNode.get("parameters");
if (parameters != null) {
KernelFunction kernelFunction = kernel.getPlugins().getFunction(pluginName, fnName);
if (kernelFunction == null) {
return Mono.empty();
}
ContextVariableType<String> variableType = ContextVariableTypes.getDefaultVariableTypeForClass(
String.class);
Map<String, ContextVariable<?>> variables = new HashMap<>();
parameters.fields().forEachRemaining(entry -> {
String paramName = entry.getKey();
String paramValue = entry.getValue().asText();
ContextVariable<String> contextVariable = new ContextVariable<>(variableType,
paramValue);
variables.put(paramName, contextVariable);
});
KernelArguments arguments = KernelArguments.builder().withVariables(variables).build();
return kernelFunction.invokeAsync(kernel, arguments, variableType);
}
return Mono.empty();
}
public static KernelFunction create(
String promptTemplate) {
return create(promptTemplate, null, null, null, null, null);