diff --git a/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java b/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java index 32420079e1..fc871891c4 100644 --- a/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java +++ b/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/OpenAIChatCompletion.java @@ -54,6 +54,7 @@ import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; +import org.apache.commons.text.StringEscapeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; @@ -146,8 +147,7 @@ public class OpenAIChatCompletion extends OpenAiService implements ChatCompletio this, messages, functions, - invocationContext, - autoInvokeAttempts))) + invocationContext))) .getOptions(); Mono> result = getClient() @@ -449,8 +449,12 @@ public class OpenAIChatCompletion extends OpenAiService implements ChatCompletio ChatCompletionService chatCompletionService, List chatRequestMessages, @Nullable List functions, - @Nullable InvocationContext invocationContext, - int autoInvokeAttempts) { + @Nullable InvocationContext invocationContext) { + + chatRequestMessages = chatRequestMessages + .stream() + .map(XMLPromptParser::unescapeRequest) + .collect(Collectors.toList()); ChatCompletionsOptions options = new ChatCompletionsOptions(chatRequestMessages) .setModel(chatCompletionService.getModelId()); @@ -633,7 +637,7 @@ public class OpenAIChatCompletion extends OpenAiService implements ChatCompletio return new ArrayList<>(); } return messages.stream() - .map(message -> getChatRequestMessage(message)) + .map(OpenAIChatCompletion::getChatRequestMessage) .collect(Collectors.toList()); } @@ -642,31 +646,10 @@ public class OpenAIChatCompletion extends OpenAiService implements ChatCompletio AuthorRole authorRole = message.getAuthorRole(); String content = message.getContent(); + switch (authorRole) { case ASSISTANT: - // TODO: break this out into a separate method and handle tools other than function calls - ChatRequestAssistantMessage asstMessage = new ChatRequestAssistantMessage(content); - List toolCalls = ((OpenAIChatMessageContent) message) - .getToolCall(); - if (toolCalls != null) { - asstMessage.setToolCalls( - toolCalls.stream() - .map(toolCall -> { - KernelFunctionArguments arguments = toolCall.getArguments(); - String args = arguments != null && !arguments.isEmpty() - ? arguments.entrySet().stream() - .map(entry -> String.format("\"%s\": \"%s\"", - entry.getKey(), entry.getValue())) - .collect(Collectors.joining("{", "}", ",")) - : "{}"; - FunctionCall fnCall = new FunctionCall(toolCall.getFunctionName(), - args); - return new ChatCompletionsFunctionToolCall(toolCall.getId(), - fnCall); - }) - .collect(Collectors.toList())); - } - return asstMessage; + return formAssistantMessage(message, content); case SYSTEM: return new ChatRequestSystemMessage(content); case USER: @@ -684,12 +667,46 @@ public class OpenAIChatCompletion extends OpenAiService implements ChatCompletio } return new ChatRequestToolMessage(content, id); default: - LOGGER.debug("Unexpected author role: " + authorRole); + LOGGER.debug("Unexpected author role: {}", authorRole); throw new SKException("Unexpected author role: " + authorRole); } } + private static ChatRequestAssistantMessage formAssistantMessage( + ChatMessageContent message, String content) { + // TODO: handle tools other than function calls + ChatRequestAssistantMessage asstMessage = new ChatRequestAssistantMessage(content); + + List toolCalls = null; + if (message instanceof OpenAIChatMessageContent) { + toolCalls = ((OpenAIChatMessageContent) message).getToolCall(); + } + + if (toolCalls != null) { + asstMessage.setToolCalls( + toolCalls.stream() + .map(toolCall -> { + KernelFunctionArguments arguments = toolCall.getArguments(); + + String args = arguments != null && !arguments.isEmpty() + ? arguments.entrySet().stream() + .map(entry -> String.format("\"%s\": \"%s\"", + StringEscapeUtils.escapeJson(entry.getKey()), + StringEscapeUtils.escapeJson( + entry.getValue().toPromptString()))) + .collect(Collectors.joining("{", "}", ",")) + : "{}"; + FunctionCall fnCall = new FunctionCall(toolCall.getFunctionName(), + args); + return new ChatCompletionsFunctionToolCall(toolCall.getId(), + fnCall); + }) + .collect(Collectors.toList())); + } + return asstMessage; + } + static ChatRequestMessage getChatRequestMessage( AuthorRole authorRole, String content) { diff --git a/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/XMLPromptParser.java b/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/XMLPromptParser.java index 42ff4b5c48..2aa182026b 100644 --- a/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/XMLPromptParser.java +++ b/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/chatcompletion/XMLPromptParser.java @@ -1,7 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.aiservices.openai.chatcompletion; +import com.azure.ai.openai.models.ChatRequestAssistantMessage; +import com.azure.ai.openai.models.ChatRequestFunctionMessage; import com.azure.ai.openai.models.ChatRequestMessage; +import com.azure.ai.openai.models.ChatRequestSystemMessage; +import com.azure.ai.openai.models.ChatRequestToolMessage; import com.azure.ai.openai.models.ChatRequestUserMessage; import com.azure.ai.openai.models.FunctionDefinition; import com.azure.core.util.BinaryData; @@ -29,6 +33,7 @@ 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.apache.commons.text.StringEscapeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -224,4 +229,45 @@ class XMLPromptParser { throw new SKException("Unknown author role: " + role); } } + + public static ChatRequestMessage unescapeRequest(ChatRequestMessage message) { + if (message instanceof ChatRequestUserMessage) { + ChatRequestUserMessage chatRequestMessage = (ChatRequestUserMessage) message; + String content = StringEscapeUtils.unescapeXml( + chatRequestMessage.getContent().toString()); + + return new ChatRequestUserMessage(content) + .setName(chatRequestMessage.getName()); + } else if (message instanceof ChatRequestSystemMessage) { + ChatRequestSystemMessage chatRequestMessage = (ChatRequestSystemMessage) message; + String content = StringEscapeUtils.unescapeXml(chatRequestMessage.getContent()); + + return new ChatRequestSystemMessage(content) + .setName(chatRequestMessage.getName()); + } else if (message instanceof ChatRequestAssistantMessage) { + ChatRequestAssistantMessage chatRequestMessage = (ChatRequestAssistantMessage) message; + String content = StringEscapeUtils.unescapeXml(chatRequestMessage.getContent()); + + return new ChatRequestAssistantMessage(content) + .setToolCalls(chatRequestMessage.getToolCalls()) + .setFunctionCall(chatRequestMessage.getFunctionCall()) + .setName(chatRequestMessage.getName()); + } else if (message instanceof ChatRequestFunctionMessage) { + ChatRequestFunctionMessage chatRequestMessage = (ChatRequestFunctionMessage) message; + String content = StringEscapeUtils.unescapeXml(chatRequestMessage.getContent()); + + return new ChatRequestFunctionMessage( + chatRequestMessage.getName(), + content); + } else if (message instanceof ChatRequestToolMessage) { + ChatRequestToolMessage chatRequestMessage = (ChatRequestToolMessage) message; + String content = StringEscapeUtils.unescapeXml(chatRequestMessage.getContent()); + + return new ChatRequestToolMessage( + content, + chatRequestMessage.getToolCallId()); + } + + throw new SKException("Unknown message type: " + message.getClass().getSimpleName()); + } } \ No newline at end of file diff --git a/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAITextGenerationService.java b/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAITextGenerationService.java index 43ce9ac258..b5bf73eb17 100644 --- a/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAITextGenerationService.java +++ b/java/aiservices/openai/src/main/java/com/microsoft/semantickernel/aiservices/openai/textcompletion/OpenAITextGenerationService.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.List; import java.util.stream.Collectors; import javax.annotation.Nullable; +import org.apache.commons.text.StringEscapeUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -106,6 +107,8 @@ public class OpenAITextGenerationService extends OpenAiService implements TextGe private CompletionsOptions getCompletionsOptions( String text, @Nullable PromptExecutionSettings requestSettings) { + text = StringEscapeUtils.unescapeXml(text); + if (requestSettings == null) { return new CompletionsOptions(Collections.singletonList(text)) .setMaxTokens(PromptExecutionSettings.DEFAULT_MAX_TOKENS); diff --git a/java/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/RenderingTest.java b/java/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/RenderingTest.java index 31539c6bb1..319940dac6 100644 --- a/java/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/RenderingTest.java +++ b/java/api-test/integration-tests/src/test/java/com/microsoft/semantickernel/tests/RenderingTest.java @@ -39,6 +39,28 @@ public class RenderingTest { wm.stop(); } + @Test + public void textSemanticKernelTemplateXml() { + buildTextKernel() + .invokeAsync( + KernelFunction + .createFromPrompt(""" + Value: {{$value}} + """) + .withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT) + .build()) + .withArguments(KernelFunctionArguments + .builder() + .withVariable("value", "\"hello world\"") + .build()) + .block(); + + // The actual body will be escaped as its json + Assertions.assertTrue( + wm.getAllServeEvents().get(0).getRequest().getBodyAsString() + .contains("\\\"hello world\\\"")); + } + @Test public void textSemanticKernelTemplate() { buildTextKernel() @@ -85,6 +107,28 @@ public class RenderingTest { wm.getAllServeEvents().get(0).getRequest().getBodyAsString().contains("{{ignore}}")); } + @Test + public void chatSemanticKernelTemplateXml() { + buildChatKernel() + .invokeAsync( + KernelFunction + .createFromPrompt(""" + Value: {{$value}} + """) + .withTemplateFormat(PromptTemplateConfig.SEMANTIC_KERNEL_TEMPLATE_FORMAT) + .build()) + .withArguments(KernelFunctionArguments + .builder() + .withVariable("value", "\"hello world\"") + .build()) + .block(); + + // The actual body will be escaped as its json + Assertions.assertTrue( + wm.getAllServeEvents().get(0).getRequest().getBodyAsString() + .contains("\\\"hello world\\\"")); + } + @Test public void chatSemanticKernelTemplate() { buildChatKernel() diff --git a/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example17_ChatGPT.java b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example17_ChatGPT.java index 286b3394c7..16b701ef6d 100644 --- a/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example17_ChatGPT.java +++ b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/Example17_ChatGPT.java @@ -37,10 +37,6 @@ public class Example17_ChatGPT { .buildAsyncClient(); } - client = new OpenAIClientBuilder() - .endpoint("http://localhost:8123") - .buildAsyncClient(); - ChatCompletionService chatGPT = OpenAIChatCompletion.builder() .withModelId(MODEL_ID) .withOpenAIAsyncClient(client) diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/CodeBlock.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/CodeBlock.java index 5db43712c8..50d7c469ae 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/CodeBlock.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/templateengine/tokenizer/blocks/CodeBlock.java @@ -16,6 +16,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; import javax.annotation.Nullable; +import org.apache.commons.text.StringEscapeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono; @@ -98,8 +99,6 @@ public final class CodeBlock extends Block implements CodeRendering { context = InvocationContext.builder().build(); } - // this.Log.LogTrace("Rendering code: `{0}`", this.Content); - switch (this.tokens.get(0).getType()) { case VALUE: case VARIABLE: @@ -115,9 +114,8 @@ public final class CodeBlock extends Block implements CodeRendering { arguments, context, context.getContextVariableTypes().getVariableTypeForClass(String.class)) - .map(it -> { - return it.getValue(); - }); + .map(ContextVariable::getValue) + .map(StringEscapeUtils::escapeXml11); case UNDEFINED: case TEXT: diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/templateengine/handlebars/HandlebarsPromptTemplate.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/templateengine/handlebars/HandlebarsPromptTemplate.java index 2c0448491a..2e7c260e64 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/templateengine/handlebars/HandlebarsPromptTemplate.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/templateengine/handlebars/HandlebarsPromptTemplate.java @@ -4,6 +4,7 @@ package com.microsoft.semantickernel.templateengine.handlebars; import static com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments.MAIN_KEY; import com.github.jknack.handlebars.Context; +import com.github.jknack.handlebars.EscapingStrategy; import com.github.jknack.handlebars.Handlebars; import com.github.jknack.handlebars.Helper; import com.github.jknack.handlebars.Options; @@ -34,6 +35,7 @@ import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import org.apache.commons.text.StringEscapeUtils; import reactor.core.publisher.Mono; /** @@ -180,7 +182,8 @@ public class HandlebarsPromptTemplate implements PromptTemplate { this.handlebars = new Handlebars(); this.handlebars .registerHelper("message", HandleBarsPromptTemplateHandler::handleMessage) - .registerHelper("each", handleEach(context)); + .registerHelper("each", handleEach(context)) + .with(EscapingStrategy.XML); addFunctionHelpers(kernel, this.handlebars, context);