Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b146cfd
Use library to generate schemas from java classes
Jonas-Isr Feb 13, 2025
1611a08
Create ResponseJsonSchema and adapt OrchestrationModuleConfig
Jonas-Isr Feb 18, 2025
d7c5039
Add additional tests, reset TestCoverage
Jonas-Isr Feb 18, 2025
cec2591
Merge branch 'main' into orch-schema-convenience
Jonas-Isr Feb 18, 2025
1ff4d5a
Add documentation etc.
Jonas-Isr Feb 19, 2025
79865aa
Small fixes
Jonas-Isr Feb 19, 2025
0520339
Improve documentation
Jonas-Isr Feb 19, 2025
b9d5d9a
Requested changes
Jonas-Isr Feb 19, 2025
28f8c63
Merge branch 'main' into orch-schema-convenience
Jonas-Isr Feb 19, 2025
6eb21bd
Update orchestration/src/main/java/com/sap/ai/sdk/orchestration/Orche…
Jonas-Isr Feb 20, 2025
9b1482d
Small fix
Jonas-Isr Feb 20, 2025
b7824d0
WIP
Jonas-Isr Feb 24, 2025
5ee6cea
Introduce new class TemplateConfig and adapt tests
Jonas-Isr Feb 26, 2025
731972a
Adding javadocs and annotations
Jonas-Isr Feb 27, 2025
fbf4944
Add tests
Jonas-Isr Feb 27, 2025
168bd71
Update docs/guides/ORCHESTRATION_CHAT_COMPLETION.md
Jonas-Isr Feb 27, 2025
7c79e3a
Merge remote-tracking branch 'origin/orch-schema-convenience' into or…
Jonas-Isr Feb 27, 2025
57cc75f
Merge branch 'main' into orch-schema-convenience
Jonas-Isr Feb 27, 2025
537f28a
Small changes
Jonas-Isr Feb 27, 2025
403cf50
Merge branch 'main' into orch-schema-convenience
Jonas-Isr Feb 27, 2025
a4ceb8d
Merge branch 'main' into orch-schema-convenience
Jonas-Isr Feb 27, 2025
a21cb6b
Update orchestration/src/main/java/com/sap/ai/sdk/orchestration/Respo…
Jonas-Isr Feb 27, 2025
ffcb22e
Formatting
bot-sdk-js Feb 27, 2025
42d7d22
Use functional interfaces instead of class (#357)
newtork Feb 27, 2025
6fe5fe7
Requested changes
Jonas-Isr Feb 27, 2025
fea3bc0
change @since to 1.4.0
Jonas-Isr Feb 27, 2025
b736fea
Merge branch 'main' into orch-schema-convenience
Jonas-Isr Feb 27, 2025
f633d90
Merge branch 'main' into orch-schema-convenience
Jonas-Isr Feb 28, 2025
18c5dfd
rename factory methods
Jonas-Isr Feb 28, 2025
2f99bb8
small fix
Jonas-Isr Feb 28, 2025
81dd4ab
Merge branch 'main' into orch-schema-convenience
Jonas-Isr Feb 28, 2025
6d1b700
small fix.finalfinal
Jonas-Isr Feb 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 29 additions & 28 deletions docs/guides/ORCHESTRATION_CHAT_COMPLETION.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,20 +311,14 @@ 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 config = new OrchestrationModuleConfig()
.withLlmConfig(OrchestrationAiModel.GPT_4O);
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.

Expand All @@ -334,8 +328,24 @@ 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 =
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 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:
<details><summary>Click to expand code</summary>

```java
var schemaMap =
Map.of(
"type",
"object",
Expand All @@ -347,26 +357,17 @@ var schema =
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);

// 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."));
var prompt = new OrchestrationPrompt(Message.user("Some message."));
var response = client.chatCompletion(prompt, configWithTemplate).getContent();
```

</details>

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
Expand Down
2 changes: 1 addition & 1 deletion docs/release-notes/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 12 additions & 4 deletions orchestration/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@
</developers>
<properties>
<project.rootdir>${project.basedir}/../</project.rootdir>
<coverage.complexity>80%</coverage.complexity>
<coverage.line>92%</coverage.line>
<coverage.instruction>93%</coverage.instruction>
<coverage.branch>71%</coverage.branch>
<coverage.complexity>82%</coverage.complexity>
<coverage.line>93%</coverage.line>
<coverage.instruction>94%</coverage.instruction>
<coverage.branch>75%</coverage.branch>
<coverage.method>95%</coverage.method>
<coverage.class>100%</coverage.class>
</properties>
Expand Down Expand Up @@ -100,6 +100,14 @@
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-generator</artifactId>
</dependency>
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-module-jackson</artifactId>
</dependency>
<!-- scope "provided" -->
<dependency>
<groupId>org.projectlombok</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -61,7 +68,9 @@ public class OrchestrationModuleConfig {
* @link <a href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/templating">SAP
* AI Core: Orchestration - Templating</a>
*/
@Nullable TemplatingModuleConfig templateConfig;
@With(AccessLevel.NONE)
@Nullable
TemplatingModuleConfig templateConfig;

/**
* A masking configuration to pseudonymous or anonymize sensitive data in the input.
Expand Down Expand Up @@ -211,4 +220,118 @@ public OrchestrationModuleConfig withGrounding(
@Nonnull final GroundingProvider groundingProvider) {
return this.withGroundingConfig(groundingProvider.createConfig());
}

/**
* Creates a new configuration with the given template configuration.
*
* @link <a href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/templating">SAP
* AI Core: Orchestration - Templating</a>
* @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) {
if (templateConfig instanceof TemplateRef) {
return new OrchestrationModuleConfig(
llmConfig, templateConfig, maskingConfig, filteringConfig, groundingConfig);
}
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 <a
* href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/structured-output">SAP
* AI Core: Orchestration - Structured Output</a>
* @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 <a
* href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/structured-output">SAP
* AI Core: Orchestration - Structured Output</a>
* @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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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 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;
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<String, Object> 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<String, Object> 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 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) {
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);
val mapper = new ObjectMapper();
final Map<String, Object> schemaMap = mapper.convertValue(jsonSchema, new TypeReference<>() {});
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;
}
}
Loading
Loading