Java: 03-SimpleRag impl to point of invoking Azure OpenAI API (#4484)

03-SimpleRag impl to point of invoking Azure OpenAI API. 
Code unwraps {{function}} handlebars template and creates a
List<FunctionDefinition> and adds it to the ChatCompletionOptions.

---------

Co-authored-by: John Oliver <[email protected]>
This commit is contained in:
David Grieve
2024-01-10 18:03:32 +00:00
committed by GitHub
co-authored by John Oliver
parent 283572722d
commit c6b85add4c
51 changed files with 2531 additions and 182 deletions
+15 -1
View File
@@ -12,13 +12,27 @@
<artifactId>aiservices-openai</artifactId>
<name>OpenAI Services</name>
<description>HuggingFace services for Semantic Kernel</description>
<description>OpenAI services for Semantic Kernel</description>
<dependencies>
<dependency>
<groupId>com.microsoft.semantic-kernel</groupId>
<artifactId>semantickernel-api</artifactId>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-ai-openai</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
@@ -4,16 +4,21 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import org.slf4j.Logger;
@@ -22,7 +27,10 @@ import org.slf4j.LoggerFactory;
import com.azure.ai.openai.OpenAIAsyncClient;
import com.azure.ai.openai.models.ChatChoice;
import com.azure.ai.openai.models.ChatCompletions;
import com.azure.ai.openai.models.ChatCompletionsFunctionToolCall;
import com.azure.ai.openai.models.ChatCompletionsFunctionToolDefinition;
import com.azure.ai.openai.models.ChatCompletionsOptions;
import com.azure.ai.openai.models.ChatCompletionsToolCall;
import com.azure.ai.openai.models.ChatRequestAssistantMessage;
import com.azure.ai.openai.models.ChatRequestMessage;
import com.azure.ai.openai.models.ChatRequestSystemMessage;
@@ -30,6 +38,12 @@ import com.azure.ai.openai.models.ChatRequestToolMessage;
import com.azure.ai.openai.models.ChatRequestUserMessage;
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.azure.core.credential.TokenCredential;
import com.azure.core.util.BinaryData;
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;
@@ -65,7 +79,8 @@ public class OpenAIChatCompletion implements ChatCompletionService {
@Nullable PromptExecutionSettings promptExecutionSettings, @Nullable Kernel kernel) {
List<ChatRequestMessage> chatRequestMessages = getChatRequestMessages(chatHistory);
return internalChatMessageContentsAsync(chatRequestMessages, promptExecutionSettings);
List<FunctionDefinition> functions = Collections.emptyList();
return internalChatMessageContentsAsync(chatRequestMessages, functions, promptExecutionSettings);
}
@@ -74,7 +89,8 @@ public class OpenAIChatCompletion implements ChatCompletionService {
PromptExecutionSettings promptExecutionSettings, Kernel kernel)
{
List<ChatRequestMessage> chatRequestMessages = getChatRequestMessages(chatHistory);
return internalStreamingChatMessageContentsAsync(chatRequestMessages, promptExecutionSettings);
List<FunctionDefinition> functions = Collections.emptyList();
return internalStreamingChatMessageContentsAsync(chatRequestMessages, functions, promptExecutionSettings);
}
@@ -82,44 +98,151 @@ public class OpenAIChatCompletion implements ChatCompletionService {
public Mono<List<ChatMessageContent>> getChatMessageContentsAsync(String prompt,
PromptExecutionSettings promptExecutionSettings, Kernel kernel) {
List<ChatRequestMessage> chatRequestMessages = getChatRequestMessages(prompt);
return internalChatMessageContentsAsync(chatRequestMessages, promptExecutionSettings);
List<FunctionDefinition> functions = getFunctionDefinitions(prompt);
return internalChatMessageContentsAsync(chatRequestMessages, functions, promptExecutionSettings);
}
@Override
public Flux<StreamingChatMessageContent> getStreamingChatMessageContentsAsync(String prompt,
PromptExecutionSettings promptExecutionSettings, Kernel kernel) {
List<ChatRequestMessage> chatRequestMessages = getChatRequestMessages(prompt);
return internalStreamingChatMessageContentsAsync(chatRequestMessages, promptExecutionSettings);
List<FunctionDefinition> functions = getFunctionDefinitions(prompt);
return internalStreamingChatMessageContentsAsync(chatRequestMessages, functions, promptExecutionSettings);
}
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, promptExecutionSettings);
ChatCompletionsOptions options = getCompletionsOptions(this, chatRequestMessages, functions, promptExecutionSettings);
return client
.getChatCompletionsStream(getModelId(), options)
.filter(chatCompletion -> chatCompletion != null)
.map(ChatCompletions::getChoices)
.filter(choices -> choices != null && !choices.isEmpty())
.collect(StringBuffer::new, (sb, choices) -> {
.reduceWith(ChatResponseCollector::new, (accumulator, choices) -> {
choices.stream()
.map(choice -> choice.getDelta())
.filter(delta -> delta != null && delta.getContent() != null)
.forEach(delta -> sb.append(delta.getContent()));
.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;
})
.map(sb -> {
return sb.toString();
})
.map(content -> new StreamingChatMessageContent(AuthorRole.ASSISTANT, content))
.map(ChatResponseCollector::toStreamingChatMessageContent)
.flux();
}
private Mono<List<ChatMessageContent>> internalChatMessageContentsAsync(
List<ChatRequestMessage> chatRequestMessages,
List<FunctionDefinition> functions,
PromptExecutionSettings promptExecutionSettings)
{
ChatCompletionsOptions options = getCompletionsOptions(this, chatRequestMessages, promptExecutionSettings);
ChatCompletionsOptions options = getCompletionsOptions(this, chatRequestMessages, functions, promptExecutionSettings);
return client
.getChatCompletions(getModelId(), options)
.flatMap(chatCompletions -> Mono.just(toChatMessageContents(chatCompletions)));
@@ -128,11 +251,21 @@ public class OpenAIChatCompletion implements ChatCompletionService {
private static ChatCompletionsOptions getCompletionsOptions(
ChatCompletionService chatCompletionService,
List<ChatRequestMessage> chatRequestMessages,
List<FunctionDefinition> functions,
PromptExecutionSettings promptExecutionSettings)
{
ChatCompletionsOptions options = new ChatCompletionsOptions(chatRequestMessages)
.setModel(chatCompletionService.getModelId());
if (functions != null && !functions.isEmpty()) {
// options.setFunctions(functions);
options.setTools(
functions.stream()
.map(ChatCompletionsFunctionToolDefinition::new)
.collect(Collectors.toList())
);
}
if (promptExecutionSettings == null) {
return options;
}
@@ -170,6 +303,9 @@ public class OpenAIChatCompletion implements ChatCompletionService {
private static List<ChatRequestMessage> getChatRequestMessages(String prompt)
{
// TODO: XML parsing should be done as a chain of XMLEvent handlers.
// If one handler does not recognize the element, it should pass it to the next handler.
// In this way, we can avoid parsing the whole prompt twice and easily extend the parsing logic.
List<ChatRequestMessage> messages = new ArrayList<>();
try (InputStream is = new ByteArrayInputStream(prompt.getBytes())) {
XMLInputFactory factory = XMLInputFactory.newFactory();
@@ -177,11 +313,11 @@ public class OpenAIChatCompletion implements ChatCompletionService {
while(reader.hasNext()) {
XMLEvent event = reader.nextEvent();
if (event.isStartElement()) {
String name = event.asStartElement().getName().getLocalPart();
String name = getElementName(event);
if (name.equals("message")) {
String role = event.asStartElement().getAttributeByName(javax.xml.namespace.QName.valueOf("role")).getValue();
String role = getAttributeValue(event, "role");
String content = reader.getElementText();
messages.add(getChatRequestMessage(AuthorRole.valueOf(role.toUpperCase()), content));
messages.add(getChatRequestMessage(role, content));
}
}
}
@@ -191,24 +327,156 @@ public class OpenAIChatCompletion implements ChatCompletionService {
return messages;
}
private static List<FunctionDefinition> getFunctionDefinitions(String prompt)
{
// TODO: XML parsing should be done as a chain of XMLEvent handlers. See previous remark.
// <function pluginName=\"%s\" name=\"%s\" description=\"%s\">
// <parameter name=\"%s\" description=\"%s\" defaultValue=\"%s\" isRequired=\"%s\" type=\"%s\"/>...
// </function>
List<FunctionDefinition> functionDefinitions = new ArrayList<>();
try (InputStream is = new ByteArrayInputStream(prompt.getBytes())) {
XMLInputFactory factory = XMLInputFactory.newFactory();
XMLEventReader reader = factory.createXMLEventReader(is);
FunctionDefinition functionDefinition = null;
Map<String, String> parameters = new HashMap<>();
List<String> requiredParmeters = new ArrayList<>();
while(reader.hasNext()) {
XMLEvent event = reader.nextEvent();
if (event.isStartElement()) {
String elementName = getElementName(event);
if (elementName.equals("function")) {
assert functionDefinition == null;
assert parameters.isEmpty();
assert requiredParmeters.isEmpty();
String pluginName = getAttributeValue(event, "pluginName");
String name = getAttributeValue(event, "name");
String description = getAttributeValue(event, "description");
// name has to match '^[a-zA-Z0-9_-]{1,64}$'
functionDefinition = new FunctionDefinition(pluginName + "-" +name)
.setDescription(description);
} else if (elementName.equals("parameter")) {
String name = getAttributeValue(event, "name");
String type = getAttributeValue(event, "type").toLowerCase(Locale.ROOT);
String description = getAttributeValue(event, "description");
parameters.put(name, String.format("{\"type\": \"%s\", \"description\": \"%s\"}", "string", description));
String isRequired = getAttributeValue(event, "isRequired");
if (Boolean.parseBoolean(isRequired)) {
requiredParmeters.add(name);
}
}
} else if (event.isEndElement()) {
String elementName = getElementName(event);
if (elementName.equals("function")) {
// Example JSON Schema:
// {
// "type": "function",
// "function": {
// "name": "get_current_weather",
// "description": "Get the current weather in a given location",
// "parameters": {
// "type": "object",
// "properties": {
// "location": {
// "type": "string",
// "description": "The city and state, e.g. San Francisco, CA",
// },
// "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
// },
// "required": ["location"],
// },
// },
//}
assert functionDefinition != null;
if (!parameters.isEmpty()) {
StringBuilder sb = new StringBuilder("{\"type\": \"object\", \"properties\": {");
parameters.forEach((name, value) -> {
// make "param": {"type": "string", "description": "desc"},
sb.append(String.format("\"%s\": %s,", name, value));
});
// strip off trailing comma and close the properties object
sb.replace(sb.length() - 1, sb.length(), "}");
if (!requiredParmeters.isEmpty()) {
sb.append(", \"required\": [");
requiredParmeters.forEach(name -> {
sb.append(String.format("\"%s\",", name));
});
// strip off trailing comma and close the required array
sb.replace(sb.length() - 1, sb.length(), "]");
}
// close the object
sb.append("}");
//System.out.println(sb.toString());
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(sb.toString());
BinaryData binaryData = BinaryData.fromObject(jsonNode);
functionDefinition.setParameters(binaryData);
}
functionDefinitions.add(functionDefinition);
functionDefinition = null;
parameters.clear();
requiredParmeters.clear();
}
}
}
} catch (IOException | XMLStreamException | IllegalArgumentException e) {
LOGGER.error("Error parsing prompt", e);
}
return functionDefinitions;
}
private static String getElementName(XMLEvent xmlEvent) {
if (xmlEvent.isStartElement()) {
return xmlEvent.asStartElement().getName().getLocalPart();
} else if (xmlEvent.isEndElement()) {
return xmlEvent.asEndElement().getName().getLocalPart();
}
// TODO: programmer's error - log at debug
return "";
}
private static String getAttributeValue(XMLEvent xmlEvent, String attributeName)
{
if (xmlEvent.isStartElement()) {
StartElement element = xmlEvent.asStartElement();
Attribute attribute = element.getAttributeByName(QName.valueOf(attributeName));
return attribute != null ? attribute.getValue() : "";
}
// TODO: programmer's error - log at debug
return "";
}
private static ChatRequestMessage getChatRequestMessage(
AuthorRole authorRole,
String role,
String content)
{
if (authorRole != null) {
switch(authorRole) {
case ASSISTANT:
return new ChatRequestAssistantMessage(content);
case SYSTEM:
return new ChatRequestSystemMessage(content);
case USER:
return new ChatRequestUserMessage(content);
case TOOL:
return new ChatRequestToolMessage(content, null);
}
try {
AuthorRole authorRole = AuthorRole.valueOf(role.toUpperCase());
return getChatRequestMessage(authorRole, content);
} catch (IllegalArgumentException e) {
LOGGER.debug("Unknown author role: " + role);
return null;
}
throw new IllegalArgumentException("Unknown author role: " + authorRole);
}
private static ChatRequestMessage getChatRequestMessage(
AuthorRole authorRole,
String content) {
switch(authorRole) {
case ASSISTANT:
return new ChatRequestAssistantMessage(content);
case SYSTEM:
return new ChatRequestSystemMessage(content);
case USER:
return new ChatRequestUserMessage(content);
case TOOL:
return new ChatRequestToolMessage(content, null);
default:
LOGGER.debug("Unexpected author role: " + authorRole);
return null;
}
}
private static List<ChatMessageContent> toChatMessageContents(ChatCompletions chatCompletions) {
@@ -234,19 +502,19 @@ public class OpenAIChatCompletion implements ChatCompletionService {
if(chatRole == ChatRole.ASSISTANT) {
return AuthorRole.ASSISTANT;
}
if(chatRole == ChatRole.ASSISTANT) {
if(chatRole == ChatRole.SYSTEM) {
return AuthorRole.SYSTEM;
}
if(chatRole == ChatRole.ASSISTANT) {
if(chatRole == ChatRole.USER) {
return AuthorRole.USER;
}
if(chatRole == ChatRole.ASSISTANT) {
}
if(chatRole == ChatRole.TOOL) {
return AuthorRole.TOOL;
}
throw new IllegalArgumentException("Unknown chat role: " + chatRole);
}
public static class Builder implements ChatCompletionService.Builder<OpenAIChatCompletion> {
public static class Builder implements ChatCompletionService.Builder {
private OpenAIAsyncClient client;
private String modelId;
@@ -19,6 +19,16 @@
<groupId>com.microsoft.semantic-kernel</groupId>
<artifactId>semantickernel-api</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
@@ -0,0 +1,104 @@
package com.microsoft.semantickernel.assistants;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Collection;
import com.microsoft.semantickernel.plugin.Plugin;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.aiservices.AIService;
import com.microsoft.semantickernel.templateengine.PromptTemplateEngine;
import com.microsoft.semantickernel.orchestration.SKFunction;
import com.microsoft.semantickernel.textcompletion.CompletionSKFunction;
import com.microsoft.semantickernel.semanticfunctions.SemanticFunctionConfig;
import com.microsoft.semantickernel.orchestration.ContextVariables;
import com.microsoft.semantickernel.orchestration.SKContext;
import com.microsoft.semantickernel.orchestration.FunctionResult;
import com.microsoft.semantickernel.KernelResult;
import com.microsoft.semantickernel.memory.SemanticTextMemory;
import com.microsoft.semantickernel.KernelConfig;
import reactor.core.publisher.Mono;
public class AssistantKernel extends Plugin implements Kernel {
public static AssistantKernel FromConfiguration(
String configurationFile,
@Nullable List<AIService> aiServices,
@Nullable List<Plugin> plugins,
@Nullable List<PromptTemplateEngine> promptTemplateEngines
) {
return null;
}
public AssistantKernel(String name, SKFunction... functions) {
super(name, functions);
}
public SKFunction getFunction(String skill, String function) {
return null;
}
public <FunctionType extends SKFunction>FunctionType registerSemanticFunction(FunctionType func) {
return null;
}
public CompletionSKFunction registerSemanticFunction(
String skillName, String functionName, SemanticFunctionConfig functionConfig) {
return null;
}
public <T extends AIService> T getService(String serviceId, Class<T> clazz) {
return null;
}
public CompletionSKFunction.Builder getSemanticFunctionBuilder() {
return null;
}
public Mono<KernelResult> runAsync(boolean b, ContextVariables variables, SKFunction... pipeline) {
return null;
}
public Mono<KernelResult> runAsync(ContextVariables variables, SKFunction... pipeline) {
return null;
}
public Mono<KernelResult> runAsync(String s, SKFunction... pipeline) {
return null;
}
public Mono<KernelResult> runAsync(SKFunction... pipeline) {
return null;
}
public SemanticTextMemory getMemory() {
return null;
}
public PromptTemplateEngine getPromptTemplateEngine() {
return null;
}
public KernelConfig getConfig() {
return null;
}
public List<SKFunction> functions() {
return null;
}
public String description() {
return null;
}
public String name() {
return null;
}
public Mono<AssistantThread> createThreadAsync() {
return null;
}
}
@@ -0,0 +1,12 @@
package com.microsoft.semantickernel.assistants;
import com.microsoft.semantickernel.orchestration.SKFunction;
import reactor.core.publisher.Mono;
public interface AssistantThread extends SKFunction {
public Mono addUserMessageAsync(String message);
}
@@ -0,0 +1,157 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.coreskills;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.SKBuilders;
import com.microsoft.semantickernel.orchestration.SKContext;
import com.microsoft.semantickernel.skilldefinition.annotations.DefineSKFunction;
import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionInputAttribute;
import com.microsoft.semantickernel.text.TextChunker;
import com.microsoft.semantickernel.textcompletion.CompletionKernelFunction;
import java.util.List;
import javax.annotation.Nullable;
import reactor.core.publisher.Mono;
/** Semantic skill that enables conversations summarization */
public class ConversationSummarySkill {
// The max tokens to process in a single semantic function call.
private static final int MaxTokens = 1024;
private final CompletionKernelFunction summarizeConversationFunction;
private final CompletionKernelFunction conversationActionItemsFunction;
private final CompletionKernelFunction conversationTopicsFunction;
private final Kernel kernel;
/**
* Initializes a new instance of the ConversationSummarySkill class
*
* @param kernel Kernel instance
*/
public ConversationSummarySkill(Kernel kernel) {
this.kernel = kernel;
this.summarizeConversationFunction =
SKBuilders.completionFunctions()
.withKernel(kernel)
.withPromptTemplate(
SemanticFunctionConstants.SummarizeConversationDefinition)
.withFunctionName("summarize")
.withSkillName("ConversationSummarySkill")
.withDescription(
"Given a section of a conversation transcript, summarize the part"
+ " of the conversation.")
.withCompletionConfig(
SKBuilders.completionConfig()
.maxTokens(MaxTokens)
.temperature(0.1)
.topP(0.5)
.build())
.build();
this.conversationActionItemsFunction =
SKBuilders.completionFunctions()
.withKernel(kernel)
.withPromptTemplate(
SemanticFunctionConstants.GetConversationActionItemsDefinition)
.withFunctionName("ActionItems")
.withSkillName("ConversationSummarySkill")
.withDescription(
"Given a section of a conversation transcript, identify action"
+ " items.")
.withCompletionConfig(
SKBuilders.completionConfig()
.maxTokens(MaxTokens)
.temperature(0.1)
.topP(0.5)
.build())
.build();
this.conversationTopicsFunction =
SKBuilders.completionFunctions()
.withKernel(kernel)
.withPromptTemplate(
SemanticFunctionConstants.GetConversationTopicsDefinition)
.withFunctionName("Topics")
.withSkillName("ConversationSummarySkill")
.withDescription(
"Analyze a conversation transcript and extract key topics worth"
+ " remembering.")
.withCompletionConfig(
SKBuilders.completionConfig()
.maxTokens(MaxTokens)
.temperature(0.1)
.topP(0.5)
.build())
.build();
}
/**
* Given a long conversation transcript, summarize the conversation
*
* @param input A long conversation transcript
* @param context The SKContext for function execution
* @return A context containing the summarized conversation
*/
@DefineSKFunction(
description = "Given a long conversation transcript, summarize the conversation.",
name = "SummarizeConversation")
public Mono<SKContext> summarizeConversationAsync(
@SKFunctionInputAttribute(description = "A long conversation transcript.") String input,
@Nullable SKContext context) {
List<String> lines = TextChunker.splitPlainTextLines(input, MaxTokens);
List<String> paragraphs = TextChunker.splitPlainTextParagraphs(lines, MaxTokens);
if (context == null) {
context = SKBuilders.context().withSkills(kernel.getSkills()).build();
}
SKContext completionContext = context.copy();
return this.summarizeConversationFunction.aggregatePartitionedResultsAsync(
paragraphs, completionContext);
}
/**
* Given a long conversation transcript, identify action items
*
* @param input A long conversation transcript
* @param context The SKContext for function execution
* @return A context containing the action items
*/
@DefineSKFunction(
description = "Given a long conversation transcript, identify action items.",
name = "GetConversationActionItems")
public Mono<SKContext> getConversationActionItemsAsync(
@SKFunctionInputAttribute(description = "A long conversation transcript.") String input,
SKContext context) {
List<String> lines = TextChunker.splitPlainTextLines(input, MaxTokens);
List<String> paragraphs = TextChunker.splitPlainTextParagraphs(lines, MaxTokens);
SKContext completionContext = context.copy();
return this.conversationActionItemsFunction.aggregatePartitionedResultsAsync(
paragraphs, completionContext);
}
/**
* Given a long conversation transcript, identify topics
*
* @param input A long conversation transcript
* @param context The SKContext for function execution
* @return A context containing the topics
*/
@DefineSKFunction(
description =
"Given a long conversation transcript, identify topics worth remembering.",
name = "GetConversationTopics")
public Mono<SKContext> getConversationTopicsAsync(
@SKFunctionInputAttribute(description = "A long conversation transcript.") String input,
SKContext context) {
List<String> lines = TextChunker.splitPlainTextLines(input, MaxTokens);
List<String> paragraphs = TextChunker.splitPlainTextParagraphs(lines, MaxTokens);
SKContext completionContext = context.copy();
return this.conversationTopicsFunction.aggregatePartitionedResultsAsync(
paragraphs, completionContext);
}
}
@@ -0,0 +1,85 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.coreskills;
import com.microsoft.semantickernel.skilldefinition.annotations.DefineSKFunction;
import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionInputAttribute;
import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionParameters;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Read and write from a file.
*
* <p>Usage: kernel.ImportSkill("file", new FileIOSkill());
*
* <p>Examples:
*
* <pre>{@code
* file.readAsync($path) => "hello world"
* file.writeAsync()
* }</pre>
*/
public class FileIOSkill {
/**
* Read a file.
*
* <p>Example:
*
* <pre>{@code
* file.readAsync($path) => "hello world"
* }</pre>
*
* @param path Source file.
* @return File content.
*/
@DefineSKFunction(description = "Read a file", name = "readAsync")
public String readFileAsync(
@SKFunctionInputAttribute(description = "Source file") String path,
@SKFunctionParameters(
name = "charset",
description = "Character set to use to read the file",
defaultValue = "UTF-8")
String charset)
throws IOException {
Path filePath = Paths.get(path);
byte[] fileBytes;
fileBytes = Files.readAllBytes(filePath);
String fileContents = new String(fileBytes, Charset.forName(charset));
return fileContents;
}
/**
* Write a file.
*
* <p>Example:
*
* <pre>{@code
* file.writeAsync()
* }</pre>
*
* @param path The destination file.
* @param content Contains the 'content' to write
*/
@DefineSKFunction(description = "Write a file", name = "writeAsync")
public void writeFileAsync(
@SKFunctionInputAttribute(description = "Destination file") String path,
@SKFunctionParameters(
name = "content",
description = "File content",
defaultValue = "",
type = String.class)
String content,
@SKFunctionParameters(
name = "charset",
description = "Character set to use to read the file",
defaultValue = "UTF-8")
String charset)
throws IOException {
Path filePath = Paths.get(path);
Files.write(filePath, content.getBytes(charset));
}
}
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.coreskills;
import com.azure.core.http.HttpClient;
import com.azure.core.http.HttpMethod;
import com.azure.core.http.HttpRequest;
import com.azure.core.http.HttpResponse;
import reactor.core.publisher.Mono;
/**
* A skill that provides HTTP functionality.
*
* <p>Usage:
*
* <p>Usage: kernel.importSkill(new HttpSkill(), "http");
*
* <p>Examples:
*
* <p>{{http.getAsync $url}}
*
* <p>{{http.postAsync $url}}
*
* <p>{{http.putAsync $url}}
*
* <p>{{http.deleteAsync $url}}
*/
public class HttpSkill {
private final HttpClient httpClient;
public HttpSkill() {
this.httpClient = HttpClient.createDefault();
}
public HttpSkill(HttpClient httpClient) {
this.httpClient = httpClient;
}
/**
* Sends an HTTP GET request to the specified URI and returns body as a string.
*
* @param url The URI to send the request to.
* @return The response body as a string.
*/
public Mono<String> getAsync(String url) {
if (url == null || url.isEmpty()) {
return Mono.error(new IllegalArgumentException("url cannot be `null` or empty"));
}
HttpRequest request = new HttpRequest(HttpMethod.GET, url);
return httpClient.send(request).flatMap(response -> response.getBodyAsString());
}
/**
* Sends an HTTP POST request to the specified URI and returns body as a string.
*
* @param url The URI to send the request to.
* @param body The response body as a string.
* @return
*/
public Mono<String> postAsync(String url, String body) {
if (url == null || url.isEmpty()) {
return Mono.error(new IllegalArgumentException("url cannot be `null` or empty"));
}
HttpRequest request = new HttpRequest(HttpMethod.POST, url);
request.setBody(body);
return httpClient.send(request).flatMap(response -> response.getBodyAsString());
}
/**
* Sends an HTTP PUT request to the specified URI and returns body as a string.
*
* @param url The URI to send the request to.
* @param body The response body as a string.
* @return
*/
public Mono<String> putAsync(String url, String body) {
if (url == null || url.isEmpty()) {
return Mono.error(new IllegalArgumentException("url cannot be `null` or empty"));
}
HttpRequest request = new HttpRequest(HttpMethod.PUT, url);
request.setBody(body);
return httpClient.send(request).flatMap(response -> response.getBodyAsString());
}
/**
* Sends an HTTP DELETE request to the specified URI and returns body as a string.
*
* @param url The URI to send the request to.
* @return The response body as a string.
*/
public Mono<HttpResponse> deleteAsync(String url) {
if (url == null || url.isEmpty()) {
return Mono.error(new IllegalArgumentException("url cannot be `null` or empty"));
}
HttpRequest request = new HttpRequest(HttpMethod.DELETE, url);
return httpClient.send(request).flatMap(response -> Mono.just(response));
}
}
@@ -0,0 +1,79 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.coreskills;
import com.microsoft.semantickernel.skilldefinition.annotations.DefineSKFunction;
import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionInputAttribute;
import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionParameters;
import java.math.BigDecimal;
/**
* MathSkill provides a set of functions to make Math calculations.
*
* <ul>
* <li><code>input</code> is the first number in the operation.
* <li><code>amount</code> is the second number in the operation.
* </ul>
*
* <p>Usage:
*
* <blockquote>
*
* <pre>
* kernel.ImportSkill("math", new MathSkill());
*
* {{math.add}} => Returns the sum of input and amount
* {{math.subtract}} => Returns the differentce of input from amount
* </pre>
*
* </blockquote>
*
* <p>This skill uses BigDecimal to perform the number conversion and the calculations.
*/
public class MathSkill {
/**
* Returns the addition result of input and amount values provided.
*
* <p>This skill attempts to convert the numbers to BigDecimal and then add them.
*
* <p>If a conversion error occurs, the skill will throw an exception.
*
* @param input Initial value as string to add the specified amount
* @param amount The amount to be added to input
* @return The resulting sum as a String.
*/
@DefineSKFunction(description = "Adds amount to a value.", name = "add")
public String add(
@SKFunctionInputAttribute(description = "The value to add to.") String input,
@SKFunctionParameters(name = "amount", description = "The amount to be added to value.")
String amount) {
BigDecimal bValue = new BigDecimal(input);
return bValue.add(new BigDecimal(amount)).toString();
}
/**
* Returns the subtraction result of input and amount values provided.
*
* <p>This skill attempts to convert the numbers to BigDecimal and then subtract them.
*
* <p>If a conversion error occurs, the skill will throw an exception.
*
* @param input Initial value as string to subtract the specified amount
* @param amount The amount to be subtracted from input
* @return The resulting difference as a String.
*/
@DefineSKFunction(description = "Subtracts amount from value.", name = "Subtract")
public String subtract(
@SKFunctionInputAttribute(description = "The value to subtract from.") String input,
@SKFunctionParameters(
name = "amount",
description = "The amount to be subtracted from value.")
String amount) {
BigDecimal bValue = new BigDecimal(input);
return bValue.subtract(new BigDecimal(amount)).toString();
}
}
@@ -0,0 +1,96 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.coreskills;
class SemanticFunctionConstants {
public static final String SummarizeConversationDefinition =
"BEGIN CONTENT TO SUMMARIZE:\n"
+ "{{$INPUT}}\n"
+ "\n"
+ "END CONTENT TO SUMMARIZE.\n"
+ "\n"
+ "Summarize the conversation in 'CONTENT TO SUMMARIZE', identifying main points of"
+ " discussion and any conclusions that were reached.\n"
+ "Do not incorporate other general knowledge.\n"
+ "Summary is in plain text, in complete sentences, with no markup or tags.\n"
+ "\n"
+ "BEGIN SUMMARY:";
public static final String GetConversationActionItemsDefinition =
"You are an action item extractor. You will be given chat history and need to make note"
+ " of action items mentioned in the chat.\n"
+ "Extract action items from the content if there are any. If there are no action,"
+ " return nothing. If a single field is missing, use an empty string.\n"
+ "Return the action items in json.\n"
+ "\n"
+ "Possible statuses for action items are: Open, Closed, In Progress.\n"
+ "\n"
+ "EXAMPLE INPUT WITH ACTION ITEMS:\n"
+ "\n"
+ "John Doe said: \"I will record a demo for the new feature by Friday\"\n"
+ "I said: \"Great, thanks John. We may not use all of it but it's good to get it"
+ " out there.\"\n"
+ "\n"
+ "EXAMPLE OUTPUT:\n"
+ "{\n"
+ " \"actionItems\": [\n"
+ " {\n"
+ " \"owner\": \"John Doe\",\n"
+ " \"actionItem\": \"Record a demo for the new feature\",\n"
+ " \"dueDate\": \"Friday\",\n"
+ " \"status\": \"Open\",\n"
+ " \"notes\": \"\"\n"
+ " }\n"
+ " ]\n"
+ "}\n"
+ "\n"
+ "EXAMPLE INPUT WITHOUT ACTION ITEMS:\n"
+ "\n"
+ "John Doe said: \"Hey I'm going to the store, do you need anything?\"\n"
+ "I said: \"No thanks, I'm good.\"\n"
+ "\n"
+ "EXAMPLE OUTPUT:\n"
+ "{\n"
+ " \"action_items\": []\n"
+ "}\n"
+ "\n"
+ "CONTENT STARTS HERE.\n"
+ "\n"
+ "{{$INPUT}}\n"
+ "\n"
+ "CONTENT STOPS HERE.\n"
+ "\n"
+ "OUTPUT:";
public static final String GetConversationTopicsDefinition =
"Analyze the following extract taken from a conversation transcript and extract key"
+ " topics.\n"
+ "- Topics only worth remembering.\n"
+ "- Be brief. Short phrases.\n"
+ "- Can use broken English.\n"
+ "- Conciseness is very important.\n"
+ "- Topics can include names of memories you want to recall.\n"
+ "- NO LONG SENTENCES. SHORT PHRASES.\n"
+ "- Return in JSON\n"
+ "[Input]\n"
+ "My name is Macbeth. I used to be King of Scotland, but I died. My wife's name is"
+ " Lady Macbeth and we were married for 15 years. We had no children. Our beloved"
+ " dog Toby McDuff was a famous hunter of rats in the forest.\n"
+ "My tragic story was immortalized by Shakespeare in a play.\n"
+ "[Output]\n"
+ "{\n"
+ " \"topics\": [\n"
+ " \"Macbeth\",\n"
+ " \"King of Scotland\",\n"
+ " \"Lady Macbeth\",\n"
+ " \"Dog\",\n"
+ " \"Toby McDuff\",\n"
+ " \"Shakespeare\",\n"
+ " \"Play\",\n"
+ " \"Tragedy\"\n"
+ " ]\n"
+ "}\n"
+ "+++++\n"
+ "[Input]\n"
+ "{{$INPUT}}\n"
+ "[Output]";
}
@@ -0,0 +1,248 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.coreskills;
import com.microsoft.semantickernel.SKBuilders;
import com.microsoft.semantickernel.memory.MemoryException;
import com.microsoft.semantickernel.memory.MemoryQueryResult;
import com.microsoft.semantickernel.memory.MemoryRecordMetadata;
import com.microsoft.semantickernel.memory.SemanticTextMemory;
import com.microsoft.semantickernel.orchestration.SKContext;
import com.microsoft.semantickernel.skilldefinition.annotations.DefineSKFunction;
import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionParameters;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import reactor.core.publisher.Mono;
/**
* TextMemorySkill provides a skill to save or recall information from the long or short term.
* Usage: {@code kernel.ImportSkill(new TextMemorySkill(), "memory");} Example: {@code
* {{memory.recall "what is the capital of France?" }} => "Paris"}
*/
public class TextMemorySkill {
/** Name of the context variable used to specify which memory collection to use. */
public static final String COLLECTION_PARAM = "collection";
/** Name of the context variable used to specify the information to store. */
public static final String INFO_PARAM = "info";
/** Name of the context variable used to specify the input to the skill. */
public static final String INPUT_PARAM = "input";
/** Name of the context variable used to specify memory search relevance score. */
public static final String RELEVANCE_PARAM = "relevance";
/**
* Name of the context variable used to specify a unique key associated with stored information.
*/
public static final String KEY_PARAM = "key";
/** Name of the context variable used to specify the number of memories to recall */
public static final String LIMIT_PARAM = "limit";
/** Default value for the collection name. */
public static final String DEFAULT_COLLECTION = "generic";
/** Default value for the relevance score. */
public static final String DEFAULT_RELEVANCE = "0.75";
/** Default value for the limit. */
public static final String DEFAULT_LIMIT = "1";
/**
* Key-based lookup for a specific memory.
*
* @param collection The collection containing the memory to retrieve
* @param key The key associated with the memory to retrieve
* @param context The context containing the semantic memory
* @return The memory associated with the key
*/
@DefineSKFunction(description = "Key-based lookup for a specific memory", name = "Retrieve")
public Mono<String> retrieveAsync(
@SKFunctionParameters(
name = COLLECTION_PARAM,
description =
"Memories collection associated with the memory to retrieve",
defaultValue = DEFAULT_COLLECTION)
String collection,
@SKFunctionParameters(
name = KEY_PARAM,
description = "The key associated with the memory to retrieve")
String key,
SKContext context) {
SemanticTextMemory memory = context.getSemanticMemory();
if (memory == null) {
return Mono.error(new RuntimeException("Memory not present"));
}
return memory.getAsync(collection, key, false)
.map(it -> it.getMetadata().getText())
.defaultIfEmpty("");
}
/**
* Save information to semantic memory.
*
* @param info The information to save
* @param collection Memories collection associated with the information to save
* @param key The key associated with the information to save
* @param context The context containing the semantic memory
* @return The key associated with the saved information
*/
@DefineSKFunction(description = "Save information to semantic memory", name = "Save")
public Mono<SKContext> saveAsync(
@SKFunctionParameters(
name = INFO_PARAM,
description = "The information to save",
defaultValue = "",
type = String.class)
String info,
@SKFunctionParameters(
name = COLLECTION_PARAM,
description =
"Memories collection associated with the information to save",
defaultValue = DEFAULT_COLLECTION,
type = String.class)
String collection,
@SKFunctionParameters(
name = KEY_PARAM,
description = "The key associated with the information to save",
defaultValue = "",
type = String.class)
String key,
SKContext context) {
SemanticTextMemory memory = context.getSemanticMemory();
if (memory == null) {
return Mono.error(
new MemoryException(MemoryException.ErrorCodes.UNKNOWN, "Memory not present"));
}
return memory.saveInformationAsync(collection, info, key, null, null)
.map(
it -> {
context.setVariable(TextMemorySkill.KEY_PARAM, it);
return SKBuilders.context()
.withVariables(context.getVariables())
.withSkills(context.getSkills())
.withMemory(context.getSemanticMemory())
.build();
});
}
/**
* Semantic search and return up to N memories related to the input text
*
* @param input The input text to find related memories for
* @param collection Memories collection to search
* @param relevance The relevance score, from 0.0 to 1.0, where 1.0 means perfect match
* @param limit The maximum number of relevant memories to recall
* @param context Contains the memory to search
* @return The list of memories related to the input text
*/
@DefineSKFunction(
description = "Semantic search and return up to N memories related to the input text",
name = "Recall")
public Mono<List<String>> recallAsync(
@SKFunctionParameters(
name = INPUT_PARAM,
description = "The information to recall",
defaultValue = "",
type = String.class)
@Nonnull
String input,
@SKFunctionParameters(
name = COLLECTION_PARAM,
description =
"Memories collection associated with the information to recall",
defaultValue = DEFAULT_COLLECTION,
type = String.class)
String collection,
@SKFunctionParameters(
name = RELEVANCE_PARAM,
description =
"The relevance score, from 0.0 to 1.0, where 1.0 means perfect"
+ " match",
defaultValue = DEFAULT_RELEVANCE,
type = Double.class)
double relevance,
@SKFunctionParameters(
name = LIMIT_PARAM,
description = "The maximum number of relevant memories to recall",
defaultValue = DEFAULT_LIMIT,
type = Integer.class)
int limit,
@Nonnull SKContext context) {
SemanticTextMemory memory = context.getSemanticMemory();
if (memory == null) {
return Mono.error(
new MemoryException(MemoryException.ErrorCodes.UNKNOWN, "Memory not present"));
}
// TODO: 1.0 fix cast
// Validate parameters
if (collection == null || collection.trim().isEmpty()) {
if (context.getVariables().get(TextMemorySkill.COLLECTION_PARAM) == null) {
collection = DEFAULT_COLLECTION;
} else {
collection =
context.getVariables().get(TextMemorySkill.COLLECTION_PARAM).toString();
}
}
final String _collection = collection;
final float _relevance = (float) Math.min(1.0, Math.max(0.0, relevance));
final int _limit = Math.max(1, limit);
// context.Log.LogTrace("Searching memories in collection '{0}', relevance '{1}'",
// collection, relevance);
// Search memory
return memory.searchAsync(_collection, input, _limit, _relevance, false)
.flatMap(
results -> {
List<String> memories =
results.stream()
.map(MemoryQueryResult::getMetadata)
.map(MemoryRecordMetadata::getText)
.collect(Collectors.toList());
return Mono.just(memories);
});
}
/**
* Remove information from semantic memory
*
* @param collection Memories collection associated with the information to remove
* @param key The key associated with the information to remove
* @param context Contains the memory to remove
* @return The reactive stream
*/
@DefineSKFunction(description = "Remove information from semantic memory", name = "Remove")
public Mono<Void> removeAsync(
@SKFunctionParameters(
name = COLLECTION_PARAM,
description =
"Memories collection associated with the information to remove",
defaultValue = DEFAULT_COLLECTION,
type = String.class)
String collection,
@SKFunctionParameters(
name = KEY_PARAM,
description = "The key associated with the information to remove",
defaultValue = "",
type = String.class)
String key,
SKContext context) {
SemanticTextMemory memory = context.getSemanticMemory();
if (memory == null) {
return Mono.error(
new MemoryException(MemoryException.ErrorCodes.UNKNOWN, "Memory not present"));
}
return memory.removeAsync(collection, key);
}
}
@@ -0,0 +1,39 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.coreskills;
import com.microsoft.semantickernel.skilldefinition.annotations.DefineSKFunction;
import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionInputAttribute;
import java.util.Locale;
/** TextSkill provides a set of functions to manipulate strings. */
public class TextSkill {
@DefineSKFunction(description = "Change all string chars to uppercase.", name = "Uppercase")
public String uppercase(
@SKFunctionInputAttribute(description = "Text to uppercase") String text) {
return text.toUpperCase(Locale.ROOT);
}
@DefineSKFunction(description = "Remove spaces to the left of a string.", name = "LStrip")
public String lStrip(@SKFunctionInputAttribute(description = "Text to edit") String text) {
return text.replaceAll("^ +", "");
}
@DefineSKFunction(description = "Remove spaces to the right of a string.", name = "RStrip")
public String rStrip(@SKFunctionInputAttribute(description = "Text to edit") String text) {
return text.replaceAll(" +$", "");
}
@DefineSKFunction(
description = "Remove spaces to the left and right of a string",
name = "Strip")
public String strip(@SKFunctionInputAttribute(description = "Text to edit") String input) {
return input.trim();
}
@DefineSKFunction(description = "Change all string chars to lowercase", name = "lowercase")
public String lowercase(
@SKFunctionInputAttribute(description = "Text to lowercase") String input) {
return input.toLowerCase(Locale.ROOT);
}
}
@@ -0,0 +1,428 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.coreskills;
import com.microsoft.semantickernel.skilldefinition.annotations.DefineSKFunction;
import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionInputAttribute;
import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionParameters;
import com.microsoft.semantickernel.util.LocaleParser;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.TextStyle;
import java.util.Locale;
/**
* Description: TimeSkill provides a set of functions to get the current time and date.
*
* <p>Usage: kernel.importSkill(new TimeSkill(), "time");
*
* <p>Examples:
*
* <p>{{time.date}} => Sunday, 12 January, 2031
*
* <p>{{time.today}} => Sunday, 12 January, 2031
*
* <p>{{time.now}} => Sunday, January 12, 2031 9:15 PM
*
* <p>{{time.utcNow}} => Sunday, January 13, 2031 5:15 AM
*
* <p>{{time.time}} => 09:15:07 PM
*
* <p>{{time.year}} => 2031
*
* <p>{{time.month}} => January
*
* <p>{{time.monthNumber}} => 01
*
* <p>{{time.day}} => 12
*
* <p>{{time.dayOfWeek}} => Sunday
*
* <p>{{time.hour}} => 9 PM
*
* <p>{{time.hourNumber}} => 21
*
* <p>{{time.days_ago $days}} => Sunday, 7 May, 2023
*
* <p>{{time.last_matching_day $dayName}} => Sunday, 7 May, 2023
*
* <p>{{time.minute}} => 15
*
* <p>{{time.second}} => 7
*
* <p>{{time.timeZoneOffset}} => -08:00
*
* <p>{{time.timeZoneName}} => Pacific Time
*/
public class TimeSkill {
public static final String DAY_MONTH_DAY_YEAR = "EEEE, MMMM d, yyyy";
/**
* Get the current date and time for the system default timezone.
*
* @return a ZonedDateTime object with the current date and time.
*/
public ZonedDateTime now() {
return ZonedDateTime.now(ZoneId.systemDefault());
}
/**
* Get the current date.
*
* <p>Example: {{time.date}} => Sunday, January 12, 2025
*
* @return The current date.
*/
@DefineSKFunction(name = "date", description = "Get the current date")
public String date(
@SKFunctionParameters(
name = "locale",
description = "Locale to use when formatting the date",
required = false)
String locale) {
// Example: Sunday, 12 January, 2025
return DateTimeFormatter.ofPattern(DAY_MONTH_DAY_YEAR)
.withLocale(parseLocale(locale))
.format(now());
}
/**
* Get the current time.
*
* <p>Example: {{time.time}} => 9:15:00 AM
*
* @return The current time.
*/
@DefineSKFunction(name = "time", description = "Get the current time")
public String time(
@SKFunctionParameters(
name = "locale",
description = "Locale to use when formatting the date",
required = false)
String locale) {
// Example: 09:15:07 PM
return DateTimeFormatter.ofPattern("hh:mm:ss a")
.withLocale(parseLocale(locale))
.format(now());
}
/**
* Get the current UTC date and time.
*
* <p>Example: {{time.utcNow}} => Sunday, January 13, 2025 5:15 AM
*
* @return The current UTC date and time.
*/
@DefineSKFunction(name = "utcNow", description = "Get the current UTC date and time")
public String utcNow(
@SKFunctionParameters(
name = "locale",
description = "Locale to use when formatting the date",
required = false)
String locale) {
return DateTimeFormatter.ofPattern(DAY_MONTH_DAY_YEAR + " h:mm a")
.withLocale(parseLocale(locale))
.format(now().withZoneSameInstant(ZoneOffset.UTC));
}
/**
* Get the current date (alias for date() method).
*
* <p>Example: {{time.today}} => Sunday, January 12, 2025
*
* @return The current date.
*/
@DefineSKFunction(name = "today", description = "Get the current date")
public String today(
@SKFunctionParameters(
name = "locale",
description = "Locale to use when formatting the date",
required = false)
String locale) {
return date(locale);
}
/**
* Get the current date and time in the local time zone.
*
* <p>Example: {{time.now}} => Sunday, January 12, 2025 9:15 AM
*
* @return The current date and time in the local time zone.
*/
@DefineSKFunction(
name = "now",
description = "Get the current date and time in the local time zone")
public String now(
@SKFunctionParameters(
name = "locale",
description = "Locale to use when formatting the date",
required = false)
String locale) {
return DateTimeFormatter.ofPattern(DAY_MONTH_DAY_YEAR + " h:mm a")
.withLocale(parseLocale(locale))
.format(now());
}
/**
* Get the current year.
*
* <p>Example: {{time.year}} => 2025
*
* @return The current year.
*/
@DefineSKFunction(name = "year", description = "Get the current year")
public String year(
@SKFunctionParameters(
name = "locale",
description = "Locale to use when formatting the date",
required = false)
String locale) {
return DateTimeFormatter.ofPattern("yyyy").withLocale(parseLocale(locale)).format(now());
}
/**
* Get the current month name.
*
* <p>Example: {{time.month}} => January
*
* @return The current month name.
*/
@DefineSKFunction(name = "month", description = "Get the current month name")
public String month(
@SKFunctionParameters(
name = "locale",
description = "Locale to use when formatting the date",
required = false)
String locale) {
return DateTimeFormatter.ofPattern("MMMM").withLocale(parseLocale(locale)).format(now());
}
/**
* Get the current month number.
*
* <p>Example: {{time.monthNumber}} => 01
*
* @return The current month number.
*/
@DefineSKFunction(name = "monthNumber", description = "Get the current month number")
public String monthNumber(
@SKFunctionParameters(
name = "locale",
description = "Locale to use when formatting the date",
required = false)
String locale) {
return DateTimeFormatter.ofPattern("MM").withLocale(parseLocale(locale)).format(now());
}
/**
* Get the current day of the month.
*
* <p>Example: {{time.day}} => 12
*
* @return The current day of the month.
*/
@DefineSKFunction(name = "day", description = "Get the current day of the month")
public String day(
@SKFunctionParameters(
name = "locale",
description = "Locale to use when formatting the date",
required = false)
String locale) {
return DateTimeFormatter.ofPattern("d").withLocale(parseLocale(locale)).format(now());
}
/**
* Get the current day of the week.
*
* <p>Example: {{time.dayOfWeek}} => Sunday
*
* @return The current day of the week.
*/
@DefineSKFunction(name = "dayOfWeek", description = "Get the current day of the week")
public String dayOfWeek(
@SKFunctionParameters(
name = "locale",
description = "Locale to use when formatting the date",
required = false)
String locale) {
return DateTimeFormatter.ofPattern("EEEE").withLocale(parseLocale(locale)).format(now());
}
/**
* Get the current clock hour.
*
* <p>Example: {{time.hour}} => 9 AM
*
* @return The current clock hour.
*/
@DefineSKFunction(name = "hour", description = "Get the current clock hour")
public String hour(
@SKFunctionParameters(
name = "locale",
description = "Locale to use when formatting the date",
required = false)
String locale) {
return DateTimeFormatter.ofPattern("h a").withLocale(parseLocale(locale)).format(now());
}
/**
* Get the current clock 24-hour number.
*
* <p>Example: {{time.hourNumber}} => 09
*
* @return The current clock 24-hour number.
*/
@DefineSKFunction(name = "hourNumber", description = "Get the current clock 24-hour number")
public String hourNumber(
@SKFunctionParameters(
name = "locale",
description = "Locale to use when formatting the date",
required = false)
String locale) {
return DateTimeFormatter.ofPattern("HH").withLocale(parseLocale(locale)).format(now());
}
/**
* Get the date of offset from today by a provided number of days
*
* <p>Example: SKContext context = SKBuilders.context().build(); context.setVariable("input",
* "3"); {{time.daysAgo $input}} => Saturday, January 11, 2031
*
* @param days Number of days to subtract from the current day
* @return The date of offset from today by a provided number of days
*/
@DefineSKFunction(
name = "daysAgo",
description = "Get the date of offset from today by a provided number of days")
public String daysAgo(
@SKFunctionInputAttribute(description = "Number of days to offset from today.")
String days,
@SKFunctionParameters(
name = "locale",
description = "Locale to use when formatting the date",
required = false)
String locale) {
int offsetDays = Integer.parseInt(days);
return DateTimeFormatter.ofPattern(DAY_MONTH_DAY_YEAR)
.withLocale(parseLocale(locale))
.format(now().minusDays(offsetDays));
}
/**
* Get the date of the last day matching the supplied week day name
*
* <p>Example: {{time.dateMatchingLastDayName "Monday"}} => Monday, January 6, 2031
*
* @param dayName Name of the day to match
* @return The date of the last day matching the supplied week day name
*/
@DefineSKFunction(
name = "dateMatchingLastDayName",
description = "Get the date of the last day matching the supplied week day name")
public String dateMatchingLastDayName(
@SKFunctionInputAttribute(description = "Week name day.") String dayName,
@SKFunctionParameters(
name = "locale",
description = "Locale to use when formatting the date",
required = false)
String locale) {
ZonedDateTime currentDate = now();
for (int i = 1; i <= 7; i++) {
currentDate = currentDate.minusDays(1);
String currentDayName =
currentDate.getDayOfWeek().getDisplayName(TextStyle.FULL, parseLocale(locale));
if (currentDayName.equalsIgnoreCase(dayName)) {
return DateTimeFormatter.ofPattern(DAY_MONTH_DAY_YEAR)
.withLocale(parseLocale(locale))
.format(currentDate);
}
}
throw new IllegalArgumentException("dayName is not recognized");
}
/**
* Get the minutes on the current hour.
*
* <p>Example: {{time.minute}} => 15
*
* @return The minutes on the current hour.
*/
@DefineSKFunction(name = "minute", description = "Get the minutes on the current hour")
public String minute(
@SKFunctionParameters(
name = "locale",
description = "Locale to use when formatting the date",
required = false)
String locale) {
return DateTimeFormatter.ofPattern("mm").withLocale(parseLocale(locale)).format(now());
}
/**
* Get the seconds on the current minute.
*
* <p>Example: {{time.second}} => 00
*
* @return The seconds on the current minute.
*/
@DefineSKFunction(name = "second", description = "Get the seconds on the current minute")
public String second(
@SKFunctionParameters(
name = "locale",
description = "Locale to use when formatting the date",
required = false)
String locale) {
return DateTimeFormatter.ofPattern("ss").withLocale(parseLocale(locale)).format(now());
}
/**
* Get the local time zone offset from UTC.
*
* <p>Example: {{time.timeZoneOffset}} => +03:00
*
* @return The local time zone offset from UTC.
*/
@DefineSKFunction(
name = "timeZoneOffset",
description = "Get the local time zone offset from UTC")
public String timeZoneOffset(
@SKFunctionParameters(
name = "locale",
description = "Locale to use when formatting the date",
required = false)
String locale) {
return DateTimeFormatter.ofPattern("XXX").withLocale(parseLocale(locale)).format(now());
}
/**
* Get the local time zone name.
*
* <p>Example: {{time.timeZoneName}} => Pacific Time
*
* @return The local time zone name.
*/
@DefineSKFunction(name = "timeZoneName", description = "Get the local time zone name")
public String timeZoneName(
@SKFunctionParameters(
name = "locale",
description = "Locale to use when formatting the date",
required = false)
String locale) {
ZoneId zoneId = ZoneId.systemDefault();
return zoneId.getDisplayName(TextStyle.FULL, parseLocale(locale));
}
/**
* Parse the locale string into a Locale object.
*
* <p>By default, it parses using the LocaleParser utility class.
*
* @param locale string
* @return a locale object
*/
protected Locale parseLocale(String locale) {
return LocaleParser.parseLocale(locale);
}
}
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.coreskills;
import com.microsoft.semantickernel.skilldefinition.annotations.DefineSKFunction;
import java.time.Duration;
import reactor.core.publisher.Mono;
/** WaitSkill provides a set of functions to wait before making the rest of operations. */
public class WaitSkill {
/**
* Wait for a certain number of seconds.
*
* <p>Examples:
*
* <p>SKContext context = SKBuilders.context().build();
*
* <p>context.setVariable("input","10");
*
* <p>{{wait.seconds $input}}
*
* @param seconds The number of seconds to wait as a string.
* @return A Mono that completes after the specified delay.
* @throws IllegalArgumentException If the input is not a valid number.
*/
@DefineSKFunction(name = "seconds", description = "Wait a given amount of seconds")
public Mono<Void> wait(String seconds) {
try {
double sec = Math.max(Double.parseDouble(seconds), 0);
long milliseconds = (long) (sec * 1000);
return Mono.delay(Duration.ofMillis(milliseconds)).then();
} catch (NumberFormatException e) {
return Mono.error(new IllegalArgumentException("seconds text must be a number", e));
}
}
}
@@ -0,0 +1,18 @@
package com.microsoft.semantickernel.plugins.web.bing;
import java.util.List;
import com.azure.core.http.HttpClient;
import reactor.core.publisher.Mono;
public class BingConnector {
public BingConnector(String apiKey, HttpClient httpClient) {
}
public Mono<List<String>> searchAsync(String query, int count, int offset) {
return Mono.error(() -> new UnsupportedOperationException("Not implemented yet"));
}
}
@@ -0,0 +1,296 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.text;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Split text in chunks, attempting to leave meaning intact. For plain text, split looking at new
* lines first, then periods, and so on. For markdown, split looking at punctuation first, and so
* on.
*/
public class TextChunker {
private static final String s_spaceChar = " ";
private static final List<Pattern> s_plaintextSplitOptions =
Stream.of("[\n\r]", "\\.", "[\\?\\!]", ";", ":", ",", "[\\)\\]\\}]", " ", "\\-", null)
.map(it -> it == null ? null : Pattern.compile(it, Pattern.MULTILINE))
.collect(Collectors.toList());
private static final List<Pattern> s_markdownSplitOptions =
Stream.of("\\.", "[\\?\\!]", ";", ":", ",", "[\\)\\]\\}]", " ", "\\-", "[\n\r]", null)
.map(it -> it == null ? null : Pattern.compile(it, Pattern.MULTILINE))
.collect(Collectors.toList());
/**
* Split plain text into lines
*
* @param text Text to split
* @param maxTokensPerLine Maximum number of tokens per line
* @return List of lines
*/
public static List<String> splitPlainTextLines(String text, int maxTokensPerLine) {
return internalSplitLines(text, maxTokensPerLine, true, s_plaintextSplitOptions);
}
/**
* Split markdown text into lines
*
* @param text Text to split
* @param maxTokensPerLine Maximum number of tokens per line
* @return List of lines
*/
public static List<String> splitMarkDownLines(String text, int maxTokensPerLine) {
List<String> result = new ArrayList<>();
internalSplitLines(text, maxTokensPerLine, true, s_markdownSplitOptions);
return result;
}
/**
* Split plain text into paragraphs
*
* @param lines Lines of text
* @param maxTokensPerParagraph Maximum number of tokens per paragraph.
* @return List of paragraphs
*/
public static List<String> splitPlainTextParagraphs(
List<String> lines, int maxTokensPerParagraph) {
return internalSplitTextParagraphs(
lines,
maxTokensPerParagraph,
(text) ->
internalSplitLines(
text, maxTokensPerParagraph, false, s_plaintextSplitOptions));
}
/**
* Split markdown text into paragraphs
*
* @param lines Lines of text
* @param maxTokensPerParagraph Maximum number of tokens per paragraph
* @return List of paragraphs
*/
public static List<String> splitMarkdownParagraphs(
List<String> lines, int maxTokensPerParagraph) {
return internalSplitTextParagraphs(
lines,
maxTokensPerParagraph,
(text) ->
internalSplitLines(
text, maxTokensPerParagraph, false, s_markdownSplitOptions));
}
private static List<String> internalSplitTextParagraphs(
List<String> lines,
int maxTokensPerParagraph,
Function<String, List<String>> longLinesSplitter) {
if (lines.size() == 0) {
return new ArrayList<>();
}
// Split long lines first
ArrayList<String> truncatedLines = new ArrayList<>();
for (String line : lines) {
truncatedLines.addAll(longLinesSplitter.apply(line));
}
lines = truncatedLines;
// Group lines in paragraphs
List<String> paragraphs = new ArrayList<>();
StringBuilder currentParagraph = new StringBuilder();
for (String line : lines) {
// "+1" to account for the "new line" added by AppendLine()
if (currentParagraph.length() > 0
&& TokenCount(currentParagraph.length()) + TokenCount(line.length()) + 1
>= maxTokensPerParagraph) {
paragraphs.add(currentParagraph.toString().trim());
currentParagraph = new StringBuilder();
}
currentParagraph.append(line).append("\n");
}
if (currentParagraph.length() > 0) {
paragraphs.add(currentParagraph.toString().trim());
}
// distribute text more evenly in the last paragraphs when the last paragraph is too short.
if (paragraphs.size() > 1) {
String lastParagraph = paragraphs.get(paragraphs.size() - 1);
String secondLastParagraph = paragraphs.get(paragraphs.size() - 2);
if (TokenCount(lastParagraph.length()) < maxTokensPerParagraph / 4) {
List<String> lastParagraphTokens =
Arrays.stream(lastParagraph.split(s_spaceChar))
.filter(it -> it.length() != 0)
.collect(Collectors.toList());
List<String> secondLastParagraphTokens =
Arrays.stream(secondLastParagraph.split(s_spaceChar))
.filter(it -> it.length() != 0)
.collect(Collectors.toList());
int lastParagraphTokensCount = lastParagraphTokens.size();
int secondLastParagraphTokensCount = secondLastParagraphTokens.size();
if (lastParagraphTokensCount + secondLastParagraphTokensCount
<= maxTokensPerParagraph) {
StringBuilder newSecondLastParagraph = new StringBuilder();
for (int i = 0; i < secondLastParagraphTokensCount; i++) {
if (newSecondLastParagraph.length() != 0) {
newSecondLastParagraph.append(' ');
}
newSecondLastParagraph.append(secondLastParagraphTokens.get(i));
}
for (int i = 0; i < lastParagraphTokensCount; i++) {
if (newSecondLastParagraph.length() != 0) {
newSecondLastParagraph.append(' ');
}
newSecondLastParagraph.append(lastParagraphTokens.get(i));
}
paragraphs.set(paragraphs.size() - 2, newSecondLastParagraph.toString().trim());
paragraphs.remove(paragraphs.size() - 1);
}
}
}
return paragraphs;
}
private static class SplitString {
public final boolean inputWasSplit;
public final List<String> result;
private SplitString(boolean inputWasSplit, List<String> result) {
this.inputWasSplit = inputWasSplit;
this.result = result;
}
}
private static List<String> internalSplitLines(
String text, int maxTokensPerLine, boolean trim, List<Pattern> splitOptions) {
text = text.replaceAll("\\r?\\n|\\r", "\n");
SplitString result =
split(text, maxTokensPerLine, Collections.singletonList(splitOptions.get(0)), trim);
if (result.inputWasSplit) {
for (int i = 1; i < splitOptions.size(); i++) {
result =
split(
result.result,
maxTokensPerLine,
Collections.singletonList(splitOptions.get(i)),
trim);
if (!result.inputWasSplit) {
break;
}
}
}
return result.result;
}
private static SplitString split(
List<String> input, int maxTokens, List<Pattern> separators, boolean trim) {
List<String> result = new ArrayList<>();
boolean modified = false;
for (String str : input) {
SplitString r = split(str, maxTokens, separators, trim);
result.addAll(r.result);
modified |= r.inputWasSplit;
}
return new SplitString(modified, result);
}
private static int indexOfAny(List<Pattern> separators, String input) {
return separators.stream()
.map(
it -> {
Matcher matcher = it.matcher(input);
if (matcher.find()) {
return matcher.start();
} else {
return -1;
}
})
.filter(it -> it != -1)
.min(Integer::compareTo)
.orElse(-1);
}
private static SplitString split(
String input, int maxTokens, List<Pattern> separators, boolean trim) {
// Debug.Assert(inputString is null || input.SequenceEqual(inputString.AsSpan()));
boolean inputWasSplit;
if (TokenCount(input.length()) > maxTokens) {
inputWasSplit = true;
int half = input.length() / 2;
int cutPoint = -1;
if (separators.size() == 1 && separators.get(0) == null) {
cutPoint = half;
} else if (input.length() > 2) {
int pos = 0;
while (true) {
int index = indexOfAny(separators, input.substring(pos, input.length() - 1));
if (index < 0) {
break;
}
index += pos;
if (Math.abs(half - index) < Math.abs(half - cutPoint)) {
cutPoint = index + 1;
}
pos = index + 1;
}
}
List<String> result = Collections.singletonList(input);
if (cutPoint > 0) {
String firstHalf = input.substring(0, cutPoint);
String secondHalf = input.substring(cutPoint);
if (trim) {
firstHalf = firstHalf.trim();
secondHalf = secondHalf.trim();
}
// Recursion
SplitString first = split(firstHalf, maxTokens, separators, trim);
SplitString second = split(secondHalf, maxTokens, separators, trim);
result =
Stream.concat(first.result.stream(), second.result.stream())
.collect(Collectors.toList());
inputWasSplit = first.inputWasSplit || second.inputWasSplit;
}
return new SplitString(inputWasSplit, result);
}
return new SplitString(false, Collections.singletonList(input));
}
private static int TokenCount(int inputLength) {
// TODO: partitioning methods should be configurable to allow for different tokenization
// strategies
// depending on the model to be called. For now, we use an extremely rough estimate.
return inputLength / 4;
}
}
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.util;
import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionParameters;
import java.util.Locale;
/**
* Locale parser to support Java 8 and Java 9+ due to JEP 252.
*
* <p>Supports parsing strings containing languageCode_countryCode and languageCode-countryCode
* styles.
*/
public final class LocaleParser {
private LocaleParser() {}
/**
* Parses the locale. If the input is "en-US", it will return a Locale based on the language
* tag. If the input is "en_US", it will return a Locale based on the language and country code.
* Otherwise, it will return a Locale based on the input.
*
* <p>'en' 'US' used as example above.
*
* @param locale the locale in String text.
* @return
*/
public static final Locale parseLocale(String locale) {
Locale parsedLocale = null;
if (locale == null
|| "".equals(locale.trim())
|| SKFunctionParameters.NO_DEFAULT_VALUE.equals(locale)) {
return Locale.getDefault();
} else if (locale.indexOf("-") > -1) {
parsedLocale = Locale.forLanguageTag(locale);
} else if (locale.indexOf("_") > -1) {
String[] parts = locale.split("_");
parsedLocale = new Locale(parts[0], parts[1]);
} else {
parsedLocale = new Locale(locale);
}
return parsedLocale;
}
}
@@ -1,18 +1,157 @@
package com.microsoft.semantickernel.plugins.web.bing;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import com.azure.core.http.HttpClient;
import com.azure.core.http.HttpHeader;
import com.azure.core.http.HttpHeaderName;
import com.azure.core.http.HttpHeaders;
import com.azure.core.http.HttpMethod;
import com.azure.core.http.HttpRequest;
import com.azure.core.http.HttpResponse;
import com.azure.core.implementation.jackson.ObjectMapperShim;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Mono;
public class BingConnector {
private static String BING_SEARCH_URL;
static {
String bingSearchUrl = System.getProperty("bing.search.url");
if (bingSearchUrl == null || bingSearchUrl.isEmpty()) bingSearchUrl = System.getenv("BING_SEARCH_URL");
if (bingSearchUrl == null || bingSearchUrl.isEmpty()) bingSearchUrl = "https://api.bing.microsoft.com/v7.0/search";
BING_SEARCH_URL = bingSearchUrl;
}
private static final String OCP_APIM_SUBSCRIPTION_KEY = "Ocp-Apim-Subscription-Key";
private static final HttpHeaderName OCP_APIM_SUBSCRIPTION_KEY_HEADER = HttpHeaderName.fromString(OCP_APIM_SUBSCRIPTION_KEY);
private static class BingSearchResponse {
private final WebPages webPages;
@JsonCreator
public BingSearchResponse(
@JsonProperty("webPages") WebPages webPages
) {
this.webPages = webPages;
}
public WebPages getWebPages() {
return webPages;
}
}
private static class WebPages {
private final WebPage[] value;
@JsonCreator
public WebPages(
@JsonProperty("value") WebPage[] value
) {
this.value = value;
}
public WebPage[] getValue() {
return value;
}
}
public static class WebPage {
@JsonProperty("name")
private String name;
@JsonProperty("url")
private String url;
@JsonProperty("snippet")
private String snippet;
public WebPage() {
}
public String getName() {
return name;
}
public String getUrl() {
return url;
}
public String getSnippet() {
return snippet;
}
public void setName(String name) {
this.name = name;
}
public void setUrl(String url) {
this.url = url;
}
public void setSnippet(String snippet) {
this.snippet = snippet;
}
}
private final String apiKey; // TODO: secure this
private final HttpClient httpClient;
public BingConnector(String apiKey, HttpClient httpClient) {
this.apiKey = apiKey;
this.httpClient = httpClient;
}
public Mono<List<String>> searchAsync(String query, int count, int offset) {
return Mono.error(() -> new UnsupportedOperationException("Not implemented yet"));
public Mono<List<String>> searchAsync(String query, int count, int offset) {
if (count <= 0 || 50 <= count) throw new IllegalArgumentException("count must be between 1 and 50");
if (offset < 0) throw new IllegalArgumentException("offset must be greater than or equal to 0");
HttpRequest request = new HttpRequest(HttpMethod.GET, searchUrl(query, count, offset));
request.setHeader(OCP_APIM_SUBSCRIPTION_KEY_HEADER, apiKey);
return httpClient.send(request).flatMap(BingConnector::handleResponse);
}
private static String searchUrl(String query, int count, int offset) {
try {
query = URLEncoder.encode(query, StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
// ignored - UTF-8 is always supported
}
String url = String.format("%s?q=%s&count=%s&offset=%s",
BING_SEARCH_URL, query, Integer.toString(count), Integer.toString(offset));
return url;
}
private static Mono<List<String>> handleResponse(HttpResponse response) {
return response.getBodyAsString()
.flatMap(body -> {
if (body == null || body.isEmpty()) return Mono.empty();
try {
ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES, false);
BingSearchResponse bingSearchResponse = objectMapper.readValue(body, BingSearchResponse.class);
List<String> urls = new ArrayList<String>();
for (WebPage webPage : bingSearchResponse.getWebPages().getValue()) {
urls.add(webPage.getUrl());
}
return Mono.just(urls);
} catch (JsonProcessingException e) {
Mono.error(e);
}
return Mono.empty();
});
}
}
@@ -20,17 +20,20 @@ output_variable:
type: string
description: The response from the assistant.
execution_settings:
- model_id: gpt-3.5-turbo
gpt-3.5-turbo:
model_id: gpt-3.5-turbo
temperature: 0.3
top_p: 0.0
presence_penalty: 0.0
frequency_penalty: 0.0
max_tokens: 256
stop_sequences: [ ]
- model_id: gpt-4
gpt-4:
model_id: gpt-4
temperature: 0.7
top_p: 0.0
presence_penalty: 0.0
frequency_penalty: 0.0
max_tokens: 256
stop_sequences: [ ]
@@ -16,7 +16,7 @@ input_variables:
- name: persona
type: string
description: The persona of the assistant.
default_value: You are a helpful assistant.
default: You are a helpful assistant.
is_required: false
- name: messages
type: ChatHistory
@@ -26,17 +26,20 @@ output_variable:
type: string
description: The response from the assistant.
execution_settings:
- model_id: gpt-3.5-turbo
gpt-3.5-turbo:
model_id: gpt-3.5-turbo
temperature: 0.3
top_p: 0.0
presence_penalty: 0.0
frequency_penalty: 0.0
max_tokens: 256
stop_sequences: [ ]
- model_id: gpt-4
gpt-4:
model_id: gpt-4
temperature: 0.7
top_p: 0.0
presence_penalty: 0.0
frequency_penalty: 0.0
max_tokens: 256
stop_sequences: [ ]
@@ -1,9 +1,8 @@
import com.azure.ai.openai.OpenAIAsyncClient;
import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.core.credential.KeyCredential;
import com.azure.core.credential.AzureKeyCredential;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.SKBuilders;
import com.microsoft.semantickernel.chatcompletion.AzureOpenAIChatCompletion;
import com.microsoft.semantickernel.chatcompletion.ChatCompletionService;
import com.microsoft.semantickernel.chatcompletion.ChatHistory;
import com.microsoft.semantickernel.orchestration.KernelFunction;
@@ -16,6 +15,7 @@ import com.microsoft.semantickernel.templateengine.handlebars.HandlebarsPromptTe
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import plugins.searchplugin.Search;
public class Main {
@@ -31,7 +31,7 @@ public class Main {
public static void main(String[] args) throws IOException {
OpenAIAsyncClient client = new OpenAIClientBuilder()
.credential(new KeyCredential(AZURE_OPENAI_API_KEY))
.credential(new AzureKeyCredential(AZURE_OPENAI_API_KEY))
.endpoint(AZURE_OPENAI_ENDPOINT)
.buildAsyncClient();
@@ -39,9 +39,9 @@ public class Main {
KernelFunction chatFunction = KernelFunctionYaml.fromYaml(
Path.of("Plugins/ChatPlugin/GroundedChat.prompt.yaml"));
ChatCompletionService gpt35Turbo = AzureOpenAIChatCompletion.builder()
ChatCompletionService gpt35Turbo = ChatCompletionService.builder()
.withOpenAIAsyncClient(client)
.withModelId(GPT_35_DEPLOYMENT_NAME != null ? GPT_35_DEPLOYMENT_NAME : "gpt-35-turbo")
.withModelId(GPT_35_DEPLOYMENT_NAME != null ? GPT_35_DEPLOYMENT_NAME : "gpt-3.5-turbo")
.build();
// Create the search plugin
@@ -5,11 +5,12 @@ import com.azure.core.http.HttpClient;
import com.microsoft.semantickernel.plugin.annotations.DefineKernelFunction;
import com.microsoft.semantickernel.plugin.annotations.KernelFunctionParameter;
import com.microsoft.semantickernel.plugins.web.bing.BingConnector;
import reactor.core.publisher.Mono;
public class Search {
private final BingConnector _bingConnector;
private final BingConnector bingConnector;
public Search(String apiKey) {
this(apiKey, HttpClient.createDefault());
@@ -17,14 +18,14 @@ public class Search {
public Search(String apiKey, HttpClient httpClient)
{
this._bingConnector = new BingConnector(apiKey, httpClient);
this.bingConnector = new BingConnector(apiKey, httpClient);
}
@DefineKernelFunction(description="Searches Bing for the given query")
@DefineKernelFunction(name="search", description="Searches Bing for the given query")
public Mono<String> searchAsync(
@KernelFunctionParameter(description="The search query", name="query", type=String.class) String query
){
return Mono.empty();
return bingConnector.searchAsync(query, 1, 0).map(results -> results.get(0));
}
}
@@ -21,7 +21,7 @@ input_variables:
- name: persona
type: string
description: The persona of the assistant.
default_value: You are a helpful assistant.
default: You are a helpful assistant.
is_required: false
- name: messages
type: ChatHistory
@@ -31,14 +31,16 @@ output_variable:
type: string
description: The response from the assistant.
execution_settings:
- model_id: gpt-3.5-turbo
gpt-3.5-turbo:
model_id: gpt-3.5-turbo
temperature: 0.3
top_p: 0.0
presence_penalty: 0.0
frequency_penalty: 0.0
max_tokens: 256
stop_sequences: [ ]
- model_id: gpt-4
gpt-4:
model_id: gpt-4
temperature: 0.7
top_p: 0.0
presence_penalty: 0.0
@@ -33,7 +33,7 @@ input_variables:
- name: persona
type: string
description: The persona of the assistant.
default_value: You are a helpful assistant.
default: You are a helpful assistant.
is_required: false
- name: messages
type: ChatHistory
@@ -43,7 +43,10 @@ output_variable:
type: string
description: The response from the assistant.
execution_settings:
- model_id_pattern: ^gpt-4
gpt-3.5-turbo:
model_id_pattern: gpt-3\.?5-turbo
temperature: 0.3
gpt-4:
model_id_pattern: ^gpt-4
temperature: 0.7
- model_id_pattern: ^gpt-3\.?5-turbo
temperature: 0.3
@@ -115,5 +115,7 @@ output_variable:
type: string
description: The action the assistant should take.
execution_settings:
- model_id_pattern: ^gpt-3\.?5-turbo
temperature: 0.3
gpt-3.5-turbo:
model_id_pattern: ^gpt-3\.?5-turbo
temperature: 0.3
@@ -41,5 +41,6 @@ output_variable:
type: string
description: The summary, inputs, and steps required to solve the problem.
execution_settings:
- model_id_pattern: ^gpt-3\.?5-turbo
temperature: 0.3
gpt-3.5-turbo:
model_id_pattern: ^gpt-3\.?5-turbo
temperature: 0.3
@@ -19,7 +19,7 @@ input_variables:
- name: persona
type: string
description: The persona of the assistant.
default_value: You are a helpful assistant.
default: You are a helpful assistant.
is_required: false
- name: messages
type: ChatHistory
@@ -29,7 +29,9 @@ output_variable:
type: string
description: The response from the assistant.
execution_settings:
- model_id_pattern: ^gpt-4
gpt-4:
model_id_pattern: ^gpt-4
temperature: 0.7
- model_id_pattern: ^gpt-3\.?5-turbo
temperature: 0.3
gpt-3.5-turbo:
model_id_pattern: ^gpt-3\.?5-turbo
temperature: 0.3
@@ -14,5 +14,7 @@ output_variable:
type: string
description: The most likely word for [MASK].
execution_settings:
- model_id_pattern: ^bert-base-uncased
- model_id_pattern: ^gpt-3\.?5-turbo
bert:
model_id_pattern: ^bert-base-uncased
gpt-3.5-turbo:
model_id_pattern: ^gpt-3\.?5-turbo
@@ -15,4 +15,5 @@ output_variable:
type: string
description: The text description of the image.
execution_settings:
- model_id_pattern: ^gpt-4-vision-preview
gpt-4:
model_id_pattern: ^gpt-4-vision-preview
@@ -14,5 +14,7 @@ output_variable:
type: string
description: The answer.
execution_settings:
- model_id_pattern: ^deepset/roberta-base-squad2
- model_id_pattern: ^gpt-3\.?5-turbo
deepset:
model_id_pattern: ^deepset/roberta-base-squad2
gpt-3.5-turbo:
model_id_pattern: ^gpt-3\.?5-turbo
@@ -14,5 +14,7 @@ output_variable:
type: string
description: The summary.
execution_settings:
- model_id_pattern: ^facebook/bart-large-cnn
- model_id_pattern: ^gpt-3\.?5-turbo
facebook:
model_id_pattern: ^facebook/bart-large-cnn
gpt-3.5-turbo:
model_id_pattern: ^gpt-3\.?5-turbo
@@ -10,4 +10,5 @@ output_variable:
type: Image
description: The generated image.
execution_settings:
- model_id_pattern: ^runwayml/stable-diffusion-v1-5
runwayml:
model_id_pattern: ^runwayml/stable-diffusion-v1-5
@@ -7,4 +7,5 @@ template_format: handlebars
description: A function that answers a math problem.
input_variables:
execution_settings:
- model_id_pattern: ^wizard-math
wizard-math:
model_id_pattern: ^wizard-math
@@ -35,5 +35,6 @@ output_variable:
type: string
description: The Bing search to perform on behalf of the user.
execution_settings:
- model_id_pattern: ^gpt-3\.?5-turbo
temperature: 0.3
gpt-3.5-turbo:
model_id_pattern: ^gpt-3\.?5-turbo
temperature: 0.3
@@ -15,4 +15,5 @@ input_variables:
- name: colorPallet
description: The color pallet of the image to generate
execution_settings:
- model_id_pattern: ^gpt-4
gpt-4:
model_id_pattern: ^gpt-4
@@ -4,4 +4,5 @@ template: |
template_format: handlebars
description: An assistant that generates images.
execution_settings:
- model_id_pattern: ^gpt-4
gpt-4:
model_id_pattern: ^gpt-4
@@ -5,5 +5,7 @@ template: |
template_format: handlebars
description: An assistant that helps manage projects.
execution_settings:
- model_id_pattern: ^gpt-4
- model_id_pattern: ^gpt-3\.?5-turbo
gpt-4:
model_id_pattern: ^gpt-4
gpt-3.5-turbo:
model_id_pattern: ^gpt-3\.?5-turbo
@@ -4,4 +4,5 @@ template: |
template_format: handlebars
description: An assistant that generates images.
execution_settings:
- model_id_pattern: ^gpt-4
gpt-4:
model_id_pattern: ^gpt-4
-1
View File
@@ -64,7 +64,6 @@
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-ai-openai</artifactId>
<version>1.0.0-beta.6</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
@@ -1,5 +1,12 @@
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 com.microsoft.semantickernel.chatcompletion.ChatCompletionService;
import com.microsoft.semantickernel.orchestration.KernelFunction;
import com.microsoft.semantickernel.orchestration.contextvariables.ContextVariable;
@@ -10,11 +17,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 reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -1,6 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel;
import java.util.List;
import javax.annotation.Nullable;
import com.microsoft.semantickernel.builders.Buildable;
import com.microsoft.semantickernel.builders.SemanticKernelBuilder;
import com.microsoft.semantickernel.orchestration.KernelFunction;
@@ -10,8 +14,7 @@ import com.microsoft.semantickernel.orchestration.contextvariables.KernelArgumen
import com.microsoft.semantickernel.plugin.KernelPlugin;
import com.microsoft.semantickernel.plugin.KernelPluginCollection;
import com.microsoft.semantickernel.semanticfunctions.PromptTemplate;
import java.util.List;
import javax.annotation.Nullable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -35,8 +35,8 @@ public enum BuildersSingleton {
"com.microsoft.semantickernel.connectors.ai.openai.textembeddings.OpenAITextEmbeddingGeneration$Builder";
private static final String FALLBACK_KERNEL_BUILDER_CLASS =
"com.microsoft.semantickernel.DefaultKernel$Builder";
private static final String FALLBACK_KERNEL_CONFIG_BUILDER_CLASS =
"com.microsoft.semantickernel.KernelConfig$Builder";
// private static final String FALLBACK_KERNEL_CONFIG_BUILDER_CLASS =
// "com.microsoft.semantickernel.KernelConfig$Builder";
private static final String FALLBACK_PROMPT_TEMPLATE_BUILDER_CLASS =
"com.microsoft.semantickernel.semanticfunctions.DefaultPromptTemplate$Builder";
private static final String FALLBACK_PROMPT_TEMPLATE_ENGINE_BUILDER_CLASS =
@@ -8,6 +8,7 @@ import com.azure.ai.openai.OpenAIClient;
import com.microsoft.semantickernel.AIService;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.builders.Buildable;
import com.microsoft.semantickernel.builders.BuildersSingleton;
import com.microsoft.semantickernel.builders.SemanticKernelBuilder;
import com.microsoft.semantickernel.orchestration.PromptExecutionSettings;
@@ -40,21 +41,25 @@ public interface ChatCompletionService extends Buildable, AIService {
Kernel kernel
);
interface Builder<T extends ChatCompletionService> extends SemanticKernelBuilder<T> {
static ChatCompletionService.Builder builder() {
return BuildersSingleton.INST.getInstance(ChatCompletionService.Builder.class);
}
interface Builder extends SemanticKernelBuilder<ChatCompletionService> {
/**
* Sets the model ID to use with the ChatCompletion service.
* @param modelId the model ID to use with the ChatCompletion service
* @return this builder
*/
Builder<T> withModelId(String modelId);
Builder withModelId(String modelId);
/**
* Sets the {@link OpenAIClient} to use for communication with the ChatCompletion service.
* @param openAIClient the {@link OpenAIClient} to use for communication with the ChatCompletion service
* @return this builder
*/
Builder<T> withOpenAIAsyncClient(OpenAIAsyncClient openAIClient);
Builder withOpenAIAsyncClient(OpenAIAsyncClient openAIClient);
}
@@ -1,6 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
package com.microsoft.semantickernel.orchestration;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.builders.Buildable;
import com.microsoft.semantickernel.orchestration.contextvariables.ContextVariable;
@@ -9,10 +14,8 @@ import com.microsoft.semantickernel.orchestration.contextvariables.KernelArgumen
import com.microsoft.semantickernel.semanticfunctions.InputVariable;
import com.microsoft.semantickernel.semanticfunctions.OutputVariable;
import com.microsoft.semantickernel.semanticfunctions.PromptTemplate;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import com.microsoft.semantickernel.semanticfunctions.PromptTemplateFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -1,33 +0,0 @@
package com.microsoft.semantickernel.plugin;
import com.microsoft.semantickernel.orchestration.KernelFunction;
import com.microsoft.semantickernel.orchestration.contextvariables.CaseInsensitiveMap;
import java.util.Iterator;
import java.util.Map;
public class DefaultKernelPlugin extends KernelPlugin {
/// <summary>The collection of functions associated with this plugin.</summary>
private final Map<String, KernelFunction> functions;
public DefaultKernelPlugin(String name, String description,
Map<String, KernelFunction> functions) {
super(name, description);
this.functions = new CaseInsensitiveMap<>(functions);
}
@Override
public Iterator<KernelFunction> iterator() {
return functions.values().iterator();
}
public Map<String, KernelFunction> getFunctions() {
return functions;
}
@Override
public KernelFunction get(String functionName) {
return functions.get(functionName);
}
}
@@ -8,7 +8,7 @@ public class KernelParameterMetadata {
private final String description;
private final String defaultValue;
private final boolean isRequired;
private Class parameterType;
private final Class<?> parameterType;
public KernelParameterMetadata(
String name,
@@ -19,6 +19,7 @@ public class KernelParameterMetadata {
this.description = description;
this.defaultValue = defaultValue;
this.isRequired = isRequired;
this.parameterType = String.class; // TODO
}
public String getName() {
@@ -32,5 +33,14 @@ public class KernelParameterMetadata {
public String getDefaultValue() {
return defaultValue;
}
public boolean isRequired() {
return isRequired;
}
public Class<?> getType() {
return parameterType;
}
//private KernelJsonSchema schema;
}
@@ -1,23 +1,41 @@
package com.microsoft.semantickernel.plugin;
import com.microsoft.semantickernel.orchestration.KernelFunction;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public abstract class KernelPlugin implements Iterable<KernelFunction> {
import com.microsoft.semantickernel.orchestration.KernelFunction;
import com.microsoft.semantickernel.orchestration.contextvariables.CaseInsensitiveMap;
public class KernelPlugin implements Iterable<KernelFunction> {
private final String name;
private final String description;
protected KernelPlugin(String name, String description) {
private final CaseInsensitiveMap<KernelFunction> functions;
public KernelPlugin(String name, String description,
Map<String, KernelFunction> plugins) {
this.name = name;
this.description = description;
this.functions = new CaseInsensitiveMap<>();
if (plugins != null) {
plugins.forEach((key,kerneFunction) -> this.functions.put(key, kerneFunction));
}
}
public abstract Iterator<KernelFunction> iterator();
public Map<String, KernelFunction> getFunctions() {
return functions;
}
public abstract Map<String, KernelFunction> getFunctions();
public KernelFunction get(String functionName) {
return functions.get(functionName);
}
@Override
public Iterator<KernelFunction> iterator() {
return functions.values().iterator();
}
public String getName() {
return name;
@@ -26,7 +44,4 @@ public abstract class KernelPlugin implements Iterable<KernelFunction> {
public String getDescription() {
return description;
}
public abstract KernelFunction get(String functionName);
}
@@ -1,17 +1,19 @@
package com.microsoft.semantickernel.plugin;
import com.microsoft.semantickernel.orchestration.KernelFunction;
import com.microsoft.semantickernel.orchestration.KernelFunctionMetadata;
import com.microsoft.semantickernel.orchestration.contextvariables.CaseInsensitiveMap;
import java.util.Collections;
import java.util.Iterator;
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.KernelFunction;
import com.microsoft.semantickernel.orchestration.KernelFunctionMetadata;
import com.microsoft.semantickernel.orchestration.contextvariables.CaseInsensitiveMap;
public class KernelPluginCollection implements Iterable<KernelPlugin> {
private static final Logger LOGGER = LoggerFactory.getLogger(KernelPluginCollection.class);
@@ -23,6 +23,7 @@ import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -88,17 +89,21 @@ public class KernelPluginFactory {
@Nullable List<KernelFunction> functions) {
if (functions == null) {
return new DefaultKernelPlugin(
return new KernelPlugin(
pluginName,
null,
new HashMap<>()
Collections.emptyMap()
);
}
return new DefaultKernelPlugin(
Map<String, KernelFunction> functionsMap = new HashMap<>();
functions.forEach((kernelFunction) ->
functionsMap.put(kernelFunction.getName(), kernelFunction));
return new KernelPlugin(
pluginName,
null,
functions.stream().collect(Collectors.toMap(KernelFunction::getName, f -> f))
functionsMap
);
}
@@ -181,7 +186,7 @@ public class KernelPluginFactory {
}
}
return new DefaultKernelPlugin(
return new KernelPlugin(
pluginDirectoryName,
null,
plugins
@@ -253,9 +258,9 @@ public class KernelPluginFactory {
plugins.put(functionName, function);
return new DefaultKernelPlugin(
return new KernelPlugin(
pluginDirectoryName,
null,
promptTemplateConfig.getDescription(),
plugins
);
}
@@ -1,8 +1,20 @@
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.chatcompletion.AuthorRole;
import com.microsoft.semantickernel.chatcompletion.ChatCompletionService;
import com.microsoft.semantickernel.orchestration.DefaultKernelFunction;
import com.microsoft.semantickernel.orchestration.KernelFunction;
@@ -11,14 +23,10 @@ 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;
@@ -70,18 +78,30 @@ public class KernelFunctionFromPrompt extends DefaultKernelFunction {
if (client instanceof ChatCompletionService) {
prompt = "<messages>" + prompt + "</messages>";
prompt = "<prompt>".concat(prompt).concat("</prompt>");
result = ((ChatCompletionService) client)
.getStreamingChatMessageContentsAsync(
prompt,
executionSettings,
kernel)
.concatMap(streamingChatMessageContent -> {
T value = variableType
.getConverter()
.fromPromptString(
streamingChatMessageContent.getContent());
return Flux.just(new StreamingContent<>(value));
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, streamingChatMessageContent.getContent());
return toolResult.flatMapMany(contextVariable -> {
T value = variableType
.getConverter()
.fromPromptString(
contextVariable.getValue());
return Flux.just(new StreamingContent<>(value));
});
}
return Flux.empty();
});
return result;
@@ -150,6 +170,52 @@ 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) {
@@ -5,6 +5,8 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.Optional;
@@ -18,8 +20,12 @@ import com.github.jknack.handlebars.ValueResolver;
import com.microsoft.semantickernel.Kernel;
import com.microsoft.semantickernel.chatcompletion.ChatHistory;
import com.microsoft.semantickernel.chatcompletion.ChatMessageContent;
import com.microsoft.semantickernel.orchestration.KernelFunction;
import com.microsoft.semantickernel.orchestration.contextvariables.ContextVariable;
import com.microsoft.semantickernel.orchestration.contextvariables.KernelArguments;
import com.microsoft.semantickernel.plugin.KernelParameterMetadata;
import com.microsoft.semantickernel.plugin.KernelPlugin;
import com.microsoft.semantickernel.plugin.KernelPluginCollection;
import com.microsoft.semantickernel.semanticfunctions.PromptTemplate;
import com.microsoft.semantickernel.semanticfunctions.PromptTemplateConfig;
@@ -44,7 +50,7 @@ public class HandlebarsPromptTemplate implements PromptTemplate {
public Mono<String> renderAsync(Kernel kernel,
@Nullable KernelArguments arguments) {
HandleBarsPromptTemplateHandler handler =
new HandleBarsPromptTemplateHandler(promptTemplate.getTemplate());
new HandleBarsPromptTemplateHandler(kernel, promptTemplate.getTemplate());
return handler.render(arguments);
}
@@ -83,6 +89,38 @@ public class HandlebarsPromptTemplate implements PromptTemplate {
}
}
private static class KernelPluginResolver implements ValueResolver {
@Override
public Object resolve(Object context, String name) {
if (context instanceof KernelPluginCollection) {
return context;
}
if (context instanceof KernelFunction) {
KernelFunction function = (KernelFunction) context;
if ("pluginname".equals(name.toLowerCase())) {
return function.getSkillName();
} else if ("name".equals(name.toLowerCase())) {
return function.getName();
}
}
return UNRESOLVED;
}
@Override
public Object resolve(Object context) {
if (context instanceof KernelPluginCollection) {
return ((KernelPluginCollection) context).iterator();
}
return UNRESOLVED;
}
@Override
public Set<Entry<String, Object>> propertySet(Object context) {
return new HashSet<>();
}
}
private static class ContextVariableResolver implements ValueResolver {
@Override
@@ -93,7 +131,7 @@ public class HandlebarsPromptTemplate implements PromptTemplate {
if (context instanceof ContextVariable) {
return ((ContextVariable<?>) context).getValue();
}
return null;
return UNRESOLVED;
}
@Override
@@ -121,10 +159,12 @@ public class HandlebarsPromptTemplate implements PromptTemplate {
public static class HandleBarsPromptTemplateHandler {
private final Kernel kernel;
private final String template;
private final Handlebars handlebars;
public HandleBarsPromptTemplateHandler(String template) {
public HandleBarsPromptTemplateHandler(Kernel kernel, String template) {
this.kernel = kernel;
this.template = template;
this.handlebars = new Handlebars();
this.handlebars.registerHelper(
@@ -134,12 +174,13 @@ public class HandlebarsPromptTemplate implements PromptTemplate {
String content = (String) options.fn(context);
if (context instanceof Optional) {
ChatMessageContent message = ((Optional<ChatMessageContent>) context).orElse(
null);
if (role == null || role.isEmpty()) {
role = message.getAuthorRole().name();
ChatMessageContent message = ((Optional<ChatMessageContent>) context).orElse(null);
if (message != null) {
if (role == null || role.isEmpty()) {
role = message.getAuthorRole().name();
}
content = message.getContent();
}
content = message.getContent();
}
if (role != null && !role.isEmpty()) {
@@ -156,10 +197,24 @@ public class HandlebarsPromptTemplate implements PromptTemplate {
"each",
(context, options) -> {
if (context instanceof ChatHistory) {
StringBuilder sb = new StringBuilder();
StringBuilder sb = new StringBuilder("<messages>");
for(ChatMessageContent message : (ChatHistory) context) {
sb.append(options.fn(message));
}
sb.append("</messages>");
return new Handlebars.SafeString(sb.toString());
}
if (context instanceof KernelPluginCollection) {
StringBuilder sb = new StringBuilder();
Iterator<KernelPlugin> plugins = ((KernelPluginCollection) context).iterator();
while (plugins.hasNext()) {
KernelPlugin plugin = plugins.next();
Iterator<KernelFunction> functions = plugin.iterator();
while (functions.hasNext()) {
KernelFunction function = functions.next();
sb.append(options.fn(function));
}
}
return new Handlebars.SafeString(sb.toString());
}
return "";
@@ -169,23 +224,34 @@ public class HandlebarsPromptTemplate implements PromptTemplate {
.registerHelper(
"functions",
(context, options) -> {
StringBuilder functions = new StringBuilder("<functions>");
// What to do here?
functions.append("</functions>");
StringBuilder sb = new StringBuilder("<functions>");
KernelPluginCollection plugins = kernel.getPlugins();
sb.append(options.fn(plugins));
sb.append("</functions>");
return functions.toString();
return new Handlebars.SafeString(sb.toString());
}
)
.registerHelper(
"function",
(context, options) -> {
String pluginName = options.hash("pluginName");
String functionName = options.hash("name");
// What to do here?
return String.format(
"<function pluginName=\"%s\" name=\"%s\" />",
pluginName, functionName);
KernelFunction function = (KernelFunction) context;
String pluginName = function.getSkillName();
String functionName = function.getName();
String description = function.getDescription();
StringBuilder sb = new StringBuilder(
String.format(
"<function pluginName=\"%s\" name=\"%s\" description=\"%s\">",
pluginName, functionName, description));
List<KernelParameterMetadata> parameters = function.getMetadata().getParameters();
parameters.forEach(p -> {
sb.append(String.format(
"<parameter name=\"%s\" description=\"%s\" defaultValue=\"%s\" isRequired=\"%s\" type=\"%s\"/>",
p.getName(), p.getDescription(), p.getDefaultValue(), p.isRequired(), p.getType()));
});
sb.append("</function>");
return new Handlebars.SafeString(sb.toString());
}
);
// TODO: 1.0 Add more helpers
@@ -196,6 +262,7 @@ public class HandlebarsPromptTemplate implements PromptTemplate {
try {
ArrayList<ValueResolver> resolvers = new ArrayList<>();
resolvers.add(new MessageResolver());
resolvers.add(new KernelPluginResolver());
resolvers.add(new ContextVariableResolver());
// resolvers.addAll(ValueResolver.defaultValueResolvers());