mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
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:
co-authored by
John Oliver
parent
3e0cfce676
commit
aca09de4f7
+343
-153
@@ -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);
|
||||
|
||||
+2
-2
@@ -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:
|
||||
|
||||
+3
-3
@@ -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:
|
||||
|
||||
+3
-3
@@ -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:
|
||||
|
||||
+3
-3
@@ -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:
|
||||
|
||||
+2
-2
@@ -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:
|
||||
|
||||
+2
-2
@@ -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:
|
||||
|
||||
+3
-3
@@ -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:
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
+2
-2
@@ -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:
|
||||
|
||||
+11
-7
@@ -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;
|
||||
|
||||
|
||||
+36
-2
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+17
-13
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-78
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user