Skip to content

Commit fcbf9fd

Browse files
Jonas-IsrCharlesDuboisSAPnewtorkbot-sdk-jsa-d
authored
feat: [Orchestration] Convenience for response format (#341)
* Use library to generate schemas from java classes * Create ResponseJsonSchema and adapt OrchestrationModuleConfig * Add additional tests, reset TestCoverage * Add documentation etc. * Small fixes * Improve documentation * Requested changes * Update orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java Co-authored-by: Charles Dubois <[email protected]> * Small fix * WIP * Introduce new class TemplateConfig and adapt tests * Adding javadocs and annotations * Add tests * Update docs/guides/ORCHESTRATION_CHAT_COMPLETION.md Co-authored-by: Alexander Dümont <[email protected]> * Small changes * Update orchestration/src/main/java/com/sap/ai/sdk/orchestration/ResponseJsonSchema.java Co-authored-by: Alexander Dümont <[email protected]> * Formatting * Use functional interfaces instead of class (#357) Co-authored-by: Alexander Dümont <[email protected]> Co-authored-by: Jonas-Isr <[email protected]> * Requested changes * change @SInCE to 1.4.0 * rename factory methods * small fix * small fix.finalfinal --------- Co-authored-by: Jonas Israel <[email protected]> Co-authored-by: Charles Dubois <[email protected]> Co-authored-by: Alexander Dümont <[email protected]> Co-authored-by: SAP Cloud SDK Bot <[email protected]> Co-authored-by: Alexander Dümont <[email protected]>
1 parent b194909 commit fcbf9fd

File tree

16 files changed

+669
-130
lines changed

16 files changed

+669
-130
lines changed

.pipeline/spotbugs-exclusions.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,9 @@
99
<Package name="com.sap.ai.sdk.grounding.model"/>
1010
</Or>
1111
</Match>
12+
<Match>
13+
<Class name="com.sap.ai.sdk.orchestration.ResponseJsonSchema" />
14+
<Method name="withStrict" />
15+
<Bug pattern="RC_REF_COMPARISON_BAD_PRACTICE_BOOLEAN" />
16+
</Match>
1217
</FindBugsFilter>

docs/guides/ORCHESTRATION_CHAT_COMPLETION.md

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -311,20 +311,15 @@ It is possible to set the response format for the chat completion. Available opt
311311
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).
312312

313313
```java
314-
var template = Message.user("What is 'apple' in German?");
315-
var templatingConfig =
316-
Template.create()
317-
.template(List.of(template.createChatMessage()))
318-
.responseFormat(
319-
ResponseFormatJsonObject.create()
320-
.type(ResponseFormatJsonObject.TypeEnum.JSON_OBJECT));
321-
var configWithTemplate = llmWithImageSupportConfig.withTemplateConfig(templatingConfig);
314+
var config = new OrchestrationModuleConfig()
315+
.withLlmConfig(OrchestrationAiModel.GPT_4O);
316+
var configWithJsonResponse =
317+
config.withTemplateConfig(TemplateConfig.create().withJsonResponse());
322318

323319
var prompt =
324320
new OrchestrationPrompt(
325-
Message.system(
326-
"You are a language translator. Answer using the following JSON format: {\"language\": ..., \"translation\": ...}"));
327-
var response = client.chatCompletion(prompt, configWithTemplate).getContent();
321+
Message.user("Some message."), Message.system("Answer using JSON."));
322+
var response = client.chatCompletion(prompt, configWithJsonResponse).getContent();
328323
```
329324
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.
330325

@@ -334,38 +329,41 @@ Note, that it is necessary to tell the AI model to actually return a JSON object
334329
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.
335330

336331
```java
337-
var template = Message.user("Whats '%s' in German?".formatted(word));
338332
var schema =
339-
Map.of(
340-
"type",
341-
"object",
342-
"properties",
343-
Map.of(
344-
"language", Map.of("type", "string"),
345-
"translation", Map.of("type", "string")),
346-
"required",
347-
List.of("language", "translation"),
348-
"additionalProperties",
349-
false);
350-
351-
// Note, that we plan to add more convenient ways to add a JSON schema in the future.
352-
var templatingConfig =
353-
Template.create()
354-
.template(List.of(template.createChatMessage()))
355-
.responseFormat(
356-
ResponseFormatJsonSchema.create()
357-
.type(ResponseFormatJsonSchema.TypeEnum.JSON_SCHEMA)
358-
.jsonSchema(
359-
ResponseFormatJsonSchemaJsonSchema.create()
360-
.name("translation_response")
361-
.schema(schema)
362-
.strict(true)
363-
.description("Output schema for language translation.")));
364-
var configWithTemplate = llmWithImageSupportConfig.withTemplateConfig(templatingConfig);
365-
366-
var prompt = new OrchestrationPrompt(Message.system("You are a language translator."));
333+
ResponseJsonSchema.fromType(MyClass.class)
334+
.withDescription("Output schema for the example class MyClass.")
335+
.withStrict(true);
336+
var config = new OrchestrationModuleConfig()
337+
.withLlmConfig(OrchestrationAiModel.GPT_4O);
338+
var configWithResponseSchema =
339+
config.withTemplateConfig(TemplateConfig.create().withJsonSchemaResponse(schema));
340+
341+
var prompt = new OrchestrationPrompt(Message.user("Some message."));
367342
var response = client.chatCompletion(prompt, configWithTemplate).getContent();
368343
```
344+
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.
345+
346+
There is also a way to generate the schema from a map of key-value pairs. This can be done as follows:
347+
<details><summary>Click to expand code</summary>
348+
349+
```java
350+
var schemaMap =
351+
Map.ofEntries(
352+
entry("type", "object"),
353+
entry("properties", Map.ofEntries(
354+
entry("language", Map.of("type", "string")),
355+
entry("translation", Map.of("type", "string"))),
356+
entry("required", List.of("language","translation")),
357+
entry("additionalProperties", false)));
358+
359+
var schemaFromMap = ResponseJsonSchema.fromMap(schemaMap, "Translator-Schema");
360+
var config = new OrchestrationModuleConfig()
361+
.withLlmConfig(OrchestrationAiModel.GPT_4O);
362+
var configWithResponseSchema =
363+
config.withTemplateConfig(TemplateConfig.create().withJsonSchemaResponse(schemaFromMap));
364+
```
365+
366+
</details>
369367

370368
Please find [an example in our Spring Boot application](../../sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java)
371369

docs/release-notes/release_notes.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515

1616
### ✨ New Functionality
1717

18-
- [Orchestration] [Add Spring AI tool calling](../guides/SPRING_AI_INTEGRATION.md#tool-calling).
18+
- [Orchestration]
19+
- [Add Spring AI tool calling](../guides/SPRING_AI_INTEGRATION.md#tool-calling).
20+
- [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)
1921
- [Document Grounding] [Add Document Grounding Client](https://github.com/SAP/ai-sdk-java/tree/main/docs/guides/GROUNDING.md)
2022
- `com.sap.ai.sdk:document-grounding:1.4.0`
2123
- [OpenAI]

orchestration/pom.xml

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@
3131
</developers>
3232
<properties>
3333
<project.rootdir>${project.basedir}/../</project.rootdir>
34-
<coverage.complexity>81%</coverage.complexity>
35-
<coverage.line>92%</coverage.line>
36-
<coverage.instruction>93%</coverage.instruction>
37-
<coverage.branch>74%</coverage.branch>
38-
<coverage.method>92%</coverage.method>
34+
<coverage.complexity>82%</coverage.complexity>
35+
<coverage.line>93%</coverage.line>
36+
<coverage.instruction>94%</coverage.instruction>
37+
<coverage.branch>76%</coverage.branch>
38+
<coverage.method>93%</coverage.method>
3939
<coverage.class>100%</coverage.class>
4040
</properties>
4141

@@ -100,6 +100,14 @@
100100
<groupId>com.google.guava</groupId>
101101
<artifactId>guava</artifactId>
102102
</dependency>
103+
<dependency>
104+
<groupId>com.github.victools</groupId>
105+
<artifactId>jsonschema-generator</artifactId>
106+
</dependency>
107+
<dependency>
108+
<groupId>com.github.victools</groupId>
109+
<artifactId>jsonschema-module-jackson</artifactId>
110+
</dependency>
103111
<!-- scope "provided" -->
104112
<dependency>
105113
<groupId>org.projectlombok</groupId>

orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.sap.ai.sdk.orchestration;
22

3+
import com.google.common.annotations.Beta;
34
import com.sap.ai.sdk.orchestration.model.FilteringModuleConfig;
45
import com.sap.ai.sdk.orchestration.model.GroundingModuleConfig;
56
import com.sap.ai.sdk.orchestration.model.InputFilteringConfig;
@@ -211,4 +212,21 @@ public OrchestrationModuleConfig withGrounding(
211212
@Nonnull final GroundingProvider groundingProvider) {
212213
return this.withGroundingConfig(groundingProvider.createConfig());
213214
}
215+
216+
/**
217+
* Creates a new configuration with the given template configuration as {@link TemplateConfig}.
218+
*
219+
* @link <a href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/templating">SAP
220+
* AI Core: Orchestration - Templating</a>
221+
* @param templateConfig The template configuration to use.
222+
* @return A new configuration with the given template configuration.
223+
* @since 1.4.0
224+
*/
225+
@Tolerate
226+
@Nonnull
227+
@Beta
228+
public OrchestrationModuleConfig withTemplateConfig(
229+
@Nonnull final TemplateConfig templateConfig) {
230+
return this.withTemplateConfig(templateConfig.toLowLevel());
231+
}
214232
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.sap.ai.sdk.orchestration;
2+
3+
import com.google.common.annotations.Beta;
4+
import com.sap.ai.sdk.orchestration.model.ChatCompletionTool;
5+
import com.sap.ai.sdk.orchestration.model.ChatMessage;
6+
import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonObject;
7+
import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchema;
8+
import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchemaJsonSchema;
9+
import com.sap.ai.sdk.orchestration.model.Template;
10+
import com.sap.ai.sdk.orchestration.model.TemplateResponseFormat;
11+
import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig;
12+
import java.util.ArrayList;
13+
import java.util.HashMap;
14+
import java.util.List;
15+
import java.util.Map;
16+
import javax.annotation.Nonnull;
17+
import javax.annotation.Nullable;
18+
import lombok.AccessLevel;
19+
import lombok.AllArgsConstructor;
20+
import lombok.EqualsAndHashCode;
21+
import lombok.NoArgsConstructor;
22+
import lombok.Value;
23+
import lombok.With;
24+
import lombok.val;
25+
26+
/**
27+
* A template to use in {@link OrchestrationModuleConfig}.
28+
*
29+
* @since 1.4.0
30+
*/
31+
@EqualsAndHashCode(callSuper = true)
32+
@Value
33+
@With
34+
@AllArgsConstructor(access = AccessLevel.PRIVATE)
35+
@NoArgsConstructor(force = true, access = AccessLevel.PACKAGE)
36+
@Beta
37+
public class OrchestrationTemplate extends TemplateConfig {
38+
@Nullable List<ChatMessage> template;
39+
@Nullable Map<String, String> defaults;
40+
41+
@With(AccessLevel.PRIVATE)
42+
@Nullable
43+
TemplateResponseFormat responseFormat;
44+
45+
@Nullable List<ChatCompletionTool> tools;
46+
47+
/**
48+
* Create a low-level representation of the template.
49+
*
50+
* @return The low-level representation of the template.
51+
*/
52+
@Override
53+
@Nonnull
54+
protected TemplatingModuleConfig toLowLevel() {
55+
final List<ChatMessage> template = this.template != null ? this.template : List.of();
56+
final Map<String, String> defaults = this.defaults != null ? this.defaults : new HashMap<>();
57+
final List<ChatCompletionTool> tools = this.tools != null ? this.tools : new ArrayList<>();
58+
return Template.create()
59+
.template(template)
60+
.defaults(defaults)
61+
.responseFormat(responseFormat)
62+
.tools(tools);
63+
}
64+
65+
/**
66+
* Set the response format to the given JSON schema.
67+
*
68+
* @param schema The JSON schema to use.
69+
* @return The updated template.
70+
*/
71+
@Nonnull
72+
public OrchestrationTemplate withJsonSchemaResponse(@Nonnull final ResponseJsonSchema schema) {
73+
val responseFormatJsonSchema =
74+
ResponseFormatJsonSchema.create()
75+
.type(ResponseFormatJsonSchema.TypeEnum.JSON_SCHEMA)
76+
.jsonSchema(
77+
ResponseFormatJsonSchemaJsonSchema.create()
78+
.name(schema.getName())
79+
.schema(schema.getSchemaMap())
80+
.strict(schema.getStrict())
81+
.description(schema.getDescription()));
82+
return this.withResponseFormat(responseFormatJsonSchema);
83+
}
84+
85+
/**
86+
* Set the response format to JSON object.
87+
*
88+
* @return The updated template.
89+
*/
90+
@Nonnull
91+
public OrchestrationTemplate withJsonResponse() {
92+
val responseFormatJsonObject =
93+
ResponseFormatJsonObject.create().type(ResponseFormatJsonObject.TypeEnum.JSON_OBJECT);
94+
return this.withResponseFormat(responseFormatJsonObject);
95+
}
96+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.sap.ai.sdk.orchestration;
2+
3+
import com.google.common.annotations.Beta;
4+
import com.sap.ai.sdk.orchestration.model.TemplateRef;
5+
import com.sap.ai.sdk.orchestration.model.TemplateRefTemplateRef;
6+
import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig;
7+
import javax.annotation.Nonnull;
8+
import lombok.AccessLevel;
9+
import lombok.AllArgsConstructor;
10+
import lombok.EqualsAndHashCode;
11+
import lombok.Value;
12+
13+
/**
14+
* A reference to a template to use in {@link OrchestrationModuleConfig}.
15+
*
16+
* @since 1.4.0
17+
*/
18+
@EqualsAndHashCode(callSuper = true)
19+
@Value
20+
@AllArgsConstructor(access = AccessLevel.PROTECTED)
21+
@Beta
22+
public class OrchestrationTemplateReference extends TemplateConfig {
23+
@Nonnull TemplateRefTemplateRef reference;
24+
25+
/**
26+
* Create a low-level representation of the template.
27+
*
28+
* @return The low-level representation of the template.
29+
*/
30+
@Nonnull
31+
@Override
32+
protected TemplatingModuleConfig toLowLevel() {
33+
return TemplateRef.create().templateRef(reference);
34+
}
35+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.sap.ai.sdk.orchestration;
2+
3+
import com.fasterxml.jackson.core.type.TypeReference;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.github.victools.jsonschema.generator.Option;
6+
import com.github.victools.jsonschema.generator.OptionPreset;
7+
import com.github.victools.jsonschema.generator.SchemaGenerator;
8+
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
9+
import com.github.victools.jsonschema.generator.SchemaVersion;
10+
import com.github.victools.jsonschema.module.jackson.JacksonModule;
11+
import com.github.victools.jsonschema.module.jackson.JacksonOption;
12+
import com.google.common.annotations.Beta;
13+
import java.lang.reflect.Type;
14+
import java.util.Map;
15+
import javax.annotation.Nonnull;
16+
import javax.annotation.Nullable;
17+
import lombok.AccessLevel;
18+
import lombok.AllArgsConstructor;
19+
import lombok.Value;
20+
import lombok.With;
21+
import lombok.val;
22+
23+
/**
24+
* The schema object to use for the response format parameter in {@link OrchestrationTemplate}.
25+
*
26+
* @since 1.4.0
27+
*/
28+
@Value
29+
@AllArgsConstructor(access = AccessLevel.PACKAGE)
30+
@With
31+
@Beta
32+
public class ResponseJsonSchema {
33+
@Nonnull Map<String, Object> schemaMap;
34+
@Nonnull String name;
35+
@Nullable String description;
36+
@Nullable Boolean strict;
37+
38+
/**
39+
* Create a new instance of {@link ResponseJsonSchema} with the given schema map and name.
40+
*
41+
* @param schemaMap The schema map
42+
* @param name The name of the schema
43+
* @return The new instance of {@link ResponseJsonSchema}
44+
*/
45+
@Nonnull
46+
public static ResponseJsonSchema fromMap(
47+
@Nonnull final Map<String, Object> schemaMap, @Nonnull final String name) {
48+
return new ResponseJsonSchema(schemaMap, name, null, null);
49+
}
50+
51+
/**
52+
* Create a new instance of {@link ResponseJsonSchema} from a given class.
53+
*
54+
* @param classType The class to generate the schema from
55+
* @return The new instance of {@link ResponseJsonSchema}
56+
*/
57+
@Nonnull
58+
public static ResponseJsonSchema fromType(@Nonnull final Type classType) {
59+
val module =
60+
new JacksonModule(
61+
JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, JacksonOption.RESPECT_JSONPROPERTY_ORDER);
62+
val generator =
63+
new SchemaGenerator(
64+
new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON)
65+
.without(Option.SCHEMA_VERSION_INDICATOR)
66+
.with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT)
67+
.with(module)
68+
.build());
69+
val jsonSchema = generator.generateSchema(classType);
70+
val mapper = new ObjectMapper();
71+
val schemaMap = mapper.convertValue(jsonSchema, new TypeReference<Map<String, Object>>() {});
72+
val schemaName = ((Class<?>) classType).getSimpleName() + "-Schema";
73+
return new ResponseJsonSchema(schemaMap, schemaName, null, null);
74+
}
75+
}

0 commit comments

Comments
 (0)