From 40e4f54a61fca139bb40a3a02f131a6cc28ef19c Mon Sep 17 00:00:00 2001 From: Asaf Ben Natan Date: Thu, 18 Jul 2024 10:49:57 +0300 Subject: [PATCH 1/2] fixed formatting added SchemaConverter interface for controlling Schema generation from classes --- .../openai/common/function/FunctionDef.java | 3 + .../common/function/SchemaConverter.java | 9 +++ .../sashirestela/openai/common/tool/Tool.java | 3 +- .../support/DefaultSchemaConverter.java | 46 ++++++++++++ .../openai/support/JsonSchemaUtil.java | 34 +-------- .../openai/support/CustomSchemaConverter.java | 49 ++++++++++++ .../support/CustomSchemaConverterTest.java | 74 +++++++++++++++++++ 7 files changed, 186 insertions(+), 32 deletions(-) create mode 100644 src/main/java/io/github/sashirestela/openai/common/function/SchemaConverter.java create mode 100644 src/main/java/io/github/sashirestela/openai/support/DefaultSchemaConverter.java create mode 100644 src/test/java/io/github/sashirestela/openai/support/CustomSchemaConverter.java create mode 100644 src/test/java/io/github/sashirestela/openai/support/CustomSchemaConverterTest.java diff --git a/src/main/java/io/github/sashirestela/openai/common/function/FunctionDef.java b/src/main/java/io/github/sashirestela/openai/common/function/FunctionDef.java index da09ecb1..1579a19c 100644 --- a/src/main/java/io/github/sashirestela/openai/common/function/FunctionDef.java +++ b/src/main/java/io/github/sashirestela/openai/common/function/FunctionDef.java @@ -1,5 +1,6 @@ package io.github.sashirestela.openai.common.function; +import io.github.sashirestela.openai.support.JsonSchemaUtil; import lombok.Builder; import lombok.Getter; import lombok.NonNull; @@ -15,5 +16,7 @@ public class FunctionDef { @NonNull private Class functionalClass; + @Builder.Default + private SchemaConverter schemaConverter = JsonSchemaUtil.defaultConverter; } diff --git a/src/main/java/io/github/sashirestela/openai/common/function/SchemaConverter.java b/src/main/java/io/github/sashirestela/openai/common/function/SchemaConverter.java new file mode 100644 index 00000000..dae40563 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/common/function/SchemaConverter.java @@ -0,0 +1,9 @@ +package io.github.sashirestela.openai.common.function; + +import com.fasterxml.jackson.databind.JsonNode; + +public interface SchemaConverter { + + JsonNode convert(Class c); + +} diff --git a/src/main/java/io/github/sashirestela/openai/common/tool/Tool.java b/src/main/java/io/github/sashirestela/openai/common/tool/Tool.java index 54536e83..5d8642fa 100644 --- a/src/main/java/io/github/sashirestela/openai/common/tool/Tool.java +++ b/src/main/java/io/github/sashirestela/openai/common/tool/Tool.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.JsonNode; import io.github.sashirestela.openai.common.function.FunctionDef; -import io.github.sashirestela.openai.support.JsonSchemaUtil; import io.github.sashirestela.slimvalidator.constraints.Required; import io.github.sashirestela.slimvalidator.constraints.Size; import lombok.AllArgsConstructor; @@ -26,7 +25,7 @@ public static Tool function(FunctionDef function) { new ToolFunctionDef( function.getName(), function.getDescription(), - JsonSchemaUtil.classToJsonSchema(function.getFunctionalClass()))); + function.getSchemaConverter().convert(function.getFunctionalClass()))); } @AllArgsConstructor diff --git a/src/main/java/io/github/sashirestela/openai/support/DefaultSchemaConverter.java b/src/main/java/io/github/sashirestela/openai/support/DefaultSchemaConverter.java new file mode 100644 index 00000000..8acc1e1c --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/support/DefaultSchemaConverter.java @@ -0,0 +1,46 @@ +package io.github.sashirestela.openai.support; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.victools.jsonschema.generator.*; +import com.github.victools.jsonschema.module.jackson.JacksonModule; +import com.github.victools.jsonschema.module.jackson.JacksonOption; +import io.github.sashirestela.openai.SimpleUncheckedException; +import io.github.sashirestela.openai.common.function.SchemaConverter; + +import static io.github.sashirestela.openai.support.JsonSchemaUtil.JSON_EMPTY_CLASS; + +public class DefaultSchemaConverter implements SchemaConverter { + + private final SchemaGenerator schemaGenerator; + private final ObjectMapper objectMapper; + + public DefaultSchemaConverter() { + objectMapper = new ObjectMapper(); + var jacksonModule = new JacksonModule(JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, + JacksonOption.RESPECT_JSONPROPERTY_ORDER); + var configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, + OptionPreset.PLAIN_JSON) + .with(jacksonModule) + .without(Option.SCHEMA_VERSION_INDICATOR); + var config = configBuilder.build(); + schemaGenerator = new SchemaGenerator(config); + } + + @Override + public JsonNode convert(Class clazz) { + JsonNode jsonSchema; + try { + jsonSchema = schemaGenerator.generateSchema(clazz); + if (jsonSchema.get("properties") == null) { + jsonSchema = objectMapper.readTree(JSON_EMPTY_CLASS); + } + + } catch (Exception e) { + throw new SimpleUncheckedException("Cannot generate the Json Schema for the class {0}.", clazz.getName(), + e); + } + return jsonSchema; + } + +} diff --git a/src/main/java/io/github/sashirestela/openai/support/JsonSchemaUtil.java b/src/main/java/io/github/sashirestela/openai/support/JsonSchemaUtil.java index f4b13822..0e966078 100644 --- a/src/main/java/io/github/sashirestela/openai/support/JsonSchemaUtil.java +++ b/src/main/java/io/github/sashirestela/openai/support/JsonSchemaUtil.java @@ -1,45 +1,19 @@ package io.github.sashirestela.openai.support; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.victools.jsonschema.generator.Option; -import com.github.victools.jsonschema.generator.OptionPreset; -import com.github.victools.jsonschema.generator.SchemaGenerator; -import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder; -import com.github.victools.jsonschema.generator.SchemaVersion; -import com.github.victools.jsonschema.module.jackson.JacksonModule; -import com.github.victools.jsonschema.module.jackson.JacksonOption; -import io.github.sashirestela.openai.SimpleUncheckedException; +import io.github.sashirestela.openai.common.function.SchemaConverter; public class JsonSchemaUtil { + public static final SchemaConverter defaultConverter = new DefaultSchemaConverter(); + public static final String JSON_EMPTY_CLASS = "{\"type\":\"object\",\"properties\":{}}"; - private static ObjectMapper objectMapper = new ObjectMapper(); private JsonSchemaUtil() { } public static JsonNode classToJsonSchema(Class clazz) { - JsonNode jsonSchema = null; - try { - var jacksonModule = new JacksonModule(JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, - JacksonOption.RESPECT_JSONPROPERTY_ORDER); - var configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, - OptionPreset.PLAIN_JSON) - .with(jacksonModule) - .without(Option.SCHEMA_VERSION_INDICATOR); - var config = configBuilder.build(); - var generator = new SchemaGenerator(config); - jsonSchema = generator.generateSchema(clazz); - if (jsonSchema.get("properties") == null) { - jsonSchema = objectMapper.readTree(JSON_EMPTY_CLASS); - } - - } catch (Exception e) { - throw new SimpleUncheckedException("Cannot generate the Json Schema for the class {0}.", - clazz.getName(), e); - } - return jsonSchema; + return defaultConverter.convert(clazz); } } diff --git a/src/test/java/io/github/sashirestela/openai/support/CustomSchemaConverter.java b/src/test/java/io/github/sashirestela/openai/support/CustomSchemaConverter.java new file mode 100644 index 00000000..50c7dfd6 --- /dev/null +++ b/src/test/java/io/github/sashirestela/openai/support/CustomSchemaConverter.java @@ -0,0 +1,49 @@ +package io.github.sashirestela.openai.support; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.victools.jsonschema.generator.*; +import com.github.victools.jsonschema.module.jackson.JacksonModule; +import com.github.victools.jsonschema.module.jackson.JacksonOption; +import io.github.sashirestela.openai.SimpleUncheckedException; +import io.github.sashirestela.openai.common.function.SchemaConverter; + +public class CustomSchemaConverter implements SchemaConverter { + + private final SchemaGenerator schemaGenerator; + private final ObjectMapper objectMapper; + public static final String JSON_EMPTY_CLASS = "{\"type\":\"object\",\"properties\":{}}"; + + public CustomSchemaConverter() { + objectMapper = new ObjectMapper(); + var jacksonModule = new JacksonModule(JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, + JacksonOption.RESPECT_JSONPROPERTY_ORDER); + var configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, + OptionPreset.PLAIN_JSON) + .with(jacksonModule) + .with(builder -> builder.forTypesInGeneral() + .withTypeAttributeOverride( + (collectedTypeAttributes, scope, context) -> collectedTypeAttributes + .put("myCustomProperty", true))) + .without(Option.SCHEMA_VERSION_INDICATOR); + var config = configBuilder.build(); + schemaGenerator = new SchemaGenerator(config); + } + + @Override + public JsonNode convert(Class clazz) { + JsonNode jsonSchema; + try { + jsonSchema = schemaGenerator.generateSchema(clazz); + if (jsonSchema.get("properties") == null) { + jsonSchema = objectMapper.readTree(JSON_EMPTY_CLASS); + } + + } catch (Exception e) { + throw new SimpleUncheckedException("Cannot generate the Json Schema for the class {0}.", clazz.getName(), + e); + } + return jsonSchema; + } + +} diff --git a/src/test/java/io/github/sashirestela/openai/support/CustomSchemaConverterTest.java b/src/test/java/io/github/sashirestela/openai/support/CustomSchemaConverterTest.java new file mode 100644 index 00000000..7995beec --- /dev/null +++ b/src/test/java/io/github/sashirestela/openai/support/CustomSchemaConverterTest.java @@ -0,0 +1,74 @@ +package io.github.sashirestela.openai.support; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.JsonNode; +import io.github.sashirestela.openai.common.function.SchemaConverter; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static io.github.sashirestela.openai.support.JsonSchemaUtil.JSON_EMPTY_CLASS; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class CustomSchemaConverterTest { + + private static SchemaConverter schemaConverter=new CustomSchemaConverter(); + + + @Test + void shouldGenerateFullJsonSchemaWhenClassHasSomeFields() { + var actualJsonSchema = schemaConverter.convert(TestClass.class).toString(); + var expectedJsonSchema = "{\"type\":\"object\",\"properties\":{\"first\":{\"type\":\"string\",\"myCustomProperty\":true},\"second\":{\"type\":\"integer\",\"myCustomProperty\":true}},\"required\":[\"first\"],\"myCustomProperty\":true}"; + assertEquals(expectedJsonSchema, actualJsonSchema); + } + + @Test + void shouldGenerateEmptyJsonSchemaWhenClassHasNoFields() { + var actualJsonSchema = schemaConverter.convert(EmptyClass.class).toString(); + var expectedJsonSchema = JSON_EMPTY_CLASS; + assertEquals(expectedJsonSchema, actualJsonSchema); + } + + @Test + void shouldGenerateOrderedJsonSchemaWhenClassHasJsonPropertyOrderAnnotation() { + var actualJsonSchema = schemaConverter.convert(OrderedTestClass.class).toString(); + var expectedJsonSchema = "{\"type\":\"object\",\"properties\":{\"first\":{\"type\":\"string\",\"myCustomProperty\":true},\"second\":{\"type\":\"integer\",\"myCustomProperty\":true},\"third\":{\"type\":\"string\",\"myCustomProperty\":true}},\"required\":[\"first\"],\"myCustomProperty\":true}"; + assertEquals(expectedJsonSchema, actualJsonSchema); + } + + @NoArgsConstructor + @AllArgsConstructor + @Getter + static class TestClass { + + @JsonProperty(required = true) + public String first; + + public Integer second; + + } + + static class EmptyClass { + } + + @NoArgsConstructor + @AllArgsConstructor + @Getter + @JsonPropertyOrder({ "first", "second", "third" }) + static class OrderedTestClass { + + @JsonProperty(required = true) + public String first; + + public Integer second; + + public String third; + + } + + + +} From 4d3fbb0fbf62ec3e75389fbbafd245ad8544111a Mon Sep 17 00:00:00 2001 From: Asaf Ben Natan Date: Sat, 20 Jul 2024 23:55:46 +0300 Subject: [PATCH 2/2] fixed formatting issues --- .../sashirestela/openai/common/function/FunctionDef.java | 1 + .../openai/support/CustomSchemaConverterTest.java | 7 +------ 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/github/sashirestela/openai/common/function/FunctionDef.java b/src/main/java/io/github/sashirestela/openai/common/function/FunctionDef.java index 1579a19c..4bd0e6da 100644 --- a/src/main/java/io/github/sashirestela/openai/common/function/FunctionDef.java +++ b/src/main/java/io/github/sashirestela/openai/common/function/FunctionDef.java @@ -16,6 +16,7 @@ public class FunctionDef { @NonNull private Class functionalClass; + @Builder.Default private SchemaConverter schemaConverter = JsonSchemaUtil.defaultConverter; diff --git a/src/test/java/io/github/sashirestela/openai/support/CustomSchemaConverterTest.java b/src/test/java/io/github/sashirestela/openai/support/CustomSchemaConverterTest.java index 7995beec..8815883d 100644 --- a/src/test/java/io/github/sashirestela/openai/support/CustomSchemaConverterTest.java +++ b/src/test/java/io/github/sashirestela/openai/support/CustomSchemaConverterTest.java @@ -2,12 +2,10 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.databind.JsonNode; import io.github.sashirestela.openai.common.function.SchemaConverter; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import static io.github.sashirestela.openai.support.JsonSchemaUtil.JSON_EMPTY_CLASS; @@ -15,8 +13,7 @@ class CustomSchemaConverterTest { - private static SchemaConverter schemaConverter=new CustomSchemaConverter(); - + private static SchemaConverter schemaConverter = new CustomSchemaConverter(); @Test void shouldGenerateFullJsonSchemaWhenClassHasSomeFields() { @@ -69,6 +66,4 @@ static class OrderedTestClass { } - - }