Skip to content

Commit 4559556

Browse files
authored
Refactor Core Classes with Builder (#211)
1 parent dbd992b commit 4559556

File tree

5 files changed

+125
-65
lines changed

5 files changed

+125
-65
lines changed

core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java

Lines changed: 87 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -88,47 +88,33 @@ public HttpDestination getBaseDestination()
8888
}
8989

9090
/**
91-
* Get a new endpoint object, targeting a specific deployment ID.
91+
* Get a destination to perform inference calls against a deployment under the default resource
92+
* group on AI Core.
9293
*
93-
* @param deploymentId The deployment id to be used for the new endpoint.
94-
* @return the new instance.
95-
*/
96-
@Nonnull
97-
public HttpDestination getDestinationForDeploymentById(
98-
@Nonnull final String resourceGroup, @Nonnull final String deploymentId) {
99-
return toInferenceDestination(resourceGroup, deploymentId);
100-
}
101-
102-
/**
103-
* Get a destination to perform inference calls for a specific model. If there are multiple
104-
* deployments of the same model, the first one is returned.
105-
*
106-
* @param model The model to be used for inference calls.
107-
* @return A new instance of the AI Core Deployment.
108-
* @throws DeploymentResolutionException if no running deployment is found for the model.
94+
* @return The destination pointing to the specific deployment ID.
95+
* @throws DestinationAccessException If there was an issue creating the base destination, e.g. in
96+
* case of invalid credentials.
97+
* @throws DestinationNotFoundException If there was an issue creating the base destination, e.g.
98+
* in case of missing credentials.
99+
* @see #getInferenceDestination(String) for specifying a custom resource group.
109100
*/
110101
@Nonnull
111-
public HttpDestination getDestinationForDeploymentByModel(
112-
@Nonnull final String resourceGroup, @Nonnull final AiModel model)
113-
throws DeploymentResolutionException {
114-
val deploymentId = deploymentResolver.getDeploymentIdByModel(resourceGroup, model);
115-
return toInferenceDestination(resourceGroup, deploymentId);
102+
public InferenceDestinationBuilder getInferenceDestination()
103+
throws DestinationAccessException, DestinationNotFoundException {
104+
return new InferenceDestinationBuilder(DEFAULT_RESOURCE_GROUP);
116105
}
117106

118107
/**
119-
* Set a specific deployment by scenario id. If there are multiple deployments of the same model,
120-
* the first one is returned.
108+
* Get a destination to perform inference calls against a deployment for the given resource group
109+
* on AI Core.
121110
*
122-
* @param scenarioId The scenario id to be used for AI Core service calls.
123-
* @return A new instance of the AI Core Deployment.
124-
* @throws DeploymentResolutionException if no running deployment is found for the scenario.
111+
* @param resourceGroup The resource group to be used for the new endpoint.
112+
* @return The destination pointing to the specific deployment ID.
113+
* @see #getInferenceDestination() for using the default resource group.
125114
*/
126115
@Nonnull
127-
public HttpDestination getDestinationForDeploymentByScenario(
128-
@Nonnull final String resourceGroup, @Nonnull final String scenarioId)
129-
throws DeploymentResolutionException {
130-
val deploymentId = deploymentResolver.getDeploymentIdByScenario(resourceGroup, scenarioId);
131-
return toInferenceDestination(resourceGroup, deploymentId);
116+
public InferenceDestinationBuilder getInferenceDestination(@Nonnull final String resourceGroup) {
117+
return new InferenceDestinationBuilder(resourceGroup);
132118
}
133119

134120
/**
@@ -160,17 +146,6 @@ public ApiClient getApiClient() {
160146
return new ApiClient(rt).setBasePath(destination.asHttp().getUri().toString());
161147
}
162148

163-
HttpDestination toInferenceDestination(
164-
@Nonnull final String resourceGroup, @Nonnull final String deploymentId) {
165-
val destination = getBaseDestination();
166-
val path = buildDeploymentPath(deploymentId);
167-
168-
return DefaultHttpDestination.fromDestination(destination)
169-
.uri(destination.getUri().resolve(path))
170-
.property(RESOURCE_GROUP_HEADER_PROPERTY, resourceGroup)
171-
.build();
172-
}
173-
174149
/**
175150
* Helper method to build the <b>relative</b> URL path for the inference endpoint of a deployment.
176151
* The result of this together with the base path defined on the destination will be used for
@@ -196,4 +171,73 @@ protected String buildDeploymentPath(@Nonnull final String deploymentId) {
196171
public void reloadCachedDeployments(@Nonnull final String resourceGroup) {
197172
deploymentResolver.reloadDeployments(resourceGroup);
198173
}
174+
175+
/** Builder for creating inference destinations. */
176+
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
177+
public class InferenceDestinationBuilder {
178+
@Nonnull private final String resourceGroup;
179+
180+
/**
181+
* Use a fixed deployment ID to identify the deployment.
182+
*
183+
* @param deploymentId The ID of the deployment to target.
184+
* @return A new destination targeting the specified deployment.
185+
* @throws DestinationAccessException If there was an issue creating the base destination, e.g.
186+
* in case of invalid credentials.
187+
* @throws DestinationNotFoundException If there was an issue creating the base destination,
188+
* e.g. in case of missing credentials.
189+
* @see #forModel(AiModel)
190+
* @see #forScenario(String)
191+
*/
192+
@Nonnull
193+
public HttpDestination usingDeploymentId(@Nonnull final String deploymentId)
194+
throws DestinationAccessException, DestinationNotFoundException {
195+
return build(deploymentId);
196+
}
197+
198+
/**
199+
* Lookup a deployment based on the given {@link AiModel}. If there are multiple deployments for
200+
* the given model, the first one is returned.
201+
*
202+
* @param model The model to be used for inference calls.
203+
* @return A new destination targeting a deployment for the given model.
204+
* @throws DeploymentResolutionException If no running deployment is found for the model.
205+
* @see #forScenario(String)
206+
* @see #usingDeploymentId(String)
207+
*/
208+
@Nonnull
209+
public HttpDestination forModel(@Nonnull final AiModel model)
210+
throws DeploymentResolutionException {
211+
val id = deploymentResolver.getDeploymentIdByModel(resourceGroup, model);
212+
return build(id);
213+
}
214+
215+
/**
216+
* Lookup a deployment based on the given scenario. If there are multiple deployments within the
217+
* same scenario, the first one is returned.
218+
*
219+
* @param scenarioId The scenario to discover deployments for.
220+
* @return A new destination targeting a deployment within the given scenario.
221+
* @throws DeploymentResolutionException If no running deployment is found within the scenario.
222+
* @see #forModel(AiModel)
223+
* @see #usingDeploymentId(String)
224+
*/
225+
@Nonnull
226+
public HttpDestination forScenario(@Nonnull final String scenarioId)
227+
throws DeploymentResolutionException {
228+
val id = deploymentResolver.getDeploymentIdByScenario(resourceGroup, scenarioId);
229+
return build(id);
230+
}
231+
232+
@Nonnull
233+
HttpDestination build(@Nonnull final String deploymentId) {
234+
val destination = getBaseDestination();
235+
val path = buildDeploymentPath(deploymentId);
236+
237+
return DefaultHttpDestination.fromDestination(destination)
238+
.uri(destination.getUri().resolve(path))
239+
.property(RESOURCE_GROUP_HEADER_PROPERTY, resourceGroup)
240+
.build();
241+
}
242+
}
199243
}

core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.assertj.core.api.Assertions.assertThatCode;
5+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
56

67
import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination;
78
import com.sap.cloud.sdk.cloudplatform.connectivity.Header;
@@ -61,8 +62,12 @@ void testCustomBaseDestination() {
6162
}
6263

6364
@Test
64-
void testToInferenceDestination() {
65-
val destination = service.toInferenceDestination("foo", "123");
65+
void testGetInferenceDestination() {
66+
var builder = service.getInferenceDestination();
67+
assertThatThrownBy(() -> builder.forScenario("doesn't exist"))
68+
.isExactlyInstanceOf(DeploymentResolutionException.class);
69+
70+
val destination = service.getInferenceDestination("foo").usingDeploymentId("123");
6671

6772
assertThat(destination.getUri())
6873
.hasHost("api.ai.com")

foundation-models/openai/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@
8787
<groupId>org.slf4j</groupId>
8888
<artifactId>slf4j-api</artifactId>
8989
</dependency>
90+
<dependency>
91+
<groupId>com.google.guava</groupId>
92+
<artifactId>guava</artifactId>
93+
</dependency>
9094
<!-- scope "provided" -->
9195
<dependency>
9296
<groupId>org.projectlombok</groupId>

foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
import com.fasterxml.jackson.core.JsonProcessingException;
77
import com.fasterxml.jackson.databind.ObjectMapper;
88
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
9+
import com.google.common.annotations.Beta;
910
import com.sap.ai.sdk.core.AiCoreService;
10-
import com.sap.ai.sdk.core.AiModel;
1111
import com.sap.ai.sdk.core.DeploymentResolutionException;
1212
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionDelta;
1313
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionOutput;
@@ -64,17 +64,21 @@ public final class OpenAiClient {
6464
@Nonnull
6565
public static OpenAiClient forModel(@Nonnull final OpenAiModel foundationModel)
6666
throws DeploymentResolutionException {
67-
final var destination =
68-
new AiCoreService()
69-
.getDestinationForDeploymentByModel(
70-
AiCoreService.DEFAULT_RESOURCE_GROUP, foundationModel);
67+
final var destination = new AiCoreService().getInferenceDestination().forModel(foundationModel);
68+
7169
final var client = new OpenAiClient(destination);
7270
return client.withApiVersion(DEFAULT_API_VERSION);
7371
}
7472

75-
// package private, but prepared in case we want to expose it later
73+
/**
74+
* Create a new OpenAI client targeting the specified API version.
75+
*
76+
* @param apiVersion the API version to target.
77+
* @return a new client.
78+
*/
79+
@Beta
7680
@Nonnull
77-
OpenAiClient withApiVersion(@Nonnull final String apiVersion) {
81+
public OpenAiClient withApiVersion(@Nonnull final String apiVersion) {
7882
final var newDestination =
7983
DefaultHttpDestination.fromDestination(this.destination)
8084
// set the API version as URL query parameter
@@ -85,18 +89,21 @@ OpenAiClient withApiVersion(@Nonnull final String apiVersion) {
8589

8690
/**
8791
* Create a new OpenAI client with a custom destination, allowing for a custom resource group or
88-
* otherwise custom destination.
92+
* otherwise custom destination. The destination needs to be configured with a URL pointing to an
93+
* OpenAI model deployment. Typically, such a destination should be obtained using {@link
94+
* AiCoreService#getInferenceDestination(String)}.
8995
*
9096
* <p>Example:
9197
*
9298
* <pre>{@code
93-
* OpenAiClient.withCustomDestination(new AiCoreService().getDestinationForDeploymentByModel("custom-rg", GPT_4O));
94-
*
99+
* var destination = new AiCoreService().getInferenceDestination("custom-rg").forModel(GPT_4O);
100+
* OpenAiClient.withCustomDestination(destination).withApiVersion("2024-10-21");
95101
* }</pre>
96102
*
97103
* @param destination The specific {@link HttpDestination} to use.
98-
* @see AiCoreService#getDestinationForDeploymentByModel(String, AiModel)
104+
* @see AiCoreService#getInferenceDestination(String)
99105
*/
106+
@Beta
100107
@Nonnull
101108
public static OpenAiClient withCustomDestination(@Nonnull final Destination destination) {
102109
return new OpenAiClient(destination);

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
1111
import com.google.common.annotations.Beta;
1212
import com.sap.ai.sdk.core.AiCoreService;
13+
import com.sap.ai.sdk.core.DeploymentResolutionException;
1314
import com.sap.ai.sdk.orchestration.model.CompletionPostRequest;
1415
import com.sap.ai.sdk.orchestration.model.CompletionPostResponse;
1516
import com.sap.ai.sdk.orchestration.model.FilterConfig;
@@ -25,7 +26,6 @@
2526
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationNotFoundException;
2627
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.HttpClientInstantiationException;
2728
import java.io.IOException;
28-
import java.util.NoSuchElementException;
2929
import java.util.function.Supplier;
3030
import javax.annotation.Nonnull;
3131
import lombok.extern.slf4j.Slf4j;
@@ -64,25 +64,25 @@ public class OrchestrationClient {
6464
/** Default constructor. */
6565
public OrchestrationClient() {
6666
destinationSupplier =
67-
() ->
68-
new AiCoreService()
69-
.getDestinationForDeploymentByScenario(
70-
AiCoreService.DEFAULT_RESOURCE_GROUP, DEFAULT_SCENARIO);
67+
() -> new AiCoreService().getInferenceDestination().forScenario(DEFAULT_SCENARIO);
7168
}
7269

7370
/**
7471
* Constructor with a custom destination, allowing for a custom resource group or otherwise custom
75-
* destination.
72+
* destination. The destination needs to be configured with a URL pointing to an orchestration
73+
* service deployment. Typically, such a destination should be obtained using {@link
74+
* AiCoreService#getInferenceDestination(String)}.
7675
*
7776
* <p>Example:
7877
*
7978
* <pre>{@code
80-
* new OrchestrationClient(new AiCoreService().getDestinationForDeploymentByScenario("custom-rg", "orchestration"));
79+
* new OrchestrationClient(new AiCoreService().getInferenceDestination("custom-rg").forScenario("orchestration"));
8180
* }</pre>
8281
*
8382
* @param destination The specific {@link HttpDestination} to use.
84-
* @see AiCoreService#getDestinationForDeploymentByScenario(String, String)
83+
* @see AiCoreService#getInferenceDestination(String)
8584
*/
85+
@Beta
8686
public OrchestrationClient(@Nonnull final HttpDestination destination) {
8787
this.destinationSupplier = () -> destination;
8888
}
@@ -210,7 +210,7 @@ CompletionPostResponse executeRequest(@Nonnull final String request) {
210210
val client = ApacheHttpClient5Accessor.getHttpClient(destination);
211211
return client.execute(
212212
postRequest, new OrchestrationResponseHandler<>(CompletionPostResponse.class));
213-
} catch (NoSuchElementException
213+
} catch (DeploymentResolutionException
214214
| DestinationAccessException
215215
| DestinationNotFoundException
216216
| HttpClientInstantiationException

0 commit comments

Comments
 (0)