Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .pipeline/spotbugs-exclusions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@
<Package name="com.sap.ai.sdk.grounding.model"/>
</Or>
</Match>
<Match>
<Class name="com.sap.ai.sdk.orchestration.ResponseJsonSchema" />
<Method name="withStrict" />
<Bug pattern="RC_REF_COMPARISON_BAD_PRACTICE_BOOLEAN" />
</Match>
</FindBugsFilter>
78 changes: 38 additions & 40 deletions docs/guides/ORCHESTRATION_CHAT_COMPLETION.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,20 +311,15 @@ 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.withTemplateConfig(TemplateConfig.create().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,38 +329,41 @@ 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.fromType(MyClass.class)
.withDescription("Output schema for the example class MyClass.")
.withStrict(true);
var config = new OrchestrationModuleConfig()
.withLlmConfig(OrchestrationAiModel.GPT_4O);
var configWithResponseSchema =
config.withTemplateConfig(TemplateConfig.create().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.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.fromMap(schemaMap, "Translator-Schema");
var config = new OrchestrationModuleConfig()
.withLlmConfig(OrchestrationAiModel.GPT_4O);
var configWithResponseSchema =
config.withTemplateConfig(TemplateConfig.create().withJsonSchemaResponse(schemaFromMap));
```

</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)

Expand Down
4 changes: 3 additions & 1 deletion docs/release-notes/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@

### ✨ New Functionality

- [Orchestration] [Add Spring AI tool calling](../guides/SPRING_AI_INTEGRATION.md#tool-calling).
- [Orchestration]
- [Add Spring AI tool calling](../guides/SPRING_AI_INTEGRATION.md#tool-calling).
- [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)
- [Document Grounding] [Add Document Grounding Client](https://github.com/SAP/ai-sdk-java/tree/main/docs/guides/GROUNDING.md)
- `com.sap.ai.sdk:document-grounding:1.4.0`
- [OpenAI]
Expand Down
18 changes: 13 additions & 5 deletions orchestration/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@
</developers>
<properties>
<project.rootdir>${project.basedir}/../</project.rootdir>
<coverage.complexity>81%</coverage.complexity>
<coverage.line>92%</coverage.line>
<coverage.instruction>93%</coverage.instruction>
<coverage.branch>74%</coverage.branch>
<coverage.method>92%</coverage.method>
<coverage.complexity>82%</coverage.complexity>
<coverage.line>93%</coverage.line>
<coverage.instruction>94%</coverage.instruction>
<coverage.branch>76%</coverage.branch>
<coverage.method>93%</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
@@ -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;
Expand Down Expand Up @@ -211,4 +212,21 @@ public OrchestrationModuleConfig withGrounding(
@Nonnull final GroundingProvider groundingProvider) {
return this.withGroundingConfig(groundingProvider.createConfig());
}

/**
* Creates a new configuration with the given template configuration as {@link TemplateConfig}.
*
* @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.
* @since 1.4.0
*/
@Tolerate
@Nonnull
@Beta
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added @Beta annotations to every new convenience method/class.

public OrchestrationModuleConfig withTemplateConfig(
@Nonnull final TemplateConfig templateConfig) {
return this.withTemplateConfig(templateConfig.toLowLevel());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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;
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 java.util.ArrayList;
import java.util.HashMap;
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;
import lombok.NoArgsConstructor;
import lombok.Value;
import lombok.With;
import lombok.val;

/**
* A template to use in {@link OrchestrationModuleConfig}.
*
* @since 1.4.0
*/
@EqualsAndHashCode(callSuper = true)
@Value
@With
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(force = true, access = AccessLevel.PACKAGE)
@Beta
public class OrchestrationTemplate extends TemplateConfig {
@Nullable List<ChatMessage> template;
@Nullable Map<String, String> defaults;

@With(AccessLevel.PRIVATE)
@Nullable
TemplateResponseFormat responseFormat;

@Nullable List<ChatCompletionTool> tools;

/**
* Create a low-level representation of the template.
*
* @return The low-level representation of the template.
*/
@Override
@Nonnull
protected TemplatingModuleConfig toLowLevel() {
final List<ChatMessage> template = this.template != null ? this.template : List.of();
final Map<String, String> defaults = this.defaults != null ? this.defaults : new HashMap<>();
final List<ChatCompletionTool> tools = this.tools != null ? this.tools : new ArrayList<>();
return Template.create()
.template(template)
.defaults(defaults)
.responseFormat(responseFormat)
.tools(tools);
}

/**
* 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)
.jsonSchema(
ResponseFormatJsonSchemaJsonSchema.create()
.name(schema.getName())
.schema(schema.getSchemaMap())
.strict(schema.getStrict())
.description(schema.getDescription()));
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);
return this.withResponseFormat(responseFormatJsonObject);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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;
import javax.annotation.Nonnull;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Value;

/**
* A reference to a template to use in {@link OrchestrationModuleConfig}.
*
* @since 1.4.0
*/
@EqualsAndHashCode(callSuper = true)
@Value
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Beta
public class OrchestrationTemplateReference extends TemplateConfig {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not add tests for this class in OrchestrationUnitTests since this is part of BLI 184

@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 TemplateRef.create().templateRef(reference);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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.google.common.annotations.Beta;
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 OrchestrationTemplate}.
*
* @since 1.4.0
*/
@Value
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@With
@Beta
public class ResponseJsonSchema {
@Nonnull Map<String, Object> schemaMap;
@Nonnull String name;
@Nullable String description;
@Nullable Boolean strict;

/**
* 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}
*/
@Nonnull
public static ResponseJsonSchema fromMap(
@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}
*/
@Nonnull
public static ResponseJsonSchema fromType(@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();
val schemaMap = mapper.convertValue(jsonSchema, new TypeReference<Map<String, Object>>() {});
val schemaName = ((Class<?>) classType).getSimpleName() + "-Schema";
return new ResponseJsonSchema(schemaMap, schemaName, null, null);
}
}
Loading
Loading