Skip to content

Commit

Permalink
Merge pull request #158 from johnoliver/custom-types
Browse files Browse the repository at this point in the history
add example for custom type conversion. Fix bug in per-invocation type conversion. Add fluent calls to ChatHistory
  • Loading branch information
dsgrieve authored Jul 31, 2024
2 parents c06d592 + 659859f commit 57db64f
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 18 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Pet, String> petToString = pet -> "name: " + pet.name() + "\n" +
"age: " + pet.age() + "\n" +
"species: " + pet.species() + "\n";

// Custom deserializer
Function<String, Pet> stringToPet = prompt -> {
Map<String, String> 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<Pet> 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<Pet> 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<Pet> 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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -309,9 +309,7 @@ public static class Builder<T> {
@SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
public Builder(Class<T> 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");
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T> the type of the context variable
* @return a new {@link ContextVariableTypeConverter}
*/
public static <T> ContextVariableTypeConverter<T> create(Class<T> 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 <T> the type of the context variable
* @return a new {@link ContextVariableTypeConverter}
*/
public static <T> ContextVariableTypeConverter<T> create(Class<T> 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 <T> the type of the context variable
* @return a new {@link Builder}
*/
public static <T> Builder<T> builder(Class<T> 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 <T> the type of the context variable
* @return a new {@link Builder}
*/
public static <T> Builder<T> builder(Class<T> 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);
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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, Instant>(OffsetDateTime.class, Instant.class) {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ private static <T> BiConsumer<FunctionResult<?>, SynchronousSink<FunctionResult<
} catch (Exception e) {
sink.error(new SKException(
"Failed to convert result to requested type: "
+ variableType.getClazz().getName(),
+ variableType.getClazz().getName() + " " + result.getResult(),
e));
}
} else {
Expand Down Expand Up @@ -196,7 +196,11 @@ public <U> FunctionInvocation<U> withResultType(ContextVariableType<U> resultTyp
* @return A new {@code FunctionInvocation} for fluent chaining.
*/
public <U> FunctionInvocation<U> withResultType(Class<U> resultType) {
return withResultType(ContextVariableTypes.getGlobalVariableTypeForClass(resultType));
try {
return withResultType(contextVariableTypes.getVariableTypeForSuperClass(resultType));
} catch (SKException e) {
return withResultType(ContextVariableTypes.getGlobalVariableTypeForClass(resultType));
}
}

/**
Expand Down
Loading

0 comments on commit 57db64f

Please sign in to comment.