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..9f34f3d7 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..60ffe2f4 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/common/function/SchemaConverter.java @@ -0,0 +1,10 @@ +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..beb067bb 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 @@ -26,7 +26,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..3051e937 --- /dev/null +++ b/src/main/java/io/github/sashirestela/openai/support/DefaultSchemaConverter.java @@ -0,0 +1,43 @@ +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..e3340e0e 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..b941a1ed --- /dev/null +++ b/src/test/java/io/github/sashirestela/openai/support/CustomSchemaConverter.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.fasterxml.jackson.databind.node.ObjectNode; +import com.github.victools.jsonschema.generator.*; +import com.github.victools.jsonschema.generator.Module; +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; + + } + + + +}