Skip to content

Commit 5d43a55

Browse files
Orchestration Client (#128)
* Orchestration Client * Documentation fix * Added error tests * Better Javadoc
1 parent 20cb326 commit 5d43a55

File tree

15 files changed

+410
-307
lines changed

15 files changed

+410
-307
lines changed

docs/guides/ORCHESTRATION_CHAT_COMPLETION.md

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,8 @@ var config =
9191
.templatingModuleConfig(templatingConfig)))
9292
.inputParams(inputParams);
9393

94-
var resourceGroupId = "default";
95-
var client = new AiCoreService().forDeploymentbyScenario("orchestration")
96-
.withResourceGroup(resourceGroupId)
97-
.client();
98-
9994
CompletionPostResponse result =
100-
new OrchestrationCompletionApi(client)
101-
.orchestrationV1EndpointsCreate(config);
95+
new OrchestrationClient().chatCompletion(config);
10296

10397
String messageResult =
10498
result.getOrchestrationResult().getChoices().get(0).getMessage().getContent();
@@ -133,14 +127,8 @@ var config =
133127
.inputParams(Map.of())
134128
.messagesHistory(messagesHistory);
135129

136-
var resourceGroupId = "default";
137-
var client = new AiCoreService().forDeploymentbyScenario("orchestration")
138-
.withResourceGroup(resourceGroupId)
139-
.client();
140-
141130
CompletionPostResponse result =
142-
new OrchestrationCompletionApi(client)
143-
.orchestrationV1EndpointsCreate(config);
131+
new OrchestrationClient().chatCompletion(config);
144132

145133
String messageResult =
146134
result.getOrchestrationResult().getChoices().get(0).getMessage().getContent();
@@ -203,15 +191,9 @@ var config =
203191
.filteringModuleConfig(filteringConfig)))
204192
.inputParams(inputParams);
205193

206-
var resourceGroupId = "default";
207-
var client = new AiCoreService().forDeploymentbyScenario("orchestration")
208-
.withResourceGroup(resourceGroupId)
209-
.client();
210-
194+
// this fails with Bad Request because the strict filter prohibits the input message
211195
CompletionPostResponse result =
212-
new OrchestrationCompletionApi(client)
213-
// this fails with Bad Request because the strict filter prohibits the input message
214-
.orchestrationV1EndpointsCreate(config);
196+
new OrchestrationClient().chatCompletion(config);
215197

216198
String messageResult =
217199
result.getOrchestrationResult().getChoices().get(0).getMessage().getContent();
@@ -249,14 +231,8 @@ CompletionPostRequest config =
249231
.maskingModuleConfig(maskingConfig)))
250232
.inputParams(inputParams);
251233

252-
var resourceGroupId = "default";
253-
var client = new AiCoreService().forDeploymentbyScenario("orchestration")
254-
.withResourceGroup(resourceGroupId)
255-
.client();
256-
257234
CompletionPostResponse result =
258-
new OrchestrationCompletionApi(client)
259-
.orchestrationV1EndpointsCreate(config);
235+
new OrchestrationClient().chatCompletion(config);
260236

261237
String messageResult =
262238
result.getOrchestrationResult().getChoices().get(0).getMessage().getContent();

foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
@WireMockTest
5252
class OpenAiClientTest {
5353
private static OpenAiClient client;
54-
private final Function<String, InputStream> TEST_FILE_LOADER =
54+
private final Function<String, InputStream> fileLoader =
5555
filename -> Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream(filename));
5656

5757
@BeforeEach
@@ -193,7 +193,7 @@ private static Callable<?>[] chatCompletionCalls() {
193193
@ParameterizedTest
194194
@MethodSource("chatCompletionCalls")
195195
void chatCompletion(@Nonnull final Callable<OpenAiChatCompletionOutput> request) {
196-
try (var inputStream = TEST_FILE_LOADER.apply("__files/chatCompletionResponse.json")) {
196+
try (var inputStream = fileLoader.apply("__files/chatCompletionResponse.json")) {
197197

198198
final String response = new String(inputStream.readAllBytes());
199199
stubFor(post("/chat/completions").willReturn(okJson(response)));
@@ -375,7 +375,7 @@ void embedding() {
375375

376376
@Test
377377
void streamChatCompletionDeltasErrorHandling() throws IOException {
378-
try (var inputStream = spy(TEST_FILE_LOADER.apply("streamChatCompletionError.txt"))) {
378+
try (var inputStream = spy(fileLoader.apply("streamChatCompletionError.txt"))) {
379379

380380
final var httpClient = mock(HttpClient.class);
381381
ApacheHttpClient5Accessor.setHttpClientFactory(destination -> httpClient);
@@ -408,7 +408,7 @@ void streamChatCompletionDeltasErrorHandling() throws IOException {
408408

409409
@Test
410410
void streamChatCompletionDeltas() throws IOException {
411-
try (var inputStream = spy(TEST_FILE_LOADER.apply("streamChatCompletion.txt"))) {
411+
try (var inputStream = spy(fileLoader.apply("streamChatCompletion.txt"))) {
412412

413413
final var httpClient = mock(HttpClient.class);
414414
ApacheHttpClient5Accessor.setHttpClientFactory(destination -> httpClient);

orchestration/pom.xml

Lines changed: 51 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -36,32 +36,66 @@
3636
<dependencies>
3737
<!-- scope "compile" -->
3838
<dependency>
39-
<groupId>com.sap.cloud.sdk.datamodel</groupId>
40-
<artifactId>openapi-core</artifactId>
39+
<groupId>com.sap.ai.sdk</groupId>
40+
<artifactId>core</artifactId>
4141
</dependency>
42+
4243
<dependency>
43-
<groupId>org.springframework</groupId>
44-
<artifactId>spring-core</artifactId>
44+
<groupId>com.sap.cloud.sdk.cloudplatform</groupId>
45+
<artifactId>cloudplatform-connectivity</artifactId>
4546
</dependency>
4647
<dependency>
47-
<groupId>org.springframework</groupId>
48-
<artifactId>spring-web</artifactId>
48+
<groupId>com.sap.cloud.sdk.cloudplatform</groupId>
49+
<artifactId>connectivity-apache-httpclient5</artifactId>
4950
</dependency>
51+
5052
<dependency>
51-
<groupId>com.sap.cloud.sdk.cloudplatform</groupId>
52-
<artifactId>cloudplatform-connectivity</artifactId>
53+
<groupId>org.apache.httpcomponents.core5</groupId>
54+
<artifactId>httpcore5</artifactId>
55+
</dependency>
56+
<dependency>
57+
<groupId>org.apache.httpcomponents.client5</groupId>
58+
<artifactId>httpclient5</artifactId>
5359
</dependency>
5460
<dependency>
5561
<groupId>com.google.code.findbugs</groupId>
5662
<artifactId>jsr305</artifactId>
5763
</dependency>
5864
<dependency>
59-
<groupId>com.google.guava</groupId>
60-
<artifactId>guava</artifactId>
65+
<groupId>com.fasterxml.jackson.core</groupId>
66+
<artifactId>jackson-annotations</artifactId>
6167
</dependency>
6268
<dependency>
6369
<groupId>com.fasterxml.jackson.core</groupId>
64-
<artifactId>jackson-annotations</artifactId>
70+
<artifactId>jackson-core</artifactId>
71+
</dependency>
72+
<dependency>
73+
<groupId>com.fasterxml.jackson.core</groupId>
74+
<artifactId>jackson-databind</artifactId>
75+
</dependency>
76+
<dependency>
77+
<groupId>com.fasterxml.jackson.datatype</groupId>
78+
<artifactId>jackson-datatype-jsr310</artifactId>
79+
</dependency>
80+
<dependency>
81+
<groupId>io.vavr</groupId>
82+
<artifactId>vavr</artifactId>
83+
</dependency>
84+
<dependency>
85+
<groupId>org.slf4j</groupId>
86+
<artifactId>slf4j-api</artifactId>
87+
</dependency>
88+
89+
<!-- TODO: only needed for JsonObjectMapperBuilder, maybe we can use Jackson natively to avoid this dependency -->
90+
<dependency>
91+
<groupId>org.springframework</groupId>
92+
<artifactId>spring-web</artifactId>
93+
</dependency>
94+
<!-- scope "provided" -->
95+
<dependency>
96+
<groupId>org.projectlombok</groupId>
97+
<artifactId>lombok</artifactId>
98+
<scope>provided</scope>
6599
</dependency>
66100
<!-- scope "test" -->
67101
<dependency>
@@ -74,54 +108,31 @@
74108
<artifactId>wiremock</artifactId>
75109
<scope>test</scope>
76110
</dependency>
77-
<dependency>
78-
<groupId>org.apache.httpcomponents.core5</groupId>
79-
<artifactId>httpcore5</artifactId>
80-
<scope>test</scope>
81-
</dependency>
82111
<dependency>
83112
<groupId>org.assertj</groupId>
84113
<artifactId>assertj-core</artifactId>
85114
<scope>test</scope>
86115
</dependency>
87116
<dependency>
88-
<groupId>com.sap.ai.sdk</groupId>
89-
<artifactId>core</artifactId>
117+
<groupId>org.mockito</groupId>
118+
<artifactId>mockito-core</artifactId>
90119
<scope>test</scope>
91120
</dependency>
92121
</dependencies>
93122

94123
<build>
95124
<plugins>
96-
<plugin>
97-
<artifactId>maven-clean-plugin</artifactId>
98-
<configuration>
99-
<filesets>
100-
<fileset>
101-
<directory>${project.basedir}/src/main/java/com/sap/ai/sdk/orchestration/client</directory>
102-
<includes>
103-
<include>**/*</include>
104-
</includes>
105-
</fileset>
106-
</filesets>
107-
</configuration>
108-
<executions>
109-
<execution>
110-
<id>delete-orchestration-generated-client</id>
111-
</execution>
112-
</executions>
113-
</plugin>
114125
<plugin>
115126
<groupId>com.sap.cloud.sdk.datamodel</groupId>
116127
<artifactId>openapi-generator-maven-plugin</artifactId>
117128
<configuration>
129+
<skip>true</skip>
130+
<!-- skip automatic generation until we can omit API classes from code generation -->
118131
<outputDirectory>${project.basedir}/src/main/java</outputDirectory>
119-
<apiMaturity>released</apiMaturity>
132+
<apiMaturity>beta</apiMaturity>
120133
<enableOneOfAnyOfGeneration>true</enableOneOfAnyOfGeneration>
121134
<compileScope>COMPILE</compileScope>
122-
<!-- Do not delete the output directory because it contains non-generated code -->
123-
<!-- The generated client is instead deleted by the maven-clean-plugin here above -->
124-
<deleteOutputDirectory>false</deleteOutputDirectory>
135+
<deleteOutputDirectory>true</deleteOutputDirectory>
125136
</configuration>
126137
<executions>
127138
<execution>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package com.sap.ai.sdk.orchestration;
2+
3+
import com.fasterxml.jackson.annotation.JsonAutoDetect;
4+
import com.fasterxml.jackson.annotation.JsonInclude;
5+
import com.fasterxml.jackson.annotation.PropertyAccessor;
6+
import com.fasterxml.jackson.core.JsonProcessingException;
7+
import com.fasterxml.jackson.databind.ObjectMapper;
8+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
9+
import com.sap.ai.sdk.core.AiCoreDeployment;
10+
import com.sap.ai.sdk.core.AiCoreService;
11+
import com.sap.ai.sdk.orchestration.client.model.CompletionPostRequest;
12+
import com.sap.ai.sdk.orchestration.client.model.CompletionPostResponse;
13+
import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor;
14+
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
15+
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationNotFoundException;
16+
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.HttpClientInstantiationException;
17+
import java.io.IOException;
18+
import java.util.NoSuchElementException;
19+
import java.util.function.Supplier;
20+
import javax.annotation.Nonnull;
21+
import lombok.extern.slf4j.Slf4j;
22+
import lombok.val;
23+
import org.apache.hc.client5.http.classic.methods.HttpPost;
24+
import org.apache.hc.core5.http.ContentType;
25+
import org.apache.hc.core5.http.io.entity.StringEntity;
26+
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
27+
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
28+
29+
/** Client to execute requests to the orchestration service. */
30+
@Slf4j
31+
public class OrchestrationClient {
32+
static final ObjectMapper JACKSON;
33+
34+
static {
35+
JACKSON =
36+
new Jackson2ObjectMapperBuilder()
37+
.modules(new JavaTimeModule())
38+
.visibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE)
39+
.visibility(PropertyAccessor.SETTER, JsonAutoDetect.Visibility.NONE)
40+
.serializationInclusion(JsonInclude.Include.NON_NULL)
41+
.build();
42+
}
43+
44+
@Nonnull private final Supplier<AiCoreDeployment> deployment;
45+
46+
/** Default constructor. */
47+
public OrchestrationClient() {
48+
deployment = () -> new AiCoreService().forDeploymentByScenario("orchestration");
49+
}
50+
51+
/**
52+
* Constructor with a custom deployment, allowing for a custom resource group or otherwise
53+
* specific deployment ID.
54+
*
55+
* <p>Example:
56+
*
57+
* <pre>{@code
58+
* new OrchestrationClient(new AiCoreService().forDeploymentByScenario("orchestration"));
59+
* }</pre>
60+
*
61+
* @param deployment The specific {@link AiCoreDeployment} to use.
62+
*/
63+
public OrchestrationClient(@Nonnull final AiCoreDeployment deployment) {
64+
this.deployment = () -> deployment;
65+
}
66+
67+
/**
68+
* Generate a completion for the given prompt.
69+
*
70+
* @param request The request to send to orchestration.
71+
* @return the completion output
72+
* @throws OrchestrationClientException if the request fails
73+
*/
74+
@Nonnull
75+
public CompletionPostResponse chatCompletion(@Nonnull final CompletionPostRequest request)
76+
throws OrchestrationClientException {
77+
return executeRequest(request);
78+
}
79+
80+
/**
81+
* Serializes the given request, executes it and deserializes the response.
82+
*
83+
* <p>Override this method to customize the request execution. For example, to modify the request
84+
* object before it is sent, use:
85+
*
86+
* <pre>{@code
87+
* @Override
88+
* protected CompletionPostResponse executeRequest(@Nonnull CompletionPostRequest request) {
89+
* request.setCustomField("myField", "myValue");
90+
* return super.executeRequest(request);
91+
* }
92+
* }</pre>
93+
*
94+
* <p>Alternatively, you can call this method directly with a fully custom request object.
95+
*
96+
* @param request The request DTO to send to orchestration.
97+
* @return The response DTO from orchestration.
98+
* @throws OrchestrationClientException If the request fails.
99+
*/
100+
@Nonnull
101+
public CompletionPostResponse executeRequest(@Nonnull final CompletionPostRequest request)
102+
throws OrchestrationClientException {
103+
final BasicClassicHttpRequest postRequest = new HttpPost("/completion");
104+
try {
105+
val json = JACKSON.writeValueAsString(request);
106+
log.debug("Serialized request into JSON payload: {}", json);
107+
postRequest.setEntity(new StringEntity(json, ContentType.APPLICATION_JSON));
108+
} catch (final JsonProcessingException e) {
109+
throw new OrchestrationClientException("Failed to serialize request parameters", e);
110+
}
111+
112+
return executeRequest(postRequest);
113+
}
114+
115+
@SuppressWarnings("UnstableApiUsage")
116+
@Nonnull
117+
CompletionPostResponse executeRequest(@Nonnull final BasicClassicHttpRequest request) {
118+
try {
119+
val destination = deployment.get().destination();
120+
log.debug("Using destination {} to connect to orchestration service", destination);
121+
val client = ApacheHttpClient5Accessor.getHttpClient(destination);
122+
return client.execute(
123+
request, new OrchestrationResponseHandler<>(CompletionPostResponse.class));
124+
} catch (NoSuchElementException
125+
| DestinationAccessException
126+
| DestinationNotFoundException
127+
| HttpClientInstantiationException
128+
| IOException e) {
129+
throw new OrchestrationClientException("Failed to execute request", e);
130+
}
131+
}
132+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.sap.ai.sdk.orchestration;
2+
3+
import lombok.experimental.StandardException;
4+
5+
/** Exception thrown by the {@link OrchestrationClient} in case of an error. */
6+
@StandardException
7+
public class OrchestrationClientException extends RuntimeException {}

0 commit comments

Comments
 (0)