Skip to content

Commit 0c4d933

Browse files
MatKuhrbot-sdk-jsCharlesDuboisSAP
authored
feat: Orchestration Prompt (#140)
* feat: Orchestration Prompt * Add OrchestrationConfig class * Formatting * Minor tweaks * fix PMD * Update orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationPrompt.java Co-authored-by: Charles Dubois <[email protected]> * Update orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java Co-authored-by: Charles Dubois <[email protected]> * Removed mentions of "DTO" (#145) * Minor updates * Update docs * Minor tweaks * Formatting * Update docs/guides/ORCHESTRATION_CHAT_COMPLETION.md Co-authored-by: Charles Dubois <[email protected]> --------- Co-authored-by: SAP Cloud SDK Bot <[email protected]> Co-authored-by: Charles Dubois <[email protected]>
1 parent 3526c92 commit 0c4d933

File tree

14 files changed

+502
-311
lines changed

14 files changed

+502
-311
lines changed

docs/guides/ORCHESTRATION_CHAT_COMPLETION.md

Lines changed: 72 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -68,90 +68,84 @@ In addition to the prerequisites above, we assume you have already set up the fo
6868
```
6969

7070
</details>
71-
72-
### Chat completion with Templates
7371

74-
Use a chat completion template to generate a response in German:
72+
### Create a Client
73+
74+
To use the Orchestration service, create a client and a configuration object:
7575

7676
```java
77-
var llmConfig = LLMModuleConfig.create().modelName("gpt-35-turbo").modelParams(Map.of());
77+
var client = new OrchestrationClient();
7878

79-
var inputParams =
80-
Map.of("input", "Reply with 'Orchestration Service is working!' in German");
81-
var template = ChatMessage.create().role("user").content("{{?input}}");
82-
var templatingConfig = TemplatingModuleConfig.create().template(template);
79+
var config = new OrchestrationModuleConfig()
80+
.withLlmConfig(LLMModuleConfig.create().modelName("gpt-35-turbo").modelParams(Map.of()));
81+
```
82+
83+
Please also refer to [our sample code](../../sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java) for this and all following code examples.
84+
85+
### Chat Completion
8386

84-
var config =
85-
CompletionPostRequest.create()
86-
.orchestrationConfig(
87-
OrchestrationConfig.create()
88-
.moduleConfigurations(
89-
ModuleConfigs.create()
90-
.llmModuleConfig(llmConfig)
91-
.templatingModuleConfig(templatingConfig)))
92-
.inputParams(inputParams);
87+
Use the Orchestration service to generate a response to a user message:
9388

94-
CompletionPostResponse result =
95-
new OrchestrationClient().chatCompletion(config);
89+
```java
90+
var prompt = new OrchestrationPrompt("Hello world! Why is this phrase so famous?");
91+
92+
var result = client.chatCompletion(prompt, config);
9693

9794
String messageResult =
98-
result.getOrchestrationResult().getChoices().get(0).getMessage().getContent();
95+
result.getOrchestrationResult().getChoices().get(0).getMessage().getContent();
9996
```
10097

101-
See [an example in our Spring Boot application](../../sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java)
98+
In this example, the Orchestration service generates a response to the user message "Hello world! Why is this phrase so famous?".
99+
The LLM response is available as the first choice under the `result.getOrchestrationResult()` object.
102100

103-
### Message history
101+
### Chat completion with Templates
104102

105-
Include a message history to maintain context in the conversation:
103+
Use a prepared template and execute requests with by passing only the input parameters:
106104

107105
```java
108-
var llmConfig = LLMModuleConfig.create().modelName("gpt-35-turbo").modelParams(Map.of());
106+
var template =
107+
ChatMessage.create()
108+
.role("user")
109+
.content("Reply with 'Orchestration Service is working!' in {{?language}}");
110+
var templatingConfig = TemplatingModuleConfig.create().template(template);
111+
var configWithTemplate = config.withTemplateConfig(templatingConfig);
112+
113+
var inputParams = Map.of("language", "German");
114+
var prompt = new OrchestrationPrompt(inputParams);
115+
116+
var result = client.chatCompletion(prompt, configWithTemplate);
117+
```
118+
119+
In this case the template is defined with the placeholder `{{?language}}` which is replaced by the value `German` in the input parameters.
120+
121+
### Message history
109122

110-
List<ChatMessage> messagesHistory =
111-
List.of(
112-
ChatMessage.create().role("user").content("What is the capital of France?"),
113-
ChatMessage.create().role("assistant").content("The capital of France is Paris."));
123+
Include a message history to maintain context in the conversation:
114124

125+
```java
126+
var messagesHistory =
127+
List.of(
128+
ChatMessage.create().role("user").content("What is the capital of France?"),
129+
ChatMessage.create().role("assistant").content("The capital of France is Paris."));
115130
var message =
116-
ChatMessage.create().role("user").content("What is the typical food there?");
117-
var templatingConfig = TemplatingModuleConfig.create().template(message);
118-
119-
var config =
120-
CompletionPostRequest.create()
121-
.orchestrationConfig(
122-
OrchestrationConfig.create()
123-
.moduleConfigurations(
124-
ModuleConfigs.create()
125-
.llmModuleConfig(llmConfig)
126-
.templatingModuleConfig(templatingConfig)))
127-
.messagesHistory(messagesHistory);
128-
129-
CompletionPostResponse result =
130-
new OrchestrationClient().chatCompletion(config);
131+
ChatMessage.create().role("user").content("What is the typical food there?");
131132

132-
String messageResult =
133-
result.getOrchestrationResult().getChoices().get(0).getMessage().getContent();
134-
```
133+
var prompt = new OrchestrationPrompt(message).messageHistory(messagesHistory);
135134

136-
See [an example in our Spring Boot application](../../sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java)
135+
var result = new OrchestrationClient().chatCompletion(prompt, config);
136+
```
137137

138138
### Chat completion filter
139139

140140
Apply content filtering to the chat completion:
141141

142142
```java
143-
var llmConfig = LLMModuleConfig.create().modelName("gpt-35-turbo").modelParams(Map.of());
144-
145-
var inputParams =
146-
Map.of(
147-
"disclaimer",
148-
"```DISCLAIMER: The area surrounding the apartment is known for prostitutes and gang violence including armed conflicts, gun violence is frequent.");
149-
var template =
150-
ChatMessage.create()
151-
.role("user")
152-
.content(
153-
"Create a rental posting for subletting my apartment in the downtown area. Keep it short. Make sure to add the following disclaimer to the end. Do not change it! {{?disclaimer}}");
154-
var templatingConfig = TemplatingModuleConfig.create().template(template);
143+
var prompt = new OrchestrationPrompt(
144+
"""
145+
Create a rental posting for subletting my apartment in the downtown area. Keep it short. Make sure to add the following disclaimer to the end. Do not change it!
146+
147+
```DISCLAIMER: The area surrounding the apartment is known for prostitutes and gang violence including armed conflicts, gun violence is frequent.
148+
""");
155149

156150
var filterStrict =
157151
FilterConfig.create()
@@ -176,40 +170,21 @@ var filterLoose =
176170
var filteringConfig =
177171
FilteringModuleConfig.create()
178172
// changing the input to filterLoose will allow the message to pass
179-
.input(FilteringConfig.create().filters(filterStrict))
180-
.output(FilteringConfig.create().filters(filterStrict));
181-
182-
var config =
183-
CompletionPostRequest.create()
184-
.orchestrationConfig(
185-
OrchestrationConfig.create()
186-
.moduleConfigurations(
187-
ModuleConfigs.create()
188-
.llmModuleConfig(llmConfig)
189-
.templatingModuleConfig(templatingConfig)
190-
.filteringModuleConfig(filteringConfig)))
191-
.inputParams(inputParams);
173+
.input(InputFilteringConfig.create().filters(filterStrict))
174+
.output(OutputFilteringConfig.create().filters(filterStrict));
192175

193-
// this fails with Bad Request because the strict filter prohibits the input message
194-
CompletionPostResponse result =
195-
new OrchestrationClient().chatCompletion(config);
176+
var configWithFilter = config.withFilteringConfig(filteringConfig);
196177

197-
String messageResult =
198-
result.getOrchestrationResult().getChoices().get(0).getMessage().getContent();
178+
// this fails with Bad Request because the strict filter prohibits the input message
179+
var result =
180+
new OrchestrationClient().chatCompletion(prompt, configWithFilter);
199181
```
200182

201-
See [an example in our Spring Boot application](../../sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java)
202-
203183
### Data masking
204184

205185
Use the data masking module to anonymize personal information in the input:
206186

207187
```java
208-
var inputParams = Map.of("privateInfo", "Patrick Morgan +49 (970) 333-3833");
209-
var template =
210-
ChatMessage.create().role("user").content("What is the nationality of {{?privateInfo}}");
211-
var templatingConfig = TemplatingModuleConfig.create().template(template);
212-
213188
var maskingProvider =
214189
MaskingProviderConfig.create()
215190
.type(MaskingProviderConfig.TypeEnum.SAP_DATA_PRIVACY_INTEGRATION)
@@ -218,29 +193,26 @@ var maskingProvider =
218193
DPIEntityConfig.create().type(DPIEntities.PHONE),
219194
DPIEntityConfig.create().type(DPIEntities.PERSON));
220195
var maskingConfig = MaskingModuleConfig.create().maskingProviders(maskingProvider);
196+
var configWithMasking = config.withMaskingConfig(maskingConfig);
221197

222-
CompletionPostRequest config =
223-
CompletionPostRequest.create()
224-
.orchestrationConfig(
225-
OrchestrationConfig.create()
226-
.moduleConfigurations(
227-
ModuleConfigs.create()
228-
.llmModuleConfig(LLM_CONFIG)
229-
.templatingModuleConfig(templatingConfig)
230-
.maskingModuleConfig(maskingConfig)))
231-
.inputParams(inputParams);
198+
var systemMessage = ChatMessage.create()
199+
.role("system")
200+
.content("Please evaluate the following user feedback and judge if the sentiment is positive or negative.");
201+
var userMessage = ChatMessage.create()
202+
.role("user")
203+
.content("""
204+
I think the SDK is good, but could use some further enhancements.
205+
My architect Alice and manager Bob pointed out that we need the grounding capabilities, which aren't supported yet.
206+
""");
232207

233-
CompletionPostResponse result =
234-
new OrchestrationClient().chatCompletion(config);
208+
var prompt = new OrchestrationPrompt(systemMessage, userMessage);
235209

236-
String messageResult =
237-
result.getOrchestrationResult().getChoices().get(0).getMessage().getContent();
210+
var result =
211+
new OrchestrationClient().chatCompletion(prompt, configWithMasking);
238212
```
239213

240214
In this example, the input will be masked before the call to the LLM. Note that data cannot be unmasked in the LLM output.
241215

242-
See [an example in our Spring Boot application](../../sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java)
243-
244216
### Set model parameters
245217

246218
Change your LLM module configuration to add model parameters:
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.sap.ai.sdk.orchestration;
2+
3+
import com.sap.ai.sdk.orchestration.client.model.CompletionPostRequest;
4+
import com.sap.ai.sdk.orchestration.client.model.ModuleConfigs;
5+
import com.sap.ai.sdk.orchestration.client.model.OrchestrationConfig;
6+
import com.sap.ai.sdk.orchestration.client.model.TemplatingModuleConfig;
7+
import io.vavr.control.Option;
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
import javax.annotation.Nonnull;
11+
import javax.annotation.Nullable;
12+
import lombok.AccessLevel;
13+
import lombok.NoArgsConstructor;
14+
import lombok.val;
15+
16+
/** Factory to create all data objects from an orchestration configuration. */
17+
@NoArgsConstructor(access = AccessLevel.NONE)
18+
final class ConfigToRequestTransformer {
19+
@Nonnull
20+
static CompletionPostRequest toCompletionPostRequest(
21+
@Nonnull final OrchestrationPrompt prompt, @Nonnull final OrchestrationModuleConfig config) {
22+
val template = toTemplateModuleConfig(prompt, config.getTemplateConfig());
23+
// note that the config is immutable and implicitly copied here
24+
// copying is required here, to not alter the original config object, which might be reused for
25+
// subsequent requests
26+
val configCopy = config.withTemplateConfig(template);
27+
28+
return CompletionPostRequest.create()
29+
.orchestrationConfig(
30+
OrchestrationConfig.create().moduleConfigurations(toModuleConfigs(configCopy)))
31+
.inputParams(prompt.getTemplateParameters())
32+
.messagesHistory(prompt.getMessagesHistory());
33+
}
34+
35+
@Nonnull
36+
static TemplatingModuleConfig toTemplateModuleConfig(
37+
@Nonnull final OrchestrationPrompt prompt, @Nullable final TemplatingModuleConfig template) {
38+
/*
39+
* Currently, we have to merge the prompt into the template configuration.
40+
* This works around the limitation that the template config is required.
41+
* This comes at the risk that the prompt unintentionally contains the templating pattern "{{? .. }}".
42+
* In this case, the request will fail, since the templating module will try to resolve the parameter.
43+
* To be fixed with https://github.tools.sap/AI/llm-orchestration/issues/662
44+
*/
45+
val messages = Option.of(template).map(TemplatingModuleConfig::getTemplate).getOrElse(List::of);
46+
val messagesWithPrompt = new ArrayList<>(messages);
47+
messagesWithPrompt.addAll(prompt.getMessages());
48+
if (messagesWithPrompt.isEmpty()) {
49+
throw new IllegalStateException(
50+
"A prompt is required. Pass at least one message or configure a template with messages or a template reference.");
51+
}
52+
return TemplatingModuleConfig.create().template(messagesWithPrompt);
53+
}
54+
55+
@Nonnull
56+
static ModuleConfigs toModuleConfigs(@Nonnull final OrchestrationModuleConfig config) {
57+
val llmConfig =
58+
Option.of(config.getLlmConfig())
59+
.getOrElseThrow(() -> new IllegalStateException("LLM config is required."));
60+
61+
//noinspection DataFlowIssue the template is always non-null here
62+
val moduleConfig =
63+
ModuleConfigs.create()
64+
.llmModuleConfig(llmConfig)
65+
.templatingModuleConfig(config.getTemplateConfig());
66+
67+
Option.of(config.getFilteringConfig()).forEach(moduleConfig::filteringModuleConfig);
68+
Option.of(config.getMaskingConfig()).forEach(moduleConfig::maskingModuleConfig);
69+
70+
return moduleConfig;
71+
}
72+
}

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

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import com.sap.ai.sdk.core.AiCoreService;
1111
import com.sap.ai.sdk.orchestration.client.model.CompletionPostRequest;
1212
import com.sap.ai.sdk.orchestration.client.model.CompletionPostResponse;
13+
import com.sap.ai.sdk.orchestration.client.model.ModuleConfigs;
14+
import com.sap.ai.sdk.orchestration.client.model.OrchestrationConfig;
1315
import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor;
1416
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
1517
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationNotFoundException;
@@ -67,13 +69,17 @@ public OrchestrationClient(@Nonnull final AiCoreDeployment deployment) {
6769
/**
6870
* Generate a completion for the given prompt.
6971
*
70-
* @param request The request to send to orchestration.
72+
* @param prompt The {@link OrchestrationPrompt} to send to orchestration.
73+
* @param config The {@link ModuleConfigs} configuration to use for the completion.
7174
* @return the completion output
72-
* @throws OrchestrationClientException if the request fails
75+
* @throws OrchestrationClientException if the request fails.
7376
*/
7477
@Nonnull
75-
public CompletionPostResponse chatCompletion(@Nonnull final CompletionPostRequest request)
78+
public CompletionPostResponse chatCompletion(
79+
@Nonnull final OrchestrationPrompt prompt, @Nonnull final OrchestrationModuleConfig config)
7680
throws OrchestrationClientException {
81+
82+
val request = toCompletionPostRequest(prompt, config);
7783
return executeRequest(request);
7884
}
7985

@@ -93,8 +99,8 @@ public CompletionPostResponse chatCompletion(@Nonnull final CompletionPostReques
9399
*
94100
* <p>Alternatively, you can call this method directly with a fully custom request object.
95101
*
96-
* @param request The request DTO to send to orchestration.
97-
* @return The response DTO from orchestration.
102+
* @param request The request data object to send to orchestration.
103+
* @return The response data object from orchestration.
98104
* @throws OrchestrationClientException If the request fails.
99105
*/
100106
@Nonnull
@@ -112,6 +118,20 @@ public CompletionPostResponse executeRequest(@Nonnull final CompletionPostReques
112118
return executeRequest(postRequest);
113119
}
114120

121+
/**
122+
* Convert the given prompt and config into a low-level request data object. The data object
123+
* allows for further customization before sending the request.
124+
*
125+
* @param prompt The {@link OrchestrationPrompt} to generate a completion for.
126+
* @param config The {@link OrchestrationConfig } configuration to use for the completion.
127+
* @return The low-level request data object to send to orchestration.
128+
*/
129+
@Nonnull
130+
public static CompletionPostRequest toCompletionPostRequest(
131+
@Nonnull final OrchestrationPrompt prompt, @Nonnull final OrchestrationModuleConfig config) {
132+
return ConfigToRequestTransformer.toCompletionPostRequest(prompt, config);
133+
}
134+
115135
@SuppressWarnings("UnstableApiUsage")
116136
@Nonnull
117137
CompletionPostResponse executeRequest(@Nonnull final BasicClassicHttpRequest request) {

0 commit comments

Comments
 (0)