Skip to content

Commit

Permalink
Merge branch 'main' into chore/license
Browse files Browse the repository at this point in the history
  • Loading branch information
CharlesDuboisSAP authored Oct 17, 2024
2 parents 7a4e0e5 + 741e9ac commit 6142408
Show file tree
Hide file tree
Showing 24 changed files with 1,060 additions and 415 deletions.
9 changes: 9 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
</dependency>
<!-- scope "runtime" -->
<dependency>
<groupId>com.sap.cloud.sdk.cloudplatform</groupId>
Expand Down Expand Up @@ -127,6 +131,11 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
103 changes: 103 additions & 0 deletions core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.sap.ai.sdk.core;

import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination;
import com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationProperty;
import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient;
import java.util.function.Function;
import javax.annotation.Nonnull;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

/** Connectivity convenience methods for AI Core with deployment. */
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class AiCoreDeployment implements AiCoreDestination {

private static final String AI_RESOURCE_GROUP = "URL.headers.AI-Resource-Group";

// the delegating AI Core Service instance
@Nonnull private final AiCoreService service;

// the deployment id handler to be used, based on instance
@Nonnull private final Function<AiCoreDeployment, String> deploymentId;

// the resource group, "default" if null
@Getter(AccessLevel.PROTECTED)
@Nonnull
private final String resourceGroup;

/**
* Default constructor with "default" resource group.
*
* @param service The AI Core service.
* @param deploymentId The deployment id handler.
*/
public AiCoreDeployment(
@Nonnull final AiCoreService service,
@Nonnull final Function<AiCoreDeployment, String> deploymentId) {
this(service, deploymentId, "default");
}

@Nonnull
@Override
public Destination destination() {
final var dest = service.baseDestinationHandler.apply(service);
final var builder = service.builderHandler.apply(service, dest);
destinationSetUrl(builder, dest);
destinationSetHeaders(builder);
return builder.build();
}

@Nonnull
@Override
public ApiClient client() {
final var destination = destination();
return service.clientHandler.apply(service, destination);
}

/**
* Update and set the URL for the destination.
*
* @param builder The destination builder.
* @param dest The original destination reference.
*/
protected void destinationSetUrl(
@Nonnull final DefaultHttpDestination.Builder builder, @Nonnull final Destination dest) {
String uri = dest.get(DestinationProperty.URI).get();
if (!uri.endsWith("/")) {
uri = uri + "/";
}
builder.uri(uri + "v2/inference/deployments/%s/".formatted(getDeploymentId()));
}

/**
* Update and set the default request headers for the destination.
*
* @param builder The destination builder.
*/
protected void destinationSetHeaders(@Nonnull final DefaultHttpDestination.Builder builder) {
builder.property(AI_RESOURCE_GROUP, getResourceGroup());
}

/**
* Set the resource group.
*
* @param resourceGroup The resource group.
* @return A new instance of the AI Core service.
*/
@Nonnull
public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) {
return new AiCoreDeployment(service, deploymentId, resourceGroup);
}

/**
* Get the deployment id.
*
* @return The deployment id.
*/
@Nonnull
protected String getDeploymentId() {
return deploymentId.apply(this);
}
}
28 changes: 28 additions & 0 deletions core/src/main/java/com/sap/ai/sdk/core/AiCoreDestination.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.sap.ai.sdk.core;

import com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient;
import javax.annotation.Nonnull;

/** Container for an API client and destination. */
@FunctionalInterface
public interface AiCoreDestination {
/**
* Get the destination.
*
* @return the destination
*/
@Nonnull
Destination destination();

/**
* Get the API client.
*
* @return the API client
*/
@Nonnull
default ApiClient client() {
final var destination = destination();
return new ApiClient(destination);
}
}
181 changes: 181 additions & 0 deletions core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package com.sap.ai.sdk.core;

import static com.sap.ai.sdk.core.DestinationResolver.AI_CLIENT_TYPE_KEY;
import static com.sap.ai.sdk.core.DestinationResolver.AI_CLIENT_TYPE_VALUE;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.common.collect.Iterables;
import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor;
import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination;
import com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationProperty;
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationNotFoundException;
import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient;
import java.util.NoSuchElementException;
import java.util.function.BiFunction;
import java.util.function.Function;
import javax.annotation.Nonnull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.client.BufferingClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

/** Connectivity convenience methods for AI Core. */
@Slf4j
@RequiredArgsConstructor
public class AiCoreService implements AiCoreDestination {

final Function<AiCoreService, Destination> baseDestinationHandler;
final BiFunction<AiCoreService, Destination, ApiClient> clientHandler;
final BiFunction<AiCoreService, Destination, DefaultHttpDestination.Builder> builderHandler;

private static final DeploymentCache DEPLOYMENT_CACHE = new DeploymentCache();

/** The default constructor. */
public AiCoreService() {
this(
AiCoreService::getBaseDestination,
AiCoreService::getApiClient,
AiCoreService::getDestinationBuilder);
}

@Nonnull
@Override
public ApiClient client() {
final var destination = destination();
return clientHandler.apply(this, destination);
}

@Nonnull
@Override
public Destination destination() {
final var dest = baseDestinationHandler.apply(this);
return builderHandler.apply(this, dest).build();
}

/**
* Set a specific base destination.
*
* @param destination The destination to be used for AI Core service calls.
* @return A new instance of the AI Core Service based on the provided destination.
*/
@Nonnull
public AiCoreService withDestination(@Nonnull final Destination destination) {
return new AiCoreService((service) -> destination, clientHandler, builderHandler);
}

/**
* Set a specific deployment by id.
*
* @param deploymentId The deployment id to be used for AI Core service calls.
* @return A new instance of the AI Core Deployment.
*/
@Nonnull
public AiCoreDeployment forDeployment(@Nonnull final String deploymentId) {
return new AiCoreDeployment(this, obj -> deploymentId);
}

/**
* Set a specific deployment by model name. If there are multiple deployments of the same model,
* the first one is returned.
*
* @param modelName The model name to be used for AI Core service calls.
* @return A new instance of the AI Core Deployment.
* @throws NoSuchElementException if no running deployment is found for the model.
*/
@Nonnull
public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName)
throws NoSuchElementException {
return new AiCoreDeployment(
this,
obj ->
DEPLOYMENT_CACHE.getDeploymentIdByModel(
this.client(), obj.getResourceGroup(), modelName));
}

/**
* Set a specific deployment by scenario id. If there are multiple deployments of the same model,
* the first one is returned.
*
* @param scenarioId The scenario id to be used for AI Core service calls.
* @return A new instance of the AI Core Deployment.
* @throws NoSuchElementException if no running deployment is found for the scenario.
*/
@Nonnull
public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId)
throws NoSuchElementException {
return new AiCoreDeployment(
this,
obj ->
DEPLOYMENT_CACHE.getDeploymentIdByScenario(
this.client(), obj.getResourceGroup(), scenarioId));
}

/**
* Get a destination using the default service binding loading logic.
*
* @return The destination.
* @throws DestinationAccessException If the destination cannot be accessed.
* @throws DestinationNotFoundException If the destination cannot be found.
*/
@Nonnull
protected Destination getBaseDestination()
throws DestinationAccessException, DestinationNotFoundException {
final var serviceKey = System.getenv("AICORE_SERVICE_KEY");
return DestinationResolver.getDestination(serviceKey);
}

/**
* Get the destination builder with adjustments for AI Core.
*
* @param destination The destination.
* @return The destination builder.
*/
@Nonnull
protected DefaultHttpDestination.Builder getDestinationBuilder(
@Nonnull final Destination destination) {
final var builder = DefaultHttpDestination.fromDestination(destination);
String uri = destination.get(DestinationProperty.URI).get();
if (!uri.endsWith("/")) {
uri = uri + "/";
}
builder.uri(uri + "v2/").property(AI_CLIENT_TYPE_KEY, AI_CLIENT_TYPE_VALUE);
return builder;
}

/**
* Get a destination using the default service binding loading logic.
*
* @return The destination.
* @throws DestinationAccessException If the destination cannot be accessed.
* @throws DestinationNotFoundException If the destination cannot be found.
*/
@SuppressWarnings("UnstableApiUsage")
@Nonnull
protected ApiClient getApiClient(@Nonnull final Destination destination) {
final var objectMapper =
new Jackson2ObjectMapperBuilder()
.modules(new JavaTimeModule())
.visibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE)
.visibility(PropertyAccessor.SETTER, JsonAutoDetect.Visibility.NONE)
.serializationInclusion(JsonInclude.Include.NON_NULL) // THIS STOPS `null` serialization
.build();

final var httpRequestFactory = new HttpComponentsClientHttpRequestFactory();
httpRequestFactory.setHttpClient(ApacheHttpClient5Accessor.getHttpClient(destination));

final var rt = new RestTemplate();
Iterables.filter(rt.getMessageConverters(), MappingJackson2HttpMessageConverter.class)
.forEach(converter -> converter.setObjectMapper(objectMapper));
rt.setRequestFactory(new BufferingClientHttpRequestFactory(httpRequestFactory));

return new ApiClient(rt).setBasePath(destination.asHttp().getUri().toString());
}
}
Loading

0 comments on commit 6142408

Please sign in to comment.