From 659859f7b975b7f492abe5c2605218ae63a3be08 Mon Sep 17 00:00:00 2001 From: John Oliver <1615532+johnoliver@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:19:20 +0100 Subject: [PATCH] add example for custom type conversion. Fix bug in per-invocation type conversion. Add fluent calls to ChatHistory --- CHANGELOG.md | 12 ++ .../java/CustomTypes_Example.java | 180 ++++++++++++++++++ .../ContextVariableTypeConverter.java | 4 +- .../ContextVariableJacksonConverter.java | 79 ++++++++ .../DateTimeContextVariableTypeConverter.java | 4 +- .../orchestration/FunctionInvocation.java | 8 +- .../services/chatcompletion/ChatHistory.java | 24 ++- 7 files changed, 293 insertions(+), 18 deletions(-) create mode 100644 samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/CustomTypes_Example.java create mode 100644 semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/ContextVariableJacksonConverter.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ad56c82..c8e406be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# 1.2.1 + +- Fix bug in `FunctionInvocation` not using per-invocation type conversion when calling `withResultType`. +- Fix bug in Global Hooks not being invoked under certain circumstances. +- Add fluent returns to `ChatHistory` `addXMessage` methods. +- Add user agent opt-out for OpenAI requests by setting the property `semantic-kernel.useragent-disable` to `true`. +- Add several convenience `invokePromptAsync` methods to `Kernel`. + +#### Non-API Changes + +- Add custom type Conversion example, CustomTypes_Example + # 1.2.0 - Add ability to use image_url as content for a OpenAi chat completion diff --git a/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/CustomTypes_Example.java b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/CustomTypes_Example.java new file mode 100644 index 00000000..e809474d --- /dev/null +++ b/samples/semantickernel-concepts/semantickernel-syntax-examples/src/main/java/com/microsoft/semantickernel/samples/syntaxexamples/java/CustomTypes_Example.java @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.samples.syntaxexamples.java; + +import com.azure.ai.openai.OpenAIAsyncClient; +import com.azure.ai.openai.OpenAIClientBuilder; +import com.azure.core.credential.AzureKeyCredential; +import com.azure.core.credential.KeyCredential; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.microsoft.semantickernel.Kernel; +import com.microsoft.semantickernel.aiservices.openai.chatcompletion.OpenAIChatCompletion; +import com.microsoft.semantickernel.contextvariables.ContextVariableTypeConverter; +import com.microsoft.semantickernel.contextvariables.ContextVariableTypes; +import com.microsoft.semantickernel.contextvariables.converters.ContextVariableJacksonConverter; +import com.microsoft.semantickernel.exceptions.ConfigurationException; +import com.microsoft.semantickernel.semanticfunctions.KernelFunctionArguments; +import com.microsoft.semantickernel.services.chatcompletion.ChatCompletionService; +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class CustomTypes_Example { + + private static final String CLIENT_KEY = System.getenv("CLIENT_KEY"); + private static final String AZURE_CLIENT_KEY = System.getenv("AZURE_CLIENT_KEY"); + + // Only required if AZURE_CLIENT_KEY is set + private static final String CLIENT_ENDPOINT = System.getenv("CLIENT_ENDPOINT"); + private static final String MODEL_ID = System.getenv() + .getOrDefault("MODEL_ID", "gpt-35-turbo-2"); + + public static void main(String[] args) throws ConfigurationException, IOException { + + OpenAIAsyncClient client; + + if (AZURE_CLIENT_KEY != null) { + client = new OpenAIClientBuilder() + .credential(new AzureKeyCredential(AZURE_CLIENT_KEY)) + .endpoint(CLIENT_ENDPOINT) + .buildAsyncClient(); + } else { + client = new OpenAIClientBuilder() + .credential(new KeyCredential(CLIENT_KEY)) + .buildAsyncClient(); + } + + ChatCompletionService chatCompletionService = OpenAIChatCompletion.builder() + .withOpenAIAsyncClient(client) + .withModelId(MODEL_ID) + .build(); + + exampleBuildingCustomConverter(chatCompletionService); + exampleUsingJackson(chatCompletionService); + exampleUsingGlobalTypes(chatCompletionService); + } + + public record Pet(String name, int age, String species) { + + @JsonCreator + public Pet( + @JsonProperty("name") String name, + @JsonProperty("age") int age, + @JsonProperty("species") String species) { + this.name = name; + this.age = age; + this.species = species; + } + + @Override + public String toString() { + return name + " " + species + " " + age; + } + } + + private static void exampleBuildingCustomConverter( + ChatCompletionService chatCompletionService) { + Pet sandy = new Pet("Sandy", 3, "Dog"); + + Kernel kernel = Kernel.builder() + .withAIService(ChatCompletionService.class, chatCompletionService) + .build(); + + // Format: + // name: Sandy + // age: 3 + // species: Dog + + // Custom serializer + Function petToString = pet -> "name: " + pet.name() + "\n" + + "age: " + pet.age() + "\n" + + "species: " + pet.species() + "\n"; + + // Custom deserializer + Function stringToPet = prompt -> { + Map properties = Arrays.stream(prompt.split("\n")) + .collect(Collectors.toMap( + line -> line.split(":")[0].trim(), + line -> line.split(":")[1].trim())); + + return new Pet( + properties.get("name"), + Integer.parseInt(properties.get("age")), + properties.get("species")); + }; + + // create custom converter + ContextVariableTypeConverter typeConverter = ContextVariableTypeConverter.builder( + Pet.class) + .toPromptString(petToString) + .fromPromptString(stringToPet) + .build(); + + Pet updated = kernel.invokePromptAsync( + "Change Sandy's name to Daisy:\n{{$Sandy}}", + KernelFunctionArguments.builder() + .withVariable("Sandy", sandy, typeConverter) + .build()) + .withTypeConverter(typeConverter) + .withResultType(Pet.class) + .block() + .getResult(); + + System.out.println("Sandy's updated record: " + updated); + } + + public static void exampleUsingJackson(ChatCompletionService chatCompletionService) { + Pet sandy = new Pet("Sandy", 3, "Dog"); + + Kernel kernel = Kernel.builder() + .withAIService(ChatCompletionService.class, chatCompletionService) + .build(); + + // Create a converter that defaults to using jackson for serialization + ContextVariableTypeConverter typeConverter = ContextVariableJacksonConverter.create( + Pet.class); + + // Invoke the prompt with the custom converter + Pet updated = kernel.invokePromptAsync( + "Increase Sandy's age by a year:\n{{$Sandy}}", + KernelFunctionArguments.builder() + .withVariable("Sandy", sandy, typeConverter) + .build()) + .withTypeConverter(typeConverter) + .withResultType(Pet.class) + .block() + .getResult(); + + System.out.println("Sandy's updated record: " + updated); + } + + public static void exampleUsingGlobalTypes(ChatCompletionService chatCompletionService) { + Pet sandy = new Pet("Sandy", 3, "Dog"); + + Kernel kernel = Kernel.builder() + .withAIService(ChatCompletionService.class, chatCompletionService) + .build(); + + // Create a converter that defaults to using jackson for serialization + ContextVariableTypeConverter typeConverter = ContextVariableJacksonConverter.create( + Pet.class); + + // Add converter to global types + ContextVariableTypes.addGlobalConverter(typeConverter); + + // No need to explicitly tell the invocation how to convert the type + Pet updated = kernel.invokePromptAsync( + "Sandy's is actually a cat correct this:\n{{$Sandy}}", + KernelFunctionArguments.builder() + .withVariable("Sandy", sandy) + .build()) + .withResultType(Pet.class) + .block() + .getResult(); + + System.out.println("Sandy's updated record: " + updated); + } + +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/ContextVariableTypeConverter.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/ContextVariableTypeConverter.java index 0a4bc758..687ef302 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/ContextVariableTypeConverter.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/ContextVariableTypeConverter.java @@ -309,9 +309,7 @@ public static class Builder { @SuppressFBWarnings("CT_CONSTRUCTOR_THROW") public Builder(Class clazz) { this.clazz = clazz; - fromObject = x -> { - throw new UnsupportedOperationException("fromObject not implemented"); - }; + fromObject = x -> ContextVariableTypes.convert(x, clazz); toPromptString = (a, b) -> { throw new UnsupportedOperationException("toPromptString not implemented"); }; diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/ContextVariableJacksonConverter.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/ContextVariableJacksonConverter.java new file mode 100644 index 00000000..ca4bfc44 --- /dev/null +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/ContextVariableJacksonConverter.java @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.contextvariables.converters; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.semantickernel.contextvariables.ContextVariableTypeConverter; +import com.microsoft.semantickernel.contextvariables.ContextVariableTypeConverter.Builder; +import com.microsoft.semantickernel.exceptions.SKException; + +/** + * A utility class for creating {@link ContextVariableTypeConverter} instances that use Jackson for + * serialization and deserialization. + */ +public final class ContextVariableJacksonConverter { + + /** + * Creates a new {@link ContextVariableTypeConverter} that uses Jackson for serialization and + * deserialization. + * + * @param type the type of the context variable + * @param mapper the {@link ObjectMapper} to use for serialization and deserialization + * @param the type of the context variable + * @return a new {@link ContextVariableTypeConverter} + */ + public static ContextVariableTypeConverter create(Class type, ObjectMapper mapper) { + return builder(type, mapper).build(); + } + + /** + * Creates a new {@link ContextVariableTypeConverter} that uses Jackson for serialization and + * deserialization. + * + * @param type the type of the context variable + * @param the type of the context variable + * @return a new {@link ContextVariableTypeConverter} + */ + public static ContextVariableTypeConverter create(Class type) { + return create(type, new ObjectMapper()); + } + + /** + * Creates a new {@link Builder} for a {@link ContextVariableTypeConverter} that uses Jackson + * for serialization and deserialization. + * + * @param type the type of the context variable + * @param the type of the context variable + * @return a new {@link Builder} + */ + public static Builder builder(Class type) { + return builder(type, new ObjectMapper()); + } + + /** + * Creates a new {@link Builder} for a {@link ContextVariableTypeConverter} that uses Jackson + * for serialization and deserialization. + * + * @param type the type of the context variable + * @param mapper the {@link ObjectMapper} to use for serialization and deserialization + * @param the type of the context variable + * @return a new {@link Builder} + */ + public static Builder builder(Class type, ObjectMapper mapper) { + return ContextVariableTypeConverter.builder(type) + .fromPromptString(str -> { + try { + return mapper.readValue(str, type); + } catch (JsonProcessingException e) { + throw new SKException("Failed to deserialize object", e); + } + }) + .toPromptString(obj -> { + try { + return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj); + } catch (JsonProcessingException e) { + throw new SKException("Failed to serialize object", e); + } + }); + } +} diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/DateTimeContextVariableTypeConverter.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/DateTimeContextVariableTypeConverter.java index 6a64ccd2..34fc741f 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/DateTimeContextVariableTypeConverter.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/contextvariables/converters/DateTimeContextVariableTypeConverter.java @@ -33,9 +33,7 @@ public DateTimeContextVariableTypeConverter() { return null; }, Object::toString, - o -> { - return ZonedDateTime.parse(o).toOffsetDateTime(); - }, + o -> ZonedDateTime.parse(o).toOffsetDateTime(), Arrays.asList( new DefaultConverter(OffsetDateTime.class, Instant.class) { @Override diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionInvocation.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionInvocation.java index 276f2c18..4cd6ae2a 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionInvocation.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/FunctionInvocation.java @@ -133,7 +133,7 @@ private static BiConsumer, SynchronousSink FunctionInvocation withResultType(ContextVariableType resultTyp * @return A new {@code FunctionInvocation} for fluent chaining. */ public FunctionInvocation withResultType(Class resultType) { - return withResultType(ContextVariableTypes.getGlobalVariableTypeForClass(resultType)); + try { + return withResultType(contextVariableTypes.getVariableTypeForSuperClass(resultType)); + } catch (SKException e) { + return withResultType(ContextVariableTypes.getGlobalVariableTypeForClass(resultType)); + } } /** diff --git a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatHistory.java b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatHistory.java index d2f391ff..8d6bdce6 100644 --- a/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatHistory.java +++ b/semantickernel-api/src/main/java/com/microsoft/semantickernel/services/chatcompletion/ChatHistory.java @@ -113,7 +113,7 @@ public Spliterator> spliterator() { * @param encoding The encoding of the message * @param metadata The metadata of the message */ - public void addMessage(AuthorRole authorRole, String content, Charset encoding, + public ChatHistory addMessage(AuthorRole authorRole, String content, Charset encoding, FunctionResultMetadata metadata) { chatMessageContents.add( ChatMessageTextContent.builder() @@ -122,6 +122,7 @@ public void addMessage(AuthorRole authorRole, String content, Charset encoding, .withEncoding(encoding) .withMetadata(metadata) .build()); + return this; } /** @@ -130,12 +131,13 @@ public void addMessage(AuthorRole authorRole, String content, Charset encoding, * @param authorRole The role of the author of the message * @param content The content of the message */ - public void addMessage(AuthorRole authorRole, String content) { + public ChatHistory addMessage(AuthorRole authorRole, String content) { chatMessageContents.add( ChatMessageTextContent.builder() .withAuthorRole(authorRole) .withContent(content) .build()); + return this; } /** @@ -143,8 +145,9 @@ public void addMessage(AuthorRole authorRole, String content) { * * @param content The content of the message */ - public void addMessage(ChatMessageContent content) { + public ChatHistory addMessage(ChatMessageContent content) { chatMessageContents.add(content); + return this; } /** @@ -152,8 +155,8 @@ public void addMessage(ChatMessageContent content) { * * @param content The content of the user message */ - public void addUserMessage(String content) { - addMessage(AuthorRole.USER, content); + public ChatHistory addUserMessage(String content) { + return addMessage(AuthorRole.USER, content); } /** @@ -161,8 +164,8 @@ public void addUserMessage(String content) { * * @param content The content of the assistant message */ - public void addAssistantMessage(String content) { - addMessage(AuthorRole.ASSISTANT, content); + public ChatHistory addAssistantMessage(String content) { + return addMessage(AuthorRole.ASSISTANT, content); } /** @@ -170,11 +173,12 @@ public void addAssistantMessage(String content) { * * @param content The content of the system message */ - public void addSystemMessage(String content) { - addMessage(AuthorRole.SYSTEM, content); + public ChatHistory addSystemMessage(String content) { + return addMessage(AuthorRole.SYSTEM, content); } - public void addAll(List> messages) { + public ChatHistory addAll(List> messages) { chatMessageContents.addAll(messages); + return this; } }