From b146cfdf03f4984761cffced5ed2a1264d746f40 Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Thu, 13 Feb 2025 16:35:01 +0100 Subject: [PATCH 01/23] Use library to generate schemas from java classes --- orchestration/pom.xml | 10 ++++ .../orchestration/OrchestrationUnitTest.java | 54 ++++++++++++++----- pom.xml | 11 ++++ sample-code/spring-app/pom.xml | 8 +++ .../app/services/OrchestrationService.java | 52 +++++++++++++----- 5 files changed, 109 insertions(+), 26 deletions(-) diff --git a/orchestration/pom.xml b/orchestration/pom.xml index 7c6988682..e48f99083 100644 --- a/orchestration/pom.xml +++ b/orchestration/pom.xml @@ -132,6 +132,16 @@ junit-jupiter-params test + + com.github.victools + jsonschema-generator + test + + + com.github.victools + jsonschema-module-jackson + test + diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java index 586a0b232..de95767bd 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java @@ -30,10 +30,23 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import com.github.tomakehurst.wiremock.stubbing.Scenario; +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 com.sap.ai.sdk.orchestration.model.DPIEntities; import com.sap.ai.sdk.orchestration.model.DataRepositoryType; import com.sap.ai.sdk.orchestration.model.DocumentGroundingFilter; @@ -777,18 +790,33 @@ void testResponseObjectJsonSchema() throws IOException { var llmWithImageSupportConfig = new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI); val template = Message.user("Whats 'apple' in German?"); - var schema = - Map.of( - "type", - "object", - "properties", - Map.of( - "language", Map.of("type", "string"), - "translation", Map.of("type", "string")), - "required", - List.of("language", "translation"), - "additionalProperties", - false); + + // Example class + class Translation { + @JsonProperty(required = true) + private String translation; + + @JsonProperty(required = true) + private String language; + } + + // Build JSON schema from class + JacksonModule module = + new JacksonModule( + JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, JacksonOption.RESPECT_JSONPROPERTY_ORDER); + + SchemaGenerator generator = + new SchemaGenerator( + new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON) + .without(Option.SCHEMA_VERSION_INDICATOR) + .with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT) + .with(module) + .build()); + JsonNode jsonSchema = generator.generateSchema(Translation.class); + + // Convert JSON schema to Map + val mapper = new ObjectMapper(); + Map schemaMap = mapper.convertValue(jsonSchema, new TypeReference<>() {}); val templatingConfig = Template.create() @@ -799,7 +827,7 @@ void testResponseObjectJsonSchema() throws IOException { .jsonSchema( ResponseFormatJsonSchemaJsonSchema.create() .name("translation_response") - .schema(schema) + .schema(schemaMap) .strict(true) .description("Output schema for language translation."))); val configWithTemplate = llmWithImageSupportConfig.withTemplateConfig(templatingConfig); diff --git a/pom.xml b/pom.xml index a8fc61ffc..3c2bf6929 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,7 @@ 3.6.12 3.1.0 5.15.2 + 4.37.0 1.14.2 20250107 @@ -129,6 +130,16 @@ jackson-module-parameter-names 2.18.2 + + com.github.victools + jsonschema-generator + ${jsonschema-generator.version} + + + com.github.victools + jsonschema-module-jackson + ${jsonschema-generator.version} + io.micrometer diff --git a/sample-code/spring-app/pom.xml b/sample-code/spring-app/pom.xml index cd4f5f713..d96631c47 100644 --- a/sample-code/spring-app/pom.xml +++ b/sample-code/spring-app/pom.xml @@ -122,6 +122,14 @@ com.fasterxml.jackson.core jackson-annotations + + com.github.victools + jsonschema-generator + + + com.github.victools + jsonschema-module-jackson + ch.qos.logback diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java index 710872a61..e8e4ca349 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java @@ -4,6 +4,17 @@ import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GPT_4O_MINI; import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.Parameter.TEMPERATURE; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.type.TypeReference; +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 com.sap.ai.sdk.core.AiCoreService; import com.sap.ai.sdk.orchestration.AzureContentFilter; import com.sap.ai.sdk.orchestration.AzureFilterThreshold; @@ -354,18 +365,33 @@ public OrchestrationChatResponse responseFormatJsonSchema(@Nonnull final String new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI); val template = Message.user("Whats '%s' in German?".formatted(word)); - val schema = - Map.of( - "type", - "object", - "properties", - Map.of( - "language", Map.of("type", "string"), - "translation", Map.of("type", "string")), - "required", - List.of("language", "translation"), - "additionalProperties", - false); + + // Example class + class Translation { + @JsonProperty(required = true) + private String translation; + + @JsonProperty(required = true) + private String language; + } + + // Build JSON schema from class + JacksonModule module = + new JacksonModule( + JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, JacksonOption.RESPECT_JSONPROPERTY_ORDER); + + SchemaGenerator generator = + new SchemaGenerator( + new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON) + .without(Option.SCHEMA_VERSION_INDICATOR) + .with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT) + .with(module) + .build()); + JsonNode jsonSchema = generator.generateSchema(Translation.class); + + // Convert JSON schema to Map + val mapper = new ObjectMapper(); + Map schemaMap = mapper.convertValue(jsonSchema, new TypeReference<>() {}); val templatingConfig = Template.create() @@ -376,7 +402,7 @@ public OrchestrationChatResponse responseFormatJsonSchema(@Nonnull final String .jsonSchema( ResponseFormatJsonSchemaJsonSchema.create() .name("translation_response") - .schema(schema) + .schema(schemaMap) .strict(true) .description("Output schema for language translation."))); val configWithTemplate = llmWithImageSupportConfig.withTemplateConfig(templatingConfig); From 1611a0880f2a0fa51bf218f8b8d01339ee70939b Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Tue, 18 Feb 2025 10:57:02 +0100 Subject: [PATCH 02/23] Create ResponseJsonSchema and adapt OrchestrationModuleConfig --- orchestration/pom.xml | 30 +++-- .../OrchestrationModuleConfig.java | 104 +++++++++++++++++- .../sdk/orchestration/ResponseJsonSchema.java | 94 ++++++++++++++++ .../orchestration/OrchestrationUnitTest.java | 87 ++++----------- .../src/test/resources/jsonSchemaRequest.json | 10 +- sample-code/spring-app/pom.xml | 8 -- .../app/services/OrchestrationService.java | 59 ++-------- 7 files changed, 248 insertions(+), 144 deletions(-) create mode 100644 orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java diff --git a/orchestration/pom.xml b/orchestration/pom.xml index e48f99083..d8c800d11 100644 --- a/orchestration/pom.xml +++ b/orchestration/pom.xml @@ -31,12 +31,12 @@ ${project.basedir}/../ - 80% - 92% - 93% - 71% - 95% - 100% + 10% + 10% + 10% + 10% + 10% + 10% @@ -100,6 +100,14 @@ com.google.guava guava + + com.github.victools + jsonschema-generator + + + com.github.victools + jsonschema-module-jackson + org.projectlombok @@ -132,16 +140,6 @@ junit-jupiter-params test - - com.github.victools - jsonschema-generator - test - - - com.github.victools - jsonschema-module-jackson - test - diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java index ff6a52f31..3e24b45ac 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java @@ -6,9 +6,16 @@ import com.sap.ai.sdk.orchestration.model.LLMModuleConfig; import com.sap.ai.sdk.orchestration.model.MaskingModuleConfig; import com.sap.ai.sdk.orchestration.model.OutputFilteringConfig; +import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonObject; +import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchema; +import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchemaJsonSchema; +import com.sap.ai.sdk.orchestration.model.Template; +import com.sap.ai.sdk.orchestration.model.TemplateRef; +import com.sap.ai.sdk.orchestration.model.TemplateResponseFormat; import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Objects; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -61,7 +68,9 @@ public class OrchestrationModuleConfig { * @link SAP * AI Core: Orchestration - Templating */ - @Nullable TemplatingModuleConfig templateConfig; + @With(AccessLevel.NONE) + @Nullable + TemplatingModuleConfig templateConfig; /** * A masking configuration to pseudonymous or anonymize sensitive data in the input. @@ -211,4 +220,97 @@ public OrchestrationModuleConfig withGrounding( @Nonnull final GroundingProvider groundingProvider) { return this.withGroundingConfig(groundingProvider.createConfig()); } + + /** + * Creates a new configuration with the given template configuration. + * + * @link SAP + * AI Core: Orchestration - Templating + * @param templateConfig + * @return A new configuration with the given template configuration. + */ + @Nonnull + public OrchestrationModuleConfig withTemplateConfig( + @Nullable final TemplatingModuleConfig templateConfig) { + + // If new templateConfig is a TemplateRef, use it. + if (templateConfig instanceof TemplateRef) { + return new OrchestrationModuleConfig( + llmConfig, templateConfig, maskingConfig, filteringConfig, groundingConfig); + } + + // Make sure old responseFormat is only overwritten if new templateConfig has one set. + var newTemplate = (Template) templateConfig; + TemplateResponseFormat responseFormat = null; + if (this.templateConfig instanceof Template oldTemplate) { + responseFormat = oldTemplate.getResponseFormat(); + } + if (newTemplate != null && newTemplate.getResponseFormat() == null) { + newTemplate.setResponseFormat(responseFormat); + } + if (newTemplate == null && responseFormat != null) { + newTemplate = Template.create().template(List.of()); + newTemplate.setResponseFormat(responseFormat); + } + + return new OrchestrationModuleConfig( + llmConfig, newTemplate, maskingConfig, filteringConfig, groundingConfig); + } + + /** + * Creates a new configuration with the given response schema. + * + * @link SAP + * AI Core: Orchestration - Structured Output + * @param schema + * @return A new configuration with the given response schema. + * @since 1.4.0 + */ + @Nonnull + public OrchestrationModuleConfig withResponseJsonSchema( + @Nonnull final ResponseJsonSchema schema) { + val responseFormatJsonSchema = + ResponseFormatJsonSchema.create() + .type(ResponseFormatJsonSchema.TypeEnum.JSON_SCHEMA) + .jsonSchema( + ResponseFormatJsonSchemaJsonSchema.create() + .name(schema.getName()) + .schema(schema.getSchemaMap()) + .strict(schema.getIsStrict()) + .description(schema.getDescription())); + + if (this.templateConfig instanceof Template template) { + template.setResponseFormat(responseFormatJsonSchema); + return this.withTemplateConfig(template); + } + val templatingConfig = + Template.create().template(List.of()).responseFormat(responseFormatJsonSchema); + return this.withTemplateConfig(templatingConfig); + } + + /** + * Creates a new configuration where the response format is set to JSON object. + * + * @link SAP + * AI Core: Orchestration - Structured Output + * @return A new configuration with response format JSON object. + * @since 1.4.0 + */ + @Nonnull + public OrchestrationModuleConfig withJsonResponse() { + if (this.templateConfig instanceof Template template) { + template.setResponseFormat( + ResponseFormatJsonObject.create().type(ResponseFormatJsonObject.TypeEnum.JSON_OBJECT)); + return this.withTemplateConfig(template); + } + val templatingConfig = + Template.create() + .template(List.of()) + .responseFormat( + ResponseFormatJsonObject.create() + .type(ResponseFormatJsonObject.TypeEnum.JSON_OBJECT)); + return this.withTemplateConfig(templatingConfig); + } } diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java new file mode 100644 index 000000000..5c8b4d0e6 --- /dev/null +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java @@ -0,0 +1,94 @@ +package com.sap.ai.sdk.orchestration; + +import com.fasterxml.jackson.core.type.TypeReference; +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 java.lang.reflect.Type; +import java.util.Map; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Value; +import lombok.With; +import lombok.val; + +/** + * The schema object to use for the response format parameter in {@link OrchestrationModuleConfig}. + * + * @since 1.4.0 + */ +@Value +@AllArgsConstructor(access = AccessLevel.PACKAGE) +@With +public class ResponseJsonSchema { + @Nonnull Map schemaMap; + @Nonnull String name; + @Nullable String description; + + @With(AccessLevel.NONE) + @Nullable + Boolean isStrict; + + /** + * Create a new instance of {@link ResponseJsonSchema} with the given strictness. + * + * @return A new ResponseJsonSchema instance with the given strictness + * @since 1.4.0 + */ + @Nonnull + public ResponseJsonSchema withStrict(@Nullable final Boolean isStrict) { + return new ResponseJsonSchema(schemaMap, name, description, isStrict); + } + + /** + * Create a new instance of {@link ResponseJsonSchema} with the given schema map and name. + * + * @param schemaMap The schema map + * @param name The name of the schema + * @return The new instance of {@link ResponseJsonSchema} + * @since 1.4.0 + */ + @Nonnull + public static ResponseJsonSchema of( + @Nonnull final Map schemaMap, @Nonnull final String name) { + return new ResponseJsonSchema(schemaMap, name, null, null); + } + + /** + * Create a new instance of {@link ResponseJsonSchema} from a given class. + * + * @param classType + * @return The new instance of {@link ResponseJsonSchema} + * @since 1.4.0 + */ + @Nonnull + public static ResponseJsonSchema from(@Nonnull final Type classType) { + // Build JSON schema from class + val module = + new JacksonModule( + JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, JacksonOption.RESPECT_JSONPROPERTY_ORDER); + val generator = + new SchemaGenerator( + new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON) + .without(Option.SCHEMA_VERSION_INDICATOR) + .with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT) + .with(module) + .build()); + val jsonSchema = generator.generateSchema(classType); + + // Convert JSON schema to Map + val mapper = new ObjectMapper(); + final Map schemaMap = mapper.convertValue(jsonSchema, new TypeReference<>() {}); + + val schemaName = ((Class) classType).getSimpleName() + "-Schema"; + + return new ResponseJsonSchema(schemaMap, schemaName, null, null); + } +} diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java index de95767bd..9a19106e6 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java @@ -32,21 +32,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import com.github.tomakehurst.wiremock.stubbing.Scenario; -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 com.sap.ai.sdk.orchestration.model.DPIEntities; import com.sap.ai.sdk.orchestration.model.DataRepositoryType; import com.sap.ai.sdk.orchestration.model.DocumentGroundingFilter; @@ -57,9 +45,6 @@ import com.sap.ai.sdk.orchestration.model.KeyValueListPair; import com.sap.ai.sdk.orchestration.model.LLMModuleResultSynchronous; import com.sap.ai.sdk.orchestration.model.LlamaGuard38b; -import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonObject; -import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchema; -import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchemaJsonSchema; import com.sap.ai.sdk.orchestration.model.ResponseFormatText; import com.sap.ai.sdk.orchestration.model.SearchDocumentKeyValueListPair; import com.sap.ai.sdk.orchestration.model.SearchSelectOptionEnum; @@ -779,7 +764,7 @@ void testMultiMessage() throws IOException { } @Test - void testResponseObjectJsonSchema() throws IOException { + void testResponseFormatJsonSchema() throws IOException { stubFor( post(anyUrl()) .willReturn( @@ -787,54 +772,28 @@ void testResponseObjectJsonSchema() throws IOException { .withBodyFile("jsonSchemaResponse.json") .withHeader("Content-Type", "application/json"))); - var llmWithImageSupportConfig = new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI); - - val template = Message.user("Whats 'apple' in German?"); + var gpt4oCustomInstance = new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI); // Example class class Translation { @JsonProperty(required = true) - private String translation; + private String language; @JsonProperty(required = true) - private String language; + private String translation; } + val schema = + ResponseJsonSchema.from(Translation.class) + .withDescription("Output schema for language translation.") + .withStrict(true); + val configWithResponseSchema = gpt4oCustomInstance.withResponseJsonSchema(schema); - // Build JSON schema from class - JacksonModule module = - new JacksonModule( - JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, JacksonOption.RESPECT_JSONPROPERTY_ORDER); - - SchemaGenerator generator = - new SchemaGenerator( - new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON) - .without(Option.SCHEMA_VERSION_INDICATOR) - .with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT) - .with(module) - .build()); - JsonNode jsonSchema = generator.generateSchema(Translation.class); - - // Convert JSON schema to Map - val mapper = new ObjectMapper(); - Map schemaMap = mapper.convertValue(jsonSchema, new TypeReference<>() {}); - - val templatingConfig = - Template.create() - .template(List.of(template.createChatMessage())) - .responseFormat( - ResponseFormatJsonSchema.create() - .type(ResponseFormatJsonSchema.TypeEnum.JSON_SCHEMA) - .jsonSchema( - ResponseFormatJsonSchemaJsonSchema.create() - .name("translation_response") - .schema(schemaMap) - .strict(true) - .description("Output schema for language translation."))); - val configWithTemplate = llmWithImageSupportConfig.withTemplateConfig(templatingConfig); - - val prompt = new OrchestrationPrompt(Message.system("You are a language translator.")); + val prompt = + new OrchestrationPrompt( + Message.user("Whats 'apple' in German?"), + Message.system("You are a language translator.")); - final var message = client.chatCompletion(prompt, configWithTemplate).getContent(); + final var message = client.chatCompletion(prompt, configWithResponseSchema).getContent(); assertThat(message).isEqualTo("{\"translation\":\"Apfel\",\"language\":\"German\"}"); try (var requestInputStream = fileLoader.apply("jsonSchemaRequest.json")) { @@ -844,7 +803,7 @@ class Translation { } @Test - void testResponseObjectJsonObject() throws IOException { + void testResponseFormatJsonObject() throws IOException { stubFor( post(anyUrl()) .willReturn( @@ -852,23 +811,17 @@ void testResponseObjectJsonObject() throws IOException { .withBodyFile("jsonObjectResponse.json") .withHeader("Content-Type", "application/json"))); - val llmWithImageSupportConfig = new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI); + val gpt4oCustomInstance = new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI); - val template = Message.user("What is 'apple' in German?"); - val templatingConfig = - Template.create() - .template(List.of(template.createChatMessage())) - .responseFormat( - ResponseFormatJsonObject.create() - .type(ResponseFormatJsonObject.TypeEnum.JSON_OBJECT)); - val configWithTemplate = llmWithImageSupportConfig.withTemplateConfig(templatingConfig); + val configWithJsonResponse = gpt4oCustomInstance.withJsonResponse(); val prompt = new OrchestrationPrompt( + Message.user("What is 'apple' in German?"), Message.system( "You are a language translator. Answer using the following JSON format: {\"language\": ..., \"translation\": ...}")); - final var message = client.chatCompletion(prompt, configWithTemplate).getContent(); + final var message = client.chatCompletion(prompt, configWithJsonResponse).getContent(); assertThat(message).isEqualTo("{\"language\": \"German\", \"translation\": \"Apfel\"}"); try (var requestInputStream = fileLoader.apply("jsonObjectRequest.json")) { @@ -878,7 +831,7 @@ void testResponseObjectJsonObject() throws IOException { } @Test - void testResponseObjectText() throws IOException { + void testResponseFormatText() throws IOException { stubFor( post(anyUrl()) .willReturn( diff --git a/orchestration/src/test/resources/jsonSchemaRequest.json b/orchestration/src/test/resources/jsonSchemaRequest.json index 98c365a5f..c2c697672 100644 --- a/orchestration/src/test/resources/jsonSchemaRequest.json +++ b/orchestration/src/test/resources/jsonSchemaRequest.json @@ -19,19 +19,19 @@ "type" : "json_schema", "json_schema" : { "description" : "Output schema for language translation.", - "name" : "translation_response", + "name" : "Translation-Schema", "schema" : { "type" : "object", "properties" : { - "translation" : { + "language" : { "type" : "string" }, - "language" : { + "translation" : { "type" : "string" } }, - "additionalProperties" : false, - "required" : [ "language", "translation" ] + "required" : [ "language", "translation" ], + "additionalProperties" : false }, "strict" : true } diff --git a/sample-code/spring-app/pom.xml b/sample-code/spring-app/pom.xml index d96631c47..cd4f5f713 100644 --- a/sample-code/spring-app/pom.xml +++ b/sample-code/spring-app/pom.xml @@ -122,14 +122,6 @@ com.fasterxml.jackson.core jackson-annotations - - com.github.victools - jsonschema-generator - - - com.github.victools - jsonschema-module-jackson - ch.qos.logback diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java index e8e4ca349..ece89e7a1 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java @@ -5,16 +5,6 @@ import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.Parameter.TEMPERATURE; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.type.TypeReference; -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 com.sap.ai.sdk.core.AiCoreService; import com.sap.ai.sdk.orchestration.AzureContentFilter; import com.sap.ai.sdk.orchestration.AzureFilterThreshold; @@ -28,14 +18,13 @@ import com.sap.ai.sdk.orchestration.OrchestrationClientException; import com.sap.ai.sdk.orchestration.OrchestrationModuleConfig; import com.sap.ai.sdk.orchestration.OrchestrationPrompt; +import com.sap.ai.sdk.orchestration.ResponseJsonSchema; import com.sap.ai.sdk.orchestration.model.DPIEntities; import com.sap.ai.sdk.orchestration.model.DataRepositoryType; import com.sap.ai.sdk.orchestration.model.DocumentGroundingFilter; import com.sap.ai.sdk.orchestration.model.GroundingFilterSearchConfiguration; import com.sap.ai.sdk.orchestration.model.LlamaGuard38b; import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonObject; -import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchema; -import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchemaJsonSchema; import com.sap.ai.sdk.orchestration.model.ResponseFormatText; import com.sap.ai.sdk.orchestration.model.SearchDocumentKeyValueListPair; import com.sap.ai.sdk.orchestration.model.SearchSelectOptionEnum; @@ -361,8 +350,7 @@ public OrchestrationChatResponse grounding(@Nonnull final String userMessage) { */ @Nonnull public OrchestrationChatResponse responseFormatJsonSchema(@Nonnull final String word) { - final var llmWithImageSupportConfig = - new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI); + val gpt4oCustomInstance = new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI); val template = Message.user("Whats '%s' in German?".formatted(word)); @@ -375,41 +363,18 @@ class Translation { private String language; } - // Build JSON schema from class - JacksonModule module = - new JacksonModule( - JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, JacksonOption.RESPECT_JSONPROPERTY_ORDER); - - SchemaGenerator generator = - new SchemaGenerator( - new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON) - .without(Option.SCHEMA_VERSION_INDICATOR) - .with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT) - .with(module) - .build()); - JsonNode jsonSchema = generator.generateSchema(Translation.class); - - // Convert JSON schema to Map - val mapper = new ObjectMapper(); - Map schemaMap = mapper.convertValue(jsonSchema, new TypeReference<>() {}); + val schema = + ResponseJsonSchema.from(Translation.class) + .withDescription("Output schema for language translation.") + .withStrict(true); + val configWithResponseSchema = gpt4oCustomInstance.withResponseJsonSchema(schema); - val templatingConfig = - Template.create() - .template(List.of(template.createChatMessage())) - .responseFormat( - ResponseFormatJsonSchema.create() - .type(ResponseFormatJsonSchema.TypeEnum.JSON_SCHEMA) - .jsonSchema( - ResponseFormatJsonSchemaJsonSchema.create() - .name("translation_response") - .schema(schemaMap) - .strict(true) - .description("Output schema for language translation."))); - val configWithTemplate = llmWithImageSupportConfig.withTemplateConfig(templatingConfig); - - val prompt = new OrchestrationPrompt(Message.system("You are a language translator.")); + val prompt = + new OrchestrationPrompt( + Message.user("Whats 'apple' in German?"), + Message.system("You are a language translator.")); - return client.chatCompletion(prompt, configWithTemplate); + return client.chatCompletion(prompt, configWithResponseSchema); } /** From d7c5039432c5f21da20c8f2176c03c4498d8cfd4 Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Tue, 18 Feb 2025 14:59:12 +0100 Subject: [PATCH 03/23] Add additional tests, reset TestCoverage --- orchestration/pom.xml | 12 +- .../OrchestrationModuleConfig.java | 20 ++- .../sdk/orchestration/ResponseJsonSchema.java | 2 +- .../OrchestrationConvenienceUnitTest.java | 77 +++++++++++ .../OrchestrationModuleConfigTest.java | 120 ++++++++++++++++++ .../orchestration/OrchestrationUnitTest.java | 2 +- .../app/services/OrchestrationService.java | 6 +- .../app/controllers/OrchestrationTest.java | 2 - 8 files changed, 216 insertions(+), 25 deletions(-) diff --git a/orchestration/pom.xml b/orchestration/pom.xml index d8c800d11..6e6b2c6d9 100644 --- a/orchestration/pom.xml +++ b/orchestration/pom.xml @@ -31,12 +31,12 @@ ${project.basedir}/../ - 10% - 10% - 10% - 10% - 10% - 10% + 82% + 93% + 94% + 75% + 95% + 100% diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java index 3e24b45ac..7b6cc03e3 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java @@ -226,9 +226,10 @@ public OrchestrationModuleConfig withGrounding( * * @link SAP * AI Core: Orchestration - Templating - * @param templateConfig + * @param templateConfig The template configuration to use. * @return A new configuration with the given template configuration. */ + @Tolerate @Nonnull public OrchestrationModuleConfig withTemplateConfig( @Nullable final TemplatingModuleConfig templateConfig) { @@ -245,6 +246,7 @@ public OrchestrationModuleConfig withTemplateConfig( if (this.templateConfig instanceof Template oldTemplate) { responseFormat = oldTemplate.getResponseFormat(); } + // Template.getResponseFormat() might return null, so the following check is necessary. if (newTemplate != null && newTemplate.getResponseFormat() == null) { newTemplate.setResponseFormat(responseFormat); } @@ -263,12 +265,12 @@ public OrchestrationModuleConfig withTemplateConfig( * @link SAP * AI Core: Orchestration - Structured Output - * @param schema + * @param schema The response schema to use. * @return A new configuration with the given response schema. * @since 1.4.0 */ @Nonnull - public OrchestrationModuleConfig withResponseJsonSchema( + public OrchestrationModuleConfig withJsonSchemaResponse( @Nonnull final ResponseJsonSchema schema) { val responseFormatJsonSchema = ResponseFormatJsonSchema.create() @@ -279,7 +281,6 @@ public OrchestrationModuleConfig withResponseJsonSchema( .schema(schema.getSchemaMap()) .strict(schema.getIsStrict()) .description(schema.getDescription())); - if (this.templateConfig instanceof Template template) { template.setResponseFormat(responseFormatJsonSchema); return this.withTemplateConfig(template); @@ -300,17 +301,14 @@ public OrchestrationModuleConfig withResponseJsonSchema( */ @Nonnull public OrchestrationModuleConfig withJsonResponse() { + val responseFormatJsonObject = + ResponseFormatJsonObject.create().type(ResponseFormatJsonObject.TypeEnum.JSON_OBJECT); if (this.templateConfig instanceof Template template) { - template.setResponseFormat( - ResponseFormatJsonObject.create().type(ResponseFormatJsonObject.TypeEnum.JSON_OBJECT)); + template.setResponseFormat(responseFormatJsonObject); return this.withTemplateConfig(template); } val templatingConfig = - Template.create() - .template(List.of()) - .responseFormat( - ResponseFormatJsonObject.create() - .type(ResponseFormatJsonObject.TypeEnum.JSON_OBJECT)); + Template.create().template(List.of()).responseFormat(responseFormatJsonObject); return this.withTemplateConfig(templatingConfig); } } diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java index 5c8b4d0e6..04f312a08 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java @@ -64,7 +64,7 @@ public static ResponseJsonSchema of( /** * Create a new instance of {@link ResponseJsonSchema} from a given class. * - * @param classType + * @param classType The class to generate the schema from * @return The new instance of {@link ResponseJsonSchema} * @since 1.4.0 */ diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java index b838a4fb3..5c7d84612 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java @@ -2,11 +2,52 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchema; +import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchemaJsonSchema; +import com.sap.ai.sdk.orchestration.model.Template; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import lombok.val; import org.junit.jupiter.api.Test; public class OrchestrationConvenienceUnitTest { + static class TestClassForSchemaGeneration { + @JsonProperty(required = true) + private String stringField; + + @JsonProperty(required = true) + private int intField; + + @JsonProperty(required = true) + private InsideTestClass complexField; + + static class InsideTestClass { + @JsonProperty(required = true) + private String anotherStringField; + } + } + + private Map generateSchemaMap() { + var schemaMap = new LinkedHashMap(); + var propertiesMap = new LinkedHashMap(); + var complexFieldMap = new LinkedHashMap(); + complexFieldMap.put("type", "object"); + complexFieldMap.put("properties", Map.of("anotherStringField", Map.of("type", "string"))); + complexFieldMap.put("required", List.of("anotherStringField")); + complexFieldMap.put("additionalProperties", false); + propertiesMap.put("complexField", complexFieldMap); + propertiesMap.put("intField", Map.of("type", "integer")); + propertiesMap.put("stringField", Map.of("type", "string")); + schemaMap.put("type", "object"); + schemaMap.put("properties", propertiesMap); + schemaMap.put("required", List.of("complexField", "intField", "stringField")); + schemaMap.put("additionalProperties", false); + return schemaMap; + } + @Test void testMessageConstructionText() { var userMessageViaStaticFactory = Message.user("Text 1"); @@ -30,4 +71,40 @@ void testMessageConstructionImage() { Message.user("Text 1").withImage("url", ImageItem.DetailLevel.AUTO); assertThat(userMessageWithImage).isEqualTo(userMessageWithImageAndDetail); } + + @Test + void testResponseFormatSchemaConstruction() { + val schemaFromClass = ResponseJsonSchema.from(TestClassForSchemaGeneration.class); + + val schemaMap = generateSchemaMap(); + val schemaFromMap = ResponseJsonSchema.of(schemaMap, "TestClassForSchemaGeneration-Schema"); + + assertThat(schemaFromClass).isEqualTo(schemaFromMap); + } + + @Test + void testConfigWithResponseSchema() { + val schemaFromClass = ResponseJsonSchema.from(TestClassForSchemaGeneration.class); + val schemaMap = generateSchemaMap(); + + var configWithResponseSchemaFromClass = + new OrchestrationModuleConfig() + .withJsonSchemaResponse( + schemaFromClass.withDescription("Description").withStrict(true)); + var configWithResponseSchemaLowLevel = + new OrchestrationModuleConfig() + .withTemplateConfig( + Template.create() + .template(List.of()) + .responseFormat( + ResponseFormatJsonSchema.create() + .type(ResponseFormatJsonSchema.TypeEnum.JSON_SCHEMA) + .jsonSchema( + ResponseFormatJsonSchemaJsonSchema.create() + .name("TestClassForSchemaGeneration-Schema") + .schema(schemaMap) + .strict(true) + .description("Description")))); + assertThat(configWithResponseSchemaFromClass).isEqualTo(configWithResponseSchemaLowLevel); + } } diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java index f81300cb4..8a6f37021 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java @@ -8,17 +8,41 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.fasterxml.jackson.annotation.JsonProperty; import com.sap.ai.sdk.orchestration.model.DPIConfig; import com.sap.ai.sdk.orchestration.model.DPIEntities; import com.sap.ai.sdk.orchestration.model.DocumentGroundingFilter; import com.sap.ai.sdk.orchestration.model.GroundingModuleConfigConfig; import com.sap.ai.sdk.orchestration.model.GroundingModuleConfigConfigFiltersInner; +import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonObject; +import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchema; +import com.sap.ai.sdk.orchestration.model.SingleChatMessage; +import com.sap.ai.sdk.orchestration.model.Template; +import com.sap.ai.sdk.orchestration.model.TemplateRef; +import com.sap.ai.sdk.orchestration.model.TemplateRefByID; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; class OrchestrationModuleConfigTest { + static class TestClassForSchemaGeneration { + @JsonProperty(required = true) + private String stringField; + + @JsonProperty(required = true) + private int intField; + + @JsonProperty(required = true) + private OrchestrationConvenienceUnitTest.TestClassForSchemaGeneration.InsideTestClass + complexField; + + static class InsideTestClass { + @JsonProperty(required = true) + private String anotherStringField; + } + } + @Test void testStackingInputAndOutputFilter() { final var config = new OrchestrationModuleConfig().withLlmConfig(GPT_4O); @@ -173,4 +197,100 @@ void testGroundingPrompt() { .isEqualTo( "{{?userMessage}} Use the following information as additional context: {{?groundingContext}}"); } + + @Test + void testResponseFormatNotOverwrittenWithNewTemplateConfig() { + var schema = ResponseJsonSchema.from(TestClassForSchemaGeneration.class); + var config = new OrchestrationModuleConfig().withJsonSchemaResponse(schema); + assertThat(((Template) config.getTemplateConfig())).isNotNull(); + assertThat( + ((ResponseFormatJsonSchema) ((Template) config.getTemplateConfig()).getResponseFormat()) + .getJsonSchema() + .getSchema()) + .isEqualTo(schema.getSchemaMap()); + + config = + config.withTemplateConfig( + Template.create() + .template(SingleChatMessage.create().role("user").content("Hello, World!"))); + assertThat(((Template) config.getTemplateConfig())).isNotNull(); + assertThat( + ((ResponseFormatJsonSchema) ((Template) config.getTemplateConfig()).getResponseFormat()) + .getJsonSchema() + .getSchema()) + .isEqualTo(schema.getSchemaMap()); + + config = config.withTemplateConfig(null); + assertThat(((Template) config.getTemplateConfig())).isNotNull(); + assertThat( + ((ResponseFormatJsonSchema) ((Template) config.getTemplateConfig()).getResponseFormat()) + .getJsonSchema() + .getSchema()) + .isEqualTo(schema.getSchemaMap()); + } + + @Test + void testResponseFormatOverwrittenByNewTemplateConfigWithResponseFormat() { + var schema = ResponseJsonSchema.from(TestClassForSchemaGeneration.class); + var config = new OrchestrationModuleConfig().withJsonSchemaResponse(schema); + assertThat(((Template) config.getTemplateConfig())).isNotNull(); + assertThat( + ((ResponseFormatJsonSchema) ((Template) config.getTemplateConfig()).getResponseFormat()) + .getJsonSchema() + .getSchema()) + .isEqualTo(schema.getSchemaMap()); + + config = + config.withTemplateConfig( + Template.create() + .template(SingleChatMessage.create().role("user").content("Hello, World!")) + .responseFormat( + ResponseFormatJsonObject.create() + .type(ResponseFormatJsonObject.TypeEnum.JSON_OBJECT))); + assertThat(((Template) config.getTemplateConfig())).isNotNull(); + assertThat(((Template) config.getTemplateConfig()).getResponseFormat()) + .isInstanceOf(ResponseFormatJsonObject.class); + } + + @Test + void testResponseFormatOverwrittenByNewTemplateRef() { + var schema = ResponseJsonSchema.from(TestClassForSchemaGeneration.class); + var config = new OrchestrationModuleConfig().withJsonSchemaResponse(schema); + assertThat(((Template) config.getTemplateConfig())).isNotNull(); + assertThat( + ((ResponseFormatJsonSchema) ((Template) config.getTemplateConfig()).getResponseFormat()) + .getJsonSchema() + .getSchema()) + .isEqualTo(schema.getSchemaMap()); + + config = + config.withTemplateConfig( + TemplateRef.create().templateRef(TemplateRefByID.create().id("123"))); + assertThat(config.getTemplateConfig()).isInstanceOf(TemplateRef.class); + } + + @Test + void testResponseFormatOverwrittenByOtherResponseFormat() { + var schema = ResponseJsonSchema.from(TestClassForSchemaGeneration.class); + var config = new OrchestrationModuleConfig().withJsonSchemaResponse(schema); + assertThat(((Template) config.getTemplateConfig())).isNotNull(); + assertThat( + ((ResponseFormatJsonSchema) ((Template) config.getTemplateConfig()).getResponseFormat()) + .getJsonSchema() + .getSchema()) + .isEqualTo(schema.getSchemaMap()); + + config = config.withJsonResponse(); + assertThat(((Template) config.getTemplateConfig())).isNotNull(); + assertThat(((Template) config.getTemplateConfig()).getResponseFormat()) + .isInstanceOf(ResponseFormatJsonObject.class); + + config = config.withJsonSchemaResponse(schema); + assertThat(((Template) config.getTemplateConfig())).isNotNull(); + assertThat( + ((ResponseFormatJsonSchema) ((Template) config.getTemplateConfig()).getResponseFormat()) + .getJsonSchema() + .getSchema()) + .isEqualTo(schema.getSchemaMap()); + } } diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java index 9a19106e6..e9b8d0049 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java @@ -786,7 +786,7 @@ class Translation { ResponseJsonSchema.from(Translation.class) .withDescription("Output schema for language translation.") .withStrict(true); - val configWithResponseSchema = gpt4oCustomInstance.withResponseJsonSchema(schema); + val configWithResponseSchema = gpt4oCustomInstance.withJsonSchemaResponse(schema); val prompt = new OrchestrationPrompt( diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java index ece89e7a1..cfdba78e7 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java @@ -352,8 +352,6 @@ public OrchestrationChatResponse grounding(@Nonnull final String userMessage) { public OrchestrationChatResponse responseFormatJsonSchema(@Nonnull final String word) { val gpt4oCustomInstance = new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI); - val template = Message.user("Whats '%s' in German?".formatted(word)); - // Example class class Translation { @JsonProperty(required = true) @@ -367,11 +365,11 @@ class Translation { ResponseJsonSchema.from(Translation.class) .withDescription("Output schema for language translation.") .withStrict(true); - val configWithResponseSchema = gpt4oCustomInstance.withResponseJsonSchema(schema); + val configWithResponseSchema = gpt4oCustomInstance.withJsonSchemaResponse(schema); val prompt = new OrchestrationPrompt( - Message.user("Whats 'apple' in German?"), + Message.user("Whats '%s' in German?".formatted(word)), Message.system("You are a language translator.")); return client.chatCompletion(prompt, configWithResponseSchema); diff --git a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java index 082138380..8b3999319 100644 --- a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java +++ b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java @@ -301,8 +301,6 @@ void testResponseFormatJsonSchema() { final var result = service.responseFormatJsonSchema("apple").getOriginalResponse(); final var choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices(); assertThat(choices.get(0).getMessage().getContent()).isNotEmpty(); - assertThat(choices.get(0).getMessage().getContent()).contains("\"language\":\"German\""); - assertThat(choices.get(0).getMessage().getContent()).contains("\"translation\":\"Apfel\""); } @Test From 1ff4d5ae99aa70e5900cfc63114c8b80908bd866 Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Wed, 19 Feb 2025 09:43:57 +0100 Subject: [PATCH 04/23] Add documentation etc. --- docs/guides/ORCHESTRATION_CHAT_COMPLETION.md | 51 +++++--------------- docs/release-notes/release_notes.md | 2 +- 2 files changed, 12 insertions(+), 41 deletions(-) diff --git a/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md b/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md index 9179055ce..fdd192620 100644 --- a/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md +++ b/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md @@ -311,20 +311,12 @@ It is possible to set the response format for the chat completion. Available opt Setting the response format to `JSON_OBJECT` tells the AI to respond with JSON, i.e., the response from the AI will be a string consisting of a valid JSON. This does, however, not guarantee that the response adheres to a specific structure (other than being valid JSON). ```java -var template = Message.user("What is 'apple' in German?"); -var templatingConfig = - Template.create() - .template(List.of(template.createChatMessage())) - .responseFormat( - ResponseFormatJsonObject.create() - .type(ResponseFormatJsonObject.TypeEnum.JSON_OBJECT)); -var configWithTemplate = llmWithImageSupportConfig.withTemplateConfig(templatingConfig); +var configWithJsonResponse = config.withJsonResponse(); var prompt = new OrchestrationPrompt( - Message.system( - "You are a language translator. Answer using the following JSON format: {\"language\": ..., \"translation\": ...}")); -var response = client.chatCompletion(prompt, configWithTemplate).getContent(); + Message.user("Some message."), Message.system("Answer using JSON.")); +var response = client.chatCompletion(prompt, configWithJsonResponse).getContent(); ``` Note, that it is necessary to tell the AI model to actually return a JSON object in the prompt. The result might not adhere exactly to the given JSON format, but it will be a JSON object. @@ -334,38 +326,17 @@ Note, that it is necessary to tell the AI model to actually return a JSON object If you want the response to not only consist of valid JSON but additionally adhere to a specific JSON schema, you can use `JSON_SCHEMA`. in order to do that, add a JSON schema to the configuration as shown below and the response will adhere to the given schema. ```java -var template = Message.user("Whats '%s' in German?".formatted(word)); var schema = - Map.of( - "type", - "object", - "properties", - Map.of( - "language", Map.of("type", "string"), - "translation", Map.of("type", "string")), - "required", - List.of("language", "translation"), - "additionalProperties", - false); - -// Note, that we plan to add more convenient ways to add a JSON schema in the future. -var templatingConfig = - Template.create() - .template(List.of(template.createChatMessage())) - .responseFormat( - ResponseFormatJsonSchema.create() - .type(ResponseFormatJsonSchema.TypeEnum.JSON_SCHEMA) - .jsonSchema( - ResponseFormatJsonSchemaJsonSchema.create() - .name("translation_response") - .schema(schema) - .strict(true) - .description("Output schema for language translation."))); -var configWithTemplate = llmWithImageSupportConfig.withTemplateConfig(templatingConfig); - -var prompt = new OrchestrationPrompt(Message.system("You are a language translator.")); + ResponseJsonSchema.from(MyClass.class) + .withDescription("Output schema for the example class MyClass.") + .withStrict(true); +var configWithResponseSchema = config.withJsonSchemaResponse(schema); + +var prompt = + new OrchestrationPrompt(Message.user("Some message.")); var response = client.chatCompletion(prompt, configWithTemplate).getContent(); ``` +Note, that the LLM will only exactly adhere to the given schema if you use `withStrict(true)`. Not all schemas are possible for OpenAI in strict mode. See [here](https://platform.openai.com/docs/guides/structured-outputs#supported-schemas) for more information. Please find [an example in our Spring Boot application](../../sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java) diff --git a/docs/release-notes/release_notes.md b/docs/release-notes/release_notes.md index 27fd11ee5..76a38d8dc 100644 --- a/docs/release-notes/release_notes.md +++ b/docs/release-notes/release_notes.md @@ -12,7 +12,7 @@ ### ✨ New Functionality -- +- [Add new convenient methods to set the response format for Orchestration.](https://github.com/SAP/ai-sdk-java/tree/main/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md#set-a-response-format) ### 📈 Improvements From 79865aa1b77924aebb842cacf314a1d739500fbf Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Wed, 19 Feb 2025 10:07:11 +0100 Subject: [PATCH 05/23] Small fixes --- .../com/sap/ai/sdk/orchestration/ResponseJsonSchema.java | 5 ----- .../ai/sdk/orchestration/OrchestrationModuleConfigTest.java | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java index 04f312a08..8131113d9 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java @@ -70,7 +70,6 @@ public static ResponseJsonSchema of( */ @Nonnull public static ResponseJsonSchema from(@Nonnull final Type classType) { - // Build JSON schema from class val module = new JacksonModule( JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, JacksonOption.RESPECT_JSONPROPERTY_ORDER); @@ -82,13 +81,9 @@ public static ResponseJsonSchema from(@Nonnull final Type classType) { .with(module) .build()); val jsonSchema = generator.generateSchema(classType); - - // Convert JSON schema to Map val mapper = new ObjectMapper(); final Map schemaMap = mapper.convertValue(jsonSchema, new TypeReference<>() {}); - val schemaName = ((Class) classType).getSimpleName() + "-Schema"; - return new ResponseJsonSchema(schemaMap, schemaName, null, null); } } diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java index 8a6f37021..c6432adc0 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java @@ -199,7 +199,7 @@ void testGroundingPrompt() { } @Test - void testResponseFormatNotOverwrittenWithNewTemplateConfig() { + void testResponseFormatNotOverwrittenByNewTemplateConfig() { var schema = ResponseJsonSchema.from(TestClassForSchemaGeneration.class); var config = new OrchestrationModuleConfig().withJsonSchemaResponse(schema); assertThat(((Template) config.getTemplateConfig())).isNotNull(); From 05203396e076fc4b7f540a1c5346d838c0d1919a Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Wed, 19 Feb 2025 15:08:55 +0100 Subject: [PATCH 06/23] Improve documentation --- docs/guides/ORCHESTRATION_CHAT_COMPLETION.md | 34 ++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md b/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md index fdd192620..9c5e20d0e 100644 --- a/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md +++ b/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md @@ -311,6 +311,8 @@ It is possible to set the response format for the chat completion. Available opt Setting the response format to `JSON_OBJECT` tells the AI to respond with JSON, i.e., the response from the AI will be a string consisting of a valid JSON. This does, however, not guarantee that the response adheres to a specific structure (other than being valid JSON). ```java +var config = new OrchestrationModuleConfig() + .withLlmConfig(OrchestrationAiModel.GPT_4O); var configWithJsonResponse = config.withJsonResponse(); var prompt = @@ -330,14 +332,42 @@ var schema = ResponseJsonSchema.from(MyClass.class) .withDescription("Output schema for the example class MyClass.") .withStrict(true); +var config = new OrchestrationModuleConfig() + .withLlmConfig(OrchestrationAiModel.GPT_4O); var configWithResponseSchema = config.withJsonSchemaResponse(schema); -var prompt = - new OrchestrationPrompt(Message.user("Some message.")); +var prompt = new OrchestrationPrompt(Message.user("Some message.")); var response = client.chatCompletion(prompt, configWithTemplate).getContent(); ``` Note, that the LLM will only exactly adhere to the given schema if you use `withStrict(true)`. Not all schemas are possible for OpenAI in strict mode. See [here](https://platform.openai.com/docs/guides/structured-outputs#supported-schemas) for more information. +There is also a way to generate the schema from a map of key-value pairs. This can be done as follows: +
Click to expand code + +```java +var schemaMap = + Map.of( + "type", + "object", + "properties", + Map.of( + "language", Map.of("type", "string"), + "translation", Map.of("type", "string")), + "required", + List.of("language", "translation"), + "additionalProperties", + false); +var schemaFromMap = ResponseJsonSchema.of(schemaMap, "Translator-Schema"); +var config = new OrchestrationModuleConfig() + .withLlmConfig(OrchestrationAiModel.GPT_4O); +var configWithResponseSchema = config.withJsonSchemaResponse(schemaFromMap); + +var prompt = new OrchestrationPrompt(Message.user("Some message.")); +var response = client.chatCompletion(prompt, configWithTemplate).getContent(); +``` + +
+ Please find [an example in our Spring Boot application](../../sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java) ## Set model parameters From b9d5d9a054aa26f68a25f7daad091e31ae46c30c Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Wed, 19 Feb 2025 17:27:10 +0100 Subject: [PATCH 07/23] Requested changes --- .../OrchestrationModuleConfig.java | 61 +++++++++++++------ .../sdk/orchestration/ResponseJsonSchema.java | 24 ++++++++ .../OrchestrationModuleConfigTest.java | 9 +-- 3 files changed, 71 insertions(+), 23 deletions(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java index 7b6cc03e3..3e62ef668 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java @@ -233,30 +233,51 @@ public OrchestrationModuleConfig withGrounding( @Nonnull public OrchestrationModuleConfig withTemplateConfig( @Nullable final TemplatingModuleConfig templateConfig) { - - // If new templateConfig is a TemplateRef, use it. if (templateConfig instanceof TemplateRef) { return new OrchestrationModuleConfig( llmConfig, templateConfig, maskingConfig, filteringConfig, groundingConfig); } - - // Make sure old responseFormat is only overwritten if new templateConfig has one set. - var newTemplate = (Template) templateConfig; - TemplateResponseFormat responseFormat = null; - if (this.templateConfig instanceof Template oldTemplate) { - responseFormat = oldTemplate.getResponseFormat(); + if (templateConfig == null) { + return handleNullTemplateConfig(); } - // Template.getResponseFormat() might return null, so the following check is necessary. - if (newTemplate != null && newTemplate.getResponseFormat() == null) { - newTemplate.setResponseFormat(responseFormat); - } - if (newTemplate == null && responseFormat != null) { - newTemplate = Template.create().template(List.of()); + return handleNonNullTemplateConfig((Template) templateConfig); + } + + @Nonnull + private OrchestrationModuleConfig handleNullTemplateConfig() { + if (getResponseFormatIfExists(this.templateConfig) != null) { + val responseFormat = ((Template) this.templateConfig).getResponseFormat(); + val newTemplate = Template.create().template(List.of()); newTemplate.setResponseFormat(responseFormat); + return new OrchestrationModuleConfig( + llmConfig, newTemplate, maskingConfig, filteringConfig, groundingConfig); } + return new OrchestrationModuleConfig( + llmConfig, null, maskingConfig, filteringConfig, groundingConfig); + } + @Nonnull + private OrchestrationModuleConfig handleNonNullTemplateConfig(final Template newTemplate) { + if (getResponseFormatIfExists(newTemplate) != null) { + return new OrchestrationModuleConfig( + llmConfig, newTemplate, maskingConfig, filteringConfig, groundingConfig); + } + val responseFormat = getResponseFormatIfExists(this.templateConfig); return new OrchestrationModuleConfig( - llmConfig, newTemplate, maskingConfig, filteringConfig, groundingConfig); + llmConfig, + ResponseJsonSchema.newTemplateWithResponseFormat(newTemplate, responseFormat), + maskingConfig, + filteringConfig, + groundingConfig); + } + + @Nullable + private static TemplateResponseFormat getResponseFormatIfExists( + final TemplatingModuleConfig templateConfig) { + if (templateConfig instanceof Template template) { + return template.getResponseFormat(); + } + return null; } /** @@ -282,8 +303,9 @@ public OrchestrationModuleConfig withJsonSchemaResponse( .strict(schema.getIsStrict()) .description(schema.getDescription())); if (this.templateConfig instanceof Template template) { - template.setResponseFormat(responseFormatJsonSchema); - return this.withTemplateConfig(template); + val newTemplate = + ResponseJsonSchema.newTemplateWithResponseFormat(template, responseFormatJsonSchema); + return this.withTemplateConfig(newTemplate); } val templatingConfig = Template.create().template(List.of()).responseFormat(responseFormatJsonSchema); @@ -304,8 +326,9 @@ public OrchestrationModuleConfig withJsonResponse() { val responseFormatJsonObject = ResponseFormatJsonObject.create().type(ResponseFormatJsonObject.TypeEnum.JSON_OBJECT); if (this.templateConfig instanceof Template template) { - template.setResponseFormat(responseFormatJsonObject); - return this.withTemplateConfig(template); + val newTemplate = + ResponseJsonSchema.newTemplateWithResponseFormat(template, responseFormatJsonObject); + return this.withTemplateConfig(newTemplate); } val templatingConfig = Template.create().template(List.of()).responseFormat(responseFormatJsonObject); diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java index 8131113d9..a30977b95 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java @@ -9,6 +9,8 @@ import com.github.victools.jsonschema.generator.SchemaVersion; import com.github.victools.jsonschema.module.jackson.JacksonModule; import com.github.victools.jsonschema.module.jackson.JacksonOption; +import com.sap.ai.sdk.orchestration.model.Template; +import com.sap.ai.sdk.orchestration.model.TemplateResponseFormat; import java.lang.reflect.Type; import java.util.Map; import javax.annotation.Nonnull; @@ -86,4 +88,26 @@ public static ResponseJsonSchema from(@Nonnull final Type classType) { val schemaName = ((Class) classType).getSimpleName() + "-Schema"; return new ResponseJsonSchema(schemaMap, schemaName, null, null); } + + /** + * Create a new instance of a {@link Template} that is a copy of the given one but with the given + * response format. + * + * @param originalTemplate The template to copy + * @param responseFormat The response format to set in the copy + * @return The new instance of {@link Template} + */ + @Nonnull + static Template newTemplateWithResponseFormat( + @Nonnull final Template originalTemplate, + @Nullable final TemplateResponseFormat responseFormat) { + val newTemplate = Template.create().template(originalTemplate.getTemplate()); + newTemplate.setDefaults(originalTemplate.getDefaults()); + newTemplate.setTools(originalTemplate.getTools()); + for (val customFieldName : originalTemplate.getCustomFieldNames()) { + newTemplate.setCustomField(customFieldName, originalTemplate.getCustomField(customFieldName)); + } + newTemplate.setResponseFormat(responseFormat); + return newTemplate; + } } diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java index c6432adc0..0c57d6269 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java @@ -209,10 +209,11 @@ void testResponseFormatNotOverwrittenByNewTemplateConfig() { .getSchema()) .isEqualTo(schema.getSchemaMap()); - config = - config.withTemplateConfig( - Template.create() - .template(SingleChatMessage.create().role("user").content("Hello, World!"))); + var template = + Template.create() + .template(SingleChatMessage.create().role("user").content("Hello, World!")); + template.setCustomField("field", 1); + config = config.withTemplateConfig(template); assertThat(((Template) config.getTemplateConfig())).isNotNull(); assertThat( ((ResponseFormatJsonSchema) ((Template) config.getTemplateConfig()).getResponseFormat()) From 6eb21bd236700463040bad45e4f6c114731b711d Mon Sep 17 00:00:00 2001 From: Jonas-Isr Date: Thu, 20 Feb 2025 10:04:55 +0100 Subject: [PATCH 08/23] Update orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java Co-authored-by: Charles Dubois <103174266+CharlesDuboisSAP@users.noreply.github.com> --- .../com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java index 3e62ef668..7a1d0215d 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java @@ -272,7 +272,7 @@ private OrchestrationModuleConfig handleNonNullTemplateConfig(final Template new } @Nullable - private static TemplateResponseFormat getResponseFormatIfExists( + private static TemplateResponseFormat getResponseFormat( final TemplatingModuleConfig templateConfig) { if (templateConfig instanceof Template template) { return template.getResponseFormat(); From 9b1482d3c50aac7621a0fcbb240b36da6d7b9979 Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Thu, 20 Feb 2025 10:09:09 +0100 Subject: [PATCH 09/23] Small fix --- .../sap/ai/sdk/orchestration/OrchestrationModuleConfig.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java index 7a1d0215d..a8c1d0837 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java @@ -245,7 +245,7 @@ public OrchestrationModuleConfig withTemplateConfig( @Nonnull private OrchestrationModuleConfig handleNullTemplateConfig() { - if (getResponseFormatIfExists(this.templateConfig) != null) { + if (getResponseFormat(this.templateConfig) != null) { val responseFormat = ((Template) this.templateConfig).getResponseFormat(); val newTemplate = Template.create().template(List.of()); newTemplate.setResponseFormat(responseFormat); @@ -258,11 +258,11 @@ private OrchestrationModuleConfig handleNullTemplateConfig() { @Nonnull private OrchestrationModuleConfig handleNonNullTemplateConfig(final Template newTemplate) { - if (getResponseFormatIfExists(newTemplate) != null) { + if (getResponseFormat(newTemplate) != null) { return new OrchestrationModuleConfig( llmConfig, newTemplate, maskingConfig, filteringConfig, groundingConfig); } - val responseFormat = getResponseFormatIfExists(this.templateConfig); + val responseFormat = getResponseFormat(this.templateConfig); return new OrchestrationModuleConfig( llmConfig, ResponseJsonSchema.newTemplateWithResponseFormat(newTemplate, responseFormat), From b7824d0af1fe2b46ec12baa93a50a49e13e5276f Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Mon, 24 Feb 2025 10:39:28 +0100 Subject: [PATCH 10/23] WIP --- .../orchestration/OrchestrationTemplate.java | 36 ++++++++++++ .../OrchestrationTemplateReference.java | 19 +++++++ .../ai/sdk/orchestration/TemplateConfig.java | 56 +++++++++++++++++++ .../orchestration/OrchestrationUnitTest.java | 8 +-- .../app/services/OrchestrationService.java | 4 +- .../app/controllers/OrchestrationTest.java | 2 + 6 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java create mode 100644 orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java create mode 100644 orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java new file mode 100644 index 000000000..8d3a7d8e6 --- /dev/null +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java @@ -0,0 +1,36 @@ +package com.sap.ai.sdk.orchestration; + +import com.sap.ai.sdk.orchestration.model.ChatCompletionTool; +import com.sap.ai.sdk.orchestration.model.ChatMessage; +import com.sap.ai.sdk.orchestration.model.SingleChatMessage; +import com.sap.ai.sdk.orchestration.model.Template; +import com.sap.ai.sdk.orchestration.model.TemplateResponseFormat; +import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.With; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@EqualsAndHashCode(callSuper = true) +@Value +@With +//@AllArgsConstructor(access = AccessLevel.NONE) +public class OrchestrationTemplate extends TemplateConfig{ + List template = new ArrayList<>(); + Map defaults = new HashMap<>(); + TemplateResponseFormat responseFormat = null; + List tools = new ArrayList<>(); + Map cloudSdkCustomFields = new LinkedHashMap<>(); + + @Override + protected TemplatingModuleConfig toLowLevel() { + return Template.create().template(template).defaults(defaults).responseFormat(responseFormat).tools(tools); + } +} diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java new file mode 100644 index 000000000..e5a4ef39f --- /dev/null +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java @@ -0,0 +1,19 @@ +package com.sap.ai.sdk.orchestration; + +import com.sap.ai.sdk.orchestration.model.TemplateRefTemplateRef; +import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig; +import lombok.Value; + +@Value +public class OrchestrationTemplateReference extends TemplateConfig{ + TemplateRefTemplateRef reference; + + public OrchestrationTemplateReference(TemplateRefTemplateRef reference) { + this.reference = reference; + } + + @Override + protected TemplatingModuleConfig toLowLevel() { + return null; + } +} diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java new file mode 100644 index 000000000..d377c4abb --- /dev/null +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java @@ -0,0 +1,56 @@ +package com.sap.ai.sdk.orchestration; + +import com.sap.ai.sdk.orchestration.model.TemplateRefByID; +import com.sap.ai.sdk.orchestration.model.TemplateRefByScenarioNameVersion; +import com.sap.ai.sdk.orchestration.model.TemplateRefTemplateRef; +import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig; + +public abstract class TemplateConfig { + abstract protected TemplatingModuleConfig toLowLevel(); + // factory method + public static OrchestrationTemplate create() { + return new OrchestrationTemplate(); + } +// // optional factory method? +// public static OrchestrationTemplate jsonResponse() { +// return create().withResponseFormat("json"); +// } + // factory method + public static ReferenceBuilder reference() { + return new ReferenceBuilder(); + } + + public static class ReferenceBuilder { + OrchestrationTemplateReference byId(String id) { + return new OrchestrationTemplateReference(TemplateRefByID.create().id(id)); + } + + ReferenceBuilder1 byScenarioNameVersion(String scenario){ + return new ReferenceBuilder1(scenario); + } + } + + public static class ReferenceBuilder1 { + private final String scenario; + + ReferenceBuilder1(String scenario) { + this.scenario = scenario; + } + ReferenceBuilder2 name(String name) { + return new ReferenceBuilder2(scenario, name); + } + } + + public static class ReferenceBuilder2 { + private final String scenario; + private final String name; + + ReferenceBuilder2(String scenario, String name) { + this.scenario = scenario; + this.name = name; + } + OrchestrationTemplateReference version(String version) { + return new OrchestrationTemplateReference(TemplateRefByScenarioNameVersion.create().scenario(scenario).name(name).version(version)); + } + } +} diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java index e9b8d0049..c8148b1be 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java @@ -772,7 +772,7 @@ void testResponseFormatJsonSchema() throws IOException { .withBodyFile("jsonSchemaResponse.json") .withHeader("Content-Type", "application/json"))); - var gpt4oCustomInstance = new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI); + var config = new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI); // Example class class Translation { @@ -786,7 +786,7 @@ class Translation { ResponseJsonSchema.from(Translation.class) .withDescription("Output schema for language translation.") .withStrict(true); - val configWithResponseSchema = gpt4oCustomInstance.withJsonSchemaResponse(schema); + val configWithResponseSchema = config.withJsonSchemaResponse(schema); val prompt = new OrchestrationPrompt( @@ -811,9 +811,9 @@ void testResponseFormatJsonObject() throws IOException { .withBodyFile("jsonObjectResponse.json") .withHeader("Content-Type", "application/json"))); - val gpt4oCustomInstance = new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI); + val config = new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI); - val configWithJsonResponse = gpt4oCustomInstance.withJsonResponse(); + val configWithJsonResponse = config.withJsonResponse(); val prompt = new OrchestrationPrompt( diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java index cfdba78e7..8a8130e54 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java @@ -350,7 +350,7 @@ public OrchestrationChatResponse grounding(@Nonnull final String userMessage) { */ @Nonnull public OrchestrationChatResponse responseFormatJsonSchema(@Nonnull final String word) { - val gpt4oCustomInstance = new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI); + val config = new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI); // Example class class Translation { @@ -365,7 +365,7 @@ class Translation { ResponseJsonSchema.from(Translation.class) .withDescription("Output schema for language translation.") .withStrict(true); - val configWithResponseSchema = gpt4oCustomInstance.withJsonSchemaResponse(schema); + val configWithResponseSchema = config.withJsonSchemaResponse(schema); val prompt = new OrchestrationPrompt( diff --git a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java index 8b3999319..e8fb3fc8f 100644 --- a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java +++ b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java @@ -308,6 +308,8 @@ void testResponseFormatJsonObject() { final var result = service.responseFormatJsonObject("apple").getOriginalResponse(); final var choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices(); assertThat(choices.get(0).getMessage().getContent()).isNotEmpty(); + assertThat(choices.get(0).getMessage().getContent()).contains("\"language\":"); + assertThat(choices.get(0).getMessage().getContent()).contains("\"translation\":"); } @Test From 5ee6cea937eebe0bccbaf18cc20caaaca2a0d397 Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Wed, 26 Feb 2025 17:29:52 +0100 Subject: [PATCH 11/23] Introduce new class TemplateConfig and adapt tests --- .../OrchestrationModuleConfig.java | 112 +----------------- .../orchestration/OrchestrationTemplate.java | 45 ++++++- .../sdk/orchestration/ResponseJsonSchema.java | 2 +- .../ai/sdk/orchestration/TemplateConfig.java | 15 +-- .../OrchestrationConvenienceUnitTest.java | 6 +- .../OrchestrationModuleConfigTest.java | 50 ++------ .../orchestration/OrchestrationUnitTest.java | 6 +- .../app/services/OrchestrationService.java | 4 +- 8 files changed, 76 insertions(+), 164 deletions(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java index a8c1d0837..b99c34cb8 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java @@ -6,11 +6,7 @@ import com.sap.ai.sdk.orchestration.model.LLMModuleConfig; import com.sap.ai.sdk.orchestration.model.MaskingModuleConfig; import com.sap.ai.sdk.orchestration.model.OutputFilteringConfig; -import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonObject; -import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchema; -import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchemaJsonSchema; import com.sap.ai.sdk.orchestration.model.Template; -import com.sap.ai.sdk.orchestration.model.TemplateRef; import com.sap.ai.sdk.orchestration.model.TemplateResponseFormat; import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig; import java.util.ArrayList; @@ -68,7 +64,7 @@ public class OrchestrationModuleConfig { * @link SAP * AI Core: Orchestration - Templating */ - @With(AccessLevel.NONE) + @With @Nullable TemplatingModuleConfig templateConfig; @@ -222,7 +218,7 @@ public OrchestrationModuleConfig withGrounding( } /** - * Creates a new configuration with the given template configuration. + * Creates a new configuration with the given template configuration as {@link TemplateConfig}. * * @link SAP * AI Core: Orchestration - Templating @@ -231,107 +227,7 @@ public OrchestrationModuleConfig withGrounding( */ @Tolerate @Nonnull - public OrchestrationModuleConfig withTemplateConfig( - @Nullable final TemplatingModuleConfig templateConfig) { - if (templateConfig instanceof TemplateRef) { - return new OrchestrationModuleConfig( - llmConfig, templateConfig, maskingConfig, filteringConfig, groundingConfig); + public OrchestrationModuleConfig withTemplateConfig(@Nonnull TemplateConfig templateConfig) { + return this.withTemplateConfig(templateConfig.toLowLevel()); } - if (templateConfig == null) { - return handleNullTemplateConfig(); - } - return handleNonNullTemplateConfig((Template) templateConfig); - } - - @Nonnull - private OrchestrationModuleConfig handleNullTemplateConfig() { - if (getResponseFormat(this.templateConfig) != null) { - val responseFormat = ((Template) this.templateConfig).getResponseFormat(); - val newTemplate = Template.create().template(List.of()); - newTemplate.setResponseFormat(responseFormat); - return new OrchestrationModuleConfig( - llmConfig, newTemplate, maskingConfig, filteringConfig, groundingConfig); - } - return new OrchestrationModuleConfig( - llmConfig, null, maskingConfig, filteringConfig, groundingConfig); - } - - @Nonnull - private OrchestrationModuleConfig handleNonNullTemplateConfig(final Template newTemplate) { - if (getResponseFormat(newTemplate) != null) { - return new OrchestrationModuleConfig( - llmConfig, newTemplate, maskingConfig, filteringConfig, groundingConfig); - } - val responseFormat = getResponseFormat(this.templateConfig); - return new OrchestrationModuleConfig( - llmConfig, - ResponseJsonSchema.newTemplateWithResponseFormat(newTemplate, responseFormat), - maskingConfig, - filteringConfig, - groundingConfig); - } - - @Nullable - private static TemplateResponseFormat getResponseFormat( - final TemplatingModuleConfig templateConfig) { - if (templateConfig instanceof Template template) { - return template.getResponseFormat(); - } - return null; - } - - /** - * Creates a new configuration with the given response schema. - * - * @link SAP - * AI Core: Orchestration - Structured Output - * @param schema The response schema to use. - * @return A new configuration with the given response schema. - * @since 1.4.0 - */ - @Nonnull - public OrchestrationModuleConfig withJsonSchemaResponse( - @Nonnull final ResponseJsonSchema schema) { - val responseFormatJsonSchema = - ResponseFormatJsonSchema.create() - .type(ResponseFormatJsonSchema.TypeEnum.JSON_SCHEMA) - .jsonSchema( - ResponseFormatJsonSchemaJsonSchema.create() - .name(schema.getName()) - .schema(schema.getSchemaMap()) - .strict(schema.getIsStrict()) - .description(schema.getDescription())); - if (this.templateConfig instanceof Template template) { - val newTemplate = - ResponseJsonSchema.newTemplateWithResponseFormat(template, responseFormatJsonSchema); - return this.withTemplateConfig(newTemplate); - } - val templatingConfig = - Template.create().template(List.of()).responseFormat(responseFormatJsonSchema); - return this.withTemplateConfig(templatingConfig); - } - - /** - * Creates a new configuration where the response format is set to JSON object. - * - * @link SAP - * AI Core: Orchestration - Structured Output - * @return A new configuration with response format JSON object. - * @since 1.4.0 - */ - @Nonnull - public OrchestrationModuleConfig withJsonResponse() { - val responseFormatJsonObject = - ResponseFormatJsonObject.create().type(ResponseFormatJsonObject.TypeEnum.JSON_OBJECT); - if (this.templateConfig instanceof Template template) { - val newTemplate = - ResponseJsonSchema.newTemplateWithResponseFormat(template, responseFormatJsonObject); - return this.withTemplateConfig(newTemplate); - } - val templatingConfig = - Template.create().template(List.of()).responseFormat(responseFormatJsonObject); - return this.withTemplateConfig(templatingConfig); - } } diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java index 8d3a7d8e6..fd0d42e8e 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java @@ -2,15 +2,19 @@ import com.sap.ai.sdk.orchestration.model.ChatCompletionTool; import com.sap.ai.sdk.orchestration.model.ChatMessage; -import com.sap.ai.sdk.orchestration.model.SingleChatMessage; +import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonObject; +import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchema; +import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchemaJsonSchema; import com.sap.ai.sdk.orchestration.model.Template; import com.sap.ai.sdk.orchestration.model.TemplateResponseFormat; import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; import lombok.Value; import lombok.With; +import lombok.val; import java.util.ArrayList; import java.util.HashMap; @@ -21,16 +25,45 @@ @EqualsAndHashCode(callSuper = true) @Value @With -//@AllArgsConstructor(access = AccessLevel.NONE) -public class OrchestrationTemplate extends TemplateConfig{ - List template = new ArrayList<>(); +@AllArgsConstructor( + access = AccessLevel.PRIVATE) // TODO: why do we need PRIVATE here instead of NONE? +@NoArgsConstructor(force = true, access = AccessLevel.PACKAGE) +public class OrchestrationTemplate extends TemplateConfig { + List template; Map defaults = new HashMap<>(); - TemplateResponseFormat responseFormat = null; + + @With(AccessLevel.PACKAGE) + TemplateResponseFormat responseFormat; + List tools = new ArrayList<>(); Map cloudSdkCustomFields = new LinkedHashMap<>(); @Override protected TemplatingModuleConfig toLowLevel() { - return Template.create().template(template).defaults(defaults).responseFormat(responseFormat).tools(tools); + List template = this.template != null ? this.template : List.of(); + return Template.create() + .template(template) + .defaults(defaults) + .responseFormat(responseFormat) + .tools(tools); + } + + public OrchestrationTemplate withJsonSchemaResponse(ResponseJsonSchema schema) { + val responseFormatJsonSchema = + ResponseFormatJsonSchema.create() + .type(ResponseFormatJsonSchema.TypeEnum.JSON_SCHEMA) + .jsonSchema( + ResponseFormatJsonSchemaJsonSchema.create() + .name(schema.getName()) + .schema(schema.getSchemaMap()) + .strict(schema.getIsStrict()) + .description(schema.getDescription())); + return this.withResponseFormat(responseFormatJsonSchema); + } + + public OrchestrationTemplate withJsonResponse() { + val responseFormatJsonObject = + ResponseFormatJsonObject.create().type(ResponseFormatJsonObject.TypeEnum.JSON_OBJECT); + return this.withResponseFormat(responseFormatJsonObject); } } diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java index a30977b95..9c03e6cc6 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java @@ -22,7 +22,7 @@ import lombok.val; /** - * The schema object to use for the response format parameter in {@link OrchestrationModuleConfig}. + * The schema object to use for the response format parameter in {@link OrchestrationTemplate}. * * @since 1.4.0 */ diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java index d377c4abb..dc578c307 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java @@ -6,15 +6,13 @@ import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig; public abstract class TemplateConfig { - abstract protected TemplatingModuleConfig toLowLevel(); + protected abstract TemplatingModuleConfig toLowLevel(); + // factory method public static OrchestrationTemplate create() { return new OrchestrationTemplate(); } -// // optional factory method? -// public static OrchestrationTemplate jsonResponse() { -// return create().withResponseFormat("json"); -// } + // factory method public static ReferenceBuilder reference() { return new ReferenceBuilder(); @@ -25,7 +23,7 @@ OrchestrationTemplateReference byId(String id) { return new OrchestrationTemplateReference(TemplateRefByID.create().id(id)); } - ReferenceBuilder1 byScenarioNameVersion(String scenario){ + ReferenceBuilder1 byScenarioNameVersion(String scenario) { return new ReferenceBuilder1(scenario); } } @@ -36,6 +34,7 @@ public static class ReferenceBuilder1 { ReferenceBuilder1(String scenario) { this.scenario = scenario; } + ReferenceBuilder2 name(String name) { return new ReferenceBuilder2(scenario, name); } @@ -49,8 +48,10 @@ public static class ReferenceBuilder2 { this.scenario = scenario; this.name = name; } + OrchestrationTemplateReference version(String version) { - return new OrchestrationTemplateReference(TemplateRefByScenarioNameVersion.create().scenario(scenario).name(name).version(version)); + return new OrchestrationTemplateReference( + TemplateRefByScenarioNameVersion.create().scenario(scenario).name(name).version(version)); } } } diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java index 5c7d84612..07f4e22d4 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java @@ -9,6 +9,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; + +import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig; import lombok.val; import org.junit.jupiter.api.Test; @@ -88,9 +90,9 @@ void testConfigWithResponseSchema() { val schemaMap = generateSchemaMap(); var configWithResponseSchemaFromClass = - new OrchestrationModuleConfig() + new OrchestrationModuleConfig().withTemplateConfig(TemplateConfig.create() .withJsonSchemaResponse( - schemaFromClass.withDescription("Description").withStrict(true)); + schemaFromClass.withDescription("Description").withStrict(true))); var configWithResponseSchemaLowLevel = new OrchestrationModuleConfig() .withTemplateConfig( diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java index 0c57d6269..3726ac782 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java @@ -22,6 +22,8 @@ import com.sap.ai.sdk.orchestration.model.TemplateRefByID; import java.util.List; import java.util.Map; + +import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig; import org.junit.jupiter.api.Test; class OrchestrationModuleConfigTest { @@ -198,42 +200,12 @@ void testGroundingPrompt() { "{{?userMessage}} Use the following information as additional context: {{?groundingContext}}"); } - @Test - void testResponseFormatNotOverwrittenByNewTemplateConfig() { - var schema = ResponseJsonSchema.from(TestClassForSchemaGeneration.class); - var config = new OrchestrationModuleConfig().withJsonSchemaResponse(schema); - assertThat(((Template) config.getTemplateConfig())).isNotNull(); - assertThat( - ((ResponseFormatJsonSchema) ((Template) config.getTemplateConfig()).getResponseFormat()) - .getJsonSchema() - .getSchema()) - .isEqualTo(schema.getSchemaMap()); - - var template = - Template.create() - .template(SingleChatMessage.create().role("user").content("Hello, World!")); - template.setCustomField("field", 1); - config = config.withTemplateConfig(template); - assertThat(((Template) config.getTemplateConfig())).isNotNull(); - assertThat( - ((ResponseFormatJsonSchema) ((Template) config.getTemplateConfig()).getResponseFormat()) - .getJsonSchema() - .getSchema()) - .isEqualTo(schema.getSchemaMap()); - - config = config.withTemplateConfig(null); - assertThat(((Template) config.getTemplateConfig())).isNotNull(); - assertThat( - ((ResponseFormatJsonSchema) ((Template) config.getTemplateConfig()).getResponseFormat()) - .getJsonSchema() - .getSchema()) - .isEqualTo(schema.getSchemaMap()); - } - @Test void testResponseFormatOverwrittenByNewTemplateConfigWithResponseFormat() { var schema = ResponseJsonSchema.from(TestClassForSchemaGeneration.class); - var config = new OrchestrationModuleConfig().withJsonSchemaResponse(schema); + var config = + new OrchestrationModuleConfig() + .withTemplateConfig(TemplateConfig.create().withJsonSchemaResponse(schema)); assertThat(((Template) config.getTemplateConfig())).isNotNull(); assertThat( ((ResponseFormatJsonSchema) ((Template) config.getTemplateConfig()).getResponseFormat()) @@ -256,7 +228,9 @@ void testResponseFormatOverwrittenByNewTemplateConfigWithResponseFormat() { @Test void testResponseFormatOverwrittenByNewTemplateRef() { var schema = ResponseJsonSchema.from(TestClassForSchemaGeneration.class); - var config = new OrchestrationModuleConfig().withJsonSchemaResponse(schema); + var config = + new OrchestrationModuleConfig() + .withTemplateConfig(TemplateConfig.create().withJsonSchemaResponse(schema)); assertThat(((Template) config.getTemplateConfig())).isNotNull(); assertThat( ((ResponseFormatJsonSchema) ((Template) config.getTemplateConfig()).getResponseFormat()) @@ -273,7 +247,9 @@ void testResponseFormatOverwrittenByNewTemplateRef() { @Test void testResponseFormatOverwrittenByOtherResponseFormat() { var schema = ResponseJsonSchema.from(TestClassForSchemaGeneration.class); - var config = new OrchestrationModuleConfig().withJsonSchemaResponse(schema); + var config = + new OrchestrationModuleConfig() + .withTemplateConfig(TemplateConfig.create().withJsonSchemaResponse(schema)); assertThat(((Template) config.getTemplateConfig())).isNotNull(); assertThat( ((ResponseFormatJsonSchema) ((Template) config.getTemplateConfig()).getResponseFormat()) @@ -281,12 +257,12 @@ void testResponseFormatOverwrittenByOtherResponseFormat() { .getSchema()) .isEqualTo(schema.getSchemaMap()); - config = config.withJsonResponse(); + config = config.withTemplateConfig(TemplateConfig.create().withJsonResponse()); assertThat(((Template) config.getTemplateConfig())).isNotNull(); assertThat(((Template) config.getTemplateConfig()).getResponseFormat()) .isInstanceOf(ResponseFormatJsonObject.class); - config = config.withJsonSchemaResponse(schema); + config = config.withTemplateConfig(TemplateConfig.create().withJsonSchemaResponse(schema)); assertThat(((Template) config.getTemplateConfig())).isNotNull(); assertThat( ((ResponseFormatJsonSchema) ((Template) config.getTemplateConfig()).getResponseFormat()) diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java index c8148b1be..fe59175d8 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java @@ -786,7 +786,8 @@ class Translation { ResponseJsonSchema.from(Translation.class) .withDescription("Output schema for language translation.") .withStrict(true); - val configWithResponseSchema = config.withJsonSchemaResponse(schema); + val configWithResponseSchema = + config.withTemplateConfig(TemplateConfig.create().withJsonSchemaResponse(schema)); val prompt = new OrchestrationPrompt( @@ -813,7 +814,8 @@ void testResponseFormatJsonObject() throws IOException { val config = new OrchestrationModuleConfig().withLlmConfig(GPT_4O_MINI); - val configWithJsonResponse = config.withJsonResponse(); + val configWithJsonResponse = + config.withTemplateConfig(TemplateConfig.create().withJsonResponse()); val prompt = new OrchestrationPrompt( diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java index 8a8130e54..a40321401 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java @@ -19,6 +19,7 @@ import com.sap.ai.sdk.orchestration.OrchestrationModuleConfig; import com.sap.ai.sdk.orchestration.OrchestrationPrompt; import com.sap.ai.sdk.orchestration.ResponseJsonSchema; +import com.sap.ai.sdk.orchestration.TemplateConfig; import com.sap.ai.sdk.orchestration.model.DPIEntities; import com.sap.ai.sdk.orchestration.model.DataRepositoryType; import com.sap.ai.sdk.orchestration.model.DocumentGroundingFilter; @@ -365,7 +366,8 @@ class Translation { ResponseJsonSchema.from(Translation.class) .withDescription("Output schema for language translation.") .withStrict(true); - val configWithResponseSchema = config.withJsonSchemaResponse(schema); + val configWithResponseSchema = + config.withTemplateConfig(TemplateConfig.create().withJsonSchemaResponse(schema)); val prompt = new OrchestrationPrompt( From 731972ada67d566af3177dbf7dda26e8bf0d4ba7 Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Thu, 27 Feb 2025 10:49:24 +0100 Subject: [PATCH 12/23] Adding javadocs and annotations --- .../OrchestrationModuleConfig.java | 14 ++-- .../orchestration/OrchestrationTemplate.java | 54 +++++++++---- .../OrchestrationTemplateReference.java | 26 ++++-- .../ai/sdk/orchestration/TemplateConfig.java | 81 ++++++++++++++++--- .../OrchestrationConvenienceUnitTest.java | 10 +-- .../OrchestrationModuleConfigTest.java | 2 - 6 files changed, 138 insertions(+), 49 deletions(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java index b99c34cb8..8bea78934 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java @@ -6,12 +6,9 @@ import com.sap.ai.sdk.orchestration.model.LLMModuleConfig; import com.sap.ai.sdk.orchestration.model.MaskingModuleConfig; import com.sap.ai.sdk.orchestration.model.OutputFilteringConfig; -import com.sap.ai.sdk.orchestration.model.Template; -import com.sap.ai.sdk.orchestration.model.TemplateResponseFormat; import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig; import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.Objects; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -64,9 +61,7 @@ public class OrchestrationModuleConfig { * @link SAP * AI Core: Orchestration - Templating */ - @With - @Nullable - TemplatingModuleConfig templateConfig; + @With @Nullable TemplatingModuleConfig templateConfig; /** * A masking configuration to pseudonymous or anonymize sensitive data in the input. @@ -227,7 +222,8 @@ public OrchestrationModuleConfig withGrounding( */ @Tolerate @Nonnull - public OrchestrationModuleConfig withTemplateConfig(@Nonnull TemplateConfig templateConfig) { - return this.withTemplateConfig(templateConfig.toLowLevel()); - } + public OrchestrationModuleConfig withTemplateConfig( + @Nonnull final TemplateConfig templateConfig) { + return this.withTemplateConfig(templateConfig.toLowLevel()); + } } diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java index fd0d42e8e..f775f9bd6 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java @@ -8,6 +8,13 @@ import com.sap.ai.sdk.orchestration.model.Template; import com.sap.ai.sdk.orchestration.model.TemplateResponseFormat; import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; @@ -16,31 +23,37 @@ import lombok.With; import lombok.val; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - +/** + * A template to use in {@link OrchestrationModuleConfig}. + * + * @since 1.5.0 + */ @EqualsAndHashCode(callSuper = true) @Value @With -@AllArgsConstructor( - access = AccessLevel.PRIVATE) // TODO: why do we need PRIVATE here instead of NONE? +// JONAS: why do we need PRIVATE here instead of NONE? +@AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(force = true, access = AccessLevel.PACKAGE) public class OrchestrationTemplate extends TemplateConfig { - List template; - Map defaults = new HashMap<>(); + @Nullable List template; + @Nonnull Map defaults = new HashMap<>(); @With(AccessLevel.PACKAGE) + @Nullable TemplateResponseFormat responseFormat; - List tools = new ArrayList<>(); - Map cloudSdkCustomFields = new LinkedHashMap<>(); + @Nonnull List tools = new ArrayList<>(); + @Nonnull Map cloudSdkCustomFields = new LinkedHashMap<>(); + /** + * Create a low-level representation of the template. + * + * @return The low-level representation of the template. + */ @Override + @Nonnull protected TemplatingModuleConfig toLowLevel() { - List template = this.template != null ? this.template : List.of(); + final List template = this.template != null ? this.template : List.of(); return Template.create() .template(template) .defaults(defaults) @@ -48,7 +61,14 @@ protected TemplatingModuleConfig toLowLevel() { .tools(tools); } - public OrchestrationTemplate withJsonSchemaResponse(ResponseJsonSchema schema) { + /** + * Set the response format to the given JSON schema. + * + * @param schema The JSON schema to use. + * @return The updated template. + */ + @Nonnull + public OrchestrationTemplate withJsonSchemaResponse(@Nonnull final ResponseJsonSchema schema) { val responseFormatJsonSchema = ResponseFormatJsonSchema.create() .type(ResponseFormatJsonSchema.TypeEnum.JSON_SCHEMA) @@ -61,6 +81,12 @@ public OrchestrationTemplate withJsonSchemaResponse(ResponseJsonSchema schema) { return this.withResponseFormat(responseFormatJsonSchema); } + /** + * Set the response format to JSON object. + * + * @return The updated template. + */ + @Nonnull public OrchestrationTemplate withJsonResponse() { val responseFormatJsonObject = ResponseFormatJsonObject.create().type(ResponseFormatJsonObject.TypeEnum.JSON_OBJECT); diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java index e5a4ef39f..f9bbc2ed6 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java @@ -1,19 +1,31 @@ package com.sap.ai.sdk.orchestration; +import com.sap.ai.sdk.orchestration.model.TemplateRef; import com.sap.ai.sdk.orchestration.model.TemplateRefTemplateRef; import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig; +import javax.annotation.Nonnull; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Value; +/** + * A reference to a template to use in {@link OrchestrationModuleConfig}. + * + * @since 1.5.0 + */ @Value -public class OrchestrationTemplateReference extends TemplateConfig{ - TemplateRefTemplateRef reference; - - public OrchestrationTemplateReference(TemplateRefTemplateRef reference) { - this.reference = reference; - } +@AllArgsConstructor(access = AccessLevel.PROTECTED) +public class OrchestrationTemplateReference extends TemplateConfig { + @Nonnull TemplateRefTemplateRef reference; + /** + * Create a low-level representation of the template. + * + * @return The low-level representation of the template. + */ + @Nonnull @Override protected TemplatingModuleConfig toLowLevel() { - return null; + return TemplateRef.create().templateRef(reference); } } diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java index dc578c307..fa5186718 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java @@ -2,54 +2,111 @@ import com.sap.ai.sdk.orchestration.model.TemplateRefByID; import com.sap.ai.sdk.orchestration.model.TemplateRefByScenarioNameVersion; -import com.sap.ai.sdk.orchestration.model.TemplateRefTemplateRef; import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig; +import javax.annotation.Nonnull; +/** + * Template configuration for the {@link OrchestrationModuleConfig}. + * + * @since 1.5.0 + */ public abstract class TemplateConfig { + + /** + * Create a low-level representation of the template. + * + * @return The low-level representation of the template. + */ + @Nonnull protected abstract TemplatingModuleConfig toLowLevel(); - // factory method + /** + * Build a template. + * + * @return A new empty template. + */ + @Nonnull public static OrchestrationTemplate create() { return new OrchestrationTemplate(); } - // factory method + /** + * Build a template reference. + * + * @return An intermediate object to build the template reference. + */ + @Nonnull public static ReferenceBuilder reference() { return new ReferenceBuilder(); } + /** Intermediate object to build a template reference. */ public static class ReferenceBuilder { - OrchestrationTemplateReference byId(String id) { + /** + * Build a template reference with the given id. + * + * @param id The id of the template. + * @return A template reference with the given id. + */ + @Nonnull + public OrchestrationTemplateReference byId(@Nonnull final String id) { return new OrchestrationTemplateReference(TemplateRefByID.create().id(id)); } - ReferenceBuilder1 byScenarioNameVersion(String scenario) { + /** + * Build a template reference with the given scenario, name, and version. + * + * @param scenario The scenario of the template. + * @return An intermediate object to build the template reference. + */ + @Nonnull + public ReferenceBuilder1 byScenarioNameVersion(@Nonnull final String scenario) { return new ReferenceBuilder1(scenario); } } + /** + * Intermediate object to build a template reference with the given scenario, name, and version. + */ public static class ReferenceBuilder1 { - private final String scenario; + @Nonnull private final String scenario; - ReferenceBuilder1(String scenario) { + private ReferenceBuilder1(@Nonnull final String scenario) { this.scenario = scenario; } - ReferenceBuilder2 name(String name) { + /** + * Build a template reference with the given scenario, name, and version. + * + * @param name The name of the template. + * @return An intermediate object to build the template reference. + */ + @Nonnull + public ReferenceBuilder2 name(@Nonnull final String name) { return new ReferenceBuilder2(scenario, name); } } + /** + * Intermediate object to build a template reference with the given scenario, name, and version. + */ public static class ReferenceBuilder2 { - private final String scenario; - private final String name; + @Nonnull private final String scenario; + @Nonnull private final String name; - ReferenceBuilder2(String scenario, String name) { + private ReferenceBuilder2(@Nonnull final String scenario, @Nonnull final String name) { this.scenario = scenario; this.name = name; } - OrchestrationTemplateReference version(String version) { + /** + * Build a template reference with the given scenario, name, and version. + * + * @param version The version of the template. + * @return A template reference with the given scenario, name, and version. + */ + @Nonnull + public OrchestrationTemplateReference version(@Nonnull final String version) { return new OrchestrationTemplateReference( TemplateRefByScenarioNameVersion.create().scenario(scenario).name(name).version(version)); } diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java index 07f4e22d4..2605b256e 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java @@ -9,8 +9,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; - -import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig; import lombok.val; import org.junit.jupiter.api.Test; @@ -90,9 +88,11 @@ void testConfigWithResponseSchema() { val schemaMap = generateSchemaMap(); var configWithResponseSchemaFromClass = - new OrchestrationModuleConfig().withTemplateConfig(TemplateConfig.create() - .withJsonSchemaResponse( - schemaFromClass.withDescription("Description").withStrict(true))); + new OrchestrationModuleConfig() + .withTemplateConfig( + TemplateConfig.create() + .withJsonSchemaResponse( + schemaFromClass.withDescription("Description").withStrict(true))); var configWithResponseSchemaLowLevel = new OrchestrationModuleConfig() .withTemplateConfig( diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java index 3726ac782..79d3ab79a 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java @@ -22,8 +22,6 @@ import com.sap.ai.sdk.orchestration.model.TemplateRefByID; import java.util.List; import java.util.Map; - -import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig; import org.junit.jupiter.api.Test; class OrchestrationModuleConfigTest { From fbf4944e29b7b12412e55fb568ebfa7f204fe992 Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Thu, 27 Feb 2025 13:18:22 +0100 Subject: [PATCH 13/23] Add tests --- orchestration/pom.xml | 2 +- .../orchestration/OrchestrationTemplate.java | 2 +- .../sdk/orchestration/ResponseJsonSchema.java | 24 ---------- .../OrchestrationConvenienceUnitTest.java | 37 +++++++++++++++ .../OrchestrationModuleConfigTest.java | 47 ++++--------------- 5 files changed, 49 insertions(+), 63 deletions(-) diff --git a/orchestration/pom.xml b/orchestration/pom.xml index af8ad0edd..24cd25500 100644 --- a/orchestration/pom.xml +++ b/orchestration/pom.xml @@ -34,7 +34,7 @@ 82% 93% 94% - 75% + 71% 95% 100% diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java index f775f9bd6..754c7e494 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java @@ -38,7 +38,7 @@ public class OrchestrationTemplate extends TemplateConfig { @Nullable List template; @Nonnull Map defaults = new HashMap<>(); - @With(AccessLevel.PACKAGE) + @With(AccessLevel.PRIVATE) @Nullable TemplateResponseFormat responseFormat; diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java index 9c03e6cc6..eaa37ae56 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java @@ -9,8 +9,6 @@ import com.github.victools.jsonschema.generator.SchemaVersion; import com.github.victools.jsonschema.module.jackson.JacksonModule; import com.github.victools.jsonschema.module.jackson.JacksonOption; -import com.sap.ai.sdk.orchestration.model.Template; -import com.sap.ai.sdk.orchestration.model.TemplateResponseFormat; import java.lang.reflect.Type; import java.util.Map; import javax.annotation.Nonnull; @@ -88,26 +86,4 @@ public static ResponseJsonSchema from(@Nonnull final Type classType) { val schemaName = ((Class) classType).getSimpleName() + "-Schema"; return new ResponseJsonSchema(schemaMap, schemaName, null, null); } - - /** - * Create a new instance of a {@link Template} that is a copy of the given one but with the given - * response format. - * - * @param originalTemplate The template to copy - * @param responseFormat The response format to set in the copy - * @return The new instance of {@link Template} - */ - @Nonnull - static Template newTemplateWithResponseFormat( - @Nonnull final Template originalTemplate, - @Nullable final TemplateResponseFormat responseFormat) { - val newTemplate = Template.create().template(originalTemplate.getTemplate()); - newTemplate.setDefaults(originalTemplate.getDefaults()); - newTemplate.setTools(originalTemplate.getTools()); - for (val customFieldName : originalTemplate.getCustomFieldNames()) { - newTemplate.setCustomField(customFieldName, originalTemplate.getCustomField(customFieldName)); - } - newTemplate.setResponseFormat(responseFormat); - return newTemplate; - } } diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java index 2605b256e..e161f52ac 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java @@ -6,6 +6,9 @@ import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchema; import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchemaJsonSchema; import com.sap.ai.sdk.orchestration.model.Template; +import com.sap.ai.sdk.orchestration.model.TemplateRef; +import com.sap.ai.sdk.orchestration.model.TemplateRefByID; +import com.sap.ai.sdk.orchestration.model.TemplateRefByScenarioNameVersion; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -109,4 +112,38 @@ void testConfigWithResponseSchema() { .description("Description")))); assertThat(configWithResponseSchemaFromClass).isEqualTo(configWithResponseSchemaLowLevel); } + + @Test + void testTemplateReferenceConstruction() { + var templateReferenceId = TemplateConfig.reference().byId("id"); + var expectedTemplateReferenceId = + new OrchestrationTemplateReference(TemplateRefByID.create().id("id")); + var templateReferenceIdLowLevel = + TemplateRef.create().templateRef(TemplateRefByID.create().id("id")); + assertThat(templateReferenceId).isEqualTo(expectedTemplateReferenceId); + assertThat(templateReferenceId.toLowLevel()).isEqualTo(templateReferenceIdLowLevel); + + var templateReferenceScenarioNameVersion = + TemplateConfig.reference() + .byScenarioNameVersion("scenario") + .name("name") + .version("version"); + var expectedTemplateReferenceScenarioNameVersion = + new OrchestrationTemplateReference( + TemplateRefByScenarioNameVersion.create() + .scenario("scenario") + .name("name") + .version("version")); + var templateReferenceScenarioNameVersionLowLevel = + TemplateRef.create() + .templateRef( + TemplateRefByScenarioNameVersion.create() + .scenario("scenario") + .name("name") + .version("version")); + assertThat(templateReferenceScenarioNameVersion) + .isEqualTo(expectedTemplateReferenceScenarioNameVersion); + assertThat(templateReferenceScenarioNameVersion.toLowLevel()) + .isEqualTo(templateReferenceScenarioNameVersionLowLevel); + } } diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java index 79d3ab79a..9c63adfbe 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java @@ -16,7 +16,6 @@ import com.sap.ai.sdk.orchestration.model.GroundingModuleConfigConfigFiltersInner; import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonObject; import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchema; -import com.sap.ai.sdk.orchestration.model.SingleChatMessage; import com.sap.ai.sdk.orchestration.model.Template; import com.sap.ai.sdk.orchestration.model.TemplateRef; import com.sap.ai.sdk.orchestration.model.TemplateRefByID; @@ -199,7 +198,7 @@ void testGroundingPrompt() { } @Test - void testResponseFormatOverwrittenByNewTemplateConfigWithResponseFormat() { + void testResponseFormatSchema() { var schema = ResponseJsonSchema.from(TestClassForSchemaGeneration.class); var config = new OrchestrationModuleConfig() @@ -210,16 +209,17 @@ void testResponseFormatOverwrittenByNewTemplateConfigWithResponseFormat() { .getJsonSchema() .getSchema()) .isEqualTo(schema.getSchemaMap()); + } - config = - config.withTemplateConfig( - Template.create() - .template(SingleChatMessage.create().role("user").content("Hello, World!")) - .responseFormat( - ResponseFormatJsonObject.create() - .type(ResponseFormatJsonObject.TypeEnum.JSON_OBJECT))); + @Test + void testResponseFormatObject() { + var config = + new OrchestrationModuleConfig() + .withTemplateConfig(TemplateConfig.create().withJsonResponse()); assertThat(((Template) config.getTemplateConfig())).isNotNull(); - assertThat(((Template) config.getTemplateConfig()).getResponseFormat()) + assertThat( + ((ResponseFormatJsonObject) + ((Template) config.getTemplateConfig()).getResponseFormat())) .isInstanceOf(ResponseFormatJsonObject.class); } @@ -241,31 +241,4 @@ void testResponseFormatOverwrittenByNewTemplateRef() { TemplateRef.create().templateRef(TemplateRefByID.create().id("123"))); assertThat(config.getTemplateConfig()).isInstanceOf(TemplateRef.class); } - - @Test - void testResponseFormatOverwrittenByOtherResponseFormat() { - var schema = ResponseJsonSchema.from(TestClassForSchemaGeneration.class); - var config = - new OrchestrationModuleConfig() - .withTemplateConfig(TemplateConfig.create().withJsonSchemaResponse(schema)); - assertThat(((Template) config.getTemplateConfig())).isNotNull(); - assertThat( - ((ResponseFormatJsonSchema) ((Template) config.getTemplateConfig()).getResponseFormat()) - .getJsonSchema() - .getSchema()) - .isEqualTo(schema.getSchemaMap()); - - config = config.withTemplateConfig(TemplateConfig.create().withJsonResponse()); - assertThat(((Template) config.getTemplateConfig())).isNotNull(); - assertThat(((Template) config.getTemplateConfig()).getResponseFormat()) - .isInstanceOf(ResponseFormatJsonObject.class); - - config = config.withTemplateConfig(TemplateConfig.create().withJsonSchemaResponse(schema)); - assertThat(((Template) config.getTemplateConfig())).isNotNull(); - assertThat( - ((ResponseFormatJsonSchema) ((Template) config.getTemplateConfig()).getResponseFormat()) - .getJsonSchema() - .getSchema()) - .isEqualTo(schema.getSchemaMap()); - } } From 168bd71823fab7e0b75e4e46532770356eca7897 Mon Sep 17 00:00:00 2001 From: Jonas-Isr Date: Thu, 27 Feb 2025 13:19:35 +0100 Subject: [PATCH 14/23] Update docs/guides/ORCHESTRATION_CHAT_COMPLETION.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alexander Dümont <22489773+newtork@users.noreply.github.com> --- docs/guides/ORCHESTRATION_CHAT_COMPLETION.md | 24 ++++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md b/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md index 9c5e20d0e..c32ebd5d2 100644 --- a/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md +++ b/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md @@ -346,24 +346,18 @@ There is also a way to generate the schema from a map of key-value pairs. This c ```java var schemaMap = - Map.of( - "type", - "object", - "properties", - Map.of( - "language", Map.of("type", "string"), - "translation", Map.of("type", "string")), - "required", - List.of("language", "translation"), - "additionalProperties", - false); + Map.ofEntries( + entry("type", "object"), + entry("properties", Map.ofEntries( + entry("language", Map.of("type", "string")), + entry("translation", Map.of("type", "string"))), + entry("required", List.of("language","translation")), + entry("additionalProperties", false))); + var schemaFromMap = ResponseJsonSchema.of(schemaMap, "Translator-Schema"); var config = new OrchestrationModuleConfig() - .withLlmConfig(OrchestrationAiModel.GPT_4O); + .withLlmConfig(OrchestrationAiModel.GPT_4O); var configWithResponseSchema = config.withJsonSchemaResponse(schemaFromMap); - -var prompt = new OrchestrationPrompt(Message.user("Some message.")); -var response = client.chatCompletion(prompt, configWithTemplate).getContent(); ``` From 537f28a321299a4adc034b716e75d15d824de27f Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Thu, 27 Feb 2025 14:05:49 +0100 Subject: [PATCH 15/23] Small changes --- docs/guides/ORCHESTRATION_CHAT_COMPLETION.md | 9 ++++++--- orchestration/pom.xml | 2 +- .../ai/sdk/orchestration/OrchestrationModuleConfig.java | 3 +++ .../sap/ai/sdk/orchestration/OrchestrationTemplate.java | 3 ++- .../orchestration/OrchestrationTemplateReference.java | 2 ++ .../com/sap/ai/sdk/orchestration/ResponseJsonSchema.java | 5 ++--- .../com/sap/ai/sdk/orchestration/TemplateConfig.java | 2 ++ 7 files changed, 18 insertions(+), 8 deletions(-) diff --git a/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md b/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md index c32ebd5d2..7f7176144 100644 --- a/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md +++ b/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md @@ -313,7 +313,8 @@ Setting the response format to `JSON_OBJECT` tells the AI to respond with JSON, ```java var config = new OrchestrationModuleConfig() .withLlmConfig(OrchestrationAiModel.GPT_4O); -var configWithJsonResponse = config.withJsonResponse(); +var configWithJsonResponse = + config.withTemplateConfig(TemplateConfig.create().withJsonResponse()); var prompt = new OrchestrationPrompt( @@ -334,7 +335,8 @@ var schema = .withStrict(true); var config = new OrchestrationModuleConfig() .withLlmConfig(OrchestrationAiModel.GPT_4O); -var configWithResponseSchema = config.withJsonSchemaResponse(schema); +var configWithResponseSchema = + config.withTemplateConfig(TemplateConfig.create().withJsonSchemaResponse(schema)); var prompt = new OrchestrationPrompt(Message.user("Some message.")); var response = client.chatCompletion(prompt, configWithTemplate).getContent(); @@ -357,7 +359,8 @@ var schemaMap = var schemaFromMap = ResponseJsonSchema.of(schemaMap, "Translator-Schema"); var config = new OrchestrationModuleConfig() .withLlmConfig(OrchestrationAiModel.GPT_4O); -var configWithResponseSchema = config.withJsonSchemaResponse(schemaFromMap); +var configWithResponseSchema = + config.withTemplateConfig(TemplateConfig.create().withJsonSchemaResponse(schemaFromMap)); ``` diff --git a/orchestration/pom.xml b/orchestration/pom.xml index ca80a398b..79224b2d6 100644 --- a/orchestration/pom.xml +++ b/orchestration/pom.xml @@ -35,7 +35,7 @@ 93% 94% 74% - 95% + 93% 100% diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java index 8bea78934..ee5f58a14 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java @@ -1,5 +1,6 @@ package com.sap.ai.sdk.orchestration; +import com.google.common.annotations.Beta; import com.sap.ai.sdk.orchestration.model.FilteringModuleConfig; import com.sap.ai.sdk.orchestration.model.GroundingModuleConfig; import com.sap.ai.sdk.orchestration.model.InputFilteringConfig; @@ -219,9 +220,11 @@ public OrchestrationModuleConfig withGrounding( * AI Core: Orchestration - Templating * @param templateConfig The template configuration to use. * @return A new configuration with the given template configuration. + * @since 1.5.0 */ @Tolerate @Nonnull + @Beta public OrchestrationModuleConfig withTemplateConfig( @Nonnull final TemplateConfig templateConfig) { return this.withTemplateConfig(templateConfig.toLowLevel()); diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java index 754c7e494..0e9db0f9a 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java @@ -1,5 +1,6 @@ package com.sap.ai.sdk.orchestration; +import com.google.common.annotations.Beta; import com.sap.ai.sdk.orchestration.model.ChatCompletionTool; import com.sap.ai.sdk.orchestration.model.ChatMessage; import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonObject; @@ -31,9 +32,9 @@ @EqualsAndHashCode(callSuper = true) @Value @With -// JONAS: why do we need PRIVATE here instead of NONE? @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(force = true, access = AccessLevel.PACKAGE) +@Beta public class OrchestrationTemplate extends TemplateConfig { @Nullable List template; @Nonnull Map defaults = new HashMap<>(); diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java index f9bbc2ed6..70526798a 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java @@ -1,5 +1,6 @@ package com.sap.ai.sdk.orchestration; +import com.google.common.annotations.Beta; import com.sap.ai.sdk.orchestration.model.TemplateRef; import com.sap.ai.sdk.orchestration.model.TemplateRefTemplateRef; import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig; @@ -15,6 +16,7 @@ */ @Value @AllArgsConstructor(access = AccessLevel.PROTECTED) +@Beta public class OrchestrationTemplateReference extends TemplateConfig { @Nonnull TemplateRefTemplateRef reference; diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java index eaa37ae56..7ccf0353b 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java @@ -9,6 +9,7 @@ import com.github.victools.jsonschema.generator.SchemaVersion; import com.github.victools.jsonschema.module.jackson.JacksonModule; import com.github.victools.jsonschema.module.jackson.JacksonOption; +import com.google.common.annotations.Beta; import java.lang.reflect.Type; import java.util.Map; import javax.annotation.Nonnull; @@ -27,6 +28,7 @@ @Value @AllArgsConstructor(access = AccessLevel.PACKAGE) @With +@Beta public class ResponseJsonSchema { @Nonnull Map schemaMap; @Nonnull String name; @@ -40,7 +42,6 @@ public class ResponseJsonSchema { * Create a new instance of {@link ResponseJsonSchema} with the given strictness. * * @return A new ResponseJsonSchema instance with the given strictness - * @since 1.4.0 */ @Nonnull public ResponseJsonSchema withStrict(@Nullable final Boolean isStrict) { @@ -53,7 +54,6 @@ public ResponseJsonSchema withStrict(@Nullable final Boolean isStrict) { * @param schemaMap The schema map * @param name The name of the schema * @return The new instance of {@link ResponseJsonSchema} - * @since 1.4.0 */ @Nonnull public static ResponseJsonSchema of( @@ -66,7 +66,6 @@ public static ResponseJsonSchema of( * * @param classType The class to generate the schema from * @return The new instance of {@link ResponseJsonSchema} - * @since 1.4.0 */ @Nonnull public static ResponseJsonSchema from(@Nonnull final Type classType) { diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java index fa5186718..8244ce7ec 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java @@ -1,5 +1,6 @@ package com.sap.ai.sdk.orchestration; +import com.google.common.annotations.Beta; import com.sap.ai.sdk.orchestration.model.TemplateRefByID; import com.sap.ai.sdk.orchestration.model.TemplateRefByScenarioNameVersion; import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig; @@ -10,6 +11,7 @@ * * @since 1.5.0 */ +@Beta public abstract class TemplateConfig { /** From a21cb6b3c6ec5791db978b526ff471c6d3b30d0b Mon Sep 17 00:00:00 2001 From: Jonas-Isr Date: Thu, 27 Feb 2025 15:14:07 +0100 Subject: [PATCH 16/23] Update orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alexander Dümont <22489773+newtork@users.noreply.github.com> --- .../java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java index 7ccf0353b..545f931fc 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java @@ -81,7 +81,7 @@ public static ResponseJsonSchema from(@Nonnull final Type classType) { .build()); val jsonSchema = generator.generateSchema(classType); val mapper = new ObjectMapper(); - final Map schemaMap = mapper.convertValue(jsonSchema, new TypeReference<>() {}); + val schemaMap = mapper.convertValue(jsonSchema, new TypeReference>() {}); val schemaName = ((Class) classType).getSimpleName() + "-Schema"; return new ResponseJsonSchema(schemaMap, schemaName, null, null); } From ffcb22e1f8b0e335044f12cbf8fa26b4fe923c1c Mon Sep 17 00:00:00 2001 From: SAP Cloud SDK Bot Date: Thu, 27 Feb 2025 14:14:52 +0000 Subject: [PATCH 17/23] Formatting --- .../java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java index 545f931fc..b290b3006 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java @@ -81,7 +81,7 @@ public static ResponseJsonSchema from(@Nonnull final Type classType) { .build()); val jsonSchema = generator.generateSchema(classType); val mapper = new ObjectMapper(); - val schemaMap = mapper.convertValue(jsonSchema, new TypeReference>() {}); + val schemaMap = mapper.convertValue(jsonSchema, new TypeReference>() {}); val schemaName = ((Class) classType).getSimpleName() + "-Schema"; return new ResponseJsonSchema(schemaMap, schemaName, null, null); } From 42d7d22cc0f16defeb6583e21129622504ff0e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= <22489773+newtork@users.noreply.github.com> Date: Thu, 27 Feb 2025 17:32:45 +0100 Subject: [PATCH 18/23] Use functional interfaces instead of class (#357) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alexander Dümont Co-authored-by: Jonas-Isr --- .../ai/sdk/orchestration/TemplateConfig.java | 40 ++++++------------- .../OrchestrationConvenienceUnitTest.java | 5 +-- 2 files changed, 13 insertions(+), 32 deletions(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java index 8244ce7ec..edf4f6d07 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java @@ -39,11 +39,13 @@ public static OrchestrationTemplate create() { */ @Nonnull public static ReferenceBuilder reference() { - return new ReferenceBuilder(); + final var templ = TemplateRefByScenarioNameVersion.create(); + return s -> n -> v -> new OrchestrationTemplateReference(templ.scenario(s).name(n).version(v)); } /** Intermediate object to build a template reference. */ - public static class ReferenceBuilder { + @FunctionalInterface + public interface ReferenceBuilder { /** * Build a template reference with the given id. * @@ -51,7 +53,7 @@ public static class ReferenceBuilder { * @return A template reference with the given id. */ @Nonnull - public OrchestrationTemplateReference byId(@Nonnull final String id) { + default OrchestrationTemplateReference byId(@Nonnull final String id) { return new OrchestrationTemplateReference(TemplateRefByID.create().id(id)); } @@ -62,20 +64,14 @@ public OrchestrationTemplateReference byId(@Nonnull final String id) { * @return An intermediate object to build the template reference. */ @Nonnull - public ReferenceBuilder1 byScenarioNameVersion(@Nonnull final String scenario) { - return new ReferenceBuilder1(scenario); - } + ReferenceBuilder1 byScenario(@Nonnull final String scenario); } /** * Intermediate object to build a template reference with the given scenario, name, and version. */ - public static class ReferenceBuilder1 { - @Nonnull private final String scenario; - - private ReferenceBuilder1(@Nonnull final String scenario) { - this.scenario = scenario; - } + @FunctionalInterface + public interface ReferenceBuilder1 { /** * Build a template reference with the given scenario, name, and version. @@ -84,23 +80,14 @@ private ReferenceBuilder1(@Nonnull final String scenario) { * @return An intermediate object to build the template reference. */ @Nonnull - public ReferenceBuilder2 name(@Nonnull final String name) { - return new ReferenceBuilder2(scenario, name); - } + ReferenceBuilder2 name(@Nonnull final String name); } /** * Intermediate object to build a template reference with the given scenario, name, and version. */ - public static class ReferenceBuilder2 { - @Nonnull private final String scenario; - @Nonnull private final String name; - - private ReferenceBuilder2(@Nonnull final String scenario, @Nonnull final String name) { - this.scenario = scenario; - this.name = name; - } - + @FunctionalInterface + public interface ReferenceBuilder2 { /** * Build a template reference with the given scenario, name, and version. * @@ -108,9 +95,6 @@ private ReferenceBuilder2(@Nonnull final String scenario, @Nonnull final String * @return A template reference with the given scenario, name, and version. */ @Nonnull - public OrchestrationTemplateReference version(@Nonnull final String version) { - return new OrchestrationTemplateReference( - TemplateRefByScenarioNameVersion.create().scenario(scenario).name(name).version(version)); - } + OrchestrationTemplateReference version(@Nonnull final String version); } } diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java index e161f52ac..ae73ec856 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java @@ -124,10 +124,7 @@ void testTemplateReferenceConstruction() { assertThat(templateReferenceId.toLowLevel()).isEqualTo(templateReferenceIdLowLevel); var templateReferenceScenarioNameVersion = - TemplateConfig.reference() - .byScenarioNameVersion("scenario") - .name("name") - .version("version"); + TemplateConfig.reference().byScenario("scenario").name("name").version("version"); var expectedTemplateReferenceScenarioNameVersion = new OrchestrationTemplateReference( TemplateRefByScenarioNameVersion.create() From 6fe5fe7b4dd224c2aa78e17316c037fcbda49f1c Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Thu, 27 Feb 2025 17:34:31 +0100 Subject: [PATCH 19/23] Requested changes --- .pipeline/spotbugs-exclusions.xml | 5 +++ orchestration/pom.xml | 2 +- .../OrchestrationModuleConfig.java | 2 +- .../orchestration/OrchestrationTemplate.java | 10 +++--- .../sdk/orchestration/ResponseJsonSchema.java | 15 +-------- .../OrchestrationConvenienceUnitTest.java | 33 +++++++++++++++++++ 6 files changed, 46 insertions(+), 21 deletions(-) diff --git a/.pipeline/spotbugs-exclusions.xml b/.pipeline/spotbugs-exclusions.xml index ded4e5eca..fb432eb0e 100644 --- a/.pipeline/spotbugs-exclusions.xml +++ b/.pipeline/spotbugs-exclusions.xml @@ -9,4 +9,9 @@ + + + + + diff --git a/orchestration/pom.xml b/orchestration/pom.xml index 79224b2d6..e7f2ff040 100644 --- a/orchestration/pom.xml +++ b/orchestration/pom.xml @@ -34,7 +34,7 @@ 82% 93% 94% - 74% + 76% 93% 100% diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java index ee5f58a14..4f53ebee4 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java @@ -62,7 +62,7 @@ public class OrchestrationModuleConfig { * @link SAP * AI Core: Orchestration - Templating */ - @With @Nullable TemplatingModuleConfig templateConfig; + @Nullable TemplatingModuleConfig templateConfig; /** * A masking configuration to pseudonymous or anonymize sensitive data in the input. diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java index 0e9db0f9a..156dbb846 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java @@ -11,7 +11,6 @@ import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig; import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.annotation.Nonnull; @@ -37,14 +36,13 @@ @Beta public class OrchestrationTemplate extends TemplateConfig { @Nullable List template; - @Nonnull Map defaults = new HashMap<>(); + @Nullable Map defaults; @With(AccessLevel.PRIVATE) @Nullable TemplateResponseFormat responseFormat; - @Nonnull List tools = new ArrayList<>(); - @Nonnull Map cloudSdkCustomFields = new LinkedHashMap<>(); + @Nullable List tools; /** * Create a low-level representation of the template. @@ -55,6 +53,8 @@ public class OrchestrationTemplate extends TemplateConfig { @Nonnull protected TemplatingModuleConfig toLowLevel() { final List template = this.template != null ? this.template : List.of(); + final Map defaults = this.defaults != null ? this.defaults : new HashMap<>(); + final List tools = this.tools != null ? this.tools : new ArrayList<>(); return Template.create() .template(template) .defaults(defaults) @@ -77,7 +77,7 @@ public OrchestrationTemplate withJsonSchemaResponse(@Nonnull final ResponseJsonS ResponseFormatJsonSchemaJsonSchema.create() .name(schema.getName()) .schema(schema.getSchemaMap()) - .strict(schema.getIsStrict()) + .strict(schema.getStrict()) .description(schema.getDescription())); return this.withResponseFormat(responseFormatJsonSchema); } diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java index b290b3006..d66491603 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java @@ -33,20 +33,7 @@ public class ResponseJsonSchema { @Nonnull Map schemaMap; @Nonnull String name; @Nullable String description; - - @With(AccessLevel.NONE) - @Nullable - Boolean isStrict; - - /** - * Create a new instance of {@link ResponseJsonSchema} with the given strictness. - * - * @return A new ResponseJsonSchema instance with the given strictness - */ - @Nonnull - public ResponseJsonSchema withStrict(@Nullable final Boolean isStrict) { - return new ResponseJsonSchema(schemaMap, name, description, isStrict); - } + @Nullable Boolean strict; /** * Create a new instance of {@link ResponseJsonSchema} with the given schema map and name. diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java index ae73ec856..a5fad660f 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java @@ -3,8 +3,13 @@ import static org.assertj.core.api.Assertions.assertThat; import com.fasterxml.jackson.annotation.JsonProperty; +import com.sap.ai.sdk.orchestration.model.ChatCompletionTool; +import com.sap.ai.sdk.orchestration.model.ChatMessage; +import com.sap.ai.sdk.orchestration.model.FunctionObject; +import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonObject; import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchema; import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchemaJsonSchema; +import com.sap.ai.sdk.orchestration.model.SingleChatMessage; import com.sap.ai.sdk.orchestration.model.Template; import com.sap.ai.sdk.orchestration.model.TemplateRef; import com.sap.ai.sdk.orchestration.model.TemplateRefByID; @@ -113,6 +118,34 @@ void testConfigWithResponseSchema() { assertThat(configWithResponseSchemaFromClass).isEqualTo(configWithResponseSchemaLowLevel); } + @Test + void testTemplateConstruction() { + List templateMessages = + List.of(SingleChatMessage.create().role("user").content("message")); + var defaults = Map.of("key", "value"); + var tools = + List.of( + ChatCompletionTool.create() + .type(ChatCompletionTool.TypeEnum.FUNCTION) + .function(FunctionObject.create().name("func"))); + var template = + TemplateConfig.create() + .withTemplate(templateMessages) + .withDefaults(defaults) + .withTools(tools) + .withJsonResponse(); + + var templateLowLevel = + Template.create() + .template(templateMessages) + .defaults(defaults) + .responseFormat( + ResponseFormatJsonObject.create() + .type(ResponseFormatJsonObject.TypeEnum.JSON_OBJECT)) + .tools(tools); + assertThat(template.toLowLevel()).isEqualTo(templateLowLevel); + } + @Test void testTemplateReferenceConstruction() { var templateReferenceId = TemplateConfig.reference().byId("id"); From fea3bc06c82e02522996c22a3e4e684fa1830b6d Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Thu, 27 Feb 2025 17:36:12 +0100 Subject: [PATCH 20/23] change @since to 1.4.0 --- .../com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java | 2 +- .../com/sap/ai/sdk/orchestration/OrchestrationTemplate.java | 2 +- .../ai/sdk/orchestration/OrchestrationTemplateReference.java | 2 +- .../main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java index 4f53ebee4..af4a4ca78 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java @@ -220,7 +220,7 @@ public OrchestrationModuleConfig withGrounding( * AI Core: Orchestration - Templating * @param templateConfig The template configuration to use. * @return A new configuration with the given template configuration. - * @since 1.5.0 + * @since 1.4.0 */ @Tolerate @Nonnull diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java index 156dbb846..c475f2eac 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplate.java @@ -26,7 +26,7 @@ /** * A template to use in {@link OrchestrationModuleConfig}. * - * @since 1.5.0 + * @since 1.4.0 */ @EqualsAndHashCode(callSuper = true) @Value diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java index 70526798a..b8c818e0d 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java @@ -12,7 +12,7 @@ /** * A reference to a template to use in {@link OrchestrationModuleConfig}. * - * @since 1.5.0 + * @since 1.4.0 */ @Value @AllArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java index edf4f6d07..31093103b 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java @@ -9,7 +9,7 @@ /** * Template configuration for the {@link OrchestrationModuleConfig}. * - * @since 1.5.0 + * @since 1.4.0 */ @Beta public abstract class TemplateConfig { From 18c5dfd6215ce6daa0e1d29c214de378bf7197b0 Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Fri, 28 Feb 2025 10:35:03 +0100 Subject: [PATCH 21/23] rename factory methods --- docs/guides/ORCHESTRATION_CHAT_COMPLETION.md | 4 ++-- .../com/sap/ai/sdk/orchestration/ResponseJsonSchema.java | 4 ++-- .../orchestration/OrchestrationConvenienceUnitTest.java | 7 ++++--- .../sdk/orchestration/OrchestrationModuleConfigTest.java | 4 ++-- .../sap/ai/sdk/orchestration/OrchestrationUnitTest.java | 2 +- .../com/sap/ai/sdk/app/services/OrchestrationService.java | 2 +- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md b/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md index 7f7176144..ce7cf6dc3 100644 --- a/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md +++ b/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md @@ -330,7 +330,7 @@ If you want the response to not only consist of valid JSON but additionally adhe ```java var schema = - ResponseJsonSchema.from(MyClass.class) + ResponseJsonSchema.fromType(MyClass.class) .withDescription("Output schema for the example class MyClass.") .withStrict(true); var config = new OrchestrationModuleConfig() @@ -356,7 +356,7 @@ var schemaMap = entry("required", List.of("language","translation")), entry("additionalProperties", false))); -var schemaFromMap = ResponseJsonSchema.of(schemaMap, "Translator-Schema"); +var schemaFromMap = ResponseJsonSchema.fromMap(schemaMap, "Translator-Schema"); var config = new OrchestrationModuleConfig() .withLlmConfig(OrchestrationAiModel.GPT_4O); var configWithResponseSchema = diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java index d66491603..829e9e30b 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java @@ -43,7 +43,7 @@ public class ResponseJsonSchema { * @return The new instance of {@link ResponseJsonSchema} */ @Nonnull - public static ResponseJsonSchema of( + public static ResponseJsonSchema fromMap( @Nonnull final Map schemaMap, @Nonnull final String name) { return new ResponseJsonSchema(schemaMap, name, null, null); } @@ -55,7 +55,7 @@ public static ResponseJsonSchema of( * @return The new instance of {@link ResponseJsonSchema} */ @Nonnull - public static ResponseJsonSchema from(@Nonnull final Type classType) { + public static ResponseJsonSchema fromType(@Nonnull final Type classType) { val module = new JacksonModule( JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, JacksonOption.RESPECT_JSONPROPERTY_ORDER); diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java index a5fad660f..9f3a4c4d3 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java @@ -82,17 +82,18 @@ void testMessageConstructionImage() { @Test void testResponseFormatSchemaConstruction() { - val schemaFromClass = ResponseJsonSchema.from(TestClassForSchemaGeneration.class); + val schemaFromClass = ResponseJsonSchema.fromType(TestClassForSchemaGeneration.class); val schemaMap = generateSchemaMap(); - val schemaFromMap = ResponseJsonSchema.of(schemaMap, "TestClassForSchemaGeneration-Schema"); + val schemaFromMap = + ResponseJsonSchema.fromMap(schemaMap, "TestClassForSchemaGeneration-Schema"); assertThat(schemaFromClass).isEqualTo(schemaFromMap); } @Test void testConfigWithResponseSchema() { - val schemaFromClass = ResponseJsonSchema.from(TestClassForSchemaGeneration.class); + val schemaFromClass = ResponseJsonSchema.fromType(TestClassForSchemaGeneration.class); val schemaMap = generateSchemaMap(); var configWithResponseSchemaFromClass = diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java index 9c63adfbe..b2e1a1d0b 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java @@ -199,7 +199,7 @@ void testGroundingPrompt() { @Test void testResponseFormatSchema() { - var schema = ResponseJsonSchema.from(TestClassForSchemaGeneration.class); + var schema = ResponseJsonSchema.fromType(TestClassForSchemaGeneration.class); var config = new OrchestrationModuleConfig() .withTemplateConfig(TemplateConfig.create().withJsonSchemaResponse(schema)); @@ -225,7 +225,7 @@ void testResponseFormatObject() { @Test void testResponseFormatOverwrittenByNewTemplateRef() { - var schema = ResponseJsonSchema.from(TestClassForSchemaGeneration.class); + var schema = ResponseJsonSchema.fromType(TestClassForSchemaGeneration.class); var config = new OrchestrationModuleConfig() .withTemplateConfig(TemplateConfig.create().withJsonSchemaResponse(schema)); diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java index 86eaf6aea..73ca18823 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java @@ -783,7 +783,7 @@ class Translation { private String translation; } val schema = - ResponseJsonSchema.from(Translation.class) + ResponseJsonSchema.fromType(Translation.class) .withDescription("Output schema for language translation.") .withStrict(true); val configWithResponseSchema = diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java index a40321401..3bf3c17f7 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java @@ -363,7 +363,7 @@ class Translation { } val schema = - ResponseJsonSchema.from(Translation.class) + ResponseJsonSchema.fromType(Translation.class) .withDescription("Output schema for language translation.") .withStrict(true); val configWithResponseSchema = From 2f99bb8774890d161ddc7d9fcf3fb9e82e1a4c73 Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Fri, 28 Feb 2025 10:52:37 +0100 Subject: [PATCH 22/23] small fix --- .../sdk/orchestration/OrchestrationTemplateReference.java | 2 ++ .../orchestration/OrchestrationConvenienceUnitTest.java | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java index b8c818e0d..abb22eeed 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java @@ -7,6 +7,7 @@ import javax.annotation.Nonnull; import lombok.AccessLevel; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Value; /** @@ -14,6 +15,7 @@ * * @since 1.4.0 */ +@EqualsAndHashCode(callSuper = true) @Value @AllArgsConstructor(access = AccessLevel.PROTECTED) @Beta diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java index 9f3a4c4d3..33a024cdc 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java @@ -154,7 +154,8 @@ void testTemplateReferenceConstruction() { new OrchestrationTemplateReference(TemplateRefByID.create().id("id")); var templateReferenceIdLowLevel = TemplateRef.create().templateRef(TemplateRefByID.create().id("id")); - assertThat(templateReferenceId).isEqualTo(expectedTemplateReferenceId); + assertThat(templateReferenceId.getReference()) + .isEqualTo(expectedTemplateReferenceId.getReference()); assertThat(templateReferenceId.toLowLevel()).isEqualTo(templateReferenceIdLowLevel); var templateReferenceScenarioNameVersion = @@ -172,8 +173,8 @@ void testTemplateReferenceConstruction() { .scenario("scenario") .name("name") .version("version")); - assertThat(templateReferenceScenarioNameVersion) - .isEqualTo(expectedTemplateReferenceScenarioNameVersion); + assertThat(templateReferenceScenarioNameVersion.getReference()) + .isEqualTo(expectedTemplateReferenceScenarioNameVersion.getReference()); assertThat(templateReferenceScenarioNameVersion.toLowLevel()) .isEqualTo(templateReferenceScenarioNameVersionLowLevel); } From 6d1b7008003f20bad7e04c28032cd7d0c831af30 Mon Sep 17 00:00:00 2001 From: Jonas Israel Date: Fri, 28 Feb 2025 11:29:07 +0100 Subject: [PATCH 23/23] small fix.finalfinal --- .../java/com/sap/ai/sdk/orchestration/TemplateConfig.java | 2 ++ .../orchestration/OrchestrationConvenienceUnitTest.java | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java index 31093103b..be6a52f0e 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TemplateConfig.java @@ -5,12 +5,14 @@ import com.sap.ai.sdk.orchestration.model.TemplateRefByScenarioNameVersion; import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig; import javax.annotation.Nonnull; +import lombok.EqualsAndHashCode; /** * Template configuration for the {@link OrchestrationModuleConfig}. * * @since 1.4.0 */ +@EqualsAndHashCode @Beta public abstract class TemplateConfig { diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java index 33a024cdc..9f3a4c4d3 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationConvenienceUnitTest.java @@ -154,8 +154,7 @@ void testTemplateReferenceConstruction() { new OrchestrationTemplateReference(TemplateRefByID.create().id("id")); var templateReferenceIdLowLevel = TemplateRef.create().templateRef(TemplateRefByID.create().id("id")); - assertThat(templateReferenceId.getReference()) - .isEqualTo(expectedTemplateReferenceId.getReference()); + assertThat(templateReferenceId).isEqualTo(expectedTemplateReferenceId); assertThat(templateReferenceId.toLowLevel()).isEqualTo(templateReferenceIdLowLevel); var templateReferenceScenarioNameVersion = @@ -173,8 +172,8 @@ void testTemplateReferenceConstruction() { .scenario("scenario") .name("name") .version("version")); - assertThat(templateReferenceScenarioNameVersion.getReference()) - .isEqualTo(expectedTemplateReferenceScenarioNameVersion.getReference()); + assertThat(templateReferenceScenarioNameVersion) + .isEqualTo(expectedTemplateReferenceScenarioNameVersion); assertThat(templateReferenceScenarioNameVersion.toLowLevel()) .isEqualTo(templateReferenceScenarioNameVersionLowLevel); }