From d4ba51c70e2d280b3d5bc6e617a06d47e6dadeee Mon Sep 17 00:00:00 2001 From: I538344 Date: Fri, 6 Sep 2024 10:20:52 +0200 Subject: [PATCH 01/79] Added query filters for Orchestration and OpenAI deployments --- .../main/java/com/sap/ai/sdk/core/Core.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/Core.java b/core/src/main/java/com/sap/ai/sdk/core/Core.java index 1732b491..b63b4060 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/Core.java +++ b/core/src/main/java/com/sap/ai/sdk/core/Core.java @@ -63,14 +63,17 @@ public static ApiClient getOrchestrationClient(@Nonnull final String resourceGro private static String getOrchestrationDeployment(@Nonnull final String resourceGroup) throws NoSuchElementException { final var deployments = - new DeploymentApi(getClient(getDestination())).deploymentQuery(resourceGroup); + new DeploymentApi(getClient()) + .deploymentQuery( + resourceGroup, null, null, "orchestration", "RUNNING", null, null, null); return deployments.getResources().stream() - .filter(deployment -> "orchestration".equals(deployment.getScenarioId())) .map(AiDeployment::getId) .findFirst() .orElseThrow( - () -> new NoSuchElementException("No deployment found with scenario id orchestration")); + () -> + new NoSuchElementException( + "No running deployment found with scenario id \"orchestration\"")); } /** @@ -259,14 +262,19 @@ public static Destination getDestinationForModel( private static String getDeploymentForModel( @Nonnull final String modelName, @Nonnull final String resourceGroup) throws NoSuchElementException { - final var deployments = new DeploymentApi(getClient()).deploymentQuery(resourceGroup); + final var deployments = + new DeploymentApi(getClient()) + .deploymentQuery( + resourceGroup, null, null, "foundation-models", "RUNNING", null, null, null); return deployments.getResources().stream() .filter(deployment -> isDeploymentOfModel(modelName, deployment)) .map(AiDeployment::getId) .findFirst() .orElseThrow( - () -> new NoSuchElementException("No deployment found with model name " + modelName)); + () -> + new NoSuchElementException( + "No running deployment found with model name " + modelName)); } /** This exists because getBackendDetails() is broken */ From 163212e4484a5c4e7b9ed5a97c5d69b889e52c81 Mon Sep 17 00:00:00 2001 From: I538344 Date: Fri, 6 Sep 2024 14:53:17 +0200 Subject: [PATCH 02/79] Added deployment cache --- .../main/java/com/sap/ai/sdk/core/Core.java | 88 +------------ .../com/sap/ai/sdk/core/DeploymentCache.java | 123 ++++++++++++++++++ .../foundationmodels/openai/OpenAiClient.java | 2 +- 3 files changed, 128 insertions(+), 85 deletions(-) create mode 100644 core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java diff --git a/core/src/main/java/com/sap/ai/sdk/core/Core.java b/core/src/main/java/com/sap/ai/sdk/core/Core.java index b63b4060..7a572819 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/Core.java +++ b/core/src/main/java/com/sap/ai/sdk/core/Core.java @@ -9,8 +9,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.sap.ai.sdk.core.client.DeploymentApi; -import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingAccessor; import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingBuilder; import com.sap.cloud.environment.servicebinding.api.ServiceBindingAccessor; @@ -25,8 +23,6 @@ import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; import javax.annotation.Nonnull; import javax.annotation.Nullable; import lombok.extern.slf4j.Slf4j; @@ -49,31 +45,8 @@ public class Core { @Nonnull public static ApiClient getOrchestrationClient(@Nonnull final String resourceGroup) { return getClient( - getDestinationForDeployment(getOrchestrationDeployment(resourceGroup), resourceGroup)); - } - - /** - * Get the deployment id from the scenario id. If there are multiple deployments of the same - * scenario id, the first one is returned. - * - * @param resourceGroup the resource group. - * @return the deployment id - * @throws NoSuchElementException if no deployment is found for the scenario id. - */ - private static String getOrchestrationDeployment(@Nonnull final String resourceGroup) - throws NoSuchElementException { - final var deployments = - new DeploymentApi(getClient()) - .deploymentQuery( - resourceGroup, null, null, "orchestration", "RUNNING", null, null, null); - - return deployments.getResources().stream() - .map(AiDeployment::getId) - .findFirst() - .orElseThrow( - () -> - new NoSuchElementException( - "No running deployment found with scenario id \"orchestration\"")); + getDestinationForDeployment( + DeploymentCache.getDeploymentId(resourceGroup, "orchestration"), resourceGroup)); } /** @@ -245,61 +218,8 @@ public static Destination getDestinationForDeployment( */ @Nonnull public static Destination getDestinationForModel( - @Nonnull final String modelName, @Nonnull final String resourceGroup) { + @Nonnull final String resourceGroup, @Nonnull final String modelName) { return getDestinationForDeployment( - getDeploymentForModel(modelName, resourceGroup), resourceGroup); - } - - /** - * Get the deployment id from the model name. If there are multiple deployments of the same model, - * the first one is returned. - * - * @param modelName the model name. - * @param resourceGroup the resource group. - * @return the deployment id - * @throws NoSuchElementException if no deployment is found for the model name. - */ - private static String getDeploymentForModel( - @Nonnull final String modelName, @Nonnull final String resourceGroup) - throws NoSuchElementException { - final var deployments = - new DeploymentApi(getClient()) - .deploymentQuery( - resourceGroup, null, null, "foundation-models", "RUNNING", null, null, null); - - return deployments.getResources().stream() - .filter(deployment -> isDeploymentOfModel(modelName, deployment)) - .map(AiDeployment::getId) - .findFirst() - .orElseThrow( - () -> - new NoSuchElementException( - "No running deployment found with model name " + modelName)); - } - - /** This exists because getBackendDetails() is broken */ - private static boolean isDeploymentOfModel( - @Nonnull final String modelName, @Nonnull final AiDeployment deployment) { - final var deploymentDetails = deployment.getDetails(); - // The AI Core specification doesn't mention that this is nullable, but it can be. - // Remove this check when the specification is fixed. - if (deploymentDetails == null) { - return false; - } - final var resources = deploymentDetails.getResources(); - if (resources == null) { - return false; - } - if (!resources.getCustomFieldNames().contains("backend_details")) { - return false; - } - final var detailsObject = resources.getCustomField("backend_details"); - - if (detailsObject instanceof Map details - && details.get("model") instanceof Map model - && model.get("name") instanceof String name) { - return modelName.equals(name); - } - return false; + DeploymentCache.getDeploymentId(resourceGroup, modelName), resourceGroup); } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java new file mode 100644 index 00000000..8d6ea712 --- /dev/null +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -0,0 +1,123 @@ +package com.sap.ai.sdk.core; + +import static com.sap.ai.sdk.core.Core.getClient; + +import com.sap.ai.sdk.core.client.DeploymentApi; +import com.sap.ai.sdk.core.client.model.AiDeployment; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import javax.annotation.Nonnull; + +class DeploymentCache { + private static final DeploymentApi API = new DeploymentApi(getClient()); + + /** + * Cache for deployment ids. The key is the scenario + model name and the value is the deployment + * id. + */ + private static final Map CACHE = new HashMap<>(); + + static { + final var deployments = API.deploymentQuery("default").getResources(); + deployments.forEach( + deployment -> + CACHE.put(deployment.getScenarioId() + getModelName(deployment), deployment.getId())); + } + + /** + * Get the deployment id for the given scenario or model name. + * + * @param resourceGroup the resource group, usually "default". + * @param name the scenario or model name. + * @return the deployment id. + */ + public static String getDeploymentId( + @Nonnull final String resourceGroup, @Nonnull final String name) { + return CACHE.computeIfAbsent( + name, + n -> { + if ("orchestration".equals(n)) { + return getOrchestrationDeployment(resourceGroup); + } else { + return getDeploymentForModel(resourceGroup, name); + } + }); + } + + /** + * Get the deployment id from the scenario id. If there are multiple deployments of the same + * scenario id, the first one is returned. + * + * @param resourceGroup the resource group. + * @return the deployment id + * @throws NoSuchElementException if no deployment is found for the scenario id. + */ + private static String getOrchestrationDeployment(@Nonnull final String resourceGroup) + throws NoSuchElementException { + final var deployments = + new DeploymentApi(getClient()) + .deploymentQuery( + resourceGroup, null, null, "orchestration", "RUNNING", null, null, null); + + return deployments.getResources().stream() + .map(AiDeployment::getId) + .findFirst() + .orElseThrow( + () -> + new NoSuchElementException( + "No running deployment found with scenario id \"orchestration\"")); + } + + /** + * Get the deployment id from the model name. If there are multiple deployments of the same model, + * the first one is returned. + * + * @param modelName the model name. + * @param resourceGroup the resource group. + * @return the deployment id + * @throws NoSuchElementException if no deployment is found for the model name. + */ + private static String getDeploymentForModel( + @Nonnull final String resourceGroup, @Nonnull final String modelName) + throws NoSuchElementException { + final var deployments = + new DeploymentApi(getClient()) + .deploymentQuery( + resourceGroup, null, null, "foundation-models", "RUNNING", null, null, null); + + return deployments.getResources().stream() + .filter(deployment -> modelName.equals(getModelName(deployment))) + .map(AiDeployment::getId) + .findFirst() + .orElseThrow( + () -> + new NoSuchElementException( + "No running deployment found with model name " + modelName)); + } + + /** This exists because getBackendDetails() is broken */ + private static String getModelName(@Nonnull final AiDeployment deployment) { + final var deploymentDetails = deployment.getDetails(); + // The AI Core specification doesn't mention that this is nullable, but it can be. + // Remove this check when the specification is fixed. + if (deploymentDetails == null) { + return ""; + } + final var resources = deploymentDetails.getResources(); + if (resources == null) { + return ""; + } + if (!resources.getCustomFieldNames().contains("backend_details")) { + return ""; + } + final var detailsObject = resources.getCustomField("backend_details"); + + if (detailsObject instanceof Map details + && details.get("model") instanceof Map model + && model.get("name") instanceof String name) { + return name; + } + return ""; + } +} diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java index 333ffbed..bc8bc6f4 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java @@ -55,7 +55,7 @@ public final class OpenAiClient { */ @Nonnull public static OpenAiClient forModel(@Nonnull final OpenAiModel foundationModel) { - final var destination = Core.getDestinationForModel(foundationModel.model(), "default"); + final var destination = Core.getDestinationForModel("default", foundationModel.model()); final var client = new OpenAiClient(destination); return client.withApiVersion(DEFAULT_API_VERSION); } From a481a9bd847d9535752b3a3659f25e1954807355 Mon Sep 17 00:00:00 2001 From: I538344 Date: Mon, 9 Sep 2024 11:50:49 +0200 Subject: [PATCH 03/79] Added cache reset --- .../com/sap/ai/sdk/core/DeploymentCache.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index 8d6ea712..5239f62d 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -9,6 +9,10 @@ import java.util.NoSuchElementException; import javax.annotation.Nonnull; +/** + * Cache for deployment IDs. This class is used to get the deployment id for the orchestration + * scenario or for a model. + */ class DeploymentCache { private static final DeploymentApi API = new DeploymentApi(getClient()); @@ -18,7 +22,18 @@ class DeploymentCache { */ private static final Map CACHE = new HashMap<>(); + // Eagerly load all deployments into the cache. static { + resetCache(); + } + + /** + * Remove all entries from the cache and reload all deployments. + * + *

Call this method if you delete a deployment. + */ + public static void resetCache() { + CACHE.clear(); final var deployments = API.deploymentQuery("default").getResources(); deployments.forEach( deployment -> @@ -26,10 +41,10 @@ class DeploymentCache { } /** - * Get the deployment id for the given scenario or model name. + * Get the deployment id for the orchestration scenario or any foundation model. * * @param resourceGroup the resource group, usually "default". - * @param name the scenario or model name. + * @param name "orchestration" or the model name. * @return the deployment id. */ public static String getDeploymentId( @@ -73,8 +88,8 @@ private static String getOrchestrationDeployment(@Nonnull final String resourceG * Get the deployment id from the model name. If there are multiple deployments of the same model, * the first one is returned. * - * @param modelName the model name. * @param resourceGroup the resource group. + * @param modelName the model name. * @return the deployment id * @throws NoSuchElementException if no deployment is found for the model name. */ From a728f07f7d0e4bbf0a1ed66ba9183163f67638fe Mon Sep 17 00:00:00 2001 From: I538344 Date: Tue, 17 Sep 2024 11:17:27 +0200 Subject: [PATCH 04/79] Added unit tests --- .../com/sap/ai/sdk/core/DeploymentCache.java | 27 ++-- .../java/com/sap/ai/sdk/core/CacheTest.java | 128 ++++++++++++++++++ .../sdk/core/client/WireMockTestServer.java | 6 +- .../controllers/ConfigurationController.java | 26 ++++ .../src/main/resources/static/index.html | 6 + 5 files changed, 175 insertions(+), 18 deletions(-) create mode 100644 core/src/test/java/com/sap/ai/sdk/core/CacheTest.java create mode 100644 e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/ConfigurationController.java diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index 5239f62d..72dbfb26 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -14,12 +14,10 @@ * scenario or for a model. */ class DeploymentCache { - private static final DeploymentApi API = new DeploymentApi(getClient()); + /** The client to use for deployment queries. */ + static DeploymentApi API = new DeploymentApi(getClient()); - /** - * Cache for deployment ids. The key is the scenario + model name and the value is the deployment - * id. - */ + /** Cache for deployment ids. The key is the model name and the value is the deployment id. */ private static final Map CACHE = new HashMap<>(); // Eagerly load all deployments into the cache. @@ -30,14 +28,12 @@ class DeploymentCache { /** * Remove all entries from the cache and reload all deployments. * - *

Call this method if you delete a deployment. + *

Call this method whenever a deployment is deleted. */ public static void resetCache() { CACHE.clear(); final var deployments = API.deploymentQuery("default").getResources(); - deployments.forEach( - deployment -> - CACHE.put(deployment.getScenarioId() + getModelName(deployment), deployment.getId())); + deployments.forEach(deployment -> CACHE.put(getModelName(deployment), deployment.getId())); } /** @@ -71,9 +67,8 @@ public static String getDeploymentId( private static String getOrchestrationDeployment(@Nonnull final String resourceGroup) throws NoSuchElementException { final var deployments = - new DeploymentApi(getClient()) - .deploymentQuery( - resourceGroup, null, null, "orchestration", "RUNNING", null, null, null); + API.deploymentQuery( + resourceGroup, null, null, "orchestration", "RUNNING", null, null, null); return deployments.getResources().stream() .map(AiDeployment::getId) @@ -97,9 +92,8 @@ private static String getDeploymentForModel( @Nonnull final String resourceGroup, @Nonnull final String modelName) throws NoSuchElementException { final var deployments = - new DeploymentApi(getClient()) - .deploymentQuery( - resourceGroup, null, null, "foundation-models", "RUNNING", null, null, null); + API.deploymentQuery( + resourceGroup, null, null, "foundation-models", "RUNNING", null, null, null); return deployments.getResources().stream() .filter(deployment -> modelName.equals(getModelName(deployment))) @@ -113,6 +107,9 @@ private static String getDeploymentForModel( /** This exists because getBackendDetails() is broken */ private static String getModelName(@Nonnull final AiDeployment deployment) { + if ("orchestration".equals(deployment.getScenarioId())) { + return "orchestration"; + } final var deploymentDetails = deployment.getDetails(); // The AI Core specification doesn't mention that this is nullable, but it can be. // Remove this check when the specification is fixed. diff --git a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java new file mode 100644 index 00000000..a31d855b --- /dev/null +++ b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java @@ -0,0 +1,128 @@ +package com.sap.ai.sdk.core; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; + +import com.sap.ai.sdk.core.client.DeploymentApi; +import com.sap.ai.sdk.core.client.WireMockTestServer; +import org.apache.hc.core5.http.HttpStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class CacheTest extends WireMockTestServer { + @BeforeEach + void setup() { + DeploymentCache.API = new DeploymentApi(destination); + wireMockServer.resetRequests(); + } + + private static void stubGPT4() { + wireMockServer.stubFor( + get(urlPathEqualTo("/lm/deployments")) + .withHeader("AI-Resource-Group", equalTo("default")) + .willReturn( + aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("content-type", "application/json") + .withBody( + """ + { + "count": 1, + "resources": [ + { + "configurationId": "7652a231-ba9b-4fcc-b473-2c355cb21b61", + "configurationName": "gpt-4-32k", + "createdAt": "2024-04-17T15:19:53Z", + "deploymentUrl": "https://api.ai.intprod-eu12.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/d19b998f347341aa", + "details": { + "resources": { + "backend_details": { + "model": { + "name": "gpt-4-32k", + "version": "latest" + } + } + }, + "scaling": { + "backend_details": {} + } + }, + "id": "d19b998f347341aa", + "lastOperation": "CREATE", + "latestRunningConfigurationId": "7652a231-ba9b-4fcc-b473-2c355cb21b61", + "modifiedAt": "2024-05-07T13:05:45Z", + "scenarioId": "foundation-models", + "startTime": "2024-04-17T15:21:15Z", + "status": "RUNNING", + "submissionTime": "2024-04-17T15:20:11Z", + "targetStatus": "RUNNING" + } + ] + } + """))); + } + + private static void stubEmpty() { + wireMockServer.stubFor( + get(urlPathEqualTo("/lm/deployments")) + .withHeader("AI-Resource-Group", equalTo("default")) + .willReturn( + aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("content-type", "application/json") + .withBody( + """ + { + "count": 0, + "resources": [] + } + """))); + } + + /** + * The user creates a deployment. + * + *

The user uses the OpenAI client and specifies only the name "foo". + * + *

The user repeatedly uses the API in the same way + * + *

Simple case, the deployment should be served from cache as much as possible + */ + @Test + void newDeployment() { + stubGPT4(); + DeploymentCache.resetCache(); + + DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); + + DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); + } + + /** + * The user creates a deployment after starting with an empty cache. + * + *

The user uses the OpenAI client and specifies only the name "foo". + * + *

The user repeatedly uses the API in the same way + * + *

Simple case, the deployment should be served from cache as much as possible + */ + @Test + void newDeploymentAfterReset() { + stubEmpty(); + DeploymentCache.resetCache(); + stubGPT4(); + + DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + // 1 reset empty and 1 cache miss + wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); + + DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); + } +} diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java b/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java index 5091331b..94ad346f 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java @@ -9,12 +9,12 @@ import org.junit.jupiter.api.BeforeAll; /** Test server for all unit tests. */ -abstract class WireMockTestServer { +public abstract class WireMockTestServer { private static final WireMockConfiguration WIREMOCK_CONFIGURATION = wireMockConfig().dynamicPort(); - static WireMockServer wireMockServer; - static Destination destination; + public static WireMockServer wireMockServer; + public static Destination destination; @BeforeAll static void setup() { diff --git a/e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/ConfigurationController.java b/e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/ConfigurationController.java new file mode 100644 index 00000000..5e94ec9d --- /dev/null +++ b/e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/ConfigurationController.java @@ -0,0 +1,26 @@ +package com.sap.ai.sdk.app.controllers; + +import static com.sap.ai.sdk.core.Core.getClient; + +import com.sap.ai.sdk.core.client.ConfigurationApi; +import com.sap.ai.sdk.core.client.model.AiConfigurationList; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** Endpoint for Configuration operations */ +@SuppressWarnings("unused") // debug class that doesn't need to be tested +@RestController +public class ConfigurationController { + + private static final ConfigurationApi API = new ConfigurationApi(getClient()); + + /** + * Get the list of configurations. + * + * @return the list of configurations + */ + @GetMapping("/configurations") + AiConfigurationList getConfigurations() { + return API.configurationQuery("default"); + } +} diff --git a/e2e-test-app/src/main/resources/static/index.html b/e2e-test-app/src/main/resources/static/index.html index b7a2d904..07272d1e 100644 --- a/e2e-test-app/src/main/resources/static/index.html +++ b/e2e-test-app/src/main/resources/static/index.html @@ -57,6 +57,12 @@

Endpoints

  • /models All available foundation models in this region
  • +
  • Orchestration

      From fc01194ad469762ea5dd771db78709ed10481f9d Mon Sep 17 00:00:00 2001 From: I538344 Date: Tue, 17 Sep 2024 12:55:15 +0200 Subject: [PATCH 05/79] Fixed unit tests --- .../main/java/com/sap/ai/sdk/core/Core.java | 7 +++- .../com/sap/ai/sdk/core/DeploymentCache.java | 40 ++++++++++++++----- .../java/com/sap/ai/sdk/core/CacheTest.java | 16 ++++---- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/Core.java b/core/src/main/java/com/sap/ai/sdk/core/Core.java index 7a572819..0de5ddd6 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/Core.java +++ b/core/src/main/java/com/sap/ai/sdk/core/Core.java @@ -36,6 +36,9 @@ @Slf4j public class Core { + /** The cache for deployment ids, is eagerly loaded. */ + private static final DeploymentCache CACHE = new DeploymentCache(); + /** * Requires an AI Core service binding. * @@ -46,7 +49,7 @@ public class Core { public static ApiClient getOrchestrationClient(@Nonnull final String resourceGroup) { return getClient( getDestinationForDeployment( - DeploymentCache.getDeploymentId(resourceGroup, "orchestration"), resourceGroup)); + CACHE.getDeploymentId(resourceGroup, "orchestration"), resourceGroup)); } /** @@ -220,6 +223,6 @@ public static Destination getDestinationForDeployment( public static Destination getDestinationForModel( @Nonnull final String resourceGroup, @Nonnull final String modelName) { return getDestinationForDeployment( - DeploymentCache.getDeploymentId(resourceGroup, modelName), resourceGroup); + CACHE.getDeploymentId(resourceGroup, modelName), resourceGroup); } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index 72dbfb26..32d9142c 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -4,24 +4,39 @@ import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiDeployment; +import com.sap.cloud.sdk.services.openapi.core.OpenApiRequestException; import java.util.HashMap; import java.util.Map; import java.util.NoSuchElementException; import javax.annotation.Nonnull; +import lombok.extern.slf4j.Slf4j; /** * Cache for deployment IDs. This class is used to get the deployment id for the orchestration * scenario or for a model. */ +@Slf4j class DeploymentCache { /** The client to use for deployment queries. */ - static DeploymentApi API = new DeploymentApi(getClient()); + DeploymentApi API; /** Cache for deployment ids. The key is the model name and the value is the deployment id. */ - private static final Map CACHE = new HashMap<>(); + private final Map CACHE = new HashMap<>(); - // Eagerly load all deployments into the cache. - static { + /* + * Create a new DeploymentCache and eagerly load all deployments into the cache. + */ + DeploymentCache() { + API = new DeploymentApi(getClient()); + resetCache(); + } + + /* + * Create a new DeploymentCache and eagerly load all deployments into the cache. + * @param client the client to use for deployment queries + */ + DeploymentCache(DeploymentApi client) { + API = client; resetCache(); } @@ -30,10 +45,14 @@ class DeploymentCache { * *

      Call this method whenever a deployment is deleted. */ - public static void resetCache() { + public void resetCache() { CACHE.clear(); - final var deployments = API.deploymentQuery("default").getResources(); - deployments.forEach(deployment -> CACHE.put(getModelName(deployment), deployment.getId())); + try { + final var deployments = API.deploymentQuery("default").getResources(); + deployments.forEach(deployment -> CACHE.put(getModelName(deployment), deployment.getId())); + } catch (final OpenApiRequestException e) { + log.error("Failed to load deployments into cache", e); + } } /** @@ -43,8 +62,7 @@ public static void resetCache() { * @param name "orchestration" or the model name. * @return the deployment id. */ - public static String getDeploymentId( - @Nonnull final String resourceGroup, @Nonnull final String name) { + public String getDeploymentId(@Nonnull final String resourceGroup, @Nonnull final String name) { return CACHE.computeIfAbsent( name, n -> { @@ -64,7 +82,7 @@ public static String getDeploymentId( * @return the deployment id * @throws NoSuchElementException if no deployment is found for the scenario id. */ - private static String getOrchestrationDeployment(@Nonnull final String resourceGroup) + private String getOrchestrationDeployment(@Nonnull final String resourceGroup) throws NoSuchElementException { final var deployments = API.deploymentQuery( @@ -88,7 +106,7 @@ private static String getOrchestrationDeployment(@Nonnull final String resourceG * @return the deployment id * @throws NoSuchElementException if no deployment is found for the model name. */ - private static String getDeploymentForModel( + private String getDeploymentForModel( @Nonnull final String resourceGroup, @Nonnull final String modelName) throws NoSuchElementException { final var deployments = diff --git a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java index a31d855b..fe37782d 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java @@ -13,9 +13,11 @@ import org.junit.jupiter.api.Test; class CacheTest extends WireMockTestServer { + + DeploymentCache cacheUnderTest = new DeploymentCache(new DeploymentApi(destination)); + @BeforeEach void setup() { - DeploymentCache.API = new DeploymentApi(destination); wireMockServer.resetRequests(); } @@ -94,12 +96,12 @@ private static void stubEmpty() { @Test void newDeployment() { stubGPT4(); - DeploymentCache.resetCache(); + cacheUnderTest.resetCache(); - DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + cacheUnderTest.getDeploymentId("default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); - DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + cacheUnderTest.getDeploymentId("default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); } @@ -115,14 +117,14 @@ void newDeployment() { @Test void newDeploymentAfterReset() { stubEmpty(); - DeploymentCache.resetCache(); + cacheUnderTest.resetCache(); stubGPT4(); - DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + cacheUnderTest.getDeploymentId("default", "gpt-4-32k"); // 1 reset empty and 1 cache miss wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); - DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + cacheUnderTest.getDeploymentId("default", "gpt-4-32k"); wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); } } From 2b2ea9512320cc134a3ff6d9a31c67038da6109d Mon Sep 17 00:00:00 2001 From: I538344 Date: Tue, 17 Sep 2024 12:57:17 +0200 Subject: [PATCH 06/79] PMD --- core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index 32d9142c..42450b49 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -35,7 +35,7 @@ class DeploymentCache { * Create a new DeploymentCache and eagerly load all deployments into the cache. * @param client the client to use for deployment queries */ - DeploymentCache(DeploymentApi client) { + DeploymentCache(@Nonnull final DeploymentApi client) { API = client; resetCache(); } From 2d0cc6e0e8980b8133723f0f7c5536c010845973 Mon Sep 17 00:00:00 2001 From: I538344 Date: Tue, 17 Sep 2024 13:31:12 +0200 Subject: [PATCH 07/79] Refactor to fix tests --- .../main/java/com/sap/ai/sdk/core/Core.java | 13 +++++--- .../com/sap/ai/sdk/core/DeploymentCache.java | 33 ++++++++----------- .../java/com/sap/ai/sdk/core/CacheTest.java | 18 +++++----- .../core/{client => }/WireMockTestServer.java | 4 ++- .../ai/sdk/core/client/ArtifactUnitTest.java | 1 + .../core/client/ConfigurationUnitTest.java | 1 + .../sdk/core/client/DeploymentUnitTest.java | 1 + .../ai/sdk/core/client/ExecutionUnitTest.java | 1 + .../ai/sdk/core/client/ScenarioUnitTest.java | 1 + 9 files changed, 39 insertions(+), 34 deletions(-) rename core/src/test/java/com/sap/ai/sdk/core/{client => }/WireMockTestServer.java (86%) diff --git a/core/src/main/java/com/sap/ai/sdk/core/Core.java b/core/src/main/java/com/sap/ai/sdk/core/Core.java index 0de5ddd6..174fb5f7 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/Core.java +++ b/core/src/main/java/com/sap/ai/sdk/core/Core.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingAccessor; import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingBuilder; import com.sap.cloud.environment.servicebinding.api.ServiceBindingAccessor; @@ -36,8 +37,12 @@ @Slf4j public class Core { - /** The cache for deployment ids, is eagerly loaded. */ - private static final DeploymentCache CACHE = new DeploymentCache(); + // for testing only, will be removed once we make this class an instance + static { + if (!DeploymentCache.isLoaded()) { + DeploymentCache.eagerlyLoaded(new DeploymentApi(getClient())); + } + } /** * Requires an AI Core service binding. @@ -49,7 +54,7 @@ public class Core { public static ApiClient getOrchestrationClient(@Nonnull final String resourceGroup) { return getClient( getDestinationForDeployment( - CACHE.getDeploymentId(resourceGroup, "orchestration"), resourceGroup)); + DeploymentCache.getDeploymentId(resourceGroup, "orchestration"), resourceGroup)); } /** @@ -223,6 +228,6 @@ public static Destination getDestinationForDeployment( public static Destination getDestinationForModel( @Nonnull final String resourceGroup, @Nonnull final String modelName) { return getDestinationForDeployment( - CACHE.getDeploymentId(resourceGroup, modelName), resourceGroup); + DeploymentCache.getDeploymentId(resourceGroup, modelName), resourceGroup); } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index 42450b49..dff805a4 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -1,7 +1,5 @@ package com.sap.ai.sdk.core; -import static com.sap.ai.sdk.core.Core.getClient; - import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.cloud.sdk.services.openapi.core.OpenApiRequestException; @@ -18,34 +16,30 @@ @Slf4j class DeploymentCache { /** The client to use for deployment queries. */ - DeploymentApi API; + static DeploymentApi API; /** Cache for deployment ids. The key is the model name and the value is the deployment id. */ - private final Map CACHE = new HashMap<>(); + private static final Map CACHE = new HashMap<>(); - /* - * Create a new DeploymentCache and eagerly load all deployments into the cache. - */ - DeploymentCache() { - API = new DeploymentApi(getClient()); - resetCache(); + static boolean isLoaded() { + return API != null; } - /* - * Create a new DeploymentCache and eagerly load all deployments into the cache. - * @param client the client to use for deployment queries - */ - DeploymentCache(@Nonnull final DeploymentApi client) { + public static void eagerlyLoaded(@Nonnull final DeploymentApi client) { API = client; resetCache(); } + public static void lazyLoaded(@Nonnull final DeploymentApi client) { + API = client; + } + /** * Remove all entries from the cache and reload all deployments. * *

      Call this method whenever a deployment is deleted. */ - public void resetCache() { + public static void resetCache() { CACHE.clear(); try { final var deployments = API.deploymentQuery("default").getResources(); @@ -62,7 +56,8 @@ public void resetCache() { * @param name "orchestration" or the model name. * @return the deployment id. */ - public String getDeploymentId(@Nonnull final String resourceGroup, @Nonnull final String name) { + public static String getDeploymentId( + @Nonnull final String resourceGroup, @Nonnull final String name) { return CACHE.computeIfAbsent( name, n -> { @@ -82,7 +77,7 @@ public String getDeploymentId(@Nonnull final String resourceGroup, @Nonnull fina * @return the deployment id * @throws NoSuchElementException if no deployment is found for the scenario id. */ - private String getOrchestrationDeployment(@Nonnull final String resourceGroup) + private static String getOrchestrationDeployment(@Nonnull final String resourceGroup) throws NoSuchElementException { final var deployments = API.deploymentQuery( @@ -106,7 +101,7 @@ private String getOrchestrationDeployment(@Nonnull final String resourceGroup) * @return the deployment id * @throws NoSuchElementException if no deployment is found for the model name. */ - private String getDeploymentForModel( + private static String getDeploymentForModel( @Nonnull final String resourceGroup, @Nonnull final String modelName) throws NoSuchElementException { final var deployments = diff --git a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java index fe37782d..fce45cc4 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java @@ -7,17 +7,15 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import com.sap.ai.sdk.core.client.DeploymentApi; -import com.sap.ai.sdk.core.client.WireMockTestServer; import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class CacheTest extends WireMockTestServer { - DeploymentCache cacheUnderTest = new DeploymentCache(new DeploymentApi(destination)); - @BeforeEach - void setup() { + void setupCache() { + DeploymentCache.lazyLoaded(new DeploymentApi(destination)); wireMockServer.resetRequests(); } @@ -96,12 +94,12 @@ private static void stubEmpty() { @Test void newDeployment() { stubGPT4(); - cacheUnderTest.resetCache(); + DeploymentCache.resetCache(); - cacheUnderTest.getDeploymentId("default", "gpt-4-32k"); + DeploymentCache.getDeploymentId("default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); - cacheUnderTest.getDeploymentId("default", "gpt-4-32k"); + DeploymentCache.getDeploymentId("default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); } @@ -117,14 +115,14 @@ void newDeployment() { @Test void newDeploymentAfterReset() { stubEmpty(); - cacheUnderTest.resetCache(); + DeploymentCache.resetCache(); stubGPT4(); - cacheUnderTest.getDeploymentId("default", "gpt-4-32k"); + DeploymentCache.getDeploymentId("default", "gpt-4-32k"); // 1 reset empty and 1 cache miss wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); - cacheUnderTest.getDeploymentId("default", "gpt-4-32k"); + DeploymentCache.getDeploymentId("default", "gpt-4-32k"); wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); } } diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java b/core/src/test/java/com/sap/ai/sdk/core/WireMockTestServer.java similarity index 86% rename from core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java rename to core/src/test/java/com/sap/ai/sdk/core/WireMockTestServer.java index 94ad346f..d88bc7ca 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java +++ b/core/src/test/java/com/sap/ai/sdk/core/WireMockTestServer.java @@ -1,9 +1,10 @@ -package com.sap.ai.sdk.core.client; +package com.sap.ai.sdk.core; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; import org.junit.jupiter.api.BeforeAll; @@ -21,5 +22,6 @@ static void setup() { wireMockServer = new WireMockServer(WIREMOCK_CONFIGURATION); wireMockServer.start(); destination = DefaultHttpDestination.builder(wireMockServer.baseUrl()).build(); + DeploymentCache.lazyLoaded(new DeploymentApi(destination)); } } diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java index b643a669..09cc513a 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java @@ -8,6 +8,7 @@ import static com.sap.ai.sdk.core.Core.getClient; import static org.assertj.core.api.Assertions.assertThat; +import com.sap.ai.sdk.core.WireMockTestServer; import com.sap.ai.sdk.core.client.model.AiArtifact; import com.sap.ai.sdk.core.client.model.AiArtifactCreationResponse; import com.sap.ai.sdk.core.client.model.AiArtifactList; diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java index f23dde83..bb1a4da4 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java @@ -8,6 +8,7 @@ import static com.sap.ai.sdk.core.Core.getClient; import static org.assertj.core.api.Assertions.assertThat; +import com.sap.ai.sdk.core.WireMockTestServer; import com.sap.ai.sdk.core.client.model.AiArtifactArgumentBinding; import com.sap.ai.sdk.core.client.model.AiConfiguration; import com.sap.ai.sdk.core.client.model.AiConfigurationBaseData; diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java index 2d4cbca7..7ba9bcc5 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java @@ -12,6 +12,7 @@ import static com.sap.ai.sdk.core.Core.getClient; import static org.assertj.core.api.Assertions.assertThat; +import com.sap.ai.sdk.core.WireMockTestServer; import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.ai.sdk.core.client.model.AiDeploymentCreationRequest; import com.sap.ai.sdk.core.client.model.AiDeploymentCreationResponse; diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java index 5d80463f..ae545e91 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java @@ -8,6 +8,7 @@ import static com.sap.ai.sdk.core.Core.getClient; import static org.assertj.core.api.Assertions.assertThat; +import com.sap.ai.sdk.core.WireMockTestServer; import com.sap.ai.sdk.core.client.model.AiArtifact; import com.sap.ai.sdk.core.client.model.AiEnactmentCreationRequest; import com.sap.ai.sdk.core.client.model.AiExecution; diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ScenarioUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ScenarioUnitTest.java index 98ae6cc1..1ee4f868 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ScenarioUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ScenarioUnitTest.java @@ -7,6 +7,7 @@ import static com.sap.ai.sdk.core.Core.getClient; import static org.assertj.core.api.Assertions.assertThat; +import com.sap.ai.sdk.core.WireMockTestServer; import com.sap.ai.sdk.core.client.model.AiScenario; import com.sap.ai.sdk.core.client.model.AiScenarioList; import org.apache.hc.core5.http.HttpStatus; From 14fe1d6be57b02f395bf8befca3ac9e6b2e20772 Mon Sep 17 00:00:00 2001 From: I538344 Date: Tue, 17 Sep 2024 14:06:57 +0200 Subject: [PATCH 08/79] fix tests for real --- .../java/com/sap/ai/sdk/core/DeploymentCache.java | 12 +++++++++++- .../foundationmodels/openai/OpenAiClientTest.java | 3 +++ .../orchestration/client/OrchestrationUnitTest.java | 3 +++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index dff805a4..2d005446 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -14,7 +14,7 @@ * scenario or for a model. */ @Slf4j -class DeploymentCache { +public class DeploymentCache { /** The client to use for deployment queries. */ static DeploymentApi API; @@ -25,11 +25,21 @@ static boolean isLoaded() { return API != null; } + /** + * Eagerly load the deployment cache with the given client. + * + * @param client the deployment client. + */ public static void eagerlyLoaded(@Nonnull final DeploymentApi client) { API = client; resetCache(); } + /** + * Lazy load the deployment cache with the given client. + * + * @param client the deployment client. + */ public static void lazyLoaded(@Nonnull final DeploymentApi client) { API = client; } diff --git a/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientTest.java b/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientTest.java index aebc3718..323519e9 100644 --- a/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientTest.java +++ b/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientTest.java @@ -14,6 +14,8 @@ import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import com.github.tomakehurst.wiremock.stubbing.Scenario; +import com.sap.ai.sdk.core.DeploymentCache; +import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionChoice; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionDelta; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionOutput; @@ -56,6 +58,7 @@ void setup(WireMockRuntimeInfo server) { final DefaultHttpDestination destination = DefaultHttpDestination.builder(server.getHttpBaseUrl()).build(); client = OpenAiClient.withCustomDestination(destination); + DeploymentCache.lazyLoaded(new DeploymentApi(destination)); ApacheHttpClient5Accessor.setHttpClientCache(ApacheHttpClient5Cache.DISABLED); } diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java index e9a2f79b..1a412dd4 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java @@ -17,6 +17,8 @@ import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import com.sap.ai.sdk.core.DeploymentCache; +import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.orchestration.client.model.AzureContentSafety; import com.sap.ai.sdk.orchestration.client.model.AzureThreshold; import com.sap.ai.sdk.orchestration.client.model.ChatMessage; @@ -112,6 +114,7 @@ public class OrchestrationUnitTest { void setup(WireMockRuntimeInfo server) { final DefaultHttpDestination destination = DefaultHttpDestination.builder(server.getHttpBaseUrl()).build(); + DeploymentCache.lazyLoaded(new DeploymentApi(destination)); client = new OrchestrationCompletionApi(getClient(destination)); } From 249e3a6a541dcc7764b22bba5084b5cc0af898b1 Mon Sep 17 00:00:00 2001 From: I538344 Date: Wed, 18 Sep 2024 10:29:23 +0200 Subject: [PATCH 09/79] PMD + added clearCache and loadCache --- .../main/java/com/sap/ai/sdk/core/Core.java | 4 ++-- .../com/sap/ai/sdk/core/DeploymentCache.java | 24 ++++++++++++++----- .../java/com/sap/ai/sdk/core/CacheTest.java | 4 ++-- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/Core.java b/core/src/main/java/com/sap/ai/sdk/core/Core.java index 174fb5f7..e843a4de 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/Core.java +++ b/core/src/main/java/com/sap/ai/sdk/core/Core.java @@ -39,8 +39,8 @@ public class Core { // for testing only, will be removed once we make this class an instance static { - if (!DeploymentCache.isLoaded()) { - DeploymentCache.eagerlyLoaded(new DeploymentApi(getClient())); + if (DeploymentCache.isEmpty()) { + DeploymentCache.lazyLoaded(new DeploymentApi(getClient())); } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index 2d005446..695cd763 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -21,8 +21,8 @@ public class DeploymentCache { /** Cache for deployment ids. The key is the model name and the value is the deployment id. */ private static final Map CACHE = new HashMap<>(); - static boolean isLoaded() { - return API != null; + static boolean isEmpty() { + return API == null; } /** @@ -32,7 +32,7 @@ static boolean isLoaded() { */ public static void eagerlyLoaded(@Nonnull final DeploymentApi client) { API = client; - resetCache(); + loadCache(); } /** @@ -45,12 +45,20 @@ public static void lazyLoaded(@Nonnull final DeploymentApi client) { } /** - * Remove all entries from the cache and reload all deployments. + * Remove all entries from the cache. * - *

      Call this method whenever a deployment is deleted. + *

      Call both clearCache and {@link #loadCache} method whenever a deployment is deleted. */ - public static void resetCache() { + public static void clearCache() { CACHE.clear(); + } + + /** + * Load all deployments into the cache + * + *

      Call both {@link #clearCache} and loadCache method whenever a deployment is deleted. + */ + public static void loadCache() { try { final var deployments = API.deploymentQuery("default").getResources(); deployments.forEach(deployment -> CACHE.put(getModelName(deployment), deployment.getId())); @@ -66,8 +74,12 @@ public static void resetCache() { * @param name "orchestration" or the model name. * @return the deployment id. */ + @Nonnull public static String getDeploymentId( @Nonnull final String resourceGroup, @Nonnull final String name) { + if (DeploymentCache.isEmpty()) { + loadCache(); + } return CACHE.computeIfAbsent( name, n -> { diff --git a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java index fce45cc4..7837322e 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java @@ -94,7 +94,7 @@ private static void stubEmpty() { @Test void newDeployment() { stubGPT4(); - DeploymentCache.resetCache(); + DeploymentCache.loadCache(); DeploymentCache.getDeploymentId("default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); @@ -115,7 +115,7 @@ void newDeployment() { @Test void newDeploymentAfterReset() { stubEmpty(); - DeploymentCache.resetCache(); + DeploymentCache.loadCache(); stubGPT4(); DeploymentCache.getDeploymentId("default", "gpt-4-32k"); From 059bb092d405345d8c2fed3880d74f655e7c406e Mon Sep 17 00:00:00 2001 From: I538344 Date: Wed, 18 Sep 2024 10:37:35 +0200 Subject: [PATCH 10/79] PMD again --- core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index 695cd763..b79beea0 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -77,7 +77,7 @@ public static void loadCache() { @Nonnull public static String getDeploymentId( @Nonnull final String resourceGroup, @Nonnull final String name) { - if (DeploymentCache.isEmpty()) { + if (isEmpty()) { loadCache(); } return CACHE.computeIfAbsent( From 18dce2a968fe6d3223ead7a8a1d4be5aa9860fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 25 Sep 2024 11:42:57 +0200 Subject: [PATCH 11/79] intermediate --- .../com/sap/ai/sdk/core/ApiClientBuilder.java | 51 ++++ .../sap/ai/sdk/core/ApiClientResolver.java | 178 +++++++++++++ .../main/java/com/sap/ai/sdk/core/Core.java | 242 +++--------------- .../sap/ai/sdk/core/DestinationResolver.java | 108 ++++++++ ...Test.java => DestinationResolverTest.java} | 11 +- .../controllers/OrchestrationController.java | 6 +- .../foundationmodels/openai/OpenAiClient.java | 5 +- 7 files changed, 389 insertions(+), 212 deletions(-) create mode 100644 core/src/main/java/com/sap/ai/sdk/core/ApiClientBuilder.java create mode 100644 core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java create mode 100644 core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java rename core/src/test/java/com/sap/ai/sdk/core/{CoreTest.java => DestinationResolverTest.java} (77%) diff --git a/core/src/main/java/com/sap/ai/sdk/core/ApiClientBuilder.java b/core/src/main/java/com/sap/ai/sdk/core/ApiClientBuilder.java new file mode 100644 index 00000000..907160ee --- /dev/null +++ b/core/src/main/java/com/sap/ai/sdk/core/ApiClientBuilder.java @@ -0,0 +1,51 @@ +package com.sap.ai.sdk.core; + +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.Destination; +import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; +import java.util.function.Function; +import javax.annotation.Nonnull; +import lombok.RequiredArgsConstructor; +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; + +class ApiClientBuilder { + + @RequiredArgsConstructor + enum ApiClientConstructor { + SERIALIZE_WITH_NULL_VALUES(ApiClient::new), + SERIALIZE_WITHOUT_NULL_VALUES(ApiClientConstructor::withoutNull); + + @SuppressWarnings("UnstableApiUsage") + private static ApiClient withoutNull(@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()); + } + + final Function finalizer; + } +} diff --git a/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java b/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java new file mode 100644 index 00000000..5f60ef0c --- /dev/null +++ b/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java @@ -0,0 +1,178 @@ +package com.sap.ai.sdk.core; + +import com.sap.ai.sdk.core.client.DeploymentApi; +import com.sap.ai.sdk.core.client.model.AiDeployment; +import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; +import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; +import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; + +import javax.annotation.Nonnull; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.function.Function; +import java.util.function.Predicate; + +@FunctionalInterface +public interface ApiClientResolver { + + Destination getDestination(); + + default T forService(@Nonnull final Service service) { + return service.builder(this); + } + + + interface Service { + Service ORCHESTRATION = resolver -> resourceGroup -> () -> { + Predicate predicate = d -> "orchestration".equals(d.getScenarioId()); + String deploymentId = getDeploymentId(resolver, resourceGroup, predicate); + return getDestinationForDeployment(resolver, deploymentId, resourceGroup); + }; + + Service OPENAI = resolver -> modelName -> resourceGroup -> () -> { + Predicate predicate = d -> isDeploymentOfModel(modelName, d); + String deploymentId = getDeploymentId(resolver, resourceGroup, predicate); + return getDestinationForDeployment(resolver, deploymentId, resourceGroup); + }; + + Service CORE = (resolver) -> () -> resolver.getDestination(); + + T builder(ApiClientResolver resolver); + + @FunctionalInterface + interface Build { + Destination getDestination(); + + default Build withNull() { + Build that = this; + return new Build() { + @Override + public Destination getDestination() { + return that.getDestination(); + } + + @Override + public ApiClient getClient() { + return ApiClientBuilder.ApiClientConstructor.SERIALIZE_WITH_NULL_VALUES.finalizer.apply(getDestination()); + } + }; + } + default Build withoutNull() { + Build that = this; + return new Build() { + @Override + public Destination getDestination() { + return that.getDestination(); + } + + @Override + public ApiClient getClient() { + return ApiClientBuilder.ApiClientConstructor.SERIALIZE_WITHOUT_NULL_VALUES.finalizer.apply(getDestination()); + } + }; + } + + default ApiClient getClient() { + return ApiClientBuilder.ApiClientConstructor.SERIALIZE_WITHOUT_NULL_VALUES.finalizer.apply(getDestination()); + } + } + @FunctionalInterface + interface CoreBuilder extends Build{ + } + @FunctionalInterface + interface OrchestrationBuilder { + @Nonnull + Build resourceGroup(@Nonnull final String resourceGroup); + } + @FunctionalInterface + interface OpenAiBuilder { + @Nonnull + OpenAiBuilder1 model(@Nonnull final String modelName); + @Nonnull + default OpenAiBuilder1 deploymentId(@Nonnull final String model) { + return null; + } + } + @FunctionalInterface + interface OpenAiBuilder1 { + @Nonnull + Build resourceGroup(@Nonnull final String resourceGroup); + } + } + + + /** + * Get the deployment id from the scenario id. If there are multiple deployments of the same + * scenario id, the first one is returned. + * + * @param resourceGroup the resource group. + * @return the deployment id + * @throws NoSuchElementException if no deployment is found for the scenario id. + */ + private static String getDeploymentId(@Nonnull final ApiClientResolver resolver, @Nonnull final String resourceGroup, @Nonnull final Predicate predicate) + throws NoSuchElementException { + final var deploymentService = new DeploymentApi(resolver.forService(Service.CORE).getClient()); + final var deployments = + deploymentService.deploymentQuery(resourceGroup); + + return deployments.getResources().stream() + .filter(predicate) + .map(AiDeployment::getId) + .findFirst() + .orElseThrow( + () -> new NoSuchElementException("No deployment found with scenario id orchestration")); + } + + + /** + * Get a destination pointing to the inference endpoint of a deployment on AI Core. Requires an + * AI Core service binding. + * + * @param deploymentId The deployment id. + * @param resourceGroup The resource group. + * @return a destination that can be used for inference calls. + */ + @Nonnull + private static Destination getDestinationForDeployment( + @Nonnull final ApiClientResolver resolver, + @Nonnull final String deploymentId, @Nonnull final String resourceGroup) { + final var destination = resolver.getDestination().asHttp(); + final DefaultHttpDestination.Builder builder = + DefaultHttpDestination.fromDestination(destination) + .uri( + destination + .getUri() + .resolve("/v2/inference/deployments/%s/".formatted(deploymentId))); + + builder.header("AI-Resource-Group", resourceGroup); + + return builder.build(); + } + + + /** This exists because getBackendDetails() is broken */ + private static boolean isDeploymentOfModel( + @Nonnull final String modelName, @Nonnull final AiDeployment deployment) { + final var deploymentDetails = deployment.getDetails(); + // The AI Core specification doesn't mention that this is nullable, but it can be. + // Remove this check when the specification is fixed. + if (deploymentDetails == null) { + return false; + } + final var resources = deploymentDetails.getResources(); + if (resources == null) { + return false; + } + if (!resources.getCustomFieldNames().contains("backend_details")) { + return false; + } + final var detailsObject = resources.getCustomField("backend_details"); + + if (detailsObject instanceof Map details + && details.get("model") instanceof Map model + && model.get("name") instanceof String name) { + return modelName.equals(name); + } + return false; + } +} diff --git a/core/src/main/java/com/sap/ai/sdk/core/Core.java b/core/src/main/java/com/sap/ai/sdk/core/Core.java index 1732b491..ac1895eb 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/Core.java +++ b/core/src/main/java/com/sap/ai/sdk/core/Core.java @@ -1,44 +1,42 @@ package com.sap.ai.sdk.core; -import static com.sap.cloud.sdk.cloudplatform.connectivity.OnBehalfOf.TECHNICAL_USER_PROVIDER; - -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiDeployment; -import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingAccessor; -import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingBuilder; -import com.sap.cloud.environment.servicebinding.api.ServiceBindingAccessor; -import com.sap.cloud.environment.servicebinding.api.ServiceBindingMerger; -import com.sap.cloud.environment.servicebinding.api.ServiceIdentifier; -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.HttpDestination; -import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationLoader; -import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions; import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; -import java.util.HashMap; -import java.util.List; + import java.util.Map; import java.util.NoSuchElementException; +import java.util.concurrent.Callable; import javax.annotation.Nonnull; -import javax.annotation.Nullable; + +import lombok.Getter; 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 -public class Core { +public class Core implements ApiClientResolver { + + @Getter + private static Core instance = new Core(); + + public static synchronized T executeWithCore(Core core, Callable callable) throws Exception { + Core oldInstance = instance; + instance = core; + try { + return callable.call(); + } + finally { + instance = oldInstance; + } + } + + @Nonnull + public ApiClientResolver withDestination(@Nonnull final Destination destination) { + return () -> destination; + } + /** * Requires an AI Core service binding. @@ -47,30 +45,9 @@ public class Core { * @return a generic Orchestration ApiClient. */ @Nonnull + @Deprecated public static ApiClient getOrchestrationClient(@Nonnull final String resourceGroup) { - return getClient( - getDestinationForDeployment(getOrchestrationDeployment(resourceGroup), resourceGroup)); - } - - /** - * Get the deployment id from the scenario id. If there are multiple deployments of the same - * scenario id, the first one is returned. - * - * @param resourceGroup the resource group. - * @return the deployment id - * @throws NoSuchElementException if no deployment is found for the scenario id. - */ - private static String getOrchestrationDeployment(@Nonnull final String resourceGroup) - throws NoSuchElementException { - final var deployments = - new DeploymentApi(getClient(getDestination())).deploymentQuery(resourceGroup); - - return deployments.getResources().stream() - .filter(deployment -> "orchestration".equals(deployment.getScenarioId())) - .map(AiDeployment::getId) - .findFirst() - .orElseThrow( - () -> new NoSuchElementException("No deployment found with scenario id orchestration")); + return getInstance().forService(Service.ORCHESTRATION).resourceGroup(resourceGroup).getClient(); } /** @@ -80,8 +57,9 @@ private static String getOrchestrationDeployment(@Nonnull final String resourceG * @return a generic AI Core ApiClient. */ @Nonnull + @Deprecated public static ApiClient getClient() { - return getClient(getDestination()); + return getInstance().forService(Service.CORE).getClient(); } /** @@ -91,27 +69,9 @@ public static ApiClient getClient() { * @return a generic AI Core ApiClient. */ @Nonnull - @SuppressWarnings("UnstableApiUsage") + @Deprecated public static ApiClient getClient(@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 restTemplate = new RestTemplate(); - restTemplate.getMessageConverters().stream() - .filter(MappingJackson2HttpMessageConverter.class::isInstance) - .map(MappingJackson2HttpMessageConverter.class::cast) - .forEach(converter -> converter.setObjectMapper(objectMapper)); - restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(httpRequestFactory)); - - return new ApiClient(restTemplate).setBasePath(destination.asHttp().getUri().toString()); + return getInstance().withDestination(destination).forService(Service.CORE).getClient(); } /** @@ -123,89 +83,10 @@ public static ApiClient getClient(@Nonnull final Destination destination) { * @return a destination pointing to the AI Core service. */ @Nonnull - public static Destination getDestination() { + @Override + public Destination getDestination() { final var serviceKey = System.getenv("AICORE_SERVICE_KEY"); - return getDestination(serviceKey); - } - - /** - * For testing only - * - *

      Get a destination pointing to the AI Core service. - * - * @param serviceKey The service key in JSON format. - * @return a destination pointing to the AI Core service. - */ - static HttpDestination getDestination(@Nullable final String serviceKey) { - final var serviceKeyPresent = serviceKey != null; - final var aiCoreBindingPresent = - DefaultServiceBindingAccessor.getInstance().getServiceBindings().stream() - .anyMatch( - serviceBinding -> - ServiceIdentifier.AI_CORE.equals( - serviceBinding.getServiceIdentifier().orElse(null))); - - if (!aiCoreBindingPresent && serviceKeyPresent) { - addServiceBinding(serviceKey); - } - - // get a destination pointing to the AI Core service - final var opts = - ServiceBindingDestinationOptions.forService(ServiceIdentifier.AI_CORE) - .onBehalfOf(TECHNICAL_USER_PROVIDER) - .build(); - var destination = ServiceBindingDestinationLoader.defaultLoaderChain().getDestination(opts); - - destination = - DefaultHttpDestination.fromDestination(destination) - // append the /v2 path here, so we don't have to do it in every request when using the - // generated code this is actually necessary, because the generated code assumes this - // path to be present on the destination - .uri(destination.getUri().resolve("/v2")) - .header("AI-Client-Type", "AI SDK Java") - .build(); - return destination; - } - - /** - * Set the AI Core service key as the service binding. This is used for local testing. - * - * @param serviceKey The service key in JSON format. - * @throws AiCoreCredentialsInvalidException if the JSON service key cannot be parsed. - */ - private static void addServiceBinding(@Nonnull final String serviceKey) { - log.info( - """ - Found a service key in environment variable "AICORE_SERVICE_KEY". - Using a service key is recommended for local testing only. - Bind the AI Core service to the application for productive usage."""); - - var credentials = new HashMap(); - try { - credentials = new ObjectMapper().readValue(serviceKey, new TypeReference<>() {}); - } catch (JsonProcessingException e) { - throw new AiCoreCredentialsInvalidException( - "Error in parsing service key from the \"AICORE_SERVICE_KEY\" environment variable.", e); - } - - final var binding = - new DefaultServiceBindingBuilder() - .withServiceIdentifier(ServiceIdentifier.AI_CORE) - .withCredentials(credentials) - .build(); - final ServiceBindingAccessor accessor = DefaultServiceBindingAccessor.getInstance(); - final var newAccessor = - new ServiceBindingMerger( - List.of(accessor, () -> List.of(binding)), ServiceBindingMerger.KEEP_EVERYTHING); - DefaultServiceBindingAccessor.setInstance(newAccessor); - } - - /** Exception thrown when the JSON AI Core service key is invalid. */ - static class AiCoreCredentialsInvalidException extends RuntimeException { - public AiCoreCredentialsInvalidException( - @Nonnull final String message, @Nonnull final Throwable cause) { - super(message, cause); - } + return DestinationResolver.getDestination(serviceKey); } /** @@ -217,9 +98,11 @@ public AiCoreCredentialsInvalidException( * @return a destination that can be used for inference calls. */ @Nonnull + @Deprecated public static Destination getDestinationForDeployment( @Nonnull final String deploymentId, @Nonnull final String resourceGroup) { - final var destination = getDestination().asHttp(); + return getInstance().forService() + final var destination = getInstance().getDestination().asHttp(); final DefaultHttpDestination.Builder builder = DefaultHttpDestination.fromDestination(destination) .uri( @@ -241,57 +124,10 @@ public static Destination getDestinationForDeployment( * @return a destination that can be used for inference calls. */ @Nonnull + @Deprecated public static Destination getDestinationForModel( @Nonnull final String modelName, @Nonnull final String resourceGroup) { - return getDestinationForDeployment( - getDeploymentForModel(modelName, resourceGroup), resourceGroup); + return getInstance().forService(Service.OPENAI).model(modelName).resourceGroup(resourceGroup).getDestination(); } - /** - * Get the deployment id from the model name. If there are multiple deployments of the same model, - * the first one is returned. - * - * @param modelName the model name. - * @param resourceGroup the resource group. - * @return the deployment id - * @throws NoSuchElementException if no deployment is found for the model name. - */ - private static String getDeploymentForModel( - @Nonnull final String modelName, @Nonnull final String resourceGroup) - throws NoSuchElementException { - final var deployments = new DeploymentApi(getClient()).deploymentQuery(resourceGroup); - - return deployments.getResources().stream() - .filter(deployment -> isDeploymentOfModel(modelName, deployment)) - .map(AiDeployment::getId) - .findFirst() - .orElseThrow( - () -> new NoSuchElementException("No deployment found with model name " + modelName)); - } - - /** This exists because getBackendDetails() is broken */ - private static boolean isDeploymentOfModel( - @Nonnull final String modelName, @Nonnull final AiDeployment deployment) { - final var deploymentDetails = deployment.getDetails(); - // The AI Core specification doesn't mention that this is nullable, but it can be. - // Remove this check when the specification is fixed. - if (deploymentDetails == null) { - return false; - } - final var resources = deploymentDetails.getResources(); - if (resources == null) { - return false; - } - if (!resources.getCustomFieldNames().contains("backend_details")) { - return false; - } - final var detailsObject = resources.getCustomField("backend_details"); - - if (detailsObject instanceof Map details - && details.get("model") instanceof Map model - && model.get("name") instanceof String name) { - return modelName.equals(name); - } - return false; - } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java b/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java new file mode 100644 index 00000000..104787e3 --- /dev/null +++ b/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java @@ -0,0 +1,108 @@ +package com.sap.ai.sdk.core; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingAccessor; +import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingBuilder; +import com.sap.cloud.environment.servicebinding.api.ServiceBindingAccessor; +import com.sap.cloud.environment.servicebinding.api.ServiceBindingMerger; +import com.sap.cloud.environment.servicebinding.api.ServiceIdentifier; +import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; +import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination; +import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationLoader; +import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.List; + +import static com.sap.cloud.sdk.cloudplatform.connectivity.OnBehalfOf.TECHNICAL_USER_PROVIDER; + +@Slf4j +class DestinationResolver { + + /** + * For testing only + * + *

      Get a destination pointing to the AI Core service. + * + * @param serviceKey The service key in JSON format. + * @return a destination pointing to the AI Core service. + */ + @SuppressWarnings("UnstableApiUsage") + static HttpDestination getDestination(@Nullable final String serviceKey) { + final var serviceKeyPresent = serviceKey != null; + final var aiCoreBindingPresent = + DefaultServiceBindingAccessor.getInstance().getServiceBindings().stream() + .anyMatch( + serviceBinding -> + ServiceIdentifier.AI_CORE.equals( + serviceBinding.getServiceIdentifier().orElse(null))); + + if (!aiCoreBindingPresent && serviceKeyPresent) { + addServiceBinding(serviceKey); + } + + // get a destination pointing to the AI Core service + final var opts = + ServiceBindingDestinationOptions.forService(ServiceIdentifier.AI_CORE) + .onBehalfOf(TECHNICAL_USER_PROVIDER) + .build(); + var destination = ServiceBindingDestinationLoader.defaultLoaderChain().getDestination(opts); + + destination = + DefaultHttpDestination.fromDestination(destination) + // append the /v2 path here, so we don't have to do it in every request when using the + // generated code this is actually necessary, because the generated code assumes this + // path to be present on the destination + .uri(destination.getUri().resolve("/v2")) + .header("AI-Client-Type", "AI SDK Java") + .build(); + return destination; + } + + /** + * Set the AI Core service key as the service binding. This is used for local testing. + * + * @param serviceKey The service key in JSON format. + * @throws AiCoreCredentialsInvalidException if the JSON service key cannot be parsed. + */ + private static void addServiceBinding(@Nonnull final String serviceKey) { + log.info( + """ + Found a service key in environment variable "AICORE_SERVICE_KEY". + Using a service key is recommended for local testing only. + Bind the AI Core service to the application for productive usage."""); + + var credentials = new HashMap(); + try { + credentials = new ObjectMapper().readValue(serviceKey, new TypeReference<>() {}); + } catch (JsonProcessingException e) { + throw new AiCoreCredentialsInvalidException( + "Error in parsing service key from the \"AICORE_SERVICE_KEY\" environment variable.", e); + } + + final var binding = + new DefaultServiceBindingBuilder() + .withServiceIdentifier(ServiceIdentifier.AI_CORE) + .withCredentials(credentials) + .build(); + final ServiceBindingAccessor accessor = DefaultServiceBindingAccessor.getInstance(); + final var newAccessor = + new ServiceBindingMerger( + List.of(accessor, () -> List.of(binding)), ServiceBindingMerger.KEEP_EVERYTHING); + DefaultServiceBindingAccessor.setInstance(newAccessor); + } + + /** Exception thrown when the JSON AI Core service key is invalid. */ + static class AiCoreCredentialsInvalidException extends RuntimeException { + public AiCoreCredentialsInvalidException( + @Nonnull final String message, @Nonnull final Throwable cause) { + super(message, cause); + } + } + +} diff --git a/core/src/test/java/com/sap/ai/sdk/core/CoreTest.java b/core/src/test/java/com/sap/ai/sdk/core/DestinationResolverTest.java similarity index 77% rename from core/src/test/java/com/sap/ai/sdk/core/CoreTest.java rename to core/src/test/java/com/sap/ai/sdk/core/DestinationResolverTest.java index 021d05aa..3f395a53 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/CoreTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/DestinationResolverTest.java @@ -1,6 +1,5 @@ package com.sap.ai.sdk.core; -import static com.sap.ai.sdk.core.Core.getDestination; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -8,12 +7,12 @@ import lombok.SneakyThrows; import org.junit.jupiter.api.Test; -public class CoreTest { +public class DestinationResolverTest { @Test @SneakyThrows void getDestinationWithoutEnvVarFailsLocally() { - assertThatThrownBy(() -> getDestination(null)) + assertThatThrownBy(() -> DestinationResolver.getDestination(null)) .isExactlyInstanceOf(DestinationAccessException.class) .hasMessage("Could not find any matching service bindings for service identifier 'aicore'"); } @@ -21,8 +20,8 @@ void getDestinationWithoutEnvVarFailsLocally() { @Test @SneakyThrows void getDestinationWithBrokenEnvVarFailsLocally() { - assertThatThrownBy(() -> getDestination("")) - .isExactlyInstanceOf(Core.AiCoreCredentialsInvalidException.class) + assertThatThrownBy(() -> DestinationResolver.getDestination("")) + .isExactlyInstanceOf(DestinationResolver.AiCoreCredentialsInvalidException.class) .hasMessage( "Error in parsing service key from the \"AICORE_SERVICE_KEY\" environment variable."); } @@ -44,7 +43,7 @@ void getDestinationWithEnvVarSucceedsLocally() { } } """; - var result = getDestination(AICORE_SERVICE_KEY).asHttp(); + var result = DestinationResolver.getDestination(AICORE_SERVICE_KEY).asHttp(); assertThat(result.getUri()).hasToString("https://api.ai.core/v2"); } } diff --git a/e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java b/e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java index 77f47531..5054fc46 100644 --- a/e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java +++ b/e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java @@ -1,6 +1,8 @@ package com.sap.ai.sdk.app.controllers; -import static com.sap.ai.sdk.core.Core.getOrchestrationClient; +import com.sap.ai.sdk.core.Core; + +import static com.sap.ai.sdk.core.ApiClientResolver.Service.ORCHESTRATION; import com.sap.ai.sdk.orchestration.client.OrchestrationCompletionApi; import com.sap.ai.sdk.orchestration.client.model.AzureContentSafety; @@ -31,7 +33,7 @@ class OrchestrationController { private static final OrchestrationCompletionApi API = - new OrchestrationCompletionApi(getOrchestrationClient("default")); + new OrchestrationCompletionApi(Core.getInstance().forService(ORCHESTRATION).resourceGroup("default").getClient()); static final String MODEL = "gpt-35-turbo"; diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java index 333ffbed..de1e2ea0 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.sap.ai.sdk.core.ApiClientResolver; import com.sap.ai.sdk.core.Core; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionDelta; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionOutput; @@ -28,6 +29,8 @@ import org.apache.hc.core5.http.message.BasicClassicHttpRequest; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import static com.sap.ai.sdk.core.ApiClientResolver.Service.OPENAI; + /** Client for interacting with OpenAI models. */ @Slf4j @RequiredArgsConstructor(access = AccessLevel.PRIVATE) @@ -55,7 +58,7 @@ public final class OpenAiClient { */ @Nonnull public static OpenAiClient forModel(@Nonnull final OpenAiModel foundationModel) { - final var destination = Core.getDestinationForModel(foundationModel.model(), "default"); + final var destination = Core.getInstance().forService(OPENAI).model(foundationModel.model()).resourceGroup("default").getDestination(); final var client = new OpenAiClient(destination); return client.withApiVersion(DEFAULT_API_VERSION); } From c35a61bb74d7cca5b995bce8f12e68e33f9509f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 25 Sep 2024 14:37:00 +0200 Subject: [PATCH 12/79] ready-to-discuss state --- ...ntBuilder.java => ApiClientContainer.java} | 31 ++- .../sap/ai/sdk/core/ApiClientResolver.java | 223 +++++------------- .../main/java/com/sap/ai/sdk/core/Core.java | 70 +++--- .../com/sap/ai/sdk/core/DeploymentChoice.java | 83 +++++++ .../sap/ai/sdk/core/DestinationResolver.java | 150 ++++++------ .../app/controllers/DeploymentController.java | 5 +- .../controllers/OrchestrationController.java | 8 +- .../app/controllers/ScenarioController.java | 5 +- .../foundationmodels/openai/OpenAiClient.java | 11 +- 9 files changed, 281 insertions(+), 305 deletions(-) rename core/src/main/java/com/sap/ai/sdk/core/{ApiClientBuilder.java => ApiClientContainer.java} (75%) create mode 100644 core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java diff --git a/core/src/main/java/com/sap/ai/sdk/core/ApiClientBuilder.java b/core/src/main/java/com/sap/ai/sdk/core/ApiClientContainer.java similarity index 75% rename from core/src/main/java/com/sap/ai/sdk/core/ApiClientBuilder.java rename to core/src/main/java/com/sap/ai/sdk/core/ApiClientContainer.java index 907160ee..038829c5 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/ApiClientBuilder.java +++ b/core/src/main/java/com/sap/ai/sdk/core/ApiClientContainer.java @@ -10,21 +10,38 @@ import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import java.util.function.Function; import javax.annotation.Nonnull; -import lombok.RequiredArgsConstructor; 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; -class ApiClientBuilder { +@FunctionalInterface +public interface ApiClientContainer { + @Nonnull + Destination getDestination(); - @RequiredArgsConstructor - enum ApiClientConstructor { - SERIALIZE_WITH_NULL_VALUES(ApiClient::new), - SERIALIZE_WITHOUT_NULL_VALUES(ApiClientConstructor::withoutNull); + @Nonnull + default ApiClient getClient() { + return getClient(ClientOptions.SERIALIZE_WITHOUT_NULL_VALUES); + } + + @Nonnull + default ApiClient getClient(@Nonnull final ClientOptions options) { + final Destination destination = getDestination(); + return options.getInitializer().apply(destination); + } + + interface ClientOptions { + @Nonnull + Function getInitializer(); + + ClientOptions SERIALIZE_WITH_NULL_VALUES = () -> ApiClient::new; + + ClientOptions SERIALIZE_WITHOUT_NULL_VALUES = () -> ClientOptions::withoutNull; @SuppressWarnings("UnstableApiUsage") + @Nonnull private static ApiClient withoutNull(@Nonnull final Destination destination) { final var objectMapper = new Jackson2ObjectMapperBuilder() @@ -45,7 +62,5 @@ private static ApiClient withoutNull(@Nonnull final Destination destination) { return new ApiClient(rt).setBasePath(destination.asHttp().getUri().toString()); } - - final Function finalizer; } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java b/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java index 5f60ef0c..e460f085 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java +++ b/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java @@ -1,178 +1,69 @@ package com.sap.ai.sdk.core; -import com.sap.ai.sdk.core.client.DeploymentApi; -import com.sap.ai.sdk.core.client.model.AiDeployment; -import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; -import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; -import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; +import static com.sap.ai.sdk.core.ApiClientResolver.DestinationProcessor.ADD_HEADER; +import static com.sap.ai.sdk.core.ApiClientResolver.DestinationProcessor.UPDATE_URI; +import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; +import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination; import javax.annotation.Nonnull; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.function.Function; -import java.util.function.Predicate; +import lombok.Value; @FunctionalInterface -public interface ApiClientResolver { - - Destination getDestination(); - - default T forService(@Nonnull final Service service) { - return service.builder(this); - } - - - interface Service { - Service ORCHESTRATION = resolver -> resourceGroup -> () -> { - Predicate predicate = d -> "orchestration".equals(d.getScenarioId()); - String deploymentId = getDeploymentId(resolver, resourceGroup, predicate); - return getDestinationForDeployment(resolver, deploymentId, resourceGroup); - }; - - Service OPENAI = resolver -> modelName -> resourceGroup -> () -> { - Predicate predicate = d -> isDeploymentOfModel(modelName, d); - String deploymentId = getDeploymentId(resolver, resourceGroup, predicate); - return getDestinationForDeployment(resolver, deploymentId, resourceGroup); - }; - - Service CORE = (resolver) -> () -> resolver.getDestination(); - - T builder(ApiClientResolver resolver); - - @FunctionalInterface - interface Build { - Destination getDestination(); - - default Build withNull() { - Build that = this; - return new Build() { - @Override - public Destination getDestination() { - return that.getDestination(); - } - - @Override - public ApiClient getClient() { - return ApiClientBuilder.ApiClientConstructor.SERIALIZE_WITH_NULL_VALUES.finalizer.apply(getDestination()); - } - }; - } - default Build withoutNull() { - Build that = this; - return new Build() { - @Override - public Destination getDestination() { - return that.getDestination(); - } - - @Override - public ApiClient getClient() { - return ApiClientBuilder.ApiClientConstructor.SERIALIZE_WITHOUT_NULL_VALUES.finalizer.apply(getDestination()); - } - }; - } - - default ApiClient getClient() { - return ApiClientBuilder.ApiClientConstructor.SERIALIZE_WITHOUT_NULL_VALUES.finalizer.apply(getDestination()); - } - } - @FunctionalInterface - interface CoreBuilder extends Build{ - } - @FunctionalInterface - interface OrchestrationBuilder { - @Nonnull - Build resourceGroup(@Nonnull final String resourceGroup); - } - @FunctionalInterface - interface OpenAiBuilder { - @Nonnull - OpenAiBuilder1 model(@Nonnull final String modelName); - @Nonnull - default OpenAiBuilder1 deploymentId(@Nonnull final String model) { - return null; - } - } - @FunctionalInterface - interface OpenAiBuilder1 { - @Nonnull - Build resourceGroup(@Nonnull final String resourceGroup); - } - } - - - /** - * Get the deployment id from the scenario id. If there are multiple deployments of the same - * scenario id, the first one is returned. - * - * @param resourceGroup the resource group. - * @return the deployment id - * @throws NoSuchElementException if no deployment is found for the scenario id. - */ - private static String getDeploymentId(@Nonnull final ApiClientResolver resolver, @Nonnull final String resourceGroup, @Nonnull final Predicate predicate) - throws NoSuchElementException { - final var deploymentService = new DeploymentApi(resolver.forService(Service.CORE).getClient()); - final var deployments = - deploymentService.deploymentQuery(resourceGroup); - - return deployments.getResources().stream() - .filter(predicate) - .map(AiDeployment::getId) - .findFirst() - .orElseThrow( - () -> new NoSuchElementException("No deployment found with scenario id orchestration")); - } - - - /** - * Get a destination pointing to the inference endpoint of a deployment on AI Core. Requires an - * AI Core service binding. - * - * @param deploymentId The deployment id. - * @param resourceGroup The resource group. - * @return a destination that can be used for inference calls. - */ +public interface ApiClientResolver extends ApiClientContainer { + + @Nonnull + default ForDeployment forDeployment(@Nonnull final DeploymentChoice choice) { + return processors -> + resourceGroup -> + () -> { + final var deploymentId = choice.getDeploymentId(this, resourceGroup); + final var destination = this.getDestination().asHttp(); + final var builder = DefaultHttpDestination.fromDestination(destination); + final var context = + new DestinationProcessor.Context(destination, deploymentId, resourceGroup); + for (final DestinationProcessor processor : processors) { + processor.process(builder, context); + } + return builder.build(); + }; + } + + @FunctionalInterface + interface ForDeployment { @Nonnull - private static Destination getDestinationForDeployment( - @Nonnull final ApiClientResolver resolver, - @Nonnull final String deploymentId, @Nonnull final String resourceGroup) { - final var destination = resolver.getDestination().asHttp(); - final DefaultHttpDestination.Builder builder = - DefaultHttpDestination.fromDestination(destination) - .uri( - destination - .getUri() - .resolve("/v2/inference/deployments/%s/".formatted(deploymentId))); - - builder.header("AI-Resource-Group", resourceGroup); - - return builder.build(); + default ApiClientContainer resourceGroup(@Nonnull final String resourceGroup) { + return withProcessors(UPDATE_URI, ADD_HEADER).resourceGroup(resourceGroup); } + @Nonnull + WithProcessors withProcessors(@Nonnull final DestinationProcessor... processors); + } - /** This exists because getBackendDetails() is broken */ - private static boolean isDeploymentOfModel( - @Nonnull final String modelName, @Nonnull final AiDeployment deployment) { - final var deploymentDetails = deployment.getDetails(); - // The AI Core specification doesn't mention that this is nullable, but it can be. - // Remove this check when the specification is fixed. - if (deploymentDetails == null) { - return false; - } - final var resources = deploymentDetails.getResources(); - if (resources == null) { - return false; - } - if (!resources.getCustomFieldNames().contains("backend_details")) { - return false; - } - final var detailsObject = resources.getCustomField("backend_details"); - - if (detailsObject instanceof Map details - && details.get("model") instanceof Map model - && model.get("name") instanceof String name) { - return modelName.equals(name); - } - return false; + @FunctionalInterface + interface WithProcessors { + @Nonnull + ApiClientContainer resourceGroup(@Nonnull final String resourceGroup); + } + + @FunctionalInterface + interface DestinationProcessor { + void process(DefaultHttpDestination.Builder builder, Context context); + + DestinationProcessor UPDATE_URI = + (builder, context) -> + builder.uri( + context + .origin + .getUri() + .resolve("/v2/inference/deployments/%s/".formatted(context.deploymentId))); + DestinationProcessor ADD_HEADER = + (builder, context) -> builder.header("AI-Resource-Group", context.resourceGroup); + + @Value + class Context { + HttpDestination origin; + String deploymentId; + String resourceGroup; } + } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/Core.java b/core/src/main/java/com/sap/ai/sdk/core/Core.java index ac1895eb..59eb034e 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/Core.java +++ b/core/src/main/java/com/sap/ai/sdk/core/Core.java @@ -1,16 +1,14 @@ package com.sap.ai.sdk.core; -import com.sap.ai.sdk.core.client.DeploymentApi; -import com.sap.ai.sdk.core.client.model.AiDeployment; -import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; +import static com.sap.ai.sdk.core.DeploymentChoice.ORCHESTRATION; +import static com.sap.ai.sdk.core.DeploymentChoice.withId; +import static com.sap.ai.sdk.core.DeploymentChoice.withModel; + +import com.google.common.annotations.Beta; import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; - -import java.util.Map; -import java.util.NoSuchElementException; import java.util.concurrent.Callable; import javax.annotation.Nonnull; - import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -18,16 +16,25 @@ @Slf4j public class Core implements ApiClientResolver { - @Getter - private static Core instance = new Core(); + @Getter private static Core instance = new Core(); - public static synchronized T executeWithCore(Core core, Callable callable) throws Exception { + /** + * ONLY USE FOR TESTING. + * + * @param core + * @param callable + * @return + * @param + * @throws Exception + */ + @Beta + public static synchronized T executeWithCore(Core core, Callable callable) + throws Exception { Core oldInstance = instance; instance = core; try { return callable.call(); - } - finally { + } finally { instance = oldInstance; } } @@ -37,7 +44,6 @@ public ApiClientResolver withDestination(@Nonnull final Destination destination) return () -> destination; } - /** * Requires an AI Core service binding. * @@ -47,19 +53,7 @@ public ApiClientResolver withDestination(@Nonnull final Destination destination) @Nonnull @Deprecated public static ApiClient getOrchestrationClient(@Nonnull final String resourceGroup) { - return getInstance().forService(Service.ORCHESTRATION).resourceGroup(resourceGroup).getClient(); - } - - /** - * Requires an AI Core service binding OR a service key in the environment variable {@code - * AICORE_SERVICE_KEY}. - * - * @return a generic AI Core ApiClient. - */ - @Nonnull - @Deprecated - public static ApiClient getClient() { - return getInstance().forService(Service.CORE).getClient(); + return getInstance().forDeployment(ORCHESTRATION).resourceGroup(resourceGroup).getClient(); } /** @@ -71,7 +65,7 @@ public static ApiClient getClient() { @Nonnull @Deprecated public static ApiClient getClient(@Nonnull final Destination destination) { - return getInstance().withDestination(destination).forService(Service.CORE).getClient(); + return getInstance().withDestination(destination).getClient(); } /** @@ -101,18 +95,10 @@ public Destination getDestination() { @Deprecated public static Destination getDestinationForDeployment( @Nonnull final String deploymentId, @Nonnull final String resourceGroup) { - return getInstance().forService() - final var destination = getInstance().getDestination().asHttp(); - final DefaultHttpDestination.Builder builder = - DefaultHttpDestination.fromDestination(destination) - .uri( - destination - .getUri() - .resolve("/v2/inference/deployments/%s/".formatted(deploymentId))); - - builder.header("AI-Resource-Group", resourceGroup); - - return builder.build(); + return getInstance() + .forDeployment(withId(deploymentId)) + .resourceGroup(resourceGroup) + .getDestination(); } /** @@ -127,7 +113,9 @@ public static Destination getDestinationForDeployment( @Deprecated public static Destination getDestinationForModel( @Nonnull final String modelName, @Nonnull final String resourceGroup) { - return getInstance().forService(Service.OPENAI).model(modelName).resourceGroup(resourceGroup).getDestination(); + return getInstance() + .forDeployment(withModel(modelName)) + .resourceGroup(resourceGroup) + .getDestination(); } - } diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java new file mode 100644 index 00000000..e802ed98 --- /dev/null +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java @@ -0,0 +1,83 @@ +package com.sap.ai.sdk.core; + +import com.sap.ai.sdk.core.client.DeploymentApi; +import com.sap.ai.sdk.core.client.model.AiDeployment; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.function.Predicate; +import javax.annotation.Nonnull; + +@FunctionalInterface +public interface DeploymentChoice { + @Nonnull + String getDeploymentId( + @Nonnull final ApiClientResolver resolver, @Nonnull final String resourceGroup); + + DeploymentChoice ORCHESTRATION = withScenario("orchestration"); + + @Nonnull + static DeploymentChoice withId(@Nonnull final String deploymentId) { + return (resolver, resourceGroup) -> deploymentId; + } + + @Nonnull + static DeploymentChoice withModel(@Nonnull final String modelName) { + final Predicate p = deployment -> isDeploymentOfModel(modelName, deployment); + return (resolver, resourceGroup) -> getDeploymentId(resolver, resourceGroup, p); + } + + @Nonnull + static DeploymentChoice withScenario(@Nonnull final String scenarioId) { + final Predicate p = deployment -> scenarioId.equals(deployment.getScenarioId()); + return (resolver, resourceGroup) -> getDeploymentId(resolver, resourceGroup, p); + } + + /** + * Get the deployment id from the scenario id. If there are multiple deployments of the same + * scenario id, the first one is returned. + * + * @param resourceGroup the resource group. + * @return the deployment id + * @throws NoSuchElementException if no deployment is found for the scenario id. + */ + @Nonnull + private static String getDeploymentId( + @Nonnull final ApiClientResolver resolver, + @Nonnull final String resourceGroup, + @Nonnull final Predicate predicate) + throws NoSuchElementException { + final var deploymentService = new DeploymentApi(resolver.getClient()); + final var deployments = deploymentService.deploymentQuery(resourceGroup); + + final var first = + deployments.getResources().stream().filter(predicate).map(AiDeployment::getId).findFirst(); + return first.orElseThrow( + () -> new NoSuchElementException("No deployment found with scenario id orchestration")); + } + + /** This exists because getBackendDetails() is broken */ + private static boolean isDeploymentOfModel( + @Nonnull final String modelName, @Nonnull final AiDeployment deployment) { + final var deploymentDetails = deployment.getDetails(); + // The AI Core specification doesn't mention that this is nullable, but it can be. + // Remove this check when the specification is fixed. + if (deploymentDetails == null) { + return false; + } + final var resources = deploymentDetails.getResources(); + if (resources == null) { + return false; + } + if (!resources.getCustomFieldNames().contains("backend_details")) { + return false; + } + final var detailsObject = resources.getCustomField("backend_details"); + + if (detailsObject instanceof Map details + && details.get("model") instanceof Map model + && model.get("name") instanceof String name) { + return modelName.equals(name); + } + return false; + } +} diff --git a/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java b/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java index 104787e3..f45241f0 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java @@ -1,5 +1,7 @@ package com.sap.ai.sdk.core; +import static com.sap.cloud.sdk.cloudplatform.connectivity.OnBehalfOf.TECHNICAL_USER_PROVIDER; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -12,97 +14,93 @@ import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationLoader; import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions; -import lombok.extern.slf4j.Slf4j; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.util.HashMap; import java.util.List; - -import static com.sap.cloud.sdk.cloudplatform.connectivity.OnBehalfOf.TECHNICAL_USER_PROVIDER; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.extern.slf4j.Slf4j; @Slf4j class DestinationResolver { - /** - * For testing only - * - *

      Get a destination pointing to the AI Core service. - * - * @param serviceKey The service key in JSON format. - * @return a destination pointing to the AI Core service. - */ - @SuppressWarnings("UnstableApiUsage") - static HttpDestination getDestination(@Nullable final String serviceKey) { - final var serviceKeyPresent = serviceKey != null; - final var aiCoreBindingPresent = - DefaultServiceBindingAccessor.getInstance().getServiceBindings().stream() - .anyMatch( - serviceBinding -> - ServiceIdentifier.AI_CORE.equals( - serviceBinding.getServiceIdentifier().orElse(null))); + /** + * For testing only + * + *

      Get a destination pointing to the AI Core service. + * + * @param serviceKey The service key in JSON format. + * @return a destination pointing to the AI Core service. + */ + @SuppressWarnings("UnstableApiUsage") + static HttpDestination getDestination(@Nullable final String serviceKey) { + final var serviceKeyPresent = serviceKey != null; + final var aiCoreBindingPresent = + DefaultServiceBindingAccessor.getInstance().getServiceBindings().stream() + .anyMatch( + serviceBinding -> + ServiceIdentifier.AI_CORE.equals( + serviceBinding.getServiceIdentifier().orElse(null))); - if (!aiCoreBindingPresent && serviceKeyPresent) { - addServiceBinding(serviceKey); - } + if (!aiCoreBindingPresent && serviceKeyPresent) { + addServiceBinding(serviceKey); + } - // get a destination pointing to the AI Core service - final var opts = - ServiceBindingDestinationOptions.forService(ServiceIdentifier.AI_CORE) - .onBehalfOf(TECHNICAL_USER_PROVIDER) - .build(); - var destination = ServiceBindingDestinationLoader.defaultLoaderChain().getDestination(opts); + // get a destination pointing to the AI Core service + final var opts = + ServiceBindingDestinationOptions.forService(ServiceIdentifier.AI_CORE) + .onBehalfOf(TECHNICAL_USER_PROVIDER) + .build(); + var destination = ServiceBindingDestinationLoader.defaultLoaderChain().getDestination(opts); - destination = - DefaultHttpDestination.fromDestination(destination) - // append the /v2 path here, so we don't have to do it in every request when using the - // generated code this is actually necessary, because the generated code assumes this - // path to be present on the destination - .uri(destination.getUri().resolve("/v2")) - .header("AI-Client-Type", "AI SDK Java") - .build(); - return destination; - } + destination = + DefaultHttpDestination.fromDestination(destination) + // append the /v2 path here, so we don't have to do it in every request when using the + // generated code this is actually necessary, because the generated code assumes this + // path to be present on the destination + .uri(destination.getUri().resolve("/v2")) + .header("AI-Client-Type", "AI SDK Java") + .build(); + return destination; + } - /** - * Set the AI Core service key as the service binding. This is used for local testing. - * - * @param serviceKey The service key in JSON format. - * @throws AiCoreCredentialsInvalidException if the JSON service key cannot be parsed. - */ - private static void addServiceBinding(@Nonnull final String serviceKey) { - log.info( - """ + /** + * Set the AI Core service key as the service binding. This is used for local testing. + * + * @param serviceKey The service key in JSON format. + * @throws AiCoreCredentialsInvalidException if the JSON service key cannot be parsed. + */ + private static void addServiceBinding(@Nonnull final String serviceKey) { + log.info( + """ Found a service key in environment variable "AICORE_SERVICE_KEY". Using a service key is recommended for local testing only. Bind the AI Core service to the application for productive usage."""); - var credentials = new HashMap(); - try { - credentials = new ObjectMapper().readValue(serviceKey, new TypeReference<>() {}); - } catch (JsonProcessingException e) { - throw new AiCoreCredentialsInvalidException( - "Error in parsing service key from the \"AICORE_SERVICE_KEY\" environment variable.", e); - } - - final var binding = - new DefaultServiceBindingBuilder() - .withServiceIdentifier(ServiceIdentifier.AI_CORE) - .withCredentials(credentials) - .build(); - final ServiceBindingAccessor accessor = DefaultServiceBindingAccessor.getInstance(); - final var newAccessor = - new ServiceBindingMerger( - List.of(accessor, () -> List.of(binding)), ServiceBindingMerger.KEEP_EVERYTHING); - DefaultServiceBindingAccessor.setInstance(newAccessor); + var credentials = new HashMap(); + try { + credentials = new ObjectMapper().readValue(serviceKey, new TypeReference<>() {}); + } catch (JsonProcessingException e) { + throw new AiCoreCredentialsInvalidException( + "Error in parsing service key from the \"AICORE_SERVICE_KEY\" environment variable.", e); } - /** Exception thrown when the JSON AI Core service key is invalid. */ - static class AiCoreCredentialsInvalidException extends RuntimeException { - public AiCoreCredentialsInvalidException( - @Nonnull final String message, @Nonnull final Throwable cause) { - super(message, cause); - } - } + final var binding = + new DefaultServiceBindingBuilder() + .withServiceIdentifier(ServiceIdentifier.AI_CORE) + .withCredentials(credentials) + .build(); + final var accessor = DefaultServiceBindingAccessor.getInstance(); + final var newAccessor = + new ServiceBindingMerger( + List.of(accessor, () -> List.of(binding)), ServiceBindingMerger.KEEP_EVERYTHING); + DefaultServiceBindingAccessor.setInstance(newAccessor); + } + /** Exception thrown when the JSON AI Core service key is invalid. */ + static class AiCoreCredentialsInvalidException extends RuntimeException { + public AiCoreCredentialsInvalidException( + @Nonnull final String message, @Nonnull final Throwable cause) { + super(message, cause); + } + } } diff --git a/e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java b/e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java index 708a3c80..c003df6e 100644 --- a/e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java +++ b/e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java @@ -1,7 +1,6 @@ package com.sap.ai.sdk.app.controllers; -import static com.sap.ai.sdk.core.Core.getClient; - +import com.sap.ai.sdk.core.Core; import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.ai.sdk.core.client.model.AiDeploymentCreationRequest; @@ -25,7 +24,7 @@ @RequestMapping("/deployments") class DeploymentController { - private static final DeploymentApi API = new DeploymentApi(getClient()); + private static final DeploymentApi API = new DeploymentApi(Core.getInstance().getClient()); /** * Create and delete a deployment with the Java specific configuration ID diff --git a/e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java b/e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java index 5054fc46..134b4dd5 100644 --- a/e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java +++ b/e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java @@ -1,9 +1,8 @@ package com.sap.ai.sdk.app.controllers; -import com.sap.ai.sdk.core.Core; - -import static com.sap.ai.sdk.core.ApiClientResolver.Service.ORCHESTRATION; +import static com.sap.ai.sdk.core.DeploymentChoice.ORCHESTRATION; +import com.sap.ai.sdk.core.Core; import com.sap.ai.sdk.orchestration.client.OrchestrationCompletionApi; import com.sap.ai.sdk.orchestration.client.model.AzureContentSafety; import com.sap.ai.sdk.orchestration.client.model.AzureThreshold; @@ -33,7 +32,8 @@ class OrchestrationController { private static final OrchestrationCompletionApi API = - new OrchestrationCompletionApi(Core.getInstance().forService(ORCHESTRATION).resourceGroup("default").getClient()); + new OrchestrationCompletionApi( + Core.getInstance().forDeployment(ORCHESTRATION).resourceGroup("default").getClient()); static final String MODEL = "gpt-35-turbo"; diff --git a/e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/ScenarioController.java b/e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/ScenarioController.java index 32ab7a03..a3f2e287 100644 --- a/e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/ScenarioController.java +++ b/e2e-test-app/src/main/java/com/sap/ai/sdk/app/controllers/ScenarioController.java @@ -1,7 +1,6 @@ package com.sap.ai.sdk.app.controllers; -import static com.sap.ai.sdk.core.Core.getClient; - +import com.sap.ai.sdk.core.Core; import com.sap.ai.sdk.core.client.ScenarioApi; import com.sap.ai.sdk.core.client.model.AiModelList; import com.sap.ai.sdk.core.client.model.AiScenarioList; @@ -14,7 +13,7 @@ @SuppressWarnings("unused") // debug method that doesn't need to be tested public class ScenarioController { - private static final ScenarioApi API = new ScenarioApi(getClient()); + private static final ScenarioApi API = new ScenarioApi(Core.getInstance().getClient()); /** * Get the list of available scenarios diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java index de1e2ea0..dc34bead 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java @@ -1,12 +1,13 @@ package com.sap.ai.sdk.foundationmodels.openai; +import static com.sap.ai.sdk.core.DeploymentChoice.withModel; + import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.sap.ai.sdk.core.ApiClientResolver; import com.sap.ai.sdk.core.Core; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionDelta; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionOutput; @@ -29,8 +30,6 @@ import org.apache.hc.core5.http.message.BasicClassicHttpRequest; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; -import static com.sap.ai.sdk.core.ApiClientResolver.Service.OPENAI; - /** Client for interacting with OpenAI models. */ @Slf4j @RequiredArgsConstructor(access = AccessLevel.PRIVATE) @@ -58,7 +57,11 @@ public final class OpenAiClient { */ @Nonnull public static OpenAiClient forModel(@Nonnull final OpenAiModel foundationModel) { - final var destination = Core.getInstance().forService(OPENAI).model(foundationModel.model()).resourceGroup("default").getDestination(); + final var destination = + Core.getInstance() + .forDeployment(withModel(foundationModel.model())) + .resourceGroup("default") + .getDestination(); final var client = new OpenAiClient(destination); return client.withApiVersion(DEFAULT_API_VERSION); } From 678a69bd69da045c296808597fe7cc894a079bb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 25 Sep 2024 14:40:57 +0200 Subject: [PATCH 13/79] Fix merge; Format --- .../java/com/sap/ai/sdk/core/ApiClientResolver.java | 5 ++--- .../java/com/sap/ai/sdk/core/DeploymentChoice.java | 10 +++++++--- .../java/com/sap/ai/sdk/core/DestinationResolver.java | 1 - 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java b/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java index e460f085..f2bca421 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java +++ b/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java @@ -16,11 +16,10 @@ default ForDeployment forDeployment(@Nonnull final DeploymentChoice choice) { return processors -> resourceGroup -> () -> { - final var deploymentId = choice.getDeploymentId(this, resourceGroup); + final var id = choice.getDeploymentId(this, resourceGroup); final var destination = this.getDestination().asHttp(); final var builder = DefaultHttpDestination.fromDestination(destination); - final var context = - new DestinationProcessor.Context(destination, deploymentId, resourceGroup); + final var context = new DestinationProcessor.Context(destination, id, resourceGroup); for (final DestinationProcessor processor : processors) { processor.process(builder, context); } diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java index e802ed98..bbe22920 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java @@ -68,10 +68,14 @@ private static boolean isDeploymentOfModel( if (resources == null) { return false; } - if (!resources.getCustomFieldNames().contains("backend_details")) { - return false; + Object detailsObject = resources.getBackendDetails(); + // workaround for AIWDF-2124 + if (detailsObject == null) { + if (!resources.getCustomFieldNames().contains("backend_details")) { + return false; + } + detailsObject = resources.getCustomField("backend_details"); } - final var detailsObject = resources.getCustomField("backend_details"); if (detailsObject instanceof Map details && details.get("model") instanceof Map model diff --git a/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java b/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java index f45241f0..e1778b9a 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java @@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingAccessor; import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingBuilder; -import com.sap.cloud.environment.servicebinding.api.ServiceBindingAccessor; import com.sap.cloud.environment.servicebinding.api.ServiceBindingMerger; import com.sap.cloud.environment.servicebinding.api.ServiceIdentifier; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; From 448a140c6b0f8e02ad73d707a0d408f848d56521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 25 Sep 2024 16:38:32 +0200 Subject: [PATCH 14/79] Add javadoc --- .../sap/ai/sdk/core/ApiClientContainer.java | 31 ++++++++++++++ .../sap/ai/sdk/core/ApiClientResolver.java | 41 ++++++++++++++++++- .../main/java/com/sap/ai/sdk/core/Core.java | 22 ++++++---- .../com/sap/ai/sdk/core/DeploymentChoice.java | 27 ++++++++++++ .../sap/ai/sdk/core/DestinationResolver.java | 1 + 5 files changed, 114 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/ApiClientContainer.java b/core/src/main/java/com/sap/ai/sdk/core/ApiClientContainer.java index 038829c5..2d075d08 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/ApiClientContainer.java +++ b/core/src/main/java/com/sap/ai/sdk/core/ApiClientContainer.java @@ -16,30 +16,61 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.client.RestTemplate; +/** Container for an API client and destination. */ @FunctionalInterface public interface ApiClientContainer { + /** + * Get the destination. + * + * @return the destination + */ @Nonnull Destination getDestination(); + /** + * Get the API client. + * + * @return the API client + */ @Nonnull default ApiClient getClient() { return getClient(ClientOptions.SERIALIZE_WITHOUT_NULL_VALUES); } + /** + * Get the API client with options. + * + * @param options the options + * @return the API client + */ @Nonnull default ApiClient getClient(@Nonnull final ClientOptions options) { final Destination destination = getDestination(); return options.getInitializer().apply(destination); } + /** Options for the API client. */ interface ClientOptions { + /** + * Get the initializer for the API client. + * + * @return the initializer + */ @Nonnull Function getInitializer(); + /** Serialize with null values. */ ClientOptions SERIALIZE_WITH_NULL_VALUES = () -> ApiClient::new; + /** Serialize without null values. */ ClientOptions SERIALIZE_WITHOUT_NULL_VALUES = () -> ClientOptions::withoutNull; + /** + * Helper method to Serialize without null values. + * + * @param destination the destination + * @return the API client + */ @SuppressWarnings("UnstableApiUsage") @Nonnull private static ApiClient withoutNull(@Nonnull final Destination destination) { diff --git a/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java b/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java index f2bca421..d59c5f09 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java +++ b/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java @@ -8,9 +8,16 @@ import javax.annotation.Nonnull; import lombok.Value; +/** Utility class. */ @FunctionalInterface public interface ApiClientResolver extends ApiClientContainer { + /** + * Focus on a specific deployment. + * + * @param choice the deployment choice. + * @return the deployment context. + */ @Nonnull default ForDeployment forDeployment(@Nonnull final DeploymentChoice choice) { return processors -> @@ -27,27 +34,56 @@ default ForDeployment forDeployment(@Nonnull final DeploymentChoice choice) { }; } + /** Helper interface to focus on a specific deployment. */ @FunctionalInterface interface ForDeployment { + /** + * Focus on a specific resource group. + * + * @param resourceGroup the resource group. + * @return the client and destination builder. + */ @Nonnull default ApiClientContainer resourceGroup(@Nonnull final String resourceGroup) { return withProcessors(UPDATE_URI, ADD_HEADER).resourceGroup(resourceGroup); } + /** + * Assign destination processors. + * + * @param processors the processors. + * @return the client and destination builder. + */ @Nonnull WithProcessors withProcessors(@Nonnull final DestinationProcessor... processors); } + /** Helper interface to focus on a specific resource group. */ @FunctionalInterface interface WithProcessors { + /** + * Focus on a specific resource group. + * + * @param resourceGroup the resource group. + * @return the client and destination builder. + */ @Nonnull ApiClientContainer resourceGroup(@Nonnull final String resourceGroup); } + /** Helper interface to process the destination. */ @FunctionalInterface interface DestinationProcessor { - void process(DefaultHttpDestination.Builder builder, Context context); + /** + * Process the destination. + * + * @param builder the destination builder. + * @param context the context. + */ + void process( + @Nonnull final DefaultHttpDestination.Builder builder, @Nonnull final Context context); + /** Update the URI. */ DestinationProcessor UPDATE_URI = (builder, context) -> builder.uri( @@ -55,9 +91,12 @@ interface DestinationProcessor { .origin .getUri() .resolve("/v2/inference/deployments/%s/".formatted(context.deploymentId))); + + /** Add the AI-Resource-Group header. */ DestinationProcessor ADD_HEADER = (builder, context) -> builder.header("AI-Resource-Group", context.resourceGroup); + /** Helper class to hold the context. */ @Value class Context { HttpDestination origin; diff --git a/core/src/main/java/com/sap/ai/sdk/core/Core.java b/core/src/main/java/com/sap/ai/sdk/core/Core.java index 59eb034e..55f27061 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/Core.java +++ b/core/src/main/java/com/sap/ai/sdk/core/Core.java @@ -9,6 +9,7 @@ import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import java.util.concurrent.Callable; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -21,15 +22,16 @@ public class Core implements ApiClientResolver { /** * ONLY USE FOR TESTING. * - * @param core - * @param callable - * @return - * @param - * @throws Exception + * @param core the core instance. + * @param callable the callable. + * @return the result of the callable. + * @param the type of the result. + * @throws Exception if the callable throws an exception. */ @Beta - public static synchronized T executeWithCore(Core core, Callable callable) - throws Exception { + @Nullable + public static synchronized T executeWithCore( + @Nonnull final Core core, @Nonnull final Callable callable) throws Exception { Core oldInstance = instance; instance = core; try { @@ -39,6 +41,12 @@ public static synchronized T executeWithCore(Core core, Callable callable } } + /** + * Get an API client resolver for destination. + * + * @param destination the destination. + * @return the API client resolver. + */ @Nonnull public ApiClientResolver withDestination(@Nonnull final Destination destination) { return () -> destination; diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java index bbe22920..84b17002 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java @@ -7,25 +7,52 @@ import java.util.function.Predicate; import javax.annotation.Nonnull; +/** A deployment choice helper class. */ @FunctionalInterface public interface DeploymentChoice { + /** + * Get the deployment id. + * + * @param resolver the API client resolver. + * @param resourceGroup The resource group. + * @return the deployment id. + */ @Nonnull String getDeploymentId( @Nonnull final ApiClientResolver resolver, @Nonnull final String resourceGroup); + /** Deployment choice for orchestration. */ DeploymentChoice ORCHESTRATION = withScenario("orchestration"); + /** + * Create a deployment choice with a specific deployment id. + * + * @param deploymentId the deployment id. + * @return the deployment choice. + */ @Nonnull static DeploymentChoice withId(@Nonnull final String deploymentId) { return (resolver, resourceGroup) -> deploymentId; } + /** + * Create a deployment choice with a specific model name. + * + * @param modelName the model name. + * @return the deployment choice. + */ @Nonnull static DeploymentChoice withModel(@Nonnull final String modelName) { final Predicate p = deployment -> isDeploymentOfModel(modelName, deployment); return (resolver, resourceGroup) -> getDeploymentId(resolver, resourceGroup, p); } + /** + * Create a deployment choice with a specific scenario id. + * + * @param scenarioId the scenario id. + * @return the deployment choice. + */ @Nonnull static DeploymentChoice withScenario(@Nonnull final String scenarioId) { final Predicate p = deployment -> scenarioId.equals(deployment.getScenarioId()); diff --git a/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java b/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java index e1778b9a..eeef6d95 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java @@ -19,6 +19,7 @@ import javax.annotation.Nullable; import lombok.extern.slf4j.Slf4j; +/** Utility class to resolve the destination pointing to the AI Core service. */ @Slf4j class DestinationResolver { From 799cee672cb3dc1edaed7a1d83c55a75d9071af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 25 Sep 2024 16:43:11 +0200 Subject: [PATCH 15/79] Fix PMD warnings --- .../com/sap/ai/sdk/core/ApiClientContainer.java | 13 +++++++------ .../com/sap/ai/sdk/core/ApiClientResolver.java | 17 +++++++++-------- .../src/main/java/com/sap/ai/sdk/core/Core.java | 3 ++- .../com/sap/ai/sdk/core/DeploymentChoice.java | 6 +++--- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/ApiClientContainer.java b/core/src/main/java/com/sap/ai/sdk/core/ApiClientContainer.java index 2d075d08..b5896d20 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/ApiClientContainer.java +++ b/core/src/main/java/com/sap/ai/sdk/core/ApiClientContainer.java @@ -51,6 +51,13 @@ default ApiClient getClient(@Nonnull final ClientOptions options) { /** Options for the API client. */ interface ClientOptions { + + /** Serialize with null values. */ + ClientOptions SERIALIZE_WITH_NULL_VALUES = () -> ApiClient::new; + + /** Serialize without null values. */ + ClientOptions SERIALIZE_WITHOUT_NULL_VALUES = () -> ClientOptions::withoutNull; + /** * Get the initializer for the API client. * @@ -59,12 +66,6 @@ interface ClientOptions { @Nonnull Function getInitializer(); - /** Serialize with null values. */ - ClientOptions SERIALIZE_WITH_NULL_VALUES = () -> ApiClient::new; - - /** Serialize without null values. */ - ClientOptions SERIALIZE_WITHOUT_NULL_VALUES = () -> ClientOptions::withoutNull; - /** * Helper method to Serialize without null values. * diff --git a/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java b/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java index d59c5f09..257997ae 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java +++ b/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java @@ -74,14 +74,6 @@ interface WithProcessors { /** Helper interface to process the destination. */ @FunctionalInterface interface DestinationProcessor { - /** - * Process the destination. - * - * @param builder the destination builder. - * @param context the context. - */ - void process( - @Nonnull final DefaultHttpDestination.Builder builder, @Nonnull final Context context); /** Update the URI. */ DestinationProcessor UPDATE_URI = @@ -96,6 +88,15 @@ void process( DestinationProcessor ADD_HEADER = (builder, context) -> builder.header("AI-Resource-Group", context.resourceGroup); + /** + * Process the destination. + * + * @param builder the destination builder. + * @param context the context. + */ + void process( + @Nonnull final DefaultHttpDestination.Builder builder, @Nonnull final Context context); + /** Helper class to hold the context. */ @Value class Context { diff --git a/core/src/main/java/com/sap/ai/sdk/core/Core.java b/core/src/main/java/com/sap/ai/sdk/core/Core.java index 55f27061..c09f7c1e 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/Core.java +++ b/core/src/main/java/com/sap/ai/sdk/core/Core.java @@ -30,9 +30,10 @@ public class Core implements ApiClientResolver { */ @Beta @Nullable + @SuppressWarnings("PMD.SignatureDeclareThrowsException") public static synchronized T executeWithCore( @Nonnull final Core core, @Nonnull final Callable callable) throws Exception { - Core oldInstance = instance; + final var oldInstance = instance; instance = core; try { return callable.call(); diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java index 84b17002..a5cc53dc 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java @@ -10,6 +10,9 @@ /** A deployment choice helper class. */ @FunctionalInterface public interface DeploymentChoice { + /** Deployment choice for orchestration. */ + DeploymentChoice ORCHESTRATION = withScenario("orchestration"); + /** * Get the deployment id. * @@ -21,9 +24,6 @@ public interface DeploymentChoice { String getDeploymentId( @Nonnull final ApiClientResolver resolver, @Nonnull final String resourceGroup); - /** Deployment choice for orchestration. */ - DeploymentChoice ORCHESTRATION = withScenario("orchestration"); - /** * Create a deployment choice with a specific deployment id. * From 867246678db87b3977266a6f49ce1cacdd61dea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Thu, 26 Sep 2024 14:46:14 +0200 Subject: [PATCH 16/79] Initial change of draft --- .../java/com/sap/ai/sdk/core/AiClient.java | 26 +++ ...ClientContainer.java => AiClientAuto.java} | 12 +- .../com/sap/ai/sdk/core/AiClientCustom.java | 193 ++++++++++++++++++ .../sap/ai/sdk/core/ApiClientResolver.java | 108 ---------- .../main/java/com/sap/ai/sdk/core/Core.java | 130 ------------ .../com/sap/ai/sdk/core/DeploymentChoice.java | 114 ----------- .../ai/sdk/core/client/ArtifactUnitTest.java | 11 +- .../core/client/ConfigurationUnitTest.java | 11 +- .../sdk/core/client/DeploymentUnitTest.java | 22 +- .../ai/sdk/core/client/ExecutionUnitTest.java | 20 +- .../ai/sdk/core/client/ScenarioUnitTest.java | 12 +- .../sdk/core/client/WireMockTestServer.java | 4 + .../foundationmodels/openai/OpenAiClient.java | 10 +- .../client/OrchestrationUnitTest.java | 5 +- .../app/controllers/DeploymentController.java | 4 +- .../controllers/OrchestrationController.java | 9 +- .../app/controllers/ScenarioController.java | 4 +- 17 files changed, 274 insertions(+), 421 deletions(-) create mode 100644 core/src/main/java/com/sap/ai/sdk/core/AiClient.java rename core/src/main/java/com/sap/ai/sdk/core/{ApiClientContainer.java => AiClientAuto.java} (91%) create mode 100644 core/src/main/java/com/sap/ai/sdk/core/AiClientCustom.java delete mode 100644 core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java delete mode 100644 core/src/main/java/com/sap/ai/sdk/core/Core.java delete mode 100644 core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiClient.java b/core/src/main/java/com/sap/ai/sdk/core/AiClient.java new file mode 100644 index 00000000..4597b647 --- /dev/null +++ b/core/src/main/java/com/sap/ai/sdk/core/AiClient.java @@ -0,0 +1,26 @@ +package com.sap.ai.sdk.core; + +import lombok.extern.slf4j.Slf4j; + +/** Connectivity convenience methods for AI Core. */ +@Slf4j +public class AiClient { + + /** + * Get a destination pointing to the AI Core service. + * + *

      Requires an AI Core service binding OR a service key in the environment variable {@code + * AICORE_SERVICE_KEY}. + * + * @return a destination pointing to the AI Core service. + */ + public static AiClientAuto auto() { + final var serviceKey = System.getenv("AICORE_SERVICE_KEY"); + return () -> DestinationResolver.getDestination(serviceKey); + } + + public static AiClientCustom custom() { + final var serviceKey = System.getenv("AICORE_SERVICE_KEY"); + return () -> DestinationResolver.getDestination(serviceKey); + } +} diff --git a/core/src/main/java/com/sap/ai/sdk/core/ApiClientContainer.java b/core/src/main/java/com/sap/ai/sdk/core/AiClientAuto.java similarity index 91% rename from core/src/main/java/com/sap/ai/sdk/core/ApiClientContainer.java rename to core/src/main/java/com/sap/ai/sdk/core/AiClientAuto.java index b5896d20..28538053 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/ApiClientContainer.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiClientAuto.java @@ -18,14 +18,14 @@ /** Container for an API client and destination. */ @FunctionalInterface -public interface ApiClientContainer { +public interface AiClientAuto { /** * Get the destination. * * @return the destination */ @Nonnull - Destination getDestination(); + Destination destination(); /** * Get the API client. @@ -33,8 +33,8 @@ public interface ApiClientContainer { * @return the API client */ @Nonnull - default ApiClient getClient() { - return getClient(ClientOptions.SERIALIZE_WITHOUT_NULL_VALUES); + default ApiClient client() { + return client(ClientOptions.SERIALIZE_WITHOUT_NULL_VALUES); } /** @@ -44,8 +44,8 @@ default ApiClient getClient() { * @return the API client */ @Nonnull - default ApiClient getClient(@Nonnull final ClientOptions options) { - final Destination destination = getDestination(); + default ApiClient client(@Nonnull final ClientOptions options) { + final Destination destination = destination(); return options.getInitializer().apply(destination); } diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiClientCustom.java b/core/src/main/java/com/sap/ai/sdk/core/AiClientCustom.java new file mode 100644 index 00000000..9100724a --- /dev/null +++ b/core/src/main/java/com/sap/ai/sdk/core/AiClientCustom.java @@ -0,0 +1,193 @@ +package com.sap.ai.sdk.core; + +import static com.sap.ai.sdk.core.AiClientCustom.DestinationProcessor.ADD_HEADER; +import static com.sap.ai.sdk.core.AiClientCustom.DestinationProcessor.UPDATE_URI; + +import com.sap.ai.sdk.core.client.DeploymentApi; +import com.sap.ai.sdk.core.client.model.AiDeployment; +import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; +import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; +import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.function.BiFunction; +import java.util.function.Predicate; +import javax.annotation.Nonnull; +import lombok.Value; + +/** Utility class. */ +@FunctionalInterface +public interface AiClientCustom extends AiClientAuto { + + /** + * Get an API client resolver for destination. + * + * @param destination the destination. + * @return the API client resolver. + */ + @Nonnull + default AiClientCustom withDestination(@Nonnull final Destination destination) { + return () -> destination; + } + + default ForDeployment forDeployment(@Nonnull final String deploymentId) { + return getDestinationForDeployment((resolver, resourceGroup) -> deploymentId); + } + + default ForDeployment forDeploymentByModel(@Nonnull final String modelName) { + final Predicate p = deployment -> isDeploymentOfModel(modelName, deployment); + return getDestinationForDeployment((r, resourceGroup) -> getDeploymentId(r, resourceGroup, p)); + } + + default ForDeployment forDeploymentByScenario(@Nonnull final String scenarioId) { + final Predicate p = deployment -> scenarioId.equals(deployment.getScenarioId()); + return getDestinationForDeployment((r, resourceGroup) -> getDeploymentId(r, resourceGroup, p)); + } + + /** + * Focus on a specific deployment. + * + * @param deploymentIdResolver the deployment choice. + * @return the deployment context. + */ + private ForDeployment getDestinationForDeployment( + @Nonnull final BiFunction deploymentIdResolver) { + return processors -> + resourceGroup -> + () -> { + final var id = deploymentIdResolver.apply(this, resourceGroup); + final var destination = this.destination().asHttp(); + final var builder = DefaultHttpDestination.fromDestination(destination); + final var context = new DestinationProcessor.Context(destination, id, resourceGroup); + for (final DestinationProcessor processor : processors) { + processor.process(builder, context); + } + return builder.build(); + }; + } + + /** Helper interface to focus on a specific deployment. */ + @FunctionalInterface + interface ForDeployment { + /** + * Focus on a specific resource group. + * + * @param resourceGroup the resource group. + * @return the client and destination builder. + */ + @Nonnull + default AiClientAuto resourceGroup(@Nonnull final String resourceGroup) { + return withProcessors(UPDATE_URI, ADD_HEADER).resourceGroup(resourceGroup); + } + + /** + * Assign destination processors. + * + * @param processors the processors. + * @return the client and destination builder. + */ + @Nonnull + WithProcessors withProcessors(@Nonnull final DestinationProcessor... processors); + } + + /** Helper interface to focus on a specific resource group. */ + @FunctionalInterface + interface WithProcessors { + /** + * Focus on a specific resource group. + * + * @param resourceGroup the resource group. + * @return the client and destination builder. + */ + @Nonnull + AiClientAuto resourceGroup(@Nonnull final String resourceGroup); + } + + /** Helper interface to process the destination. */ + @FunctionalInterface + interface DestinationProcessor { + + /** Update the URI. */ + DestinationProcessor UPDATE_URI = + (builder, context) -> + builder.uri( + context + .origin + .getUri() + .resolve("/v2/inference/deployments/%s/".formatted(context.deploymentId))); + + /** Add the AI-Resource-Group header. */ + DestinationProcessor ADD_HEADER = + (builder, context) -> builder.header("AI-Resource-Group", context.resourceGroup); + + /** + * Process the destination. + * + * @param builder the destination builder. + * @param context the context. + */ + void process( + @Nonnull final DefaultHttpDestination.Builder builder, @Nonnull final Context context); + + /** Helper class to hold the context. */ + @Value + class Context { + HttpDestination origin; + String deploymentId; + String resourceGroup; + } + } + + /** This exists because getBackendDetails() is broken */ + private static boolean isDeploymentOfModel( + @Nonnull final String modelName, @Nonnull final AiDeployment deployment) { + final var deploymentDetails = deployment.getDetails(); + // The AI Core specification doesn't mention that this is nullable, but it can be. + // Remove this check when the specification is fixed. + if (deploymentDetails == null) { + return false; + } + final var resources = deploymentDetails.getResources(); + if (resources == null) { + return false; + } + Object detailsObject = resources.getBackendDetails(); + // workaround for AIWDF-2124 + if (detailsObject == null) { + if (!resources.getCustomFieldNames().contains("backend_details")) { + return false; + } + detailsObject = resources.getCustomField("backend_details"); + } + + if (detailsObject instanceof Map details + && details.get("model") instanceof Map model + && model.get("name") instanceof String name) { + return modelName.equals(name); + } + return false; + } + + /** + * Get the deployment id from the scenario id. If there are multiple deployments of the same + * scenario id, the first one is returned. + * + * @param resourceGroup the resource group. + * @return the deployment id + * @throws NoSuchElementException if no deployment is found for the scenario id. + */ + @Nonnull + private static String getDeploymentId( + @Nonnull final AiClientCustom resolver, + @Nonnull final String resourceGroup, + @Nonnull final Predicate predicate) + throws NoSuchElementException { + final var deploymentService = new DeploymentApi(resolver.client()); + final var deployments = deploymentService.deploymentQuery(resourceGroup); + + final var first = + deployments.getResources().stream().filter(predicate).map(AiDeployment::getId).findFirst(); + return first.orElseThrow( + () -> new NoSuchElementException("No deployment found with scenario id orchestration")); + } +} diff --git a/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java b/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java deleted file mode 100644 index 257997ae..00000000 --- a/core/src/main/java/com/sap/ai/sdk/core/ApiClientResolver.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.sap.ai.sdk.core; - -import static com.sap.ai.sdk.core.ApiClientResolver.DestinationProcessor.ADD_HEADER; -import static com.sap.ai.sdk.core.ApiClientResolver.DestinationProcessor.UPDATE_URI; - -import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; -import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination; -import javax.annotation.Nonnull; -import lombok.Value; - -/** Utility class. */ -@FunctionalInterface -public interface ApiClientResolver extends ApiClientContainer { - - /** - * Focus on a specific deployment. - * - * @param choice the deployment choice. - * @return the deployment context. - */ - @Nonnull - default ForDeployment forDeployment(@Nonnull final DeploymentChoice choice) { - return processors -> - resourceGroup -> - () -> { - final var id = choice.getDeploymentId(this, resourceGroup); - final var destination = this.getDestination().asHttp(); - final var builder = DefaultHttpDestination.fromDestination(destination); - final var context = new DestinationProcessor.Context(destination, id, resourceGroup); - for (final DestinationProcessor processor : processors) { - processor.process(builder, context); - } - return builder.build(); - }; - } - - /** Helper interface to focus on a specific deployment. */ - @FunctionalInterface - interface ForDeployment { - /** - * Focus on a specific resource group. - * - * @param resourceGroup the resource group. - * @return the client and destination builder. - */ - @Nonnull - default ApiClientContainer resourceGroup(@Nonnull final String resourceGroup) { - return withProcessors(UPDATE_URI, ADD_HEADER).resourceGroup(resourceGroup); - } - - /** - * Assign destination processors. - * - * @param processors the processors. - * @return the client and destination builder. - */ - @Nonnull - WithProcessors withProcessors(@Nonnull final DestinationProcessor... processors); - } - - /** Helper interface to focus on a specific resource group. */ - @FunctionalInterface - interface WithProcessors { - /** - * Focus on a specific resource group. - * - * @param resourceGroup the resource group. - * @return the client and destination builder. - */ - @Nonnull - ApiClientContainer resourceGroup(@Nonnull final String resourceGroup); - } - - /** Helper interface to process the destination. */ - @FunctionalInterface - interface DestinationProcessor { - - /** Update the URI. */ - DestinationProcessor UPDATE_URI = - (builder, context) -> - builder.uri( - context - .origin - .getUri() - .resolve("/v2/inference/deployments/%s/".formatted(context.deploymentId))); - - /** Add the AI-Resource-Group header. */ - DestinationProcessor ADD_HEADER = - (builder, context) -> builder.header("AI-Resource-Group", context.resourceGroup); - - /** - * Process the destination. - * - * @param builder the destination builder. - * @param context the context. - */ - void process( - @Nonnull final DefaultHttpDestination.Builder builder, @Nonnull final Context context); - - /** Helper class to hold the context. */ - @Value - class Context { - HttpDestination origin; - String deploymentId; - String resourceGroup; - } - } -} diff --git a/core/src/main/java/com/sap/ai/sdk/core/Core.java b/core/src/main/java/com/sap/ai/sdk/core/Core.java deleted file mode 100644 index c09f7c1e..00000000 --- a/core/src/main/java/com/sap/ai/sdk/core/Core.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.sap.ai.sdk.core; - -import static com.sap.ai.sdk.core.DeploymentChoice.ORCHESTRATION; -import static com.sap.ai.sdk.core.DeploymentChoice.withId; -import static com.sap.ai.sdk.core.DeploymentChoice.withModel; - -import com.google.common.annotations.Beta; -import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; -import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; -import java.util.concurrent.Callable; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -/** Connectivity convenience methods for AI Core. */ -@Slf4j -public class Core implements ApiClientResolver { - - @Getter private static Core instance = new Core(); - - /** - * ONLY USE FOR TESTING. - * - * @param core the core instance. - * @param callable the callable. - * @return the result of the callable. - * @param the type of the result. - * @throws Exception if the callable throws an exception. - */ - @Beta - @Nullable - @SuppressWarnings("PMD.SignatureDeclareThrowsException") - public static synchronized T executeWithCore( - @Nonnull final Core core, @Nonnull final Callable callable) throws Exception { - final var oldInstance = instance; - instance = core; - try { - return callable.call(); - } finally { - instance = oldInstance; - } - } - - /** - * Get an API client resolver for destination. - * - * @param destination the destination. - * @return the API client resolver. - */ - @Nonnull - public ApiClientResolver withDestination(@Nonnull final Destination destination) { - return () -> destination; - } - - /** - * Requires an AI Core service binding. - * - * @param resourceGroup the resource group. - * @return a generic Orchestration ApiClient. - */ - @Nonnull - @Deprecated - public static ApiClient getOrchestrationClient(@Nonnull final String resourceGroup) { - return getInstance().forDeployment(ORCHESTRATION).resourceGroup(resourceGroup).getClient(); - } - - /** - * Get a generic AI Core ApiClient for testing purposes. - * - * @param destination The destination to use. - * @return a generic AI Core ApiClient. - */ - @Nonnull - @Deprecated - public static ApiClient getClient(@Nonnull final Destination destination) { - return getInstance().withDestination(destination).getClient(); - } - - /** - * Get a destination pointing to the AI Core service. - * - *

      Requires an AI Core service binding OR a service key in the environment variable {@code - * AICORE_SERVICE_KEY}. - * - * @return a destination pointing to the AI Core service. - */ - @Nonnull - @Override - public Destination getDestination() { - final var serviceKey = System.getenv("AICORE_SERVICE_KEY"); - return DestinationResolver.getDestination(serviceKey); - } - - /** - * Get a destination pointing to the inference endpoint of a deployment on AI Core. Requires an - * AI Core service binding. - * - * @param deploymentId The deployment id. - * @param resourceGroup The resource group. - * @return a destination that can be used for inference calls. - */ - @Nonnull - @Deprecated - public static Destination getDestinationForDeployment( - @Nonnull final String deploymentId, @Nonnull final String resourceGroup) { - return getInstance() - .forDeployment(withId(deploymentId)) - .resourceGroup(resourceGroup) - .getDestination(); - } - - /** - * Get a destination pointing to the inference endpoint of a deployment on AI Core. Requires an - * AI Core service binding. - * - * @param modelName The name of the foundation model that is used by a deployment. - * @param resourceGroup The resource group. - * @return a destination that can be used for inference calls. - */ - @Nonnull - @Deprecated - public static Destination getDestinationForModel( - @Nonnull final String modelName, @Nonnull final String resourceGroup) { - return getInstance() - .forDeployment(withModel(modelName)) - .resourceGroup(resourceGroup) - .getDestination(); - } -} diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java deleted file mode 100644 index a5cc53dc..00000000 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentChoice.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.sap.ai.sdk.core; - -import com.sap.ai.sdk.core.client.DeploymentApi; -import com.sap.ai.sdk.core.client.model.AiDeployment; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.function.Predicate; -import javax.annotation.Nonnull; - -/** A deployment choice helper class. */ -@FunctionalInterface -public interface DeploymentChoice { - /** Deployment choice for orchestration. */ - DeploymentChoice ORCHESTRATION = withScenario("orchestration"); - - /** - * Get the deployment id. - * - * @param resolver the API client resolver. - * @param resourceGroup The resource group. - * @return the deployment id. - */ - @Nonnull - String getDeploymentId( - @Nonnull final ApiClientResolver resolver, @Nonnull final String resourceGroup); - - /** - * Create a deployment choice with a specific deployment id. - * - * @param deploymentId the deployment id. - * @return the deployment choice. - */ - @Nonnull - static DeploymentChoice withId(@Nonnull final String deploymentId) { - return (resolver, resourceGroup) -> deploymentId; - } - - /** - * Create a deployment choice with a specific model name. - * - * @param modelName the model name. - * @return the deployment choice. - */ - @Nonnull - static DeploymentChoice withModel(@Nonnull final String modelName) { - final Predicate p = deployment -> isDeploymentOfModel(modelName, deployment); - return (resolver, resourceGroup) -> getDeploymentId(resolver, resourceGroup, p); - } - - /** - * Create a deployment choice with a specific scenario id. - * - * @param scenarioId the scenario id. - * @return the deployment choice. - */ - @Nonnull - static DeploymentChoice withScenario(@Nonnull final String scenarioId) { - final Predicate p = deployment -> scenarioId.equals(deployment.getScenarioId()); - return (resolver, resourceGroup) -> getDeploymentId(resolver, resourceGroup, p); - } - - /** - * Get the deployment id from the scenario id. If there are multiple deployments of the same - * scenario id, the first one is returned. - * - * @param resourceGroup the resource group. - * @return the deployment id - * @throws NoSuchElementException if no deployment is found for the scenario id. - */ - @Nonnull - private static String getDeploymentId( - @Nonnull final ApiClientResolver resolver, - @Nonnull final String resourceGroup, - @Nonnull final Predicate predicate) - throws NoSuchElementException { - final var deploymentService = new DeploymentApi(resolver.getClient()); - final var deployments = deploymentService.deploymentQuery(resourceGroup); - - final var first = - deployments.getResources().stream().filter(predicate).map(AiDeployment::getId).findFirst(); - return first.orElseThrow( - () -> new NoSuchElementException("No deployment found with scenario id orchestration")); - } - - /** This exists because getBackendDetails() is broken */ - private static boolean isDeploymentOfModel( - @Nonnull final String modelName, @Nonnull final AiDeployment deployment) { - final var deploymentDetails = deployment.getDetails(); - // The AI Core specification doesn't mention that this is nullable, but it can be. - // Remove this check when the specification is fixed. - if (deploymentDetails == null) { - return false; - } - final var resources = deploymentDetails.getResources(); - if (resources == null) { - return false; - } - Object detailsObject = resources.getBackendDetails(); - // workaround for AIWDF-2124 - if (detailsObject == null) { - if (!resources.getCustomFieldNames().contains("backend_details")) { - return false; - } - detailsObject = resources.getCustomField("backend_details"); - } - - if (detailsObject instanceof Map details - && details.get("model") instanceof Map model - && model.get("name") instanceof String name) { - return modelName.equals(name); - } - return false; - } -} diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java index 1afb106f..430f5b48 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java @@ -7,7 +7,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static com.sap.ai.sdk.core.Core.getClient; import static org.assertj.core.api.Assertions.assertThat; import com.sap.ai.sdk.core.client.model.AiArtifact; @@ -50,8 +49,7 @@ void getArtifacts() { } """))); - final AiArtifactList artifactList = - new ArtifactApi(getClient(destination)).artifactQuery("default"); + final AiArtifactList artifactList = new ArtifactApi(client).artifactQuery("default"); assertThat(artifactList).isNotNull(); assertThat(artifactList.getCount()).isEqualTo(1); @@ -96,7 +94,7 @@ void postArtifact() { .scenarioId("foundation-models") .description("dataset for aicore training"); final AiArtifactCreationResponse artifact = - new ArtifactApi(getClient(destination)).artifactCreate("default", artifactPostData); + new ArtifactApi(client).artifactCreate("default", artifactPostData); assertThat(artifact).isNotNull(); assertThat(artifact.getId()).isEqualTo("1a84bb38-4a84-4d12-a5aa-300ae7d33fb4"); @@ -143,8 +141,7 @@ void getArtifactById() { """))); final AiArtifact artifact = - new ArtifactApi(getClient(destination)) - .artifactGet("default", "777dea85-e9b1-4a7b-9bea-14769b977633"); + new ArtifactApi(client).artifactGet("default", "777dea85-e9b1-4a7b-9bea-14769b977633"); assertThat(artifact).isNotNull(); assertThat(artifact.getCreatedAt()).isEqualTo("2024-08-23T09:13:21Z"); @@ -171,7 +168,7 @@ void getArtifactCount() { 4 """))); - final int count = new ArtifactApi(getClient(destination)).artifactCount("default"); + final int count = new ArtifactApi(client).artifactCount("default"); assertThat(count).isEqualTo(4); } diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java index 88b7b846..61c674e0 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java @@ -7,7 +7,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static com.sap.ai.sdk.core.Core.getClient; import static org.assertj.core.api.Assertions.assertThat; import com.sap.ai.sdk.core.client.model.AiArtifactArgumentBinding; @@ -61,7 +60,7 @@ void getConfigurations() { """))); final AiConfigurationList configurationList = - new ConfigurationApi(getClient(destination)).configurationQuery("default"); + new ConfigurationApi(client).configurationQuery("default"); assertThat(configurationList).isNotNull(); assertThat(configurationList.getCount()).isEqualTo(1); @@ -107,8 +106,7 @@ void postConfiguration() { .scenarioId("foundation-models") .addInputArtifactBindingsItem(inputArtifactBindingsItem); final AiConfigurationCreationResponse configuration = - new ConfigurationApi(getClient(destination)) - .configurationCreate("default", configurationBaseData); + new ConfigurationApi(client).configurationCreate("default", configurationBaseData); assertThat(configuration).isNotNull(); assertThat(configuration.getId()).isEqualTo("f88e7581-ade7-45c6-94e9-807889b523ec"); @@ -148,8 +146,7 @@ void getConfigurationCount() { 3 """))); - final int configurationCount = - new ConfigurationApi(getClient(destination)).configurationCount("default"); + final int configurationCount = new ConfigurationApi(client).configurationCount("default"); assertThat(configurationCount).isEqualTo(3); } @@ -187,7 +184,7 @@ void getConfigurationById() { """))); final AiConfiguration configuration = - new ConfigurationApi(getClient(destination)) + new ConfigurationApi(client) .configurationGet("default", "6ff6cb80-87db-45f0-b718-4e1d96e66332"); assertThat(configuration).isNotNull(); diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java index acb0020c..c6f3402e 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java @@ -10,7 +10,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static com.sap.ai.sdk.core.Core.getClient; import static org.assertj.core.api.Assertions.assertThat; import com.sap.ai.sdk.core.client.model.AiDeployment; @@ -86,8 +85,7 @@ void getDeployments() { } """))); - final AiDeploymentList deploymentList = - new DeploymentApi(getClient(destination)).deploymentQuery("default"); + final AiDeploymentList deploymentList = new DeploymentApi(client).deploymentQuery("default"); assertThat(deploymentList).isNotNull(); assertThat(deploymentList.getCount()).isEqualTo(1); @@ -141,8 +139,7 @@ void postDeployment() { AiDeploymentCreationRequest.create() .configurationId("7652a231-ba9b-4fcc-b473-2c355cb21b61"); final AiDeploymentCreationResponse deployment = - new DeploymentApi(getClient(destination)) - .deploymentCreate("default", deploymentCreationRequest); + new DeploymentApi(client).deploymentCreate("default", deploymentCreationRequest); assertThat(deployment).isNotNull(); assertThat(deployment.getDeploymentUrl()).isEmpty(); @@ -181,7 +178,7 @@ void patchDeploymentStatus() { final AiDeploymentModificationRequest configModification = AiDeploymentModificationRequest.create().targetStatus(AiDeploymentTargetStatus.STOPPED); final AiDeploymentModificationResponse deployment = - new DeploymentApi(getClient(destination)) + new DeploymentApi(client) .deploymentModify("default", "d19b998f347341aa", configModification); assertThat(deployment).isNotNull(); @@ -220,7 +217,7 @@ void deleteDeployment() { """))); final AiDeploymentDeletionResponse deployment = - new DeploymentApi(getClient(destination)).deploymentDelete("default", "d5b764fe55b3e87c"); + new DeploymentApi(client).deploymentDelete("default", "d5b764fe55b3e87c"); assertThat(deployment).isNotNull(); assertThat(deployment.getId()).isEqualTo("d5b764fe55b3e87c"); @@ -265,7 +262,7 @@ void getDeploymentById() { """))); final AiDeploymentResponseWithDetails deployment = - new DeploymentApi(getClient(destination)).deploymentGet("default", "db1d64d9f06be467"); + new DeploymentApi(client).deploymentGet("default", "db1d64d9f06be467"); assertThat(deployment).isNotNull(); assertThat(deployment.getConfigurationId()).isEqualTo("dd80625e-ad86-426a-b1a7-1494c083428f"); @@ -312,7 +309,7 @@ void patchDeploymentConfiguration() { AiDeploymentModificationRequest.create() .configurationId("6ff6cb80-87db-45f0-b718-4e1d96e66332"); final AiDeploymentModificationResponse deployment = - new DeploymentApi(getClient(destination)) + new DeploymentApi(client) .deploymentModify("default", "d03050a2ab7055cc", configModification); assertThat(deployment).isNotNull(); @@ -345,7 +342,7 @@ void getDeploymentCount() { 1 """))); - final int count = new DeploymentApi(getClient(destination)).deploymentCount("default"); + final int count = new DeploymentApi(client).deploymentCount("default"); assertThat(count).isEqualTo(1); } @@ -378,7 +375,7 @@ void getDeploymentLogs() { // `Ai-Resource-Group` header needs explicit inclusion as kubesubmitV4DeploymentsGetLogs missed // to include the header on the request. final RTALogCommonResponse logs = - new DeploymentApi(getClient(destination).addDefaultHeader("Ai-Resource-Group", "default")) + new DeploymentApi(client.addDefaultHeader("Ai-Resource-Group", "default")) .kubesubmitV4DeploymentsGetLogs("d19b998f347341aa"); assertThat(logs).isNotNull(); @@ -428,8 +425,7 @@ void patchBulkDeployments() { AiDeploymentModificationRequestWithIdentifier.TargetStatusEnum .STOPPED))); final AiDeploymentBulkModificationResponse bulkModificationResponse = - new DeploymentApi(getClient(destination)) - .deploymentBatchModify("default", bulkModificationRequest); + new DeploymentApi(client).deploymentBatchModify("default", bulkModificationRequest); assertThat(bulkModificationResponse).isNotNull(); assertThat(bulkModificationResponse.getDeployments()).hasSize(1); diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java index b9375ef9..9d1c174f 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java @@ -10,7 +10,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static com.sap.ai.sdk.core.Core.getClient; import static org.assertj.core.api.Assertions.assertThat; import com.sap.ai.sdk.core.client.model.AiArtifact; @@ -83,8 +82,7 @@ void getExecutions() { } """))); - final AiExecutionList executionList = - new ExecutionApi(getClient(destination)).executionQuery("default"); + final AiExecutionList executionList = new ExecutionApi(client).executionQuery("default"); assertThat(executionList).isNotNull(); assertThat(executionList.getCount()).isEqualTo(1); @@ -139,8 +137,7 @@ void postExecution() { final AiEnactmentCreationRequest enactmentCreationRequest = AiEnactmentCreationRequest.create().configurationId("e0a9eb2e-9ea1-43bf-aff5-7660db166676"); final AiExecutionCreationResponse execution = - new ExecutionApi(getClient(destination)) - .executionCreate("default", enactmentCreationRequest); + new ExecutionApi(client).executionCreate("default", enactmentCreationRequest); assertThat(execution).isNotNull(); assertThat(execution.getId()).isEqualTo("eab289226fe981da"); @@ -200,7 +197,7 @@ void getExecutionById() { """))); final AiExecutionResponseWithDetails execution = - new ExecutionApi(getClient(destination)).executionGet("default", "e529e8bd58740bc9"); + new ExecutionApi(client).executionGet("default", "e529e8bd58740bc9"); assertThat(execution).isNotNull(); assertThat(execution.getCompletionTime()).isEqualTo("2024-09-09T19:10:58Z"); @@ -251,7 +248,7 @@ void deleteExecution() { """))); final AiExecutionDeletionResponse execution = - new ExecutionApi(getClient(destination)).executionDelete("default", "e529e8bd58740bc9"); + new ExecutionApi(client).executionDelete("default", "e529e8bd58740bc9"); assertThat(execution).isNotNull(); assertThat(execution.getId()).isEqualTo("e529e8bd58740bc9"); @@ -281,7 +278,7 @@ void patchExecution() { AiExecutionModificationRequest.create() .targetStatus(AiExecutionModificationRequest.TargetStatusEnum.STOPPED); final AiExecutionModificationResponse aiExecutionModificationResponse = - new ExecutionApi(getClient(destination)) + new ExecutionApi(client) .executionModify("default", "eec3c6ea18bac6da", aiExecutionModificationRequest); assertThat(aiExecutionModificationResponse).isNotNull(); @@ -308,7 +305,7 @@ void getExecutionCount() { 1 """))); - final int count = new ExecutionApi(getClient(destination)).executionCount("default"); + final int count = new ExecutionApi(client).executionCount("default"); assertThat(count).isEqualTo(1); } @@ -339,7 +336,7 @@ void getExecutionLogs() { """))); final RTALogCommonResponse logResponse = - new ExecutionApi(getClient(destination).addDefaultHeader("AI-Resource-Group", "default")) + new ExecutionApi(client.addDefaultHeader("AI-Resource-Group", "default")) .kubesubmitV4ExecutionsGetLogs("ee467bea5af28adb"); assertThat(logResponse).isNotNull(); @@ -388,8 +385,7 @@ void patchBulkExecutions() { AiExecutionModificationRequestWithIdentifier.TargetStatusEnum .STOPPED))); final AiExecutionBulkModificationResponse executionBulkModificationResponse = - new ExecutionApi(getClient(destination)) - .executionBatchModify("default", executionBulkModificationRequest); + new ExecutionApi(client).executionBatchModify("default", executionBulkModificationRequest); assertThat(executionBulkModificationResponse).isNotNull(); assertThat(executionBulkModificationResponse.getExecutions().size()).isEqualTo(1); diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ScenarioUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ScenarioUnitTest.java index 8dddcffe..7c85bd7c 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ScenarioUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ScenarioUnitTest.java @@ -4,7 +4,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import static com.sap.ai.sdk.core.Core.getClient; import static org.assertj.core.api.Assertions.assertThat; import com.sap.ai.sdk.core.client.model.AiModelBaseData; @@ -54,8 +53,7 @@ void getScenarios() { } """))); - final AiScenarioList scenarioList = - new ScenarioApi(getClient(destination)).scenarioQuery("default"); + final AiScenarioList scenarioList = new ScenarioApi(client).scenarioQuery("default"); assertThat(scenarioList).isNotNull(); assertThat(scenarioList.getCount()).isEqualTo(1); @@ -95,8 +93,7 @@ void getScenarioVersions() { """))); final AiVersionList versionList = - new ScenarioApi(getClient(destination)) - .scenarioQueryVersions("default", "foundation-models"); + new ScenarioApi(client).scenarioQueryVersions("default", "foundation-models"); assertThat(versionList).isNotNull(); assertThat(versionList.getCount()).isEqualTo(1); @@ -136,8 +133,7 @@ void getScenarioById() { } """))); - final AiScenario scenario = - new ScenarioApi(getClient(destination)).scenarioGet("default", "foundation-models"); + final AiScenario scenario = new ScenarioApi(client).scenarioGet("default", "foundation-models"); assertThat(scenario).isNotNull(); assertThat(scenario.getCreatedAt()).isEqualTo("2023-11-03T14:02:46+00:00"); @@ -183,7 +179,7 @@ void getScenarioModels() { """))); final AiModelList scenarioList = - new ScenarioApi(getClient(destination)).modelsGet("foundation-models", "default"); + new ScenarioApi(client).modelsGet("foundation-models", "default"); assertThat(scenarioList).isNotNull(); assertThat(scenarioList.getCount()).isEqualTo(1); diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java b/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java index 816e1f02..cd3cc5e3 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java @@ -4,8 +4,10 @@ import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.sap.ai.sdk.core.AiClient; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; +import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -16,12 +18,14 @@ abstract class WireMockTestServer { static WireMockServer wireMockServer; static Destination destination; + static ApiClient client; @BeforeAll static void setup() { wireMockServer = new WireMockServer(WIREMOCK_CONFIGURATION); wireMockServer.start(); destination = DefaultHttpDestination.builder(wireMockServer.baseUrl()).build(); + client = AiClient.custom().withDestination(destination).client(); } // Reset WireMock before each test to ensure clean state diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java index 5ab7554f..64d22db4 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java @@ -1,14 +1,12 @@ package com.sap.ai.sdk.foundationmodels.openai; -import static com.sap.ai.sdk.core.DeploymentChoice.withModel; - import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.sap.ai.sdk.core.Core; +import com.sap.ai.sdk.core.AiClient; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionDelta; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionOutput; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionParameters; @@ -61,10 +59,10 @@ public final class OpenAiClient { @Nonnull public static OpenAiClient forModel(@Nonnull final OpenAiModel foundationModel) { final var destination = - Core.getInstance() - .forDeployment(withModel(foundationModel.model())) + AiClient.custom() + .forDeploymentByModel(foundationModel.model()) .resourceGroup("default") - .getDestination(); + .destination(); final var client = new OpenAiClient(destination); return client.withApiVersion(DEFAULT_API_VERSION); } diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java index e9a2f79b..47be6ba4 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java @@ -8,7 +8,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.verify; -import static com.sap.ai.sdk.core.Core.getClient; import static com.sap.ai.sdk.orchestration.client.model.AzureThreshold.NUMBER_0; import static com.sap.ai.sdk.orchestration.client.model.AzureThreshold.NUMBER_4; import static org.apache.hc.core5.http.HttpStatus.SC_BAD_REQUEST; @@ -17,6 +16,7 @@ import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import com.sap.ai.sdk.core.AiClient; import com.sap.ai.sdk.orchestration.client.model.AzureContentSafety; import com.sap.ai.sdk.orchestration.client.model.AzureThreshold; import com.sap.ai.sdk.orchestration.client.model.ChatMessage; @@ -112,7 +112,8 @@ public class OrchestrationUnitTest { void setup(WireMockRuntimeInfo server) { final DefaultHttpDestination destination = DefaultHttpDestination.builder(server.getHttpBaseUrl()).build(); - client = new OrchestrationCompletionApi(getClient(destination)); + final var apiClient = AiClient.custom().withDestination(destination).client(); + client = new OrchestrationCompletionApi(apiClient); } @Test diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java index c003df6e..9b773bf5 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java @@ -1,6 +1,6 @@ package com.sap.ai.sdk.app.controllers; -import com.sap.ai.sdk.core.Core; +import com.sap.ai.sdk.core.AiClient; import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.ai.sdk.core.client.model.AiDeploymentCreationRequest; @@ -24,7 +24,7 @@ @RequestMapping("/deployments") class DeploymentController { - private static final DeploymentApi API = new DeploymentApi(Core.getInstance().getClient()); + private static final DeploymentApi API = new DeploymentApi(AiClient.custom().client()); /** * Create and delete a deployment with the Java specific configuration ID diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java index 134b4dd5..cf95572f 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java @@ -1,8 +1,6 @@ package com.sap.ai.sdk.app.controllers; -import static com.sap.ai.sdk.core.DeploymentChoice.ORCHESTRATION; - -import com.sap.ai.sdk.core.Core; +import com.sap.ai.sdk.core.AiClient; import com.sap.ai.sdk.orchestration.client.OrchestrationCompletionApi; import com.sap.ai.sdk.orchestration.client.model.AzureContentSafety; import com.sap.ai.sdk.orchestration.client.model.AzureThreshold; @@ -33,7 +31,10 @@ class OrchestrationController { private static final OrchestrationCompletionApi API = new OrchestrationCompletionApi( - Core.getInstance().forDeployment(ORCHESTRATION).resourceGroup("default").getClient()); + AiClient.custom() + .forDeploymentByScenario("orchestration") + .resourceGroup("default") + .client()); static final String MODEL = "gpt-35-turbo"; diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ScenarioController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ScenarioController.java index a3f2e287..359cc857 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ScenarioController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ScenarioController.java @@ -1,6 +1,6 @@ package com.sap.ai.sdk.app.controllers; -import com.sap.ai.sdk.core.Core; +import com.sap.ai.sdk.core.AiClient; import com.sap.ai.sdk.core.client.ScenarioApi; import com.sap.ai.sdk.core.client.model.AiModelList; import com.sap.ai.sdk.core.client.model.AiScenarioList; @@ -13,7 +13,7 @@ @SuppressWarnings("unused") // debug method that doesn't need to be tested public class ScenarioController { - private static final ScenarioApi API = new ScenarioApi(Core.getInstance().getClient()); + private static final ScenarioApi API = new ScenarioApi(AiClient.auto().client()); /** * Get the list of available scenarios From 6830b717e6575433fb60ad2ba49589d72581edd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 1 Oct 2024 11:02:57 +0200 Subject: [PATCH 17/79] Initial API change --- .../java/com/sap/ai/sdk/core/AiClient.java | 26 --- .../com/sap/ai/sdk/core/AiClientCustom.java | 193 ------------------ .../com/sap/ai/sdk/core/AiCoreService.java | 88 ++++++++ ...ClientAuto.java => AiCoreServiceStub.java} | 2 +- .../sdk/core/AiCoreServiceWithDeployment.java | 173 ++++++++++++++++ .../sdk/core/client/WireMockTestServer.java | 4 +- .../foundationmodels/openai/OpenAiClient.java | 8 +- .../client/OrchestrationUnitTest.java | 4 +- .../app/controllers/DeploymentController.java | 4 +- .../controllers/OrchestrationController.java | 8 +- .../app/controllers/ScenarioController.java | 4 +- .../controllers/ScenarioControllerTest.java | 40 ++++ 12 files changed, 318 insertions(+), 236 deletions(-) delete mode 100644 core/src/main/java/com/sap/ai/sdk/core/AiClient.java delete mode 100644 core/src/main/java/com/sap/ai/sdk/core/AiClientCustom.java create mode 100644 core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java rename core/src/main/java/com/sap/ai/sdk/core/{AiClientAuto.java => AiCoreServiceStub.java} (98%) create mode 100644 core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java create mode 100644 sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/ScenarioControllerTest.java diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiClient.java b/core/src/main/java/com/sap/ai/sdk/core/AiClient.java deleted file mode 100644 index 4597b647..00000000 --- a/core/src/main/java/com/sap/ai/sdk/core/AiClient.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.sap.ai.sdk.core; - -import lombok.extern.slf4j.Slf4j; - -/** Connectivity convenience methods for AI Core. */ -@Slf4j -public class AiClient { - - /** - * Get a destination pointing to the AI Core service. - * - *

      Requires an AI Core service binding OR a service key in the environment variable {@code - * AICORE_SERVICE_KEY}. - * - * @return a destination pointing to the AI Core service. - */ - public static AiClientAuto auto() { - final var serviceKey = System.getenv("AICORE_SERVICE_KEY"); - return () -> DestinationResolver.getDestination(serviceKey); - } - - public static AiClientCustom custom() { - final var serviceKey = System.getenv("AICORE_SERVICE_KEY"); - return () -> DestinationResolver.getDestination(serviceKey); - } -} diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiClientCustom.java b/core/src/main/java/com/sap/ai/sdk/core/AiClientCustom.java deleted file mode 100644 index 9100724a..00000000 --- a/core/src/main/java/com/sap/ai/sdk/core/AiClientCustom.java +++ /dev/null @@ -1,193 +0,0 @@ -package com.sap.ai.sdk.core; - -import static com.sap.ai.sdk.core.AiClientCustom.DestinationProcessor.ADD_HEADER; -import static com.sap.ai.sdk.core.AiClientCustom.DestinationProcessor.UPDATE_URI; - -import com.sap.ai.sdk.core.client.DeploymentApi; -import com.sap.ai.sdk.core.client.model.AiDeployment; -import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; -import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; -import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.function.BiFunction; -import java.util.function.Predicate; -import javax.annotation.Nonnull; -import lombok.Value; - -/** Utility class. */ -@FunctionalInterface -public interface AiClientCustom extends AiClientAuto { - - /** - * Get an API client resolver for destination. - * - * @param destination the destination. - * @return the API client resolver. - */ - @Nonnull - default AiClientCustom withDestination(@Nonnull final Destination destination) { - return () -> destination; - } - - default ForDeployment forDeployment(@Nonnull final String deploymentId) { - return getDestinationForDeployment((resolver, resourceGroup) -> deploymentId); - } - - default ForDeployment forDeploymentByModel(@Nonnull final String modelName) { - final Predicate p = deployment -> isDeploymentOfModel(modelName, deployment); - return getDestinationForDeployment((r, resourceGroup) -> getDeploymentId(r, resourceGroup, p)); - } - - default ForDeployment forDeploymentByScenario(@Nonnull final String scenarioId) { - final Predicate p = deployment -> scenarioId.equals(deployment.getScenarioId()); - return getDestinationForDeployment((r, resourceGroup) -> getDeploymentId(r, resourceGroup, p)); - } - - /** - * Focus on a specific deployment. - * - * @param deploymentIdResolver the deployment choice. - * @return the deployment context. - */ - private ForDeployment getDestinationForDeployment( - @Nonnull final BiFunction deploymentIdResolver) { - return processors -> - resourceGroup -> - () -> { - final var id = deploymentIdResolver.apply(this, resourceGroup); - final var destination = this.destination().asHttp(); - final var builder = DefaultHttpDestination.fromDestination(destination); - final var context = new DestinationProcessor.Context(destination, id, resourceGroup); - for (final DestinationProcessor processor : processors) { - processor.process(builder, context); - } - return builder.build(); - }; - } - - /** Helper interface to focus on a specific deployment. */ - @FunctionalInterface - interface ForDeployment { - /** - * Focus on a specific resource group. - * - * @param resourceGroup the resource group. - * @return the client and destination builder. - */ - @Nonnull - default AiClientAuto resourceGroup(@Nonnull final String resourceGroup) { - return withProcessors(UPDATE_URI, ADD_HEADER).resourceGroup(resourceGroup); - } - - /** - * Assign destination processors. - * - * @param processors the processors. - * @return the client and destination builder. - */ - @Nonnull - WithProcessors withProcessors(@Nonnull final DestinationProcessor... processors); - } - - /** Helper interface to focus on a specific resource group. */ - @FunctionalInterface - interface WithProcessors { - /** - * Focus on a specific resource group. - * - * @param resourceGroup the resource group. - * @return the client and destination builder. - */ - @Nonnull - AiClientAuto resourceGroup(@Nonnull final String resourceGroup); - } - - /** Helper interface to process the destination. */ - @FunctionalInterface - interface DestinationProcessor { - - /** Update the URI. */ - DestinationProcessor UPDATE_URI = - (builder, context) -> - builder.uri( - context - .origin - .getUri() - .resolve("/v2/inference/deployments/%s/".formatted(context.deploymentId))); - - /** Add the AI-Resource-Group header. */ - DestinationProcessor ADD_HEADER = - (builder, context) -> builder.header("AI-Resource-Group", context.resourceGroup); - - /** - * Process the destination. - * - * @param builder the destination builder. - * @param context the context. - */ - void process( - @Nonnull final DefaultHttpDestination.Builder builder, @Nonnull final Context context); - - /** Helper class to hold the context. */ - @Value - class Context { - HttpDestination origin; - String deploymentId; - String resourceGroup; - } - } - - /** This exists because getBackendDetails() is broken */ - private static boolean isDeploymentOfModel( - @Nonnull final String modelName, @Nonnull final AiDeployment deployment) { - final var deploymentDetails = deployment.getDetails(); - // The AI Core specification doesn't mention that this is nullable, but it can be. - // Remove this check when the specification is fixed. - if (deploymentDetails == null) { - return false; - } - final var resources = deploymentDetails.getResources(); - if (resources == null) { - return false; - } - Object detailsObject = resources.getBackendDetails(); - // workaround for AIWDF-2124 - if (detailsObject == null) { - if (!resources.getCustomFieldNames().contains("backend_details")) { - return false; - } - detailsObject = resources.getCustomField("backend_details"); - } - - if (detailsObject instanceof Map details - && details.get("model") instanceof Map model - && model.get("name") instanceof String name) { - return modelName.equals(name); - } - return false; - } - - /** - * Get the deployment id from the scenario id. If there are multiple deployments of the same - * scenario id, the first one is returned. - * - * @param resourceGroup the resource group. - * @return the deployment id - * @throws NoSuchElementException if no deployment is found for the scenario id. - */ - @Nonnull - private static String getDeploymentId( - @Nonnull final AiClientCustom resolver, - @Nonnull final String resourceGroup, - @Nonnull final Predicate predicate) - throws NoSuchElementException { - final var deploymentService = new DeploymentApi(resolver.client()); - final var deployments = deploymentService.deploymentQuery(resourceGroup); - - final var first = - deployments.getResources().stream().filter(predicate).map(AiDeployment::getId).findFirst(); - return first.orElseThrow( - () -> new NoSuchElementException("No deployment found with scenario id orchestration")); - } -} diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java new file mode 100644 index 00000000..85140478 --- /dev/null +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -0,0 +1,88 @@ +package com.sap.ai.sdk.core; + +import static com.sap.ai.sdk.core.AiCoreServiceWithDeployment.getDeploymentId; +import static com.sap.ai.sdk.core.AiCoreServiceWithDeployment.isDeploymentOfModel; + +import com.sap.ai.sdk.core.client.model.AiDeployment; +import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; +import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException; +import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationNotFoundException; +import java.util.function.Predicate; +import java.util.function.Supplier; +import javax.annotation.Nonnull; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** Connectivity convenience methods for AI Core. */ +@Slf4j +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +public class AiCoreService implements AiCoreServiceStub { + + // the destination to be used for AI Core service calls. + @Nonnull private final Supplier destination; + + /** Create a new instance of the AI Core service. */ + public AiCoreService() { + this(AiCoreService::getDefaultDestination); + } + + @Nonnull + @Override + public Destination destination() { + return destination.get(); + } + + /** + * 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. + */ + @Nonnull + public AiCoreService withDestination(@Nonnull final Destination destination) { + return new AiCoreService(() -> destination); + } + + /** + * 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 service. + */ + @Nonnull + public AiCoreServiceWithDeployment withDeployment(@Nonnull final String deploymentId) { + return new AiCoreServiceWithDeployment(c -> deploymentId, c -> this.destination()); + } + + /** + * Set a specific deployment by model name. + * + * @param modelName The model name to be used for AI Core service calls. + * @return A new instance of the AI Core service. + */ + @Nonnull + public AiCoreServiceWithDeployment withDeploymentByModel(@Nonnull final String modelName) { + final Predicate p = deployment -> isDeploymentOfModel(modelName, deployment); + return new AiCoreServiceWithDeployment(c -> getDeploymentId(c, p), c -> this.destination()); + } + + /** + * Set a specific deployment by scenario id. + * + * @param scenarioId The scenario id to be used for AI Core service calls. + * @return A new instance of the AI Core service. + */ + @Nonnull + public AiCoreServiceWithDeployment withDeploymentByScenario(@Nonnull final String scenarioId) { + final Predicate p = deployment -> scenarioId.equals(deployment.getScenarioId()); + return new AiCoreServiceWithDeployment(c -> getDeploymentId(c, p), c -> this.destination()); + } + + @Nonnull + private static Destination getDefaultDestination() + throws DestinationAccessException, DestinationNotFoundException { + final var serviceKey = System.getenv("AICORE_SERVICE_KEY"); + return DestinationResolver.getDestination(serviceKey); + } +} diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiClientAuto.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceStub.java similarity index 98% rename from core/src/main/java/com/sap/ai/sdk/core/AiClientAuto.java rename to core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceStub.java index 28538053..bf7e9c12 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiClientAuto.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceStub.java @@ -18,7 +18,7 @@ /** Container for an API client and destination. */ @FunctionalInterface -public interface AiClientAuto { +public interface AiCoreServiceStub { /** * Get the destination. * diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java new file mode 100644 index 00000000..dcb27629 --- /dev/null +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java @@ -0,0 +1,173 @@ +package com.sap.ai.sdk.core; + +import com.sap.ai.sdk.core.client.DeploymentApi; +import com.sap.ai.sdk.core.client.model.AiDeployment; +import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; +import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; +import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.function.Function; +import java.util.function.Predicate; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +/** Connectivity convenience methods for AI Core with deployment. */ +@RequiredArgsConstructor(access = AccessLevel.PUBLIC) +public class AiCoreServiceWithDeployment implements AiCoreServiceStub { + + // the deployment id to be used + @Nonnull private final Function deploymentId; + + // the base destination to be used + @Nonnull private final Function destination; + + // the resource group, "default" if null + @Nullable private final Function resourceGroup; + + /** + * Create a new instance of the AI Core service with a specific deployment id and destination. + * + * @param deploymentId The deployment id handler. + * @param destination The destination handler. + */ + public AiCoreServiceWithDeployment( + @Nonnull final Function deploymentId, + @Nonnull final Function destination) { + this(deploymentId, destination, null); + } + + @Nonnull + @Override + public Destination destination() { + final var dest = destination.apply(this).asHttp(); + final var builder = DefaultHttpDestination.fromDestination(dest); + updateDestination(builder, dest); + return builder.build(); + } + + /** + * Set the resource group. + * + * @param resourceGroup The resource group. + * @return A new instance of the AI Core service. + */ + @Nonnull + public AiCoreServiceWithDeployment withResourceGroup(@Nonnull final String resourceGroup) { + return withResourceGroup((core) -> resourceGroup); + } + + /** + * Set the resource group. + * + * @param resourceGroup The resource group handler. + * @return A new instance of the AI Core service. + */ + public AiCoreServiceWithDeployment withResourceGroup( + Function resourceGroup) { + return new AiCoreServiceWithDeployment(deploymentId, destination, resourceGroup); + } + + /** + * Set the destination. + * + * @param destination The destination. + * @return A new instance of the AI Core service. + */ + public AiCoreServiceWithDeployment withDestination(Destination destination) { + return withDestination((core) -> destination); + } + + /** + * Set the destination. + * + * @param destination The destination handler. + * @return A new instance of the AI Core service. + */ + public AiCoreServiceWithDeployment withDestination( + Function destination) { + return new AiCoreServiceWithDeployment(deploymentId, destination, resourceGroup); + } + + /** + * Update the destination builder. + * + * @param builder The new destination builder. + * @param d The original destination. + */ + protected void updateDestination(DefaultHttpDestination.Builder builder, HttpDestination d) { + builder.uri(d.getUri().resolve("/v2/inference/deployments/%s/".formatted(getDeploymentId()))); + builder.header("AI-Resource-Group", getResourceGroup()); + } + + /** + * Get the resource group. + * + * @return The resource group. + */ + protected String getResourceGroup() { + return resourceGroup == null ? "default" : resourceGroup.apply(this); + } + + /** + * Get the deployment id. + * + * @return The deployment id. + */ + protected String getDeploymentId() { + return deploymentId.apply(this); + } + + /** This exists because getBackendDetails() is broken */ + static boolean isDeploymentOfModel( + @Nonnull final String modelName, @Nonnull final AiDeployment deployment) { + final var deploymentDetails = deployment.getDetails(); + // The AI Core specification doesn't mention that this is nullable, but it can be. + // Remove this check when the specification is fixed. + if (deploymentDetails == null) { + return false; + } + final var resources = deploymentDetails.getResources(); + if (resources == null) { + return false; + } + Object detailsObject = resources.getBackendDetails(); + // workaround for AIWDF-2124 + if (detailsObject == null) { + if (!resources.getCustomFieldNames().contains("backend_details")) { + return false; + } + detailsObject = resources.getCustomField("backend_details"); + } + + if (detailsObject instanceof Map details + && details.get("model") instanceof Map model + && model.get("name") instanceof String name) { + return modelName.equals(name); + } + return false; + } + + /** + * Get the deployment id from the scenario id. If there are multiple deployments of the same + * scenario id, the first one is returned. + * + * @return the deployment id + * @throws NoSuchElementException if no deployment is found for the scenario id. + */ + @Nonnull + static String getDeploymentId( + @Nonnull final AiCoreServiceWithDeployment core, + @Nonnull final Predicate predicate) + throws NoSuchElementException { + final var deploymentService = new DeploymentApi(core.client()); + final var deployments = deploymentService.deploymentQuery(core.getDeploymentId()); + + final var first = + deployments.getResources().stream().filter(predicate).map(AiDeployment::getId).findFirst(); + return first.orElseThrow( + () -> new NoSuchElementException("No deployment found with scenario id orchestration")); + } +} diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java b/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java index cd3cc5e3..08e5a89d 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java @@ -4,7 +4,7 @@ import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; -import com.sap.ai.sdk.core.AiClient; +import com.sap.ai.sdk.core.AiCoreService; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; @@ -25,7 +25,7 @@ static void setup() { wireMockServer = new WireMockServer(WIREMOCK_CONFIGURATION); wireMockServer.start(); destination = DefaultHttpDestination.builder(wireMockServer.baseUrl()).build(); - client = AiClient.custom().withDestination(destination).client(); + client = new AiCoreService().withDestination(destination).client(); } // Reset WireMock before each test to ensure clean state diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java index 64d22db4..24d3f6de 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java @@ -6,7 +6,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.sap.ai.sdk.core.AiClient; +import com.sap.ai.sdk.core.AiCoreService; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionDelta; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionOutput; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionParameters; @@ -59,9 +59,9 @@ public final class OpenAiClient { @Nonnull public static OpenAiClient forModel(@Nonnull final OpenAiModel foundationModel) { final var destination = - AiClient.custom() - .forDeploymentByModel(foundationModel.model()) - .resourceGroup("default") + new AiCoreService() + .withDeploymentByModel(foundationModel.model()) + .withResourceGroup("default") .destination(); final var client = new OpenAiClient(destination); return client.withApiVersion(DEFAULT_API_VERSION); diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java index 47be6ba4..dce3bd67 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java @@ -16,7 +16,7 @@ import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import com.sap.ai.sdk.core.AiClient; +import com.sap.ai.sdk.core.AiCoreService; import com.sap.ai.sdk.orchestration.client.model.AzureContentSafety; import com.sap.ai.sdk.orchestration.client.model.AzureThreshold; import com.sap.ai.sdk.orchestration.client.model.ChatMessage; @@ -112,7 +112,7 @@ public class OrchestrationUnitTest { void setup(WireMockRuntimeInfo server) { final DefaultHttpDestination destination = DefaultHttpDestination.builder(server.getHttpBaseUrl()).build(); - final var apiClient = AiClient.custom().withDestination(destination).client(); + final var apiClient = new AiCoreService().withDestination(destination).client(); client = new OrchestrationCompletionApi(apiClient); } diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java index 9b773bf5..9c53ccbf 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java @@ -1,6 +1,6 @@ package com.sap.ai.sdk.app.controllers; -import com.sap.ai.sdk.core.AiClient; +import com.sap.ai.sdk.core.AiCoreService; import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.ai.sdk.core.client.model.AiDeploymentCreationRequest; @@ -24,7 +24,7 @@ @RequestMapping("/deployments") class DeploymentController { - private static final DeploymentApi API = new DeploymentApi(AiClient.custom().client()); + private static final DeploymentApi API = new DeploymentApi(new AiCoreService().client()); /** * Create and delete a deployment with the Java specific configuration ID diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java index cf95572f..07f2d8cf 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java @@ -1,6 +1,6 @@ package com.sap.ai.sdk.app.controllers; -import com.sap.ai.sdk.core.AiClient; +import com.sap.ai.sdk.core.AiCoreService; import com.sap.ai.sdk.orchestration.client.OrchestrationCompletionApi; import com.sap.ai.sdk.orchestration.client.model.AzureContentSafety; import com.sap.ai.sdk.orchestration.client.model.AzureThreshold; @@ -31,9 +31,9 @@ class OrchestrationController { private static final OrchestrationCompletionApi API = new OrchestrationCompletionApi( - AiClient.custom() - .forDeploymentByScenario("orchestration") - .resourceGroup("default") + new AiCoreService() + .withDeploymentByScenario("orchestration") + .withResourceGroup("default") .client()); static final String MODEL = "gpt-35-turbo"; diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ScenarioController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ScenarioController.java index 359cc857..aaa298fe 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ScenarioController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ScenarioController.java @@ -1,6 +1,6 @@ package com.sap.ai.sdk.app.controllers; -import com.sap.ai.sdk.core.AiClient; +import com.sap.ai.sdk.core.AiCoreService; import com.sap.ai.sdk.core.client.ScenarioApi; import com.sap.ai.sdk.core.client.model.AiModelList; import com.sap.ai.sdk.core.client.model.AiScenarioList; @@ -13,7 +13,7 @@ @SuppressWarnings("unused") // debug method that doesn't need to be tested public class ScenarioController { - private static final ScenarioApi API = new ScenarioApi(AiClient.auto().client()); + private static final ScenarioApi API = new ScenarioApi(new AiCoreService().client()); /** * Get the list of available scenarios diff --git a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/ScenarioControllerTest.java b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/ScenarioControllerTest.java new file mode 100644 index 00000000..9eac2bed --- /dev/null +++ b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/ScenarioControllerTest.java @@ -0,0 +1,40 @@ +package com.sap.ai.sdk.app.controllers; + +import com.sap.ai.sdk.core.client.model.AiDeploymentDeletionResponse; +import com.sap.ai.sdk.core.client.model.AiModelBaseData; +import com.sap.ai.sdk.core.client.model.AiModelList; +import com.sap.ai.sdk.foundationmodels.openai.OpenAiModel; +import io.vavr.control.Try; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ScenarioControllerTest { + + @Test + void testOpenAiModels() { + + ScenarioController controller = new ScenarioController(); + AiModelList models = controller.getModels(); + List expectedModelNames = models.getResources() + .stream() + .filter(modelEntry -> modelEntry.getExecutableId().equals("azure-openai")) + .map(AiModelBaseData::getModel) + .toList(); + + Field[] declaredFields = OpenAiModel.class.getDeclaredFields(); + List existingModels = Arrays.stream(declaredFields) + .filter(field -> field.getType().equals(OpenAiModel.class)) + .map(field -> Try.of(() -> (OpenAiModel) field.get(null)).getOrNull()) + .filter(Objects::nonNull) + .map(OpenAiModel::model) + .toList(); + + assertThat(existingModels).hasSameElementsAs(expectedModelNames); + } +} From 94e8c74bdc583225ae9f79512e3c359d6e513344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 1 Oct 2024 11:12:07 +0200 Subject: [PATCH 18/79] Fix merge conflicts --- .../sdk/core/AiCoreServiceWithDeployment.java | 2 +- .../ai/sdk/core/client/ArtifactUnitTest.java | 3 +- .../core/client/ConfigurationUnitTest.java | 6 +-- .../sdk/core/client/DeploymentUnitTest.java | 6 +-- .../foundationmodels/openai/OpenAiClient.java | 2 +- .../controllers/ScenarioControllerTest.java | 40 ------------------- 6 files changed, 7 insertions(+), 52 deletions(-) delete mode 100644 sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/ScenarioControllerTest.java diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java index dcb27629..db250f7d 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java @@ -163,7 +163,7 @@ static String getDeploymentId( @Nonnull final Predicate predicate) throws NoSuchElementException { final var deploymentService = new DeploymentApi(core.client()); - final var deployments = deploymentService.deploymentQuery(core.getDeploymentId()); + final var deployments = deploymentService.query(core.getDeploymentId()); final var first = deployments.getResources().stream().filter(predicate).map(AiDeployment::getId).findFirst(); diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java index ed46ca4a..a4783ac4 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java @@ -141,8 +141,7 @@ void getArtifactById() { """))); final AiArtifact artifact = - new ArtifactApi(getClient(destination)) - .get("default", "777dea85-e9b1-4a7b-9bea-14769b977633"); + new ArtifactApi(client).get("default", "777dea85-e9b1-4a7b-9bea-14769b977633"); assertThat(artifact).isNotNull(); assertThat(artifact.getCreatedAt()).isEqualTo("2024-08-23T09:13:21Z"); diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java index 8ca418ab..6f419781 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java @@ -59,8 +59,7 @@ void getConfigurations() { } """))); - final AiConfigurationList configurationList = - new ConfigurationApi(client).query("default"); + final AiConfigurationList configurationList = new ConfigurationApi(client).query("default"); assertThat(configurationList).isNotNull(); assertThat(configurationList.getCount()).isEqualTo(1); @@ -184,8 +183,7 @@ void getConfigurationById() { """))); final AiConfiguration configuration = - new ConfigurationApi(client) - .get("default", "6ff6cb80-87db-45f0-b718-4e1d96e66332"); + new ConfigurationApi(client).get("default", "6ff6cb80-87db-45f0-b718-4e1d96e66332"); assertThat(configuration).isNotNull(); assertThat(configuration.getCreatedAt()).isEqualTo("2024-09-11T09:14:31Z"); diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java index 75e5504d..fda31895 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java @@ -178,8 +178,7 @@ void patchDeploymentStatus() { final AiDeploymentModificationRequest configModification = AiDeploymentModificationRequest.create().targetStatus(AiDeploymentTargetStatus.STOPPED); final AiDeploymentModificationResponse deployment = - new DeploymentApi(client) - .modify("default", "d19b998f347341aa", configModification); + new DeploymentApi(client).modify("default", "d19b998f347341aa", configModification); assertThat(deployment).isNotNull(); assertThat(deployment.getId()).isEqualTo("d5b764fe55b3e87c"); @@ -309,8 +308,7 @@ void patchDeploymentConfiguration() { AiDeploymentModificationRequest.create() .configurationId("6ff6cb80-87db-45f0-b718-4e1d96e66332"); final AiDeploymentModificationResponse deployment = - new DeploymentApi(client) - .modify("default", "d03050a2ab7055cc", configModification); + new DeploymentApi(client).modify("default", "d03050a2ab7055cc", configModification); assertThat(deployment).isNotNull(); assertThat(deployment.getId()).isEqualTo("d03050a2ab7055cc"); diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java index 24d3f6de..0f8fee34 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java @@ -59,7 +59,7 @@ public final class OpenAiClient { @Nonnull public static OpenAiClient forModel(@Nonnull final OpenAiModel foundationModel) { final var destination = - new AiCoreService() + new AiCoreService() .withDeploymentByModel(foundationModel.model()) .withResourceGroup("default") .destination(); diff --git a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/ScenarioControllerTest.java b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/ScenarioControllerTest.java deleted file mode 100644 index 9eac2bed..00000000 --- a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/ScenarioControllerTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.sap.ai.sdk.app.controllers; - -import com.sap.ai.sdk.core.client.model.AiDeploymentDeletionResponse; -import com.sap.ai.sdk.core.client.model.AiModelBaseData; -import com.sap.ai.sdk.core.client.model.AiModelList; -import com.sap.ai.sdk.foundationmodels.openai.OpenAiModel; -import io.vavr.control.Try; -import org.junit.jupiter.api.Test; - -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; - -import static org.assertj.core.api.Assertions.assertThat; - -public class ScenarioControllerTest { - - @Test - void testOpenAiModels() { - - ScenarioController controller = new ScenarioController(); - AiModelList models = controller.getModels(); - List expectedModelNames = models.getResources() - .stream() - .filter(modelEntry -> modelEntry.getExecutableId().equals("azure-openai")) - .map(AiModelBaseData::getModel) - .toList(); - - Field[] declaredFields = OpenAiModel.class.getDeclaredFields(); - List existingModels = Arrays.stream(declaredFields) - .filter(field -> field.getType().equals(OpenAiModel.class)) - .map(field -> Try.of(() -> (OpenAiModel) field.get(null)).getOrNull()) - .filter(Objects::nonNull) - .map(OpenAiModel::model) - .toList(); - - assertThat(existingModels).hasSameElementsAs(expectedModelNames); - } -} From 8d179bfa58d5a09e78e89aa571728fd60f90418c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 1 Oct 2024 11:13:38 +0200 Subject: [PATCH 19/79] Update readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e4e1e224..5a34eaeb 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ See [an example pom in our Spring Boot application](sample-code/spring-app/pom.x public AiDeploymentCreationResponse createDeployment() { final AiDeploymentCreationResponse deployment = - new DeploymentApi(getClient()) + new DeploymentApi(new AiCoreService().cllient()) .create( "default", AiDeploymentCreationRequest.create() @@ -95,7 +95,7 @@ See [an example in our Spring Boot application](sample-code/spring-app/src/main/ ```java public AiDeploymentDeletionResponse deleteDeployment(AiDeploymentCreationResponse deployment) { - DeploymentApi client = new DeploymentApi(getClient()); + DeploymentApi client = new DeploymentApi(new AiCoreService().cllient()); if (deployment.getStatus() == AiExecutionStatus.RUNNING) { // Only RUNNING deployments can be STOPPED @@ -479,7 +479,7 @@ See [an example in our unit test](orchestration/src/test/java/com/sap/ai/sdk/orc To add a header to AI Core requests, use the following code: ```java -ApiClient client = Core.getClient().addDefaultHeader("header-key", "header-value"); +ApiClient client = new AiCoreService().cllient().addDefaultHeader("header-key", "header-value"); DeploymentApi api = new DeploymentApi(client); ``` From d1e1291ab2480a69f2236f106c8c771749948fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 1 Oct 2024 11:34:28 +0200 Subject: [PATCH 20/79] Fix PMD --- .../java/com/sap/ai/sdk/core/AiCoreService.java | 9 ++++++++- .../ai/sdk/core/AiCoreServiceWithDeployment.java | 14 ++++++++++---- .../app/controllers/OrchestrationController.java | 12 ++++++------ 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index 85140478..9ab9b6bd 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -79,8 +79,15 @@ public AiCoreServiceWithDeployment withDeploymentByScenario(@Nonnull final Strin return new AiCoreServiceWithDeployment(c -> getDeploymentId(c, p), c -> this.destination()); } + /** + * 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 - private static Destination getDefaultDestination() + protected static Destination getDefaultDestination() throws DestinationAccessException, DestinationNotFoundException { final var serviceKey = System.getenv("AICORE_SERVICE_KEY"); return DestinationResolver.getDestination(serviceKey); diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java index db250f7d..6059565f 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java @@ -65,8 +65,9 @@ public AiCoreServiceWithDeployment withResourceGroup(@Nonnull final String resou * @param resourceGroup The resource group handler. * @return A new instance of the AI Core service. */ + @Nonnull public AiCoreServiceWithDeployment withResourceGroup( - Function resourceGroup) { + @Nonnull final Function resourceGroup) { return new AiCoreServiceWithDeployment(deploymentId, destination, resourceGroup); } @@ -76,7 +77,8 @@ public AiCoreServiceWithDeployment withResourceGroup( * @param destination The destination. * @return A new instance of the AI Core service. */ - public AiCoreServiceWithDeployment withDestination(Destination destination) { + @Nonnull + public AiCoreServiceWithDeployment withDestination(@Nonnull final Destination destination) { return withDestination((core) -> destination); } @@ -86,8 +88,9 @@ public AiCoreServiceWithDeployment withDestination(Destination destination) { * @param destination The destination handler. * @return A new instance of the AI Core service. */ + @Nonnull public AiCoreServiceWithDeployment withDestination( - Function destination) { + @Nonnull final Function destination) { return new AiCoreServiceWithDeployment(deploymentId, destination, resourceGroup); } @@ -97,7 +100,8 @@ public AiCoreServiceWithDeployment withDestination( * @param builder The new destination builder. * @param d The original destination. */ - protected void updateDestination(DefaultHttpDestination.Builder builder, HttpDestination d) { + protected void updateDestination( + @Nonnull final DefaultHttpDestination.Builder builder, @Nonnull final HttpDestination d) { builder.uri(d.getUri().resolve("/v2/inference/deployments/%s/".formatted(getDeploymentId()))); builder.header("AI-Resource-Group", getResourceGroup()); } @@ -107,6 +111,7 @@ protected void updateDestination(DefaultHttpDestination.Builder builder, HttpDes * * @return The resource group. */ + @Nonnull protected String getResourceGroup() { return resourceGroup == null ? "default" : resourceGroup.apply(this); } @@ -116,6 +121,7 @@ protected String getResourceGroup() { * * @return The deployment id. */ + @Nonnull protected String getDeploymentId() { return deploymentId.apply(this); } diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java index 07f2d8cf..4077f861 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java @@ -29,12 +29,12 @@ @RequestMapping("/orchestration") class OrchestrationController { - private static final OrchestrationCompletionApi API = - new OrchestrationCompletionApi( - new AiCoreService() - .withDeploymentByScenario("orchestration") - .withResourceGroup("default") - .client()); +private static final OrchestrationCompletionApi API = + new OrchestrationCompletionApi( + new AiCoreService() + .withDeploymentByScenario("orchestration") + .withResourceGroup("default") + .client()); static final String MODEL = "gpt-35-turbo"; From 5a1521c58302d56176ccdb400e560312fff607a4 Mon Sep 17 00:00:00 2001 From: SAP Cloud SDK Bot Date: Tue, 1 Oct 2024 09:35:09 +0000 Subject: [PATCH 21/79] Formatting --- .../sdk/app/controllers/OrchestrationController.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java index 4077f861..07f2d8cf 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java @@ -29,12 +29,12 @@ @RequestMapping("/orchestration") class OrchestrationController { -private static final OrchestrationCompletionApi API = - new OrchestrationCompletionApi( - new AiCoreService() - .withDeploymentByScenario("orchestration") - .withResourceGroup("default") - .client()); + private static final OrchestrationCompletionApi API = + new OrchestrationCompletionApi( + new AiCoreService() + .withDeploymentByScenario("orchestration") + .withResourceGroup("default") + .client()); static final String MODEL = "gpt-35-turbo"; From 8cbba6accb79f1b3567d5570b36728bb5df39ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= <22489773+newtork@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:25:47 +0200 Subject: [PATCH 22/79] Apply suggestions from code review Co-authored-by: Charles Dubois <103174266+CharlesDuboisSAP@users.noreply.github.com> --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5a34eaeb..a8ef699f 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ See [an example pom in our Spring Boot application](sample-code/spring-app/pom.x public AiDeploymentCreationResponse createDeployment() { final AiDeploymentCreationResponse deployment = - new DeploymentApi(new AiCoreService().cllient()) + new DeploymentApi(new AiCoreService().client()) .create( "default", AiDeploymentCreationRequest.create() @@ -95,7 +95,7 @@ See [an example in our Spring Boot application](sample-code/spring-app/src/main/ ```java public AiDeploymentDeletionResponse deleteDeployment(AiDeploymentCreationResponse deployment) { - DeploymentApi client = new DeploymentApi(new AiCoreService().cllient()); + DeploymentApi client = new DeploymentApi(new AiCoreService().client()); if (deployment.getStatus() == AiExecutionStatus.RUNNING) { // Only RUNNING deployments can be STOPPED @@ -479,7 +479,7 @@ See [an example in our unit test](orchestration/src/test/java/com/sap/ai/sdk/orc To add a header to AI Core requests, use the following code: ```java -ApiClient client = new AiCoreService().cllient().addDefaultHeader("header-key", "header-value"); +ApiClient client = new AiCoreService().client().addDefaultHeader("header-key", "header-value"); DeploymentApi api = new DeploymentApi(client); ``` From e16895c2eb4c575a5c1c99911236c85de295453b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 1 Oct 2024 15:33:37 +0200 Subject: [PATCH 23/79] Defuse functions --- .../com/sap/ai/sdk/core/AiCoreService.java | 6 +-- .../sdk/core/AiCoreServiceWithDeployment.java | 39 ++++--------------- .../controllers/OrchestrationController.java | 9 ++--- 3 files changed, 14 insertions(+), 40 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index 9ab9b6bd..e355a3d8 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -52,7 +52,7 @@ public AiCoreService withDestination(@Nonnull final Destination destination) { */ @Nonnull public AiCoreServiceWithDeployment withDeployment(@Nonnull final String deploymentId) { - return new AiCoreServiceWithDeployment(c -> deploymentId, c -> this.destination()); + return new AiCoreServiceWithDeployment(c -> deploymentId, this::destination); } /** @@ -64,7 +64,7 @@ public AiCoreServiceWithDeployment withDeployment(@Nonnull final String deployme @Nonnull public AiCoreServiceWithDeployment withDeploymentByModel(@Nonnull final String modelName) { final Predicate p = deployment -> isDeploymentOfModel(modelName, deployment); - return new AiCoreServiceWithDeployment(c -> getDeploymentId(c, p), c -> this.destination()); + return new AiCoreServiceWithDeployment(c -> getDeploymentId(c, p), this::destination); } /** @@ -76,7 +76,7 @@ public AiCoreServiceWithDeployment withDeploymentByModel(@Nonnull final String m @Nonnull public AiCoreServiceWithDeployment withDeploymentByScenario(@Nonnull final String scenarioId) { final Predicate p = deployment -> scenarioId.equals(deployment.getScenarioId()); - return new AiCoreServiceWithDeployment(c -> getDeploymentId(c, p), c -> this.destination()); + return new AiCoreServiceWithDeployment(c -> getDeploymentId(c, p), this::destination); } /** diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java index 6059565f..51f99653 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java @@ -9,23 +9,24 @@ import java.util.NoSuchElementException; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import javax.annotation.Nonnull; import javax.annotation.Nullable; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; /** Connectivity convenience methods for AI Core with deployment. */ -@RequiredArgsConstructor(access = AccessLevel.PUBLIC) +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) public class AiCoreServiceWithDeployment implements AiCoreServiceStub { // the deployment id to be used @Nonnull private final Function deploymentId; // the base destination to be used - @Nonnull private final Function destination; + @Nonnull private final Supplier destination; // the resource group, "default" if null - @Nullable private final Function resourceGroup; + @Nullable private final String resourceGroup; /** * Create a new instance of the AI Core service with a specific deployment id and destination. @@ -35,14 +36,14 @@ public class AiCoreServiceWithDeployment implements AiCoreServiceStub { */ public AiCoreServiceWithDeployment( @Nonnull final Function deploymentId, - @Nonnull final Function destination) { + @Nonnull final Supplier destination) { this(deploymentId, destination, null); } @Nonnull @Override public Destination destination() { - final var dest = destination.apply(this).asHttp(); + final var dest = destination.get().asHttp(); final var builder = DefaultHttpDestination.fromDestination(dest); updateDestination(builder, dest); return builder.build(); @@ -56,18 +57,6 @@ public Destination destination() { */ @Nonnull public AiCoreServiceWithDeployment withResourceGroup(@Nonnull final String resourceGroup) { - return withResourceGroup((core) -> resourceGroup); - } - - /** - * Set the resource group. - * - * @param resourceGroup The resource group handler. - * @return A new instance of the AI Core service. - */ - @Nonnull - public AiCoreServiceWithDeployment withResourceGroup( - @Nonnull final Function resourceGroup) { return new AiCoreServiceWithDeployment(deploymentId, destination, resourceGroup); } @@ -79,19 +68,7 @@ public AiCoreServiceWithDeployment withResourceGroup( */ @Nonnull public AiCoreServiceWithDeployment withDestination(@Nonnull final Destination destination) { - return withDestination((core) -> destination); - } - - /** - * Set the destination. - * - * @param destination The destination handler. - * @return A new instance of the AI Core service. - */ - @Nonnull - public AiCoreServiceWithDeployment withDestination( - @Nonnull final Function destination) { - return new AiCoreServiceWithDeployment(deploymentId, destination, resourceGroup); + return new AiCoreServiceWithDeployment(deploymentId, () -> destination, resourceGroup); } /** @@ -113,7 +90,7 @@ protected void updateDestination( */ @Nonnull protected String getResourceGroup() { - return resourceGroup == null ? "default" : resourceGroup.apply(this); + return resourceGroup == null ? "default" : resourceGroup; } /** diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java index 4077f861..e1bc0add 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java @@ -29,12 +29,9 @@ @RequestMapping("/orchestration") class OrchestrationController { -private static final OrchestrationCompletionApi API = - new OrchestrationCompletionApi( - new AiCoreService() - .withDeploymentByScenario("orchestration") - .withResourceGroup("default") - .client()); + private static final OrchestrationCompletionApi API = + new OrchestrationCompletionApi( + new AiCoreService().withDeploymentByScenario("orchestration").client()); static final String MODEL = "gpt-35-turbo"; From 1b1a44a0842667e491be9a6bcd8665646e7ae7ee Mon Sep 17 00:00:00 2001 From: I538344 Date: Fri, 6 Sep 2024 10:20:52 +0200 Subject: [PATCH 24/79] Added query filters for Orchestration and OpenAI deployments --- .../main/java/com/sap/ai/sdk/core/Core.java | 305 ++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 core/src/main/java/com/sap/ai/sdk/core/Core.java diff --git a/core/src/main/java/com/sap/ai/sdk/core/Core.java b/core/src/main/java/com/sap/ai/sdk/core/Core.java new file mode 100644 index 00000000..b63b4060 --- /dev/null +++ b/core/src/main/java/com/sap/ai/sdk/core/Core.java @@ -0,0 +1,305 @@ +package com.sap.ai.sdk.core; + +import static com.sap.cloud.sdk.cloudplatform.connectivity.OnBehalfOf.TECHNICAL_USER_PROVIDER; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.sap.ai.sdk.core.client.DeploymentApi; +import com.sap.ai.sdk.core.client.model.AiDeployment; +import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingAccessor; +import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingBuilder; +import com.sap.cloud.environment.servicebinding.api.ServiceBindingAccessor; +import com.sap.cloud.environment.servicebinding.api.ServiceBindingMerger; +import com.sap.cloud.environment.servicebinding.api.ServiceIdentifier; +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.HttpDestination; +import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationLoader; +import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions; +import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +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 +public class Core { + + /** + * Requires an AI Core service binding. + * + * @param resourceGroup the resource group. + * @return a generic Orchestration ApiClient. + */ + @Nonnull + public static ApiClient getOrchestrationClient(@Nonnull final String resourceGroup) { + return getClient( + getDestinationForDeployment(getOrchestrationDeployment(resourceGroup), resourceGroup)); + } + + /** + * Get the deployment id from the scenario id. If there are multiple deployments of the same + * scenario id, the first one is returned. + * + * @param resourceGroup the resource group. + * @return the deployment id + * @throws NoSuchElementException if no deployment is found for the scenario id. + */ + private static String getOrchestrationDeployment(@Nonnull final String resourceGroup) + throws NoSuchElementException { + final var deployments = + new DeploymentApi(getClient()) + .deploymentQuery( + resourceGroup, null, null, "orchestration", "RUNNING", null, null, null); + + return deployments.getResources().stream() + .map(AiDeployment::getId) + .findFirst() + .orElseThrow( + () -> + new NoSuchElementException( + "No running deployment found with scenario id \"orchestration\"")); + } + + /** + * Requires an AI Core service binding OR a service key in the environment variable {@code + * AICORE_SERVICE_KEY}. + * + * @return a generic AI Core ApiClient. + */ + @Nonnull + public static ApiClient getClient() { + return getClient(getDestination()); + } + + /** + * Get a generic AI Core ApiClient for testing purposes. + * + * @param destination The destination to use. + * @return a generic AI Core ApiClient. + */ + @Nonnull + @SuppressWarnings("UnstableApiUsage") + public static ApiClient getClient(@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 restTemplate = new RestTemplate(); + restTemplate.getMessageConverters().stream() + .filter(MappingJackson2HttpMessageConverter.class::isInstance) + .map(MappingJackson2HttpMessageConverter.class::cast) + .forEach(converter -> converter.setObjectMapper(objectMapper)); + restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(httpRequestFactory)); + + return new ApiClient(restTemplate).setBasePath(destination.asHttp().getUri().toString()); + } + + /** + * Get a destination pointing to the AI Core service. + * + *

      Requires an AI Core service binding OR a service key in the environment variable {@code + * AICORE_SERVICE_KEY}. + * + * @return a destination pointing to the AI Core service. + */ + @Nonnull + public static Destination getDestination() { + final var serviceKey = System.getenv("AICORE_SERVICE_KEY"); + return getDestination(serviceKey); + } + + /** + * For testing only + * + *

      Get a destination pointing to the AI Core service. + * + * @param serviceKey The service key in JSON format. + * @return a destination pointing to the AI Core service. + */ + static HttpDestination getDestination(@Nullable final String serviceKey) { + final var serviceKeyPresent = serviceKey != null; + final var aiCoreBindingPresent = + DefaultServiceBindingAccessor.getInstance().getServiceBindings().stream() + .anyMatch( + serviceBinding -> + ServiceIdentifier.AI_CORE.equals( + serviceBinding.getServiceIdentifier().orElse(null))); + + if (!aiCoreBindingPresent && serviceKeyPresent) { + addServiceBinding(serviceKey); + } + + // get a destination pointing to the AI Core service + final var opts = + ServiceBindingDestinationOptions.forService(ServiceIdentifier.AI_CORE) + .onBehalfOf(TECHNICAL_USER_PROVIDER) + .build(); + var destination = ServiceBindingDestinationLoader.defaultLoaderChain().getDestination(opts); + + destination = + DefaultHttpDestination.fromDestination(destination) + // append the /v2 path here, so we don't have to do it in every request when using the + // generated code this is actually necessary, because the generated code assumes this + // path to be present on the destination + .uri(destination.getUri().resolve("/v2")) + .header("AI-Client-Type", "AI SDK Java") + .build(); + return destination; + } + + /** + * Set the AI Core service key as the service binding. This is used for local testing. + * + * @param serviceKey The service key in JSON format. + * @throws AiCoreCredentialsInvalidException if the JSON service key cannot be parsed. + */ + private static void addServiceBinding(@Nonnull final String serviceKey) { + log.info( + """ + Found a service key in environment variable "AICORE_SERVICE_KEY". + Using a service key is recommended for local testing only. + Bind the AI Core service to the application for productive usage."""); + + var credentials = new HashMap(); + try { + credentials = new ObjectMapper().readValue(serviceKey, new TypeReference<>() {}); + } catch (JsonProcessingException e) { + throw new AiCoreCredentialsInvalidException( + "Error in parsing service key from the \"AICORE_SERVICE_KEY\" environment variable.", e); + } + + final var binding = + new DefaultServiceBindingBuilder() + .withServiceIdentifier(ServiceIdentifier.AI_CORE) + .withCredentials(credentials) + .build(); + final ServiceBindingAccessor accessor = DefaultServiceBindingAccessor.getInstance(); + final var newAccessor = + new ServiceBindingMerger( + List.of(accessor, () -> List.of(binding)), ServiceBindingMerger.KEEP_EVERYTHING); + DefaultServiceBindingAccessor.setInstance(newAccessor); + } + + /** Exception thrown when the JSON AI Core service key is invalid. */ + static class AiCoreCredentialsInvalidException extends RuntimeException { + public AiCoreCredentialsInvalidException( + @Nonnull final String message, @Nonnull final Throwable cause) { + super(message, cause); + } + } + + /** + * Get a destination pointing to the inference endpoint of a deployment on AI Core. Requires an + * AI Core service binding. + * + * @param deploymentId The deployment id. + * @param resourceGroup The resource group. + * @return a destination that can be used for inference calls. + */ + @Nonnull + public static Destination getDestinationForDeployment( + @Nonnull final String deploymentId, @Nonnull final String resourceGroup) { + final var destination = getDestination().asHttp(); + final DefaultHttpDestination.Builder builder = + DefaultHttpDestination.fromDestination(destination) + .uri( + destination + .getUri() + .resolve("/v2/inference/deployments/%s/".formatted(deploymentId))); + + builder.header("AI-Resource-Group", resourceGroup); + + return builder.build(); + } + + /** + * Get a destination pointing to the inference endpoint of a deployment on AI Core. Requires an + * AI Core service binding. + * + * @param modelName The name of the foundation model that is used by a deployment. + * @param resourceGroup The resource group. + * @return a destination that can be used for inference calls. + */ + @Nonnull + public static Destination getDestinationForModel( + @Nonnull final String modelName, @Nonnull final String resourceGroup) { + return getDestinationForDeployment( + getDeploymentForModel(modelName, resourceGroup), resourceGroup); + } + + /** + * Get the deployment id from the model name. If there are multiple deployments of the same model, + * the first one is returned. + * + * @param modelName the model name. + * @param resourceGroup the resource group. + * @return the deployment id + * @throws NoSuchElementException if no deployment is found for the model name. + */ + private static String getDeploymentForModel( + @Nonnull final String modelName, @Nonnull final String resourceGroup) + throws NoSuchElementException { + final var deployments = + new DeploymentApi(getClient()) + .deploymentQuery( + resourceGroup, null, null, "foundation-models", "RUNNING", null, null, null); + + return deployments.getResources().stream() + .filter(deployment -> isDeploymentOfModel(modelName, deployment)) + .map(AiDeployment::getId) + .findFirst() + .orElseThrow( + () -> + new NoSuchElementException( + "No running deployment found with model name " + modelName)); + } + + /** This exists because getBackendDetails() is broken */ + private static boolean isDeploymentOfModel( + @Nonnull final String modelName, @Nonnull final AiDeployment deployment) { + final var deploymentDetails = deployment.getDetails(); + // The AI Core specification doesn't mention that this is nullable, but it can be. + // Remove this check when the specification is fixed. + if (deploymentDetails == null) { + return false; + } + final var resources = deploymentDetails.getResources(); + if (resources == null) { + return false; + } + if (!resources.getCustomFieldNames().contains("backend_details")) { + return false; + } + final var detailsObject = resources.getCustomField("backend_details"); + + if (detailsObject instanceof Map details + && details.get("model") instanceof Map model + && model.get("name") instanceof String name) { + return modelName.equals(name); + } + return false; + } +} From f6da7483e32cbdf923ebfdf975e7b6629437a367 Mon Sep 17 00:00:00 2001 From: I538344 Date: Fri, 6 Sep 2024 14:53:17 +0200 Subject: [PATCH 25/79] Added deployment cache --- .../main/java/com/sap/ai/sdk/core/Core.java | 88 +------------ .../com/sap/ai/sdk/core/DeploymentCache.java | 123 ++++++++++++++++++ 2 files changed, 127 insertions(+), 84 deletions(-) create mode 100644 core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java diff --git a/core/src/main/java/com/sap/ai/sdk/core/Core.java b/core/src/main/java/com/sap/ai/sdk/core/Core.java index b63b4060..7a572819 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/Core.java +++ b/core/src/main/java/com/sap/ai/sdk/core/Core.java @@ -9,8 +9,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.sap.ai.sdk.core.client.DeploymentApi; -import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingAccessor; import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingBuilder; import com.sap.cloud.environment.servicebinding.api.ServiceBindingAccessor; @@ -25,8 +23,6 @@ import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; import javax.annotation.Nonnull; import javax.annotation.Nullable; import lombok.extern.slf4j.Slf4j; @@ -49,31 +45,8 @@ public class Core { @Nonnull public static ApiClient getOrchestrationClient(@Nonnull final String resourceGroup) { return getClient( - getDestinationForDeployment(getOrchestrationDeployment(resourceGroup), resourceGroup)); - } - - /** - * Get the deployment id from the scenario id. If there are multiple deployments of the same - * scenario id, the first one is returned. - * - * @param resourceGroup the resource group. - * @return the deployment id - * @throws NoSuchElementException if no deployment is found for the scenario id. - */ - private static String getOrchestrationDeployment(@Nonnull final String resourceGroup) - throws NoSuchElementException { - final var deployments = - new DeploymentApi(getClient()) - .deploymentQuery( - resourceGroup, null, null, "orchestration", "RUNNING", null, null, null); - - return deployments.getResources().stream() - .map(AiDeployment::getId) - .findFirst() - .orElseThrow( - () -> - new NoSuchElementException( - "No running deployment found with scenario id \"orchestration\"")); + getDestinationForDeployment( + DeploymentCache.getDeploymentId(resourceGroup, "orchestration"), resourceGroup)); } /** @@ -245,61 +218,8 @@ public static Destination getDestinationForDeployment( */ @Nonnull public static Destination getDestinationForModel( - @Nonnull final String modelName, @Nonnull final String resourceGroup) { + @Nonnull final String resourceGroup, @Nonnull final String modelName) { return getDestinationForDeployment( - getDeploymentForModel(modelName, resourceGroup), resourceGroup); - } - - /** - * Get the deployment id from the model name. If there are multiple deployments of the same model, - * the first one is returned. - * - * @param modelName the model name. - * @param resourceGroup the resource group. - * @return the deployment id - * @throws NoSuchElementException if no deployment is found for the model name. - */ - private static String getDeploymentForModel( - @Nonnull final String modelName, @Nonnull final String resourceGroup) - throws NoSuchElementException { - final var deployments = - new DeploymentApi(getClient()) - .deploymentQuery( - resourceGroup, null, null, "foundation-models", "RUNNING", null, null, null); - - return deployments.getResources().stream() - .filter(deployment -> isDeploymentOfModel(modelName, deployment)) - .map(AiDeployment::getId) - .findFirst() - .orElseThrow( - () -> - new NoSuchElementException( - "No running deployment found with model name " + modelName)); - } - - /** This exists because getBackendDetails() is broken */ - private static boolean isDeploymentOfModel( - @Nonnull final String modelName, @Nonnull final AiDeployment deployment) { - final var deploymentDetails = deployment.getDetails(); - // The AI Core specification doesn't mention that this is nullable, but it can be. - // Remove this check when the specification is fixed. - if (deploymentDetails == null) { - return false; - } - final var resources = deploymentDetails.getResources(); - if (resources == null) { - return false; - } - if (!resources.getCustomFieldNames().contains("backend_details")) { - return false; - } - final var detailsObject = resources.getCustomField("backend_details"); - - if (detailsObject instanceof Map details - && details.get("model") instanceof Map model - && model.get("name") instanceof String name) { - return modelName.equals(name); - } - return false; + DeploymentCache.getDeploymentId(resourceGroup, modelName), resourceGroup); } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java new file mode 100644 index 00000000..8d6ea712 --- /dev/null +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -0,0 +1,123 @@ +package com.sap.ai.sdk.core; + +import static com.sap.ai.sdk.core.Core.getClient; + +import com.sap.ai.sdk.core.client.DeploymentApi; +import com.sap.ai.sdk.core.client.model.AiDeployment; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import javax.annotation.Nonnull; + +class DeploymentCache { + private static final DeploymentApi API = new DeploymentApi(getClient()); + + /** + * Cache for deployment ids. The key is the scenario + model name and the value is the deployment + * id. + */ + private static final Map CACHE = new HashMap<>(); + + static { + final var deployments = API.deploymentQuery("default").getResources(); + deployments.forEach( + deployment -> + CACHE.put(deployment.getScenarioId() + getModelName(deployment), deployment.getId())); + } + + /** + * Get the deployment id for the given scenario or model name. + * + * @param resourceGroup the resource group, usually "default". + * @param name the scenario or model name. + * @return the deployment id. + */ + public static String getDeploymentId( + @Nonnull final String resourceGroup, @Nonnull final String name) { + return CACHE.computeIfAbsent( + name, + n -> { + if ("orchestration".equals(n)) { + return getOrchestrationDeployment(resourceGroup); + } else { + return getDeploymentForModel(resourceGroup, name); + } + }); + } + + /** + * Get the deployment id from the scenario id. If there are multiple deployments of the same + * scenario id, the first one is returned. + * + * @param resourceGroup the resource group. + * @return the deployment id + * @throws NoSuchElementException if no deployment is found for the scenario id. + */ + private static String getOrchestrationDeployment(@Nonnull final String resourceGroup) + throws NoSuchElementException { + final var deployments = + new DeploymentApi(getClient()) + .deploymentQuery( + resourceGroup, null, null, "orchestration", "RUNNING", null, null, null); + + return deployments.getResources().stream() + .map(AiDeployment::getId) + .findFirst() + .orElseThrow( + () -> + new NoSuchElementException( + "No running deployment found with scenario id \"orchestration\"")); + } + + /** + * Get the deployment id from the model name. If there are multiple deployments of the same model, + * the first one is returned. + * + * @param modelName the model name. + * @param resourceGroup the resource group. + * @return the deployment id + * @throws NoSuchElementException if no deployment is found for the model name. + */ + private static String getDeploymentForModel( + @Nonnull final String resourceGroup, @Nonnull final String modelName) + throws NoSuchElementException { + final var deployments = + new DeploymentApi(getClient()) + .deploymentQuery( + resourceGroup, null, null, "foundation-models", "RUNNING", null, null, null); + + return deployments.getResources().stream() + .filter(deployment -> modelName.equals(getModelName(deployment))) + .map(AiDeployment::getId) + .findFirst() + .orElseThrow( + () -> + new NoSuchElementException( + "No running deployment found with model name " + modelName)); + } + + /** This exists because getBackendDetails() is broken */ + private static String getModelName(@Nonnull final AiDeployment deployment) { + final var deploymentDetails = deployment.getDetails(); + // The AI Core specification doesn't mention that this is nullable, but it can be. + // Remove this check when the specification is fixed. + if (deploymentDetails == null) { + return ""; + } + final var resources = deploymentDetails.getResources(); + if (resources == null) { + return ""; + } + if (!resources.getCustomFieldNames().contains("backend_details")) { + return ""; + } + final var detailsObject = resources.getCustomField("backend_details"); + + if (detailsObject instanceof Map details + && details.get("model") instanceof Map model + && model.get("name") instanceof String name) { + return name; + } + return ""; + } +} From 8e2cd9f9d6e287c6842885503744e6911903e64a Mon Sep 17 00:00:00 2001 From: I538344 Date: Mon, 9 Sep 2024 11:50:49 +0200 Subject: [PATCH 26/79] Added cache reset --- .../com/sap/ai/sdk/core/DeploymentCache.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index 8d6ea712..5239f62d 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -9,6 +9,10 @@ import java.util.NoSuchElementException; import javax.annotation.Nonnull; +/** + * Cache for deployment IDs. This class is used to get the deployment id for the orchestration + * scenario or for a model. + */ class DeploymentCache { private static final DeploymentApi API = new DeploymentApi(getClient()); @@ -18,7 +22,18 @@ class DeploymentCache { */ private static final Map CACHE = new HashMap<>(); + // Eagerly load all deployments into the cache. static { + resetCache(); + } + + /** + * Remove all entries from the cache and reload all deployments. + * + *

      Call this method if you delete a deployment. + */ + public static void resetCache() { + CACHE.clear(); final var deployments = API.deploymentQuery("default").getResources(); deployments.forEach( deployment -> @@ -26,10 +41,10 @@ class DeploymentCache { } /** - * Get the deployment id for the given scenario or model name. + * Get the deployment id for the orchestration scenario or any foundation model. * * @param resourceGroup the resource group, usually "default". - * @param name the scenario or model name. + * @param name "orchestration" or the model name. * @return the deployment id. */ public static String getDeploymentId( @@ -73,8 +88,8 @@ private static String getOrchestrationDeployment(@Nonnull final String resourceG * Get the deployment id from the model name. If there are multiple deployments of the same model, * the first one is returned. * - * @param modelName the model name. * @param resourceGroup the resource group. + * @param modelName the model name. * @return the deployment id * @throws NoSuchElementException if no deployment is found for the model name. */ From 7fa8398e78c1821e448d520ecdfb4e6788e3ccea Mon Sep 17 00:00:00 2001 From: I538344 Date: Tue, 17 Sep 2024 11:17:27 +0200 Subject: [PATCH 27/79] Added unit tests --- .../com/sap/ai/sdk/core/DeploymentCache.java | 27 ++-- .../java/com/sap/ai/sdk/core/CacheTest.java | 128 ++++++++++++++++++ .../sdk/core/client/WireMockTestServer.java | 2 +- .../controllers/ConfigurationController.java | 26 ++++ .../src/main/resources/static/index.html | 6 + 5 files changed, 173 insertions(+), 16 deletions(-) create mode 100644 core/src/test/java/com/sap/ai/sdk/core/CacheTest.java create mode 100644 sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ConfigurationController.java diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index 5239f62d..72dbfb26 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -14,12 +14,10 @@ * scenario or for a model. */ class DeploymentCache { - private static final DeploymentApi API = new DeploymentApi(getClient()); + /** The client to use for deployment queries. */ + static DeploymentApi API = new DeploymentApi(getClient()); - /** - * Cache for deployment ids. The key is the scenario + model name and the value is the deployment - * id. - */ + /** Cache for deployment ids. The key is the model name and the value is the deployment id. */ private static final Map CACHE = new HashMap<>(); // Eagerly load all deployments into the cache. @@ -30,14 +28,12 @@ class DeploymentCache { /** * Remove all entries from the cache and reload all deployments. * - *

      Call this method if you delete a deployment. + *

      Call this method whenever a deployment is deleted. */ public static void resetCache() { CACHE.clear(); final var deployments = API.deploymentQuery("default").getResources(); - deployments.forEach( - deployment -> - CACHE.put(deployment.getScenarioId() + getModelName(deployment), deployment.getId())); + deployments.forEach(deployment -> CACHE.put(getModelName(deployment), deployment.getId())); } /** @@ -71,9 +67,8 @@ public static String getDeploymentId( private static String getOrchestrationDeployment(@Nonnull final String resourceGroup) throws NoSuchElementException { final var deployments = - new DeploymentApi(getClient()) - .deploymentQuery( - resourceGroup, null, null, "orchestration", "RUNNING", null, null, null); + API.deploymentQuery( + resourceGroup, null, null, "orchestration", "RUNNING", null, null, null); return deployments.getResources().stream() .map(AiDeployment::getId) @@ -97,9 +92,8 @@ private static String getDeploymentForModel( @Nonnull final String resourceGroup, @Nonnull final String modelName) throws NoSuchElementException { final var deployments = - new DeploymentApi(getClient()) - .deploymentQuery( - resourceGroup, null, null, "foundation-models", "RUNNING", null, null, null); + API.deploymentQuery( + resourceGroup, null, null, "foundation-models", "RUNNING", null, null, null); return deployments.getResources().stream() .filter(deployment -> modelName.equals(getModelName(deployment))) @@ -113,6 +107,9 @@ private static String getDeploymentForModel( /** This exists because getBackendDetails() is broken */ private static String getModelName(@Nonnull final AiDeployment deployment) { + if ("orchestration".equals(deployment.getScenarioId())) { + return "orchestration"; + } final var deploymentDetails = deployment.getDetails(); // The AI Core specification doesn't mention that this is nullable, but it can be. // Remove this check when the specification is fixed. diff --git a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java new file mode 100644 index 00000000..a31d855b --- /dev/null +++ b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java @@ -0,0 +1,128 @@ +package com.sap.ai.sdk.core; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; + +import com.sap.ai.sdk.core.client.DeploymentApi; +import com.sap.ai.sdk.core.client.WireMockTestServer; +import org.apache.hc.core5.http.HttpStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class CacheTest extends WireMockTestServer { + @BeforeEach + void setup() { + DeploymentCache.API = new DeploymentApi(destination); + wireMockServer.resetRequests(); + } + + private static void stubGPT4() { + wireMockServer.stubFor( + get(urlPathEqualTo("/lm/deployments")) + .withHeader("AI-Resource-Group", equalTo("default")) + .willReturn( + aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("content-type", "application/json") + .withBody( + """ + { + "count": 1, + "resources": [ + { + "configurationId": "7652a231-ba9b-4fcc-b473-2c355cb21b61", + "configurationName": "gpt-4-32k", + "createdAt": "2024-04-17T15:19:53Z", + "deploymentUrl": "https://api.ai.intprod-eu12.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/d19b998f347341aa", + "details": { + "resources": { + "backend_details": { + "model": { + "name": "gpt-4-32k", + "version": "latest" + } + } + }, + "scaling": { + "backend_details": {} + } + }, + "id": "d19b998f347341aa", + "lastOperation": "CREATE", + "latestRunningConfigurationId": "7652a231-ba9b-4fcc-b473-2c355cb21b61", + "modifiedAt": "2024-05-07T13:05:45Z", + "scenarioId": "foundation-models", + "startTime": "2024-04-17T15:21:15Z", + "status": "RUNNING", + "submissionTime": "2024-04-17T15:20:11Z", + "targetStatus": "RUNNING" + } + ] + } + """))); + } + + private static void stubEmpty() { + wireMockServer.stubFor( + get(urlPathEqualTo("/lm/deployments")) + .withHeader("AI-Resource-Group", equalTo("default")) + .willReturn( + aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("content-type", "application/json") + .withBody( + """ + { + "count": 0, + "resources": [] + } + """))); + } + + /** + * The user creates a deployment. + * + *

      The user uses the OpenAI client and specifies only the name "foo". + * + *

      The user repeatedly uses the API in the same way + * + *

      Simple case, the deployment should be served from cache as much as possible + */ + @Test + void newDeployment() { + stubGPT4(); + DeploymentCache.resetCache(); + + DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); + + DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); + } + + /** + * The user creates a deployment after starting with an empty cache. + * + *

      The user uses the OpenAI client and specifies only the name "foo". + * + *

      The user repeatedly uses the API in the same way + * + *

      Simple case, the deployment should be served from cache as much as possible + */ + @Test + void newDeploymentAfterReset() { + stubEmpty(); + DeploymentCache.resetCache(); + stubGPT4(); + + DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + // 1 reset empty and 1 cache miss + wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); + + DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); + } +} diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java b/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java index 08e5a89d..c9870791 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java @@ -12,7 +12,7 @@ import org.junit.jupiter.api.BeforeAll; /** Test server for all unit tests. */ -abstract class WireMockTestServer { +public abstract class WireMockTestServer { private static final WireMockConfiguration WIREMOCK_CONFIGURATION = wireMockConfig().dynamicPort(); diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ConfigurationController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ConfigurationController.java new file mode 100644 index 00000000..5e94ec9d --- /dev/null +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ConfigurationController.java @@ -0,0 +1,26 @@ +package com.sap.ai.sdk.app.controllers; + +import static com.sap.ai.sdk.core.Core.getClient; + +import com.sap.ai.sdk.core.client.ConfigurationApi; +import com.sap.ai.sdk.core.client.model.AiConfigurationList; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** Endpoint for Configuration operations */ +@SuppressWarnings("unused") // debug class that doesn't need to be tested +@RestController +public class ConfigurationController { + + private static final ConfigurationApi API = new ConfigurationApi(getClient()); + + /** + * Get the list of configurations. + * + * @return the list of configurations + */ + @GetMapping("/configurations") + AiConfigurationList getConfigurations() { + return API.configurationQuery("default"); + } +} diff --git a/sample-code/spring-app/src/main/resources/static/index.html b/sample-code/spring-app/src/main/resources/static/index.html index b7a2d904..07272d1e 100644 --- a/sample-code/spring-app/src/main/resources/static/index.html +++ b/sample-code/spring-app/src/main/resources/static/index.html @@ -57,6 +57,12 @@

      Endpoints

    • /models All available foundation models in this region
    +
  • Orchestration

      From 7f6af650f0af1fecdb75117d4811f1c10c550774 Mon Sep 17 00:00:00 2001 From: I538344 Date: Tue, 17 Sep 2024 12:55:15 +0200 Subject: [PATCH 28/79] Fixed unit tests --- .../main/java/com/sap/ai/sdk/core/Core.java | 7 +++- .../com/sap/ai/sdk/core/DeploymentCache.java | 40 ++++++++++++++----- .../java/com/sap/ai/sdk/core/CacheTest.java | 16 ++++---- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/Core.java b/core/src/main/java/com/sap/ai/sdk/core/Core.java index 7a572819..0de5ddd6 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/Core.java +++ b/core/src/main/java/com/sap/ai/sdk/core/Core.java @@ -36,6 +36,9 @@ @Slf4j public class Core { + /** The cache for deployment ids, is eagerly loaded. */ + private static final DeploymentCache CACHE = new DeploymentCache(); + /** * Requires an AI Core service binding. * @@ -46,7 +49,7 @@ public class Core { public static ApiClient getOrchestrationClient(@Nonnull final String resourceGroup) { return getClient( getDestinationForDeployment( - DeploymentCache.getDeploymentId(resourceGroup, "orchestration"), resourceGroup)); + CACHE.getDeploymentId(resourceGroup, "orchestration"), resourceGroup)); } /** @@ -220,6 +223,6 @@ public static Destination getDestinationForDeployment( public static Destination getDestinationForModel( @Nonnull final String resourceGroup, @Nonnull final String modelName) { return getDestinationForDeployment( - DeploymentCache.getDeploymentId(resourceGroup, modelName), resourceGroup); + CACHE.getDeploymentId(resourceGroup, modelName), resourceGroup); } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index 72dbfb26..32d9142c 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -4,24 +4,39 @@ import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiDeployment; +import com.sap.cloud.sdk.services.openapi.core.OpenApiRequestException; import java.util.HashMap; import java.util.Map; import java.util.NoSuchElementException; import javax.annotation.Nonnull; +import lombok.extern.slf4j.Slf4j; /** * Cache for deployment IDs. This class is used to get the deployment id for the orchestration * scenario or for a model. */ +@Slf4j class DeploymentCache { /** The client to use for deployment queries. */ - static DeploymentApi API = new DeploymentApi(getClient()); + DeploymentApi API; /** Cache for deployment ids. The key is the model name and the value is the deployment id. */ - private static final Map CACHE = new HashMap<>(); + private final Map CACHE = new HashMap<>(); - // Eagerly load all deployments into the cache. - static { + /* + * Create a new DeploymentCache and eagerly load all deployments into the cache. + */ + DeploymentCache() { + API = new DeploymentApi(getClient()); + resetCache(); + } + + /* + * Create a new DeploymentCache and eagerly load all deployments into the cache. + * @param client the client to use for deployment queries + */ + DeploymentCache(DeploymentApi client) { + API = client; resetCache(); } @@ -30,10 +45,14 @@ class DeploymentCache { * *

      Call this method whenever a deployment is deleted. */ - public static void resetCache() { + public void resetCache() { CACHE.clear(); - final var deployments = API.deploymentQuery("default").getResources(); - deployments.forEach(deployment -> CACHE.put(getModelName(deployment), deployment.getId())); + try { + final var deployments = API.deploymentQuery("default").getResources(); + deployments.forEach(deployment -> CACHE.put(getModelName(deployment), deployment.getId())); + } catch (final OpenApiRequestException e) { + log.error("Failed to load deployments into cache", e); + } } /** @@ -43,8 +62,7 @@ public static void resetCache() { * @param name "orchestration" or the model name. * @return the deployment id. */ - public static String getDeploymentId( - @Nonnull final String resourceGroup, @Nonnull final String name) { + public String getDeploymentId(@Nonnull final String resourceGroup, @Nonnull final String name) { return CACHE.computeIfAbsent( name, n -> { @@ -64,7 +82,7 @@ public static String getDeploymentId( * @return the deployment id * @throws NoSuchElementException if no deployment is found for the scenario id. */ - private static String getOrchestrationDeployment(@Nonnull final String resourceGroup) + private String getOrchestrationDeployment(@Nonnull final String resourceGroup) throws NoSuchElementException { final var deployments = API.deploymentQuery( @@ -88,7 +106,7 @@ private static String getOrchestrationDeployment(@Nonnull final String resourceG * @return the deployment id * @throws NoSuchElementException if no deployment is found for the model name. */ - private static String getDeploymentForModel( + private String getDeploymentForModel( @Nonnull final String resourceGroup, @Nonnull final String modelName) throws NoSuchElementException { final var deployments = diff --git a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java index a31d855b..fe37782d 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java @@ -13,9 +13,11 @@ import org.junit.jupiter.api.Test; class CacheTest extends WireMockTestServer { + + DeploymentCache cacheUnderTest = new DeploymentCache(new DeploymentApi(destination)); + @BeforeEach void setup() { - DeploymentCache.API = new DeploymentApi(destination); wireMockServer.resetRequests(); } @@ -94,12 +96,12 @@ private static void stubEmpty() { @Test void newDeployment() { stubGPT4(); - DeploymentCache.resetCache(); + cacheUnderTest.resetCache(); - DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + cacheUnderTest.getDeploymentId("default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); - DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + cacheUnderTest.getDeploymentId("default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); } @@ -115,14 +117,14 @@ void newDeployment() { @Test void newDeploymentAfterReset() { stubEmpty(); - DeploymentCache.resetCache(); + cacheUnderTest.resetCache(); stubGPT4(); - DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + cacheUnderTest.getDeploymentId("default", "gpt-4-32k"); // 1 reset empty and 1 cache miss wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); - DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + cacheUnderTest.getDeploymentId("default", "gpt-4-32k"); wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); } } From 7df86f1709cf43f14a6c37c638dc52156c8ef373 Mon Sep 17 00:00:00 2001 From: I538344 Date: Tue, 17 Sep 2024 12:57:17 +0200 Subject: [PATCH 29/79] PMD --- core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index 32d9142c..42450b49 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -35,7 +35,7 @@ class DeploymentCache { * Create a new DeploymentCache and eagerly load all deployments into the cache. * @param client the client to use for deployment queries */ - DeploymentCache(DeploymentApi client) { + DeploymentCache(@Nonnull final DeploymentApi client) { API = client; resetCache(); } From 481d5b62abf4072de179ef035cd51151d6c17b40 Mon Sep 17 00:00:00 2001 From: I538344 Date: Tue, 17 Sep 2024 13:31:12 +0200 Subject: [PATCH 30/79] Refactor to fix tests --- .../main/java/com/sap/ai/sdk/core/Core.java | 13 +++++--- .../com/sap/ai/sdk/core/DeploymentCache.java | 33 ++++++++----------- .../java/com/sap/ai/sdk/core/CacheTest.java | 18 +++++----- .../core/{client => }/WireMockTestServer.java | 0 .../ai/sdk/core/client/ArtifactUnitTest.java | 1 + .../core/client/ConfigurationUnitTest.java | 1 + .../sdk/core/client/DeploymentUnitTest.java | 1 + .../ai/sdk/core/client/ExecutionUnitTest.java | 1 + 8 files changed, 35 insertions(+), 33 deletions(-) rename core/src/test/java/com/sap/ai/sdk/core/{client => }/WireMockTestServer.java (100%) diff --git a/core/src/main/java/com/sap/ai/sdk/core/Core.java b/core/src/main/java/com/sap/ai/sdk/core/Core.java index 0de5ddd6..174fb5f7 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/Core.java +++ b/core/src/main/java/com/sap/ai/sdk/core/Core.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingAccessor; import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingBuilder; import com.sap.cloud.environment.servicebinding.api.ServiceBindingAccessor; @@ -36,8 +37,12 @@ @Slf4j public class Core { - /** The cache for deployment ids, is eagerly loaded. */ - private static final DeploymentCache CACHE = new DeploymentCache(); + // for testing only, will be removed once we make this class an instance + static { + if (!DeploymentCache.isLoaded()) { + DeploymentCache.eagerlyLoaded(new DeploymentApi(getClient())); + } + } /** * Requires an AI Core service binding. @@ -49,7 +54,7 @@ public class Core { public static ApiClient getOrchestrationClient(@Nonnull final String resourceGroup) { return getClient( getDestinationForDeployment( - CACHE.getDeploymentId(resourceGroup, "orchestration"), resourceGroup)); + DeploymentCache.getDeploymentId(resourceGroup, "orchestration"), resourceGroup)); } /** @@ -223,6 +228,6 @@ public static Destination getDestinationForDeployment( public static Destination getDestinationForModel( @Nonnull final String resourceGroup, @Nonnull final String modelName) { return getDestinationForDeployment( - CACHE.getDeploymentId(resourceGroup, modelName), resourceGroup); + DeploymentCache.getDeploymentId(resourceGroup, modelName), resourceGroup); } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index 42450b49..dff805a4 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -1,7 +1,5 @@ package com.sap.ai.sdk.core; -import static com.sap.ai.sdk.core.Core.getClient; - import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.cloud.sdk.services.openapi.core.OpenApiRequestException; @@ -18,34 +16,30 @@ @Slf4j class DeploymentCache { /** The client to use for deployment queries. */ - DeploymentApi API; + static DeploymentApi API; /** Cache for deployment ids. The key is the model name and the value is the deployment id. */ - private final Map CACHE = new HashMap<>(); + private static final Map CACHE = new HashMap<>(); - /* - * Create a new DeploymentCache and eagerly load all deployments into the cache. - */ - DeploymentCache() { - API = new DeploymentApi(getClient()); - resetCache(); + static boolean isLoaded() { + return API != null; } - /* - * Create a new DeploymentCache and eagerly load all deployments into the cache. - * @param client the client to use for deployment queries - */ - DeploymentCache(@Nonnull final DeploymentApi client) { + public static void eagerlyLoaded(@Nonnull final DeploymentApi client) { API = client; resetCache(); } + public static void lazyLoaded(@Nonnull final DeploymentApi client) { + API = client; + } + /** * Remove all entries from the cache and reload all deployments. * *

      Call this method whenever a deployment is deleted. */ - public void resetCache() { + public static void resetCache() { CACHE.clear(); try { final var deployments = API.deploymentQuery("default").getResources(); @@ -62,7 +56,8 @@ public void resetCache() { * @param name "orchestration" or the model name. * @return the deployment id. */ - public String getDeploymentId(@Nonnull final String resourceGroup, @Nonnull final String name) { + public static String getDeploymentId( + @Nonnull final String resourceGroup, @Nonnull final String name) { return CACHE.computeIfAbsent( name, n -> { @@ -82,7 +77,7 @@ public String getDeploymentId(@Nonnull final String resourceGroup, @Nonnull fina * @return the deployment id * @throws NoSuchElementException if no deployment is found for the scenario id. */ - private String getOrchestrationDeployment(@Nonnull final String resourceGroup) + private static String getOrchestrationDeployment(@Nonnull final String resourceGroup) throws NoSuchElementException { final var deployments = API.deploymentQuery( @@ -106,7 +101,7 @@ private String getOrchestrationDeployment(@Nonnull final String resourceGroup) * @return the deployment id * @throws NoSuchElementException if no deployment is found for the model name. */ - private String getDeploymentForModel( + private static String getDeploymentForModel( @Nonnull final String resourceGroup, @Nonnull final String modelName) throws NoSuchElementException { final var deployments = diff --git a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java index fe37782d..fce45cc4 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java @@ -7,17 +7,15 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import com.sap.ai.sdk.core.client.DeploymentApi; -import com.sap.ai.sdk.core.client.WireMockTestServer; import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class CacheTest extends WireMockTestServer { - DeploymentCache cacheUnderTest = new DeploymentCache(new DeploymentApi(destination)); - @BeforeEach - void setup() { + void setupCache() { + DeploymentCache.lazyLoaded(new DeploymentApi(destination)); wireMockServer.resetRequests(); } @@ -96,12 +94,12 @@ private static void stubEmpty() { @Test void newDeployment() { stubGPT4(); - cacheUnderTest.resetCache(); + DeploymentCache.resetCache(); - cacheUnderTest.getDeploymentId("default", "gpt-4-32k"); + DeploymentCache.getDeploymentId("default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); - cacheUnderTest.getDeploymentId("default", "gpt-4-32k"); + DeploymentCache.getDeploymentId("default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); } @@ -117,14 +115,14 @@ void newDeployment() { @Test void newDeploymentAfterReset() { stubEmpty(); - cacheUnderTest.resetCache(); + DeploymentCache.resetCache(); stubGPT4(); - cacheUnderTest.getDeploymentId("default", "gpt-4-32k"); + DeploymentCache.getDeploymentId("default", "gpt-4-32k"); // 1 reset empty and 1 cache miss wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); - cacheUnderTest.getDeploymentId("default", "gpt-4-32k"); + DeploymentCache.getDeploymentId("default", "gpt-4-32k"); wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); } } diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java b/core/src/test/java/com/sap/ai/sdk/core/WireMockTestServer.java similarity index 100% rename from core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java rename to core/src/test/java/com/sap/ai/sdk/core/WireMockTestServer.java diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java index a4783ac4..366d57fb 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java @@ -9,6 +9,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static org.assertj.core.api.Assertions.assertThat; +import com.sap.ai.sdk.core.WireMockTestServer; import com.sap.ai.sdk.core.client.model.AiArtifact; import com.sap.ai.sdk.core.client.model.AiArtifactCreationResponse; import com.sap.ai.sdk.core.client.model.AiArtifactList; diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java index 6f419781..fa0c01da 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java @@ -9,6 +9,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static org.assertj.core.api.Assertions.assertThat; +import com.sap.ai.sdk.core.WireMockTestServer; import com.sap.ai.sdk.core.client.model.AiArtifactArgumentBinding; import com.sap.ai.sdk.core.client.model.AiConfiguration; import com.sap.ai.sdk.core.client.model.AiConfigurationBaseData; diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java index fda31895..0e862eb6 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java @@ -12,6 +12,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static org.assertj.core.api.Assertions.assertThat; +import com.sap.ai.sdk.core.WireMockTestServer; import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.ai.sdk.core.client.model.AiDeploymentBulkModificationRequest; import com.sap.ai.sdk.core.client.model.AiDeploymentBulkModificationResponse; diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java index 5a79089b..9345a65a 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java @@ -12,6 +12,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static org.assertj.core.api.Assertions.assertThat; +import com.sap.ai.sdk.core.WireMockTestServer; import com.sap.ai.sdk.core.client.model.AiArtifact; import com.sap.ai.sdk.core.client.model.AiEnactmentCreationRequest; import com.sap.ai.sdk.core.client.model.AiExecution; From b1df254bc0b7ce4b1b46b11525df4b2c175d8342 Mon Sep 17 00:00:00 2001 From: I538344 Date: Tue, 17 Sep 2024 14:06:57 +0200 Subject: [PATCH 31/79] fix tests for real --- .../java/com/sap/ai/sdk/core/DeploymentCache.java | 12 +++++++++++- .../foundationmodels/openai/OpenAiClientTest.java | 3 +++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index dff805a4..2d005446 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -14,7 +14,7 @@ * scenario or for a model. */ @Slf4j -class DeploymentCache { +public class DeploymentCache { /** The client to use for deployment queries. */ static DeploymentApi API; @@ -25,11 +25,21 @@ static boolean isLoaded() { return API != null; } + /** + * Eagerly load the deployment cache with the given client. + * + * @param client the deployment client. + */ public static void eagerlyLoaded(@Nonnull final DeploymentApi client) { API = client; resetCache(); } + /** + * Lazy load the deployment cache with the given client. + * + * @param client the deployment client. + */ public static void lazyLoaded(@Nonnull final DeploymentApi client) { API = client; } diff --git a/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientTest.java b/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientTest.java index 3fdce3ca..11c11986 100644 --- a/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientTest.java +++ b/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientTest.java @@ -14,6 +14,8 @@ import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import com.github.tomakehurst.wiremock.stubbing.Scenario; +import com.sap.ai.sdk.core.DeploymentCache; +import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionChoice; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionDelta; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionOutput; @@ -59,6 +61,7 @@ void setup(WireMockRuntimeInfo server) { final DefaultHttpDestination destination = DefaultHttpDestination.builder(server.getHttpBaseUrl()).build(); client = OpenAiClient.withCustomDestination(destination); + DeploymentCache.lazyLoaded(new DeploymentApi(destination)); ApacheHttpClient5Accessor.setHttpClientCache(ApacheHttpClient5Cache.DISABLED); } From 1d5c15570a16ff9fa03d2d36172c82c872e9fada Mon Sep 17 00:00:00 2001 From: I538344 Date: Wed, 18 Sep 2024 10:29:23 +0200 Subject: [PATCH 32/79] PMD + added clearCache and loadCache --- .../main/java/com/sap/ai/sdk/core/Core.java | 4 ++-- .../com/sap/ai/sdk/core/DeploymentCache.java | 24 ++++++++++++++----- .../java/com/sap/ai/sdk/core/CacheTest.java | 4 ++-- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/Core.java b/core/src/main/java/com/sap/ai/sdk/core/Core.java index 174fb5f7..e843a4de 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/Core.java +++ b/core/src/main/java/com/sap/ai/sdk/core/Core.java @@ -39,8 +39,8 @@ public class Core { // for testing only, will be removed once we make this class an instance static { - if (!DeploymentCache.isLoaded()) { - DeploymentCache.eagerlyLoaded(new DeploymentApi(getClient())); + if (DeploymentCache.isEmpty()) { + DeploymentCache.lazyLoaded(new DeploymentApi(getClient())); } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index 2d005446..695cd763 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -21,8 +21,8 @@ public class DeploymentCache { /** Cache for deployment ids. The key is the model name and the value is the deployment id. */ private static final Map CACHE = new HashMap<>(); - static boolean isLoaded() { - return API != null; + static boolean isEmpty() { + return API == null; } /** @@ -32,7 +32,7 @@ static boolean isLoaded() { */ public static void eagerlyLoaded(@Nonnull final DeploymentApi client) { API = client; - resetCache(); + loadCache(); } /** @@ -45,12 +45,20 @@ public static void lazyLoaded(@Nonnull final DeploymentApi client) { } /** - * Remove all entries from the cache and reload all deployments. + * Remove all entries from the cache. * - *

      Call this method whenever a deployment is deleted. + *

      Call both clearCache and {@link #loadCache} method whenever a deployment is deleted. */ - public static void resetCache() { + public static void clearCache() { CACHE.clear(); + } + + /** + * Load all deployments into the cache + * + *

      Call both {@link #clearCache} and loadCache method whenever a deployment is deleted. + */ + public static void loadCache() { try { final var deployments = API.deploymentQuery("default").getResources(); deployments.forEach(deployment -> CACHE.put(getModelName(deployment), deployment.getId())); @@ -66,8 +74,12 @@ public static void resetCache() { * @param name "orchestration" or the model name. * @return the deployment id. */ + @Nonnull public static String getDeploymentId( @Nonnull final String resourceGroup, @Nonnull final String name) { + if (DeploymentCache.isEmpty()) { + loadCache(); + } return CACHE.computeIfAbsent( name, n -> { diff --git a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java index fce45cc4..7837322e 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java @@ -94,7 +94,7 @@ private static void stubEmpty() { @Test void newDeployment() { stubGPT4(); - DeploymentCache.resetCache(); + DeploymentCache.loadCache(); DeploymentCache.getDeploymentId("default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); @@ -115,7 +115,7 @@ void newDeployment() { @Test void newDeploymentAfterReset() { stubEmpty(); - DeploymentCache.resetCache(); + DeploymentCache.loadCache(); stubGPT4(); DeploymentCache.getDeploymentId("default", "gpt-4-32k"); From 771a7b6eb293f715eca95eef37f4c904489dd585 Mon Sep 17 00:00:00 2001 From: I538344 Date: Wed, 18 Sep 2024 10:37:35 +0200 Subject: [PATCH 33/79] PMD again --- core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index 695cd763..b79beea0 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -77,7 +77,7 @@ public static void loadCache() { @Nonnull public static String getDeploymentId( @Nonnull final String resourceGroup, @Nonnull final String name) { - if (DeploymentCache.isEmpty()) { + if (isEmpty()) { loadCache(); } return CACHE.computeIfAbsent( From 8a45bb03d199b676867cd48d7d44b912bd0f3d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= <22489773+newtork@users.noreply.github.com> Date: Tue, 8 Oct 2024 01:01:22 +0200 Subject: [PATCH 34/79] Update core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java Co-authored-by: Charles Dubois <103174266+CharlesDuboisSAP@users.noreply.github.com> --- .../java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java index 51f99653..c15fbece 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java @@ -26,7 +26,7 @@ public class AiCoreServiceWithDeployment implements AiCoreServiceStub { @Nonnull private final Supplier destination; // the resource group, "default" if null - @Nullable private final String resourceGroup; + @Nonnull private final String resourceGroup; /** * Create a new instance of the AI Core service with a specific deployment id and destination. @@ -37,7 +37,7 @@ public class AiCoreServiceWithDeployment implements AiCoreServiceStub { public AiCoreServiceWithDeployment( @Nonnull final Function deploymentId, @Nonnull final Supplier destination) { - this(deploymentId, destination, null); + this(deploymentId, destination, "default"); } @Nonnull From 4e138bed88c222ff95add95bc80a1d71e8ad4ae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 8 Oct 2024 01:06:51 +0200 Subject: [PATCH 35/79] Make service binding accessor changeable, e.g. for future testing --- .../main/java/com/sap/ai/sdk/core/DestinationResolver.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java b/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java index eeef6d95..af5433c5 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingAccessor; import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingBuilder; +import com.sap.cloud.environment.servicebinding.api.ServiceBindingAccessor; import com.sap.cloud.environment.servicebinding.api.ServiceBindingMerger; import com.sap.cloud.environment.servicebinding.api.ServiceIdentifier; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; @@ -22,6 +23,7 @@ /** Utility class to resolve the destination pointing to the AI Core service. */ @Slf4j class DestinationResolver { + static ServiceBindingAccessor accessor = DefaultServiceBindingAccessor.getInstance(); /** * For testing only @@ -35,7 +37,7 @@ class DestinationResolver { static HttpDestination getDestination(@Nullable final String serviceKey) { final var serviceKeyPresent = serviceKey != null; final var aiCoreBindingPresent = - DefaultServiceBindingAccessor.getInstance().getServiceBindings().stream() + accessor.getServiceBindings().stream() .anyMatch( serviceBinding -> ServiceIdentifier.AI_CORE.equals( @@ -89,7 +91,6 @@ private static void addServiceBinding(@Nonnull final String serviceKey) { .withServiceIdentifier(ServiceIdentifier.AI_CORE) .withCredentials(credentials) .build(); - final var accessor = DefaultServiceBindingAccessor.getInstance(); final var newAccessor = new ServiceBindingMerger( List.of(accessor, () -> List.of(binding)), ServiceBindingMerger.KEEP_EVERYTHING); From 129409b1341af60873f4d6a5f13bd85c14bd54ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 8 Oct 2024 01:20:41 +0200 Subject: [PATCH 36/79] Rename methods and types; Fix merge --- ...hDeployment.java => AiCoreDeployment.java} | 19 +++++++++---------- ...erviceStub.java => AiCoreDestination.java} | 3 ++- .../com/sap/ai/sdk/core/AiCoreService.java | 18 +++++++++--------- .../foundationmodels/openai/OpenAiClient.java | 2 +- .../app/controllers/DeploymentController.java | 7 ++++--- .../controllers/OrchestrationController.java | 2 +- 6 files changed, 26 insertions(+), 25 deletions(-) rename core/src/main/java/com/sap/ai/sdk/core/{AiCoreServiceWithDeployment.java => AiCoreDeployment.java} (85%) rename core/src/main/java/com/sap/ai/sdk/core/{AiCoreServiceStub.java => AiCoreDestination.java} (98%) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java similarity index 85% rename from core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java rename to core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index 51f99653..a75e7a83 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceWithDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -17,10 +17,10 @@ /** Connectivity convenience methods for AI Core with deployment. */ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) -public class AiCoreServiceWithDeployment implements AiCoreServiceStub { +public class AiCoreDeployment implements AiCoreDestination { // the deployment id to be used - @Nonnull private final Function deploymentId; + @Nonnull private final Function deploymentId; // the base destination to be used @Nonnull private final Supplier destination; @@ -34,8 +34,8 @@ public class AiCoreServiceWithDeployment implements AiCoreServiceStub { * @param deploymentId The deployment id handler. * @param destination The destination handler. */ - public AiCoreServiceWithDeployment( - @Nonnull final Function deploymentId, + public AiCoreDeployment( + @Nonnull final Function deploymentId, @Nonnull final Supplier destination) { this(deploymentId, destination, null); } @@ -56,8 +56,8 @@ public Destination destination() { * @return A new instance of the AI Core service. */ @Nonnull - public AiCoreServiceWithDeployment withResourceGroup(@Nonnull final String resourceGroup) { - return new AiCoreServiceWithDeployment(deploymentId, destination, resourceGroup); + public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { + return new AiCoreDeployment(deploymentId, destination, resourceGroup); } /** @@ -67,8 +67,8 @@ public AiCoreServiceWithDeployment withResourceGroup(@Nonnull final String resou * @return A new instance of the AI Core service. */ @Nonnull - public AiCoreServiceWithDeployment withDestination(@Nonnull final Destination destination) { - return new AiCoreServiceWithDeployment(deploymentId, () -> destination, resourceGroup); + public AiCoreDeployment withDestination(@Nonnull final Destination destination) { + return new AiCoreDeployment(deploymentId, () -> destination, resourceGroup); } /** @@ -142,8 +142,7 @@ static boolean isDeploymentOfModel( */ @Nonnull static String getDeploymentId( - @Nonnull final AiCoreServiceWithDeployment core, - @Nonnull final Predicate predicate) + @Nonnull final AiCoreDeployment core, @Nonnull final Predicate predicate) throws NoSuchElementException { final var deploymentService = new DeploymentApi(core.client()); final var deployments = deploymentService.query(core.getDeploymentId()); diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceStub.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDestination.java similarity index 98% rename from core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceStub.java rename to core/src/main/java/com/sap/ai/sdk/core/AiCoreDestination.java index bf7e9c12..ba53a47e 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceStub.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDestination.java @@ -18,7 +18,7 @@ /** Container for an API client and destination. */ @FunctionalInterface -public interface AiCoreServiceStub { +public interface AiCoreDestination { /** * Get the destination. * @@ -50,6 +50,7 @@ default ApiClient client(@Nonnull final ClientOptions options) { } /** Options for the API client. */ + @FunctionalInterface interface ClientOptions { /** Serialize with null values. */ diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index e355a3d8..d26db9e4 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -1,7 +1,7 @@ package com.sap.ai.sdk.core; -import static com.sap.ai.sdk.core.AiCoreServiceWithDeployment.getDeploymentId; -import static com.sap.ai.sdk.core.AiCoreServiceWithDeployment.isDeploymentOfModel; +import static com.sap.ai.sdk.core.AiCoreDeployment.getDeploymentId; +import static com.sap.ai.sdk.core.AiCoreDeployment.isDeploymentOfModel; import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; @@ -17,7 +17,7 @@ /** Connectivity convenience methods for AI Core. */ @Slf4j @RequiredArgsConstructor(access = AccessLevel.PROTECTED) -public class AiCoreService implements AiCoreServiceStub { +public class AiCoreService implements AiCoreDestination { // the destination to be used for AI Core service calls. @Nonnull private final Supplier destination; @@ -51,8 +51,8 @@ public AiCoreService withDestination(@Nonnull final Destination destination) { * @return A new instance of the AI Core service. */ @Nonnull - public AiCoreServiceWithDeployment withDeployment(@Nonnull final String deploymentId) { - return new AiCoreServiceWithDeployment(c -> deploymentId, this::destination); + public AiCoreDeployment forDeployment(@Nonnull final String deploymentId) { + return new AiCoreDeployment(c -> deploymentId, this::destination); } /** @@ -62,9 +62,9 @@ public AiCoreServiceWithDeployment withDeployment(@Nonnull final String deployme * @return A new instance of the AI Core service. */ @Nonnull - public AiCoreServiceWithDeployment withDeploymentByModel(@Nonnull final String modelName) { + public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) { final Predicate p = deployment -> isDeploymentOfModel(modelName, deployment); - return new AiCoreServiceWithDeployment(c -> getDeploymentId(c, p), this::destination); + return new AiCoreDeployment(c -> getDeploymentId(c, p), this::destination); } /** @@ -74,9 +74,9 @@ public AiCoreServiceWithDeployment withDeploymentByModel(@Nonnull final String m * @return A new instance of the AI Core service. */ @Nonnull - public AiCoreServiceWithDeployment withDeploymentByScenario(@Nonnull final String scenarioId) { + public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId) { final Predicate p = deployment -> scenarioId.equals(deployment.getScenarioId()); - return new AiCoreServiceWithDeployment(c -> getDeploymentId(c, p), this::destination); + return new AiCoreDeployment(c -> getDeploymentId(c, p), this::destination); } /** diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java index 0f8fee34..25a5d922 100644 --- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java +++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java @@ -60,7 +60,7 @@ public final class OpenAiClient { public static OpenAiClient forModel(@Nonnull final OpenAiModel foundationModel) { final var destination = new AiCoreService() - .withDeploymentByModel(foundationModel.model()) + .forDeploymentByModel(foundationModel.model()) .withResourceGroup("default") .destination(); final var client = new OpenAiClient(destination); diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java index b80f666f..b093d52f 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java @@ -1,7 +1,6 @@ package com.sap.ai.sdk.app.controllers; import com.sap.ai.sdk.core.AiCoreService; - import com.sap.ai.sdk.core.client.ConfigurationApi; import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiConfigurationBaseData; @@ -16,6 +15,7 @@ import com.sap.ai.sdk.core.client.model.AiDeploymentTargetStatus; import com.sap.ai.sdk.core.client.model.AiParameterArgumentBinding; import com.sap.ai.sdk.foundationmodels.openai.OpenAiModel; +import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import java.util.List; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -31,7 +31,8 @@ @RequestMapping("/deployments") class DeploymentController { - private static final DeploymentApi API = new DeploymentApi(new AiCoreService().client()); + private static final ApiClient API_CLIENT = new AiCoreService().client(); + private static final DeploymentApi API = new DeploymentApi(API_CLIENT); /** * Create and delete a deployment with the Java specific configuration ID @@ -154,7 +155,7 @@ public AiDeploymentCreationResponse createConfigAndDeploy(final OpenAiModel mode .addParameterBindingsItem(modelVersion); final AiConfigurationCreationResponse configuration = - new ConfigurationApi(getClient()).create("default", configurationBaseData); + new ConfigurationApi(API_CLIENT).create("default", configurationBaseData); // Create a deployment from the configuration final var deploymentCreationRequest = diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java index 21ed0c96..5c73fc19 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java @@ -36,7 +36,7 @@ class OrchestrationController { private static final OrchestrationCompletionApi API = new OrchestrationCompletionApi( - new AiCoreService().withDeploymentByScenario("orchestration").client()); + new AiCoreService().forDeploymentByScenario("orchestration").client()); static final String MODEL = "gpt-35-turbo"; From 188e1505acd6e334e481d8238ff04dda0c76c0eb Mon Sep 17 00:00:00 2001 From: SAP Cloud SDK Bot Date: Mon, 7 Oct 2024 23:21:47 +0000 Subject: [PATCH 37/79] Formatting --- core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index d83ee8ab..f2b66f4c 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -11,7 +11,6 @@ import java.util.function.Predicate; import java.util.function.Supplier; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; From 62e0df03193ea51493e480d9acd3c51355307420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 8 Oct 2024 10:11:12 +0200 Subject: [PATCH 38/79] Fix code, tests are working --- .../com/sap/ai/sdk/core/AiCoreDeployment.java | 33 ++++++++----------- .../com/sap/ai/sdk/core/AiCoreService.java | 6 ++-- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index f2b66f4c..90a53641 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -5,6 +5,7 @@ import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination; +import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import java.util.Map; import java.util.NoSuchElementException; import java.util.function.Function; @@ -12,20 +13,23 @@ import java.util.function.Supplier; import javax.annotation.Nonnull; import lombok.AccessLevel; +import lombok.Getter; import lombok.RequiredArgsConstructor; /** Connectivity convenience methods for AI Core with deployment. */ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) public class AiCoreDeployment implements AiCoreDestination { - // the deployment id to be used - @Nonnull private final Function deploymentId; + // the deployment id handler to be used, based on resource group + @Nonnull private final Function deploymentId; - // the base destination to be used + // the base destination handler to be used @Nonnull private final Supplier destination; // the resource group, "default" if null - @Nonnull private final String resourceGroup; + @Getter(AccessLevel.PROTECTED) + @Nonnull + private final String resourceGroup; /** * Create a new instance of the AI Core service with a specific deployment id and destination. @@ -34,7 +38,7 @@ public class AiCoreDeployment implements AiCoreDestination { * @param destination The destination handler. */ public AiCoreDeployment( - @Nonnull final Function deploymentId, + @Nonnull final Function deploymentId, @Nonnull final Supplier destination) { this(deploymentId, destination, "default"); } @@ -82,16 +86,6 @@ protected void updateDestination( builder.header("AI-Resource-Group", getResourceGroup()); } - /** - * Get the resource group. - * - * @return The resource group. - */ - @Nonnull - protected String getResourceGroup() { - return resourceGroup == null ? "default" : resourceGroup; - } - /** * Get the deployment id. * @@ -99,7 +93,7 @@ protected String getResourceGroup() { */ @Nonnull protected String getDeploymentId() { - return deploymentId.apply(this); + return deploymentId.apply(getResourceGroup()); } /** This exists because getBackendDetails() is broken */ @@ -141,10 +135,11 @@ static boolean isDeploymentOfModel( */ @Nonnull static String getDeploymentId( - @Nonnull final AiCoreDeployment core, @Nonnull final Predicate predicate) + @Nonnull final ApiClient client, + @Nonnull final String resourceGroup, + @Nonnull final Predicate predicate) throws NoSuchElementException { - final var deploymentService = new DeploymentApi(core.client()); - final var deployments = deploymentService.query(core.getDeploymentId()); + final var deployments = new DeploymentApi(client).query(resourceGroup); final var first = deployments.getResources().stream().filter(predicate).map(AiDeployment::getId).findFirst(); diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index d26db9e4..773ceb51 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -52,7 +52,7 @@ public AiCoreService withDestination(@Nonnull final Destination destination) { */ @Nonnull public AiCoreDeployment forDeployment(@Nonnull final String deploymentId) { - return new AiCoreDeployment(c -> deploymentId, this::destination); + return new AiCoreDeployment(res -> deploymentId, this::destination); } /** @@ -64,7 +64,7 @@ public AiCoreDeployment forDeployment(@Nonnull final String deploymentId) { @Nonnull public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) { final Predicate p = deployment -> isDeploymentOfModel(modelName, deployment); - return new AiCoreDeployment(c -> getDeploymentId(c, p), this::destination); + return new AiCoreDeployment(res -> getDeploymentId(client(), res, p), this::destination); } /** @@ -76,7 +76,7 @@ public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) { @Nonnull public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId) { final Predicate p = deployment -> scenarioId.equals(deployment.getScenarioId()); - return new AiCoreDeployment(c -> getDeploymentId(c, p), this::destination); + return new AiCoreDeployment(res -> getDeploymentId(client(), res, p), this::destination); } /** From e24e4d62472d9eb74e8070da8dd2ed6c5028f30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 8 Oct 2024 11:11:03 +0200 Subject: [PATCH 39/79] Fix header provisioning --- .../com/sap/ai/sdk/core/AiCoreDeployment.java | 21 ++++++++++++++----- .../sap/ai/sdk/core/DestinationResolver.java | 4 +++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index 90a53641..197a5279 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -1,5 +1,8 @@ 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.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; @@ -19,6 +22,7 @@ /** Connectivity convenience methods for AI Core with deployment. */ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) public class AiCoreDeployment implements AiCoreDestination { + private static final String AI_RESOURCE_GROUP = "URL.headers.AI-Resource-Group"; // the deployment id handler to be used, based on resource group @Nonnull private final Function deploymentId; @@ -34,7 +38,7 @@ public class AiCoreDeployment implements AiCoreDestination { /** * Create a new instance of the AI Core service with a specific deployment id and destination. * - * @param deploymentId The deployment id handler. + * @param deploymentId The deployment id handler, based on resource group. * @param destination The destination handler. */ public AiCoreDeployment( @@ -83,7 +87,8 @@ public AiCoreDeployment withDestination(@Nonnull final Destination destination) protected void updateDestination( @Nonnull final DefaultHttpDestination.Builder builder, @Nonnull final HttpDestination d) { builder.uri(d.getUri().resolve("/v2/inference/deployments/%s/".formatted(getDeploymentId()))); - builder.header("AI-Resource-Group", getResourceGroup()); + builder.property(AI_CLIENT_TYPE_KEY, AI_CLIENT_TYPE_VALUE); + builder.property(AI_RESOURCE_GROUP, getResourceGroup()); } /** @@ -96,8 +101,14 @@ protected String getDeploymentId() { return deploymentId.apply(getResourceGroup()); } - /** This exists because getBackendDetails() is broken */ - static boolean isDeploymentOfModel( + /** + * This exists because getBackendDetails() is broken + * + * @param modelName The model name. + * @param deployment The deployment. + * @return true if the deployment is of the model. + */ + protected static boolean isDeploymentOfModel( @Nonnull final String modelName, @Nonnull final AiDeployment deployment) { final var deploymentDetails = deployment.getDetails(); // The AI Core specification doesn't mention that this is nullable, but it can be. @@ -134,7 +145,7 @@ static boolean isDeploymentOfModel( * @throws NoSuchElementException if no deployment is found for the scenario id. */ @Nonnull - static String getDeploymentId( + protected static String getDeploymentId( @Nonnull final ApiClient client, @Nonnull final String resourceGroup, @Nonnull final Predicate predicate) diff --git a/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java b/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java index af5433c5..03b826d2 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java @@ -23,6 +23,8 @@ /** Utility class to resolve the destination pointing to the AI Core service. */ @Slf4j class DestinationResolver { + static final String AI_CLIENT_TYPE_KEY = "URL.headers.AI-Client-Type"; + static final String AI_CLIENT_TYPE_VALUE = "AI SDK Java"; static ServiceBindingAccessor accessor = DefaultServiceBindingAccessor.getInstance(); /** @@ -60,7 +62,7 @@ static HttpDestination getDestination(@Nullable final String serviceKey) { // generated code this is actually necessary, because the generated code assumes this // path to be present on the destination .uri(destination.getUri().resolve("/v2")) - .header("AI-Client-Type", "AI SDK Java") + .property(AI_CLIENT_TYPE_KEY, AI_CLIENT_TYPE_VALUE) .build(); return destination; } From 662645661b98fbeb43f8223225d0ad4b1e8c0ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Fri, 11 Oct 2024 10:19:49 +0200 Subject: [PATCH 40/79] Add tests; Move stuff around --- core/pom.xml | 5 + .../com/sap/ai/sdk/core/AiCoreDeployment.java | 27 ++--- .../com/sap/ai/sdk/core/AiCoreService.java | 36 +++++- .../sap/ai/sdk/core/DestinationResolver.java | 51 ++++---- .../sap/ai/sdk/core/AiCoreServiceTest.java | 113 ++++++++++++++++++ 5 files changed, 190 insertions(+), 42 deletions(-) create mode 100644 core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java diff --git a/core/pom.xml b/core/pom.xml index ffdf6b29..90a4ce5c 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -124,6 +124,11 @@ assertj-core test + + org.mockito + mockito-core + test + diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index 197a5279..763dea8c 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -13,8 +13,7 @@ import java.util.NoSuchElementException; import java.util.function.Function; import java.util.function.Predicate; -import java.util.function.Supplier; -import javax.annotation.Nonnull; + import javax.annotation.Nonnull; import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -24,12 +23,12 @@ 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 resource group @Nonnull private final Function deploymentId; - // the base destination handler to be used - @Nonnull private final Supplier destination; - // the resource group, "default" if null @Getter(AccessLevel.PROTECTED) @Nonnull @@ -38,19 +37,18 @@ public class AiCoreDeployment implements AiCoreDestination { /** * Create a new instance of the AI Core service with a specific deployment id and destination. * + * @param service The AI Core Service instance. * @param deploymentId The deployment id handler, based on resource group. - * @param destination The destination handler. */ - public AiCoreDeployment( - @Nonnull final Function deploymentId, - @Nonnull final Supplier destination) { - this(deploymentId, destination, "default"); + public AiCoreDeployment( @Nonnull final AiCoreService service, + @Nonnull final Function deploymentId) { + this(service, deploymentId, "default"); } @Nonnull @Override public Destination destination() { - final var dest = destination.get().asHttp(); + final var dest = service.destination().asHttp(); final var builder = DefaultHttpDestination.fromDestination(dest); updateDestination(builder, dest); return builder.build(); @@ -64,7 +62,7 @@ public Destination destination() { */ @Nonnull public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { - return new AiCoreDeployment(deploymentId, destination, resourceGroup); + return new AiCoreDeployment( service,deploymentId, resourceGroup); } /** @@ -75,7 +73,7 @@ public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { */ @Nonnull public AiCoreDeployment withDestination(@Nonnull final Destination destination) { - return new AiCoreDeployment(deploymentId, () -> destination, resourceGroup); + return new AiCoreDeployment(service.withDestination(destination),deploymentId, resourceGroup); } /** @@ -86,8 +84,7 @@ public AiCoreDeployment withDestination(@Nonnull final Destination destination) */ protected void updateDestination( @Nonnull final DefaultHttpDestination.Builder builder, @Nonnull final HttpDestination d) { - builder.uri(d.getUri().resolve("/v2/inference/deployments/%s/".formatted(getDeploymentId()))); - builder.property(AI_CLIENT_TYPE_KEY, AI_CLIENT_TYPE_VALUE); + builder.uri(d.getUri().resolve("inference/deployments/%s/".formatted(getDeploymentId()))); builder.property(AI_RESOURCE_GROUP, getResourceGroup()); } diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index 773ceb51..c9afa372 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -2,17 +2,25 @@ import static com.sap.ai.sdk.core.AiCoreDeployment.getDeploymentId; import static com.sap.ai.sdk.core.AiCoreDeployment.isDeploymentOfModel; +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.sap.ai.sdk.core.client.model.AiDeployment; +import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; +import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationProperties; +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 java.util.function.Predicate; import java.util.function.Supplier; import javax.annotation.Nonnull; + +import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; /** Connectivity convenience methods for AI Core. */ @Slf4j @@ -41,7 +49,19 @@ public Destination destination() { */ @Nonnull public AiCoreService withDestination(@Nonnull final Destination destination) { - return new AiCoreService(() -> destination); + + final var newDestinationBuilder = DefaultHttpDestination.fromDestination(destination); + final var newDestination = refineDestination(newDestinationBuilder, destination); + return new AiCoreService(() -> newDestination); + } + + @Nonnull + protected Destination refineDestination( + @Nonnull final DefaultHttpDestination.Builder builder, + @Nonnull final DestinationProperties properties) { + String uri = properties.get(DestinationProperty.URI).get(); + uri = StringUtils.appendIfMissing(uri, "/") + "v2/"; + return builder.uri(uri).property(AI_CLIENT_TYPE_KEY, AI_CLIENT_TYPE_VALUE).build(); } /** @@ -52,7 +72,7 @@ public AiCoreService withDestination(@Nonnull final Destination destination) { */ @Nonnull public AiCoreDeployment forDeployment(@Nonnull final String deploymentId) { - return new AiCoreDeployment(res -> deploymentId, this::destination); + return new AiCoreDeployment(this, res -> deploymentId); } /** @@ -64,7 +84,7 @@ public AiCoreDeployment forDeployment(@Nonnull final String deploymentId) { @Nonnull public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) { final Predicate p = deployment -> isDeploymentOfModel(modelName, deployment); - return new AiCoreDeployment(res -> getDeploymentId(client(), res, p), this::destination); + return new AiCoreDeployment(this, res -> getDeploymentId(client(), res, p)); } /** @@ -76,7 +96,7 @@ public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) { @Nonnull public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId) { final Predicate p = deployment -> scenarioId.equals(deployment.getScenarioId()); - return new AiCoreDeployment(res -> getDeploymentId(client(), res, p), this::destination); + return new AiCoreDeployment(this, res -> getDeploymentId(client(), res, p)); } /** @@ -88,7 +108,13 @@ public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId */ @Nonnull protected static Destination getDefaultDestination() - throws DestinationAccessException, DestinationNotFoundException { + throws DestinationAccessException, DestinationNotFoundException { + final var serviceKey = System.getenv("AICORE_SERVICE_KEY"); + return DestinationResolver.getDestination(serviceKey); + } + @Nonnull + protected static ApiClient getApiClient() + throws DestinationAccessException, DestinationNotFoundException { final var serviceKey = System.getenv("AICORE_SERVICE_KEY"); return DestinationResolver.getDestination(serviceKey); } diff --git a/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java b/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java index 03b826d2..4c7c0ce1 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java @@ -1,6 +1,8 @@ package com.sap.ai.sdk.core; +import static com.google.common.collect.Iterables.tryFind; import static com.sap.cloud.sdk.cloudplatform.connectivity.OnBehalfOf.TECHNICAL_USER_PROVIDER; +import static com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions.forService; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; @@ -10,14 +12,16 @@ import com.sap.cloud.environment.servicebinding.api.ServiceBindingAccessor; import com.sap.cloud.environment.servicebinding.api.ServiceBindingMerger; import com.sap.cloud.environment.servicebinding.api.ServiceIdentifier; -import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationLoader; -import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions; import java.util.HashMap; import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import lombok.AccessLevel; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; /** Utility class to resolve the destination pointing to the AI Core service. */ @@ -25,7 +29,10 @@ class DestinationResolver { static final String AI_CLIENT_TYPE_KEY = "URL.headers.AI-Client-Type"; static final String AI_CLIENT_TYPE_VALUE = "AI SDK Java"; - static ServiceBindingAccessor accessor = DefaultServiceBindingAccessor.getInstance(); + + @Getter(AccessLevel.PROTECTED) + @Nonnull + private static ServiceBindingAccessor accessor = DefaultServiceBindingAccessor.getInstance(); /** * For testing only @@ -37,34 +44,24 @@ class DestinationResolver { */ @SuppressWarnings("UnstableApiUsage") static HttpDestination getDestination(@Nullable final String serviceKey) { - final var serviceKeyPresent = serviceKey != null; - final var aiCoreBindingPresent = - accessor.getServiceBindings().stream() - .anyMatch( - serviceBinding -> - ServiceIdentifier.AI_CORE.equals( - serviceBinding.getServiceIdentifier().orElse(null))); + final Predicate aiCore = Optional.of(ServiceIdentifier.AI_CORE)::equals; + final var serviceBindings = accessor.getServiceBindings(); + final var aiCoreBinding = tryFind(serviceBindings, b -> aiCore.test(b.getServiceIdentifier())); - if (!aiCoreBindingPresent && serviceKeyPresent) { + final var serviceKeyPresent = serviceKey != null; + if (!aiCoreBinding.isPresent() && serviceKeyPresent) { addServiceBinding(serviceKey); } // get a destination pointing to the AI Core service final var opts = - ServiceBindingDestinationOptions.forService(ServiceIdentifier.AI_CORE) + (aiCoreBinding.isPresent() + ? forService(aiCoreBinding.get()) + : forService(ServiceIdentifier.AI_CORE)) .onBehalfOf(TECHNICAL_USER_PROVIDER) .build(); - var destination = ServiceBindingDestinationLoader.defaultLoaderChain().getDestination(opts); - destination = - DefaultHttpDestination.fromDestination(destination) - // append the /v2 path here, so we don't have to do it in every request when using the - // generated code this is actually necessary, because the generated code assumes this - // path to be present on the destination - .uri(destination.getUri().resolve("/v2")) - .property(AI_CLIENT_TYPE_KEY, AI_CLIENT_TYPE_VALUE) - .build(); - return destination; + return ServiceBindingDestinationLoader.defaultLoaderChain().getDestination(opts); } /** @@ -106,4 +103,14 @@ public AiCoreCredentialsInvalidException( super(message, cause); } } + + /** + * For testing set the accessor to be used for service binding resolution. + * + * @param accessor The accessor to be used for service binding resolution. + */ + static void setAccessor(@Nullable ServiceBindingAccessor accessor) { + DestinationResolver.accessor = + accessor == null ? DefaultServiceBindingAccessor.getInstance() : accessor; + } } diff --git a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java new file mode 100644 index 00000000..2d1c52cd --- /dev/null +++ b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java @@ -0,0 +1,113 @@ +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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.STRING; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.sap.cloud.environment.servicebinding.api.DefaultServiceBinding; +import com.sap.cloud.environment.servicebinding.api.ServiceBindingAccessor; +import com.sap.cloud.environment.servicebinding.api.ServiceIdentifier; +import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; +import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationProperty; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +public class AiCoreServiceTest { + + // setup + private static final Map URLS = Map.of("AI_API_URL", "https://srv"); + private static final Map CREDENTIALS = + Map.of("clientid", "id", "clientsecret", "pw", "url", "https://auth", "serviceurls", URLS); + private static final DefaultServiceBinding BINDING = + DefaultServiceBinding.builder() + .copy(Map.of()) + .withServiceIdentifier(ServiceIdentifier.AI_CORE) + .withCredentials(CREDENTIALS) + .build(); + + @AfterEach + void tearDown() { + DestinationResolver.setAccessor(null); + } + + @Test + void testLazyEvaluation() { + // setup + final var accessor = mock(ServiceBindingAccessor.class); + DestinationResolver.setAccessor(accessor); + + // execution without errors + new AiCoreService(); + + // verification + verify(accessor, never()).getServiceBindings(); + } + + @Test + void testSimpleCase() { + // setup + final var accessor = mock(ServiceBindingAccessor.class); + DestinationResolver.setAccessor(accessor); + doReturn(List.of(BINDING)).when(accessor).getServiceBindings(); + + // execution without errors + final var core = new AiCoreService(); + final var destination = core.destination(); + final var client = core.client(); + + // verification + assertThat(destination.get(DestinationProperty.URI)).contains("https://srv/v2"); + assertThat(destination.get(DestinationProperty.AUTH_TYPE)).isEmpty(); + assertThat(destination.get(DestinationProperty.NAME)).singleElement(STRING).contains("aicore"); + assertThat(destination.get(AI_CLIENT_TYPE_KEY)).contains(AI_CLIENT_TYPE_VALUE); + assertThat(client.getBasePath()).isEqualTo("https://srv/v2"); + verify(accessor, times(2)).getServiceBindings(); + } + + @Test + void testBaseDestination() { + // setup + DestinationResolver.setAccessor(Collections::emptyList); + + // execution without errors + final var customDestination = DefaultHttpDestination.builder("https://foo.bar").build(); + final var core = new AiCoreService().withDestination(customDestination); + final var destination = core.destination(); + final var client = core.client(); + + // verification + assertThat(destination.get(DestinationProperty.URI)).contains("https://foo.bar"); + assertThat(destination.get(DestinationProperty.AUTH_TYPE)).isEmpty(); + assertThat(destination.get(DestinationProperty.NAME)).isEmpty(); + assertThat(destination.get(AI_CLIENT_TYPE_KEY)).contains(AI_CLIENT_TYPE_VALUE); + assertThat(client.getBasePath()).isEqualTo("https://foo.bar/v2/"); + } + + @Test + void testDeploymnt() { + final var accessor = mock(ServiceBindingAccessor.class); + DestinationResolver.setAccessor(accessor); + doReturn(List.of(BINDING)).when(accessor).getServiceBindings(); + + // execution without errors + final var destination = new AiCoreService().destination(); + final var ciient = new AiCoreService().client(); + + // verification + assertThat(destination.get(DestinationProperty.URI)).contains("https://srv/v2/"); + assertThat(destination.get(DestinationProperty.AUTH_TYPE)).isEmpty(); + assertThat(destination.get(DestinationProperty.NAME)).singleElement(STRING).contains("aicore"); + assertThat(destination.get(AI_CLIENT_TYPE_KEY)).contains(AI_CLIENT_TYPE_VALUE); + assertThat(ciient.getBasePath()).isEqualTo("https://srv/v2/"); + verify(accessor, times(2)).getServiceBindings(); + } +} From e217da9afbbdf3d782f1b7b6ed64b9fe36d08935 Mon Sep 17 00:00:00 2001 From: I538344 Date: Fri, 11 Oct 2024 11:40:18 +0200 Subject: [PATCH 41/79] WiP --- .../com/sap/ai/sdk/core/AiCoreDeployment.java | 26 +++++-------------- .../com/sap/ai/sdk/core/AiCoreService.java | 15 ++++++----- .../com/sap/ai/sdk/core/DeploymentCache.java | 8 +++--- .../java/com/sap/ai/sdk/core/CacheTest.java | 3 ++- .../sap/ai/sdk/core/WireMockTestServer.java | 9 +++---- 5 files changed, 23 insertions(+), 38 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index 197a5279..292dd187 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -3,7 +3,6 @@ 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.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; @@ -11,8 +10,6 @@ import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import java.util.Map; import java.util.NoSuchElementException; -import java.util.function.Function; -import java.util.function.Predicate; import java.util.function.Supplier; import javax.annotation.Nonnull; import lombok.AccessLevel; @@ -24,9 +21,6 @@ public class AiCoreDeployment implements AiCoreDestination { private static final String AI_RESOURCE_GROUP = "URL.headers.AI-Resource-Group"; - // the deployment id handler to be used, based on resource group - @Nonnull private final Function deploymentId; - // the base destination handler to be used @Nonnull private final Supplier destination; @@ -38,13 +32,11 @@ public class AiCoreDeployment implements AiCoreDestination { /** * Create a new instance of the AI Core service with a specific deployment id and destination. * - * @param deploymentId The deployment id handler, based on resource group. * @param destination The destination handler. */ public AiCoreDeployment( - @Nonnull final Function deploymentId, @Nonnull final Supplier destination) { - this(deploymentId, destination, "default"); + this( destination, "default"); } @Nonnull @@ -64,7 +56,7 @@ public Destination destination() { */ @Nonnull public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { - return new AiCoreDeployment(deploymentId, destination, resourceGroup); + return new AiCoreDeployment(destination, resourceGroup); } /** @@ -75,7 +67,7 @@ public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { */ @Nonnull public AiCoreDeployment withDestination(@Nonnull final Destination destination) { - return new AiCoreDeployment(deploymentId, () -> destination, resourceGroup); + return new AiCoreDeployment(() -> destination, resourceGroup); } /** @@ -145,16 +137,10 @@ protected static boolean isDeploymentOfModel( * @throws NoSuchElementException if no deployment is found for the scenario id. */ @Nonnull - protected static String getDeploymentId( + protected String getDeploymentId( @Nonnull final ApiClient client, - @Nonnull final String resourceGroup, - @Nonnull final Predicate predicate) + @Nonnull final String name) throws NoSuchElementException { - final var deployments = new DeploymentApi(client).query(resourceGroup); - - final var first = - deployments.getResources().stream().filter(predicate).map(AiDeployment::getId).findFirst(); - return first.orElseThrow( - () -> new NoSuchElementException("No deployment found with scenario id orchestration")); + return DeploymentCache.getDeploymentId(resourceGroup, name); } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index 773ceb51..c960d39d 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -7,7 +7,8 @@ import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException; import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationNotFoundException; -import java.util.function.Predicate; + +import java.util.NoSuchElementException; import java.util.function.Supplier; import javax.annotation.Nonnull; import lombok.AccessLevel; @@ -62,9 +63,9 @@ public AiCoreDeployment forDeployment(@Nonnull final String deploymentId) { * @return A new instance of the AI Core service. */ @Nonnull - public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) { - final Predicate p = deployment -> isDeploymentOfModel(modelName, deployment); - return new AiCoreDeployment(res -> getDeploymentId(client(), res, p), this::destination); + public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) throws NoSuchElementException + { + return new AiCoreDeployment(this::destination).getDeploymentId(client(), modelName); } /** @@ -74,9 +75,9 @@ public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) { * @return A new instance of the AI Core service. */ @Nonnull - public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId) { - final Predicate p = deployment -> scenarioId.equals(deployment.getScenarioId()); - return new AiCoreDeployment(res -> getDeploymentId(client(), res, p), this::destination); + public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId) throws NoSuchElementException + { + return new AiCoreDeployment(this::destination).getDeploymentId(client(), scenarioId); } /** diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index b79beea0..b9454976 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -60,7 +60,7 @@ public static void clearCache() { */ public static void loadCache() { try { - final var deployments = API.deploymentQuery("default").getResources(); + final var deployments = API.query("default").getResources(); deployments.forEach(deployment -> CACHE.put(getModelName(deployment), deployment.getId())); } catch (final OpenApiRequestException e) { log.error("Failed to load deployments into cache", e); @@ -102,8 +102,7 @@ public static String getDeploymentId( private static String getOrchestrationDeployment(@Nonnull final String resourceGroup) throws NoSuchElementException { final var deployments = - API.deploymentQuery( - resourceGroup, null, null, "orchestration", "RUNNING", null, null, null); + API.query(resourceGroup, null, null, "orchestration", "RUNNING", null, null, null); return deployments.getResources().stream() .map(AiDeployment::getId) @@ -127,8 +126,7 @@ private static String getDeploymentForModel( @Nonnull final String resourceGroup, @Nonnull final String modelName) throws NoSuchElementException { final var deployments = - API.deploymentQuery( - resourceGroup, null, null, "foundation-models", "RUNNING", null, null, null); + API.query(resourceGroup, null, null, "foundation-models", "RUNNING", null, null, null); return deployments.getResources().stream() .filter(deployment -> modelName.equals(getModelName(deployment))) diff --git a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java index 7837322e..5d304a18 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java @@ -11,7 +11,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class CacheTest extends WireMockTestServer { +class CacheTest extends WireMockTestServer +{ @BeforeEach void setupCache() { diff --git a/core/src/test/java/com/sap/ai/sdk/core/WireMockTestServer.java b/core/src/test/java/com/sap/ai/sdk/core/WireMockTestServer.java index c9870791..d94f5b93 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/WireMockTestServer.java +++ b/core/src/test/java/com/sap/ai/sdk/core/WireMockTestServer.java @@ -1,10 +1,9 @@ -package com.sap.ai.sdk.core.client; +package com.sap.ai.sdk.core; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; -import com.sap.ai.sdk.core.AiCoreService; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; @@ -16,9 +15,9 @@ public abstract class WireMockTestServer { private static final WireMockConfiguration WIREMOCK_CONFIGURATION = wireMockConfig().dynamicPort(); - static WireMockServer wireMockServer; - static Destination destination; - static ApiClient client; + public static WireMockServer wireMockServer; + public static Destination destination; + public static ApiClient client; @BeforeAll static void setup() { From ba53051eb6392d0f863939275950f9ec5b787462 Mon Sep 17 00:00:00 2001 From: I538344 Date: Fri, 11 Oct 2024 11:41:47 +0200 Subject: [PATCH 42/79] Delete Core --- .../main/java/com/sap/ai/sdk/core/Core.java | 233 ------------------ 1 file changed, 233 deletions(-) delete mode 100644 core/src/main/java/com/sap/ai/sdk/core/Core.java diff --git a/core/src/main/java/com/sap/ai/sdk/core/Core.java b/core/src/main/java/com/sap/ai/sdk/core/Core.java deleted file mode 100644 index e843a4de..00000000 --- a/core/src/main/java/com/sap/ai/sdk/core/Core.java +++ /dev/null @@ -1,233 +0,0 @@ -package com.sap.ai.sdk.core; - -import static com.sap.cloud.sdk.cloudplatform.connectivity.OnBehalfOf.TECHNICAL_USER_PROVIDER; - -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.sap.ai.sdk.core.client.DeploymentApi; -import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingAccessor; -import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingBuilder; -import com.sap.cloud.environment.servicebinding.api.ServiceBindingAccessor; -import com.sap.cloud.environment.servicebinding.api.ServiceBindingMerger; -import com.sap.cloud.environment.servicebinding.api.ServiceIdentifier; -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.HttpDestination; -import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationLoader; -import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions; -import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; -import java.util.HashMap; -import java.util.List; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -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 -public class Core { - - // for testing only, will be removed once we make this class an instance - static { - if (DeploymentCache.isEmpty()) { - DeploymentCache.lazyLoaded(new DeploymentApi(getClient())); - } - } - - /** - * Requires an AI Core service binding. - * - * @param resourceGroup the resource group. - * @return a generic Orchestration ApiClient. - */ - @Nonnull - public static ApiClient getOrchestrationClient(@Nonnull final String resourceGroup) { - return getClient( - getDestinationForDeployment( - DeploymentCache.getDeploymentId(resourceGroup, "orchestration"), resourceGroup)); - } - - /** - * Requires an AI Core service binding OR a service key in the environment variable {@code - * AICORE_SERVICE_KEY}. - * - * @return a generic AI Core ApiClient. - */ - @Nonnull - public static ApiClient getClient() { - return getClient(getDestination()); - } - - /** - * Get a generic AI Core ApiClient for testing purposes. - * - * @param destination The destination to use. - * @return a generic AI Core ApiClient. - */ - @Nonnull - @SuppressWarnings("UnstableApiUsage") - public static ApiClient getClient(@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 restTemplate = new RestTemplate(); - restTemplate.getMessageConverters().stream() - .filter(MappingJackson2HttpMessageConverter.class::isInstance) - .map(MappingJackson2HttpMessageConverter.class::cast) - .forEach(converter -> converter.setObjectMapper(objectMapper)); - restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(httpRequestFactory)); - - return new ApiClient(restTemplate).setBasePath(destination.asHttp().getUri().toString()); - } - - /** - * Get a destination pointing to the AI Core service. - * - *

      Requires an AI Core service binding OR a service key in the environment variable {@code - * AICORE_SERVICE_KEY}. - * - * @return a destination pointing to the AI Core service. - */ - @Nonnull - public static Destination getDestination() { - final var serviceKey = System.getenv("AICORE_SERVICE_KEY"); - return getDestination(serviceKey); - } - - /** - * For testing only - * - *

      Get a destination pointing to the AI Core service. - * - * @param serviceKey The service key in JSON format. - * @return a destination pointing to the AI Core service. - */ - static HttpDestination getDestination(@Nullable final String serviceKey) { - final var serviceKeyPresent = serviceKey != null; - final var aiCoreBindingPresent = - DefaultServiceBindingAccessor.getInstance().getServiceBindings().stream() - .anyMatch( - serviceBinding -> - ServiceIdentifier.AI_CORE.equals( - serviceBinding.getServiceIdentifier().orElse(null))); - - if (!aiCoreBindingPresent && serviceKeyPresent) { - addServiceBinding(serviceKey); - } - - // get a destination pointing to the AI Core service - final var opts = - ServiceBindingDestinationOptions.forService(ServiceIdentifier.AI_CORE) - .onBehalfOf(TECHNICAL_USER_PROVIDER) - .build(); - var destination = ServiceBindingDestinationLoader.defaultLoaderChain().getDestination(opts); - - destination = - DefaultHttpDestination.fromDestination(destination) - // append the /v2 path here, so we don't have to do it in every request when using the - // generated code this is actually necessary, because the generated code assumes this - // path to be present on the destination - .uri(destination.getUri().resolve("/v2")) - .header("AI-Client-Type", "AI SDK Java") - .build(); - return destination; - } - - /** - * Set the AI Core service key as the service binding. This is used for local testing. - * - * @param serviceKey The service key in JSON format. - * @throws AiCoreCredentialsInvalidException if the JSON service key cannot be parsed. - */ - private static void addServiceBinding(@Nonnull final String serviceKey) { - log.info( - """ - Found a service key in environment variable "AICORE_SERVICE_KEY". - Using a service key is recommended for local testing only. - Bind the AI Core service to the application for productive usage."""); - - var credentials = new HashMap(); - try { - credentials = new ObjectMapper().readValue(serviceKey, new TypeReference<>() {}); - } catch (JsonProcessingException e) { - throw new AiCoreCredentialsInvalidException( - "Error in parsing service key from the \"AICORE_SERVICE_KEY\" environment variable.", e); - } - - final var binding = - new DefaultServiceBindingBuilder() - .withServiceIdentifier(ServiceIdentifier.AI_CORE) - .withCredentials(credentials) - .build(); - final ServiceBindingAccessor accessor = DefaultServiceBindingAccessor.getInstance(); - final var newAccessor = - new ServiceBindingMerger( - List.of(accessor, () -> List.of(binding)), ServiceBindingMerger.KEEP_EVERYTHING); - DefaultServiceBindingAccessor.setInstance(newAccessor); - } - - /** Exception thrown when the JSON AI Core service key is invalid. */ - static class AiCoreCredentialsInvalidException extends RuntimeException { - public AiCoreCredentialsInvalidException( - @Nonnull final String message, @Nonnull final Throwable cause) { - super(message, cause); - } - } - - /** - * Get a destination pointing to the inference endpoint of a deployment on AI Core. Requires an - * AI Core service binding. - * - * @param deploymentId The deployment id. - * @param resourceGroup The resource group. - * @return a destination that can be used for inference calls. - */ - @Nonnull - public static Destination getDestinationForDeployment( - @Nonnull final String deploymentId, @Nonnull final String resourceGroup) { - final var destination = getDestination().asHttp(); - final DefaultHttpDestination.Builder builder = - DefaultHttpDestination.fromDestination(destination) - .uri( - destination - .getUri() - .resolve("/v2/inference/deployments/%s/".formatted(deploymentId))); - - builder.header("AI-Resource-Group", resourceGroup); - - return builder.build(); - } - - /** - * Get a destination pointing to the inference endpoint of a deployment on AI Core. Requires an - * AI Core service binding. - * - * @param modelName The name of the foundation model that is used by a deployment. - * @param resourceGroup The resource group. - * @return a destination that can be used for inference calls. - */ - @Nonnull - public static Destination getDestinationForModel( - @Nonnull final String resourceGroup, @Nonnull final String modelName) { - return getDestinationForDeployment( - DeploymentCache.getDeploymentId(resourceGroup, modelName), resourceGroup); - } -} From c696c03d640206707d7740cdf6b4a6a0164b3c3b Mon Sep 17 00:00:00 2001 From: SAP Cloud SDK Bot Date: Fri, 11 Oct 2024 09:42:29 +0000 Subject: [PATCH 43/79] Formatting --- .../java/com/sap/ai/sdk/core/AiCoreDeployment.java | 9 +++------ .../java/com/sap/ai/sdk/core/AiCoreService.java | 13 ++++--------- .../test/java/com/sap/ai/sdk/core/CacheTest.java | 3 +-- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index 292dd187..6dc5e170 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -34,9 +34,8 @@ public class AiCoreDeployment implements AiCoreDestination { * * @param destination The destination handler. */ - public AiCoreDeployment( - @Nonnull final Supplier destination) { - this( destination, "default"); + public AiCoreDeployment(@Nonnull final Supplier destination) { + this(destination, "default"); } @Nonnull @@ -137,9 +136,7 @@ protected static boolean isDeploymentOfModel( * @throws NoSuchElementException if no deployment is found for the scenario id. */ @Nonnull - protected String getDeploymentId( - @Nonnull final ApiClient client, - @Nonnull final String name) + protected String getDeploymentId(@Nonnull final ApiClient client, @Nonnull final String name) throws NoSuchElementException { return DeploymentCache.getDeploymentId(resourceGroup, name); } diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index c960d39d..3119dae2 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -1,13 +1,8 @@ package com.sap.ai.sdk.core; -import static com.sap.ai.sdk.core.AiCoreDeployment.getDeploymentId; -import static com.sap.ai.sdk.core.AiCoreDeployment.isDeploymentOfModel; - -import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException; import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationNotFoundException; - import java.util.NoSuchElementException; import java.util.function.Supplier; import javax.annotation.Nonnull; @@ -63,8 +58,8 @@ public AiCoreDeployment forDeployment(@Nonnull final String deploymentId) { * @return A new instance of the AI Core service. */ @Nonnull - public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) throws NoSuchElementException - { + public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) + throws NoSuchElementException { return new AiCoreDeployment(this::destination).getDeploymentId(client(), modelName); } @@ -75,8 +70,8 @@ public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) th * @return A new instance of the AI Core service. */ @Nonnull - public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId) throws NoSuchElementException - { + public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId) + throws NoSuchElementException { return new AiCoreDeployment(this::destination).getDeploymentId(client(), scenarioId); } diff --git a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java index 5d304a18..7837322e 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java @@ -11,8 +11,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class CacheTest extends WireMockTestServer -{ +class CacheTest extends WireMockTestServer { @BeforeEach void setupCache() { From 37db59f5dc80e70e1e9ab8b08af758ed8a4776f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Mon, 14 Oct 2024 11:14:27 +0200 Subject: [PATCH 44/79] make base class extensible --- .../com/sap/ai/sdk/core/AiCoreDeployment.java | 37 +++++---- .../sap/ai/sdk/core/AiCoreDestination.java | 75 +------------------ .../com/sap/ai/sdk/core/AiCoreService.java | 39 +++------- .../ai/sdk/core/AiCoreServiceExtension.java | 68 +++++++++++++++++ 4 files changed, 105 insertions(+), 114 deletions(-) create mode 100644 core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceExtension.java diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index 763dea8c..2b040cf6 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -1,8 +1,5 @@ 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.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; @@ -13,7 +10,8 @@ import java.util.NoSuchElementException; import java.util.function.Function; import java.util.function.Predicate; - import javax.annotation.Nonnull; +import java.util.function.Supplier; +import javax.annotation.Nonnull; import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -24,7 +22,10 @@ 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; + @Nonnull private final AiCoreServiceExtension service; + + // the destination handler to be used + @Nonnull private final Supplier destination; // the deployment id handler to be used, based on resource group @Nonnull private final Function deploymentId; @@ -40,18 +41,24 @@ public class AiCoreDeployment implements AiCoreDestination { * @param service The AI Core Service instance. * @param deploymentId The deployment id handler, based on resource group. */ - public AiCoreDeployment( @Nonnull final AiCoreService service, - @Nonnull final Function deploymentId) { - this(service, deploymentId, "default"); + public AiCoreDeployment( + @Nonnull final AiCoreService service, @Nonnull final Function deploymentId) { + this(service, service::destination, deploymentId, "default"); } @Nonnull @Override public Destination destination() { - final var dest = service.destination().asHttp(); + final var dest = destination.get().asHttp(); final var builder = DefaultHttpDestination.fromDestination(dest); - updateDestination(builder, dest); - return builder.build(); + return createDestination(builder, dest); + } + + @Nonnull + @Override + public ApiClient client() { + final var destination = destination(); + return service.createApiClient(destination); } /** @@ -62,7 +69,7 @@ public Destination destination() { */ @Nonnull public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { - return new AiCoreDeployment( service,deploymentId, resourceGroup); + return new AiCoreDeployment(service, destination, deploymentId, resourceGroup); } /** @@ -73,7 +80,7 @@ public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { */ @Nonnull public AiCoreDeployment withDestination(@Nonnull final Destination destination) { - return new AiCoreDeployment(service.withDestination(destination),deploymentId, resourceGroup); + return new AiCoreDeployment(service, () -> destination, deploymentId, resourceGroup); } /** @@ -81,11 +88,13 @@ public AiCoreDeployment withDestination(@Nonnull final Destination destination) * * @param builder The new destination builder. * @param d The original destination. + * @return The updated destination. */ - protected void updateDestination( + protected Destination createDestination( @Nonnull final DefaultHttpDestination.Builder builder, @Nonnull final HttpDestination d) { builder.uri(d.getUri().resolve("inference/deployments/%s/".formatted(getDeploymentId()))); builder.property(AI_RESOURCE_GROUP, getResourceGroup()); + return service.createDestination(builder, d); } /** diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDestination.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDestination.java index ba53a47e..a61bd494 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDestination.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDestination.java @@ -1,20 +1,8 @@ package com.sap.ai.sdk.core; -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.Destination; import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; -import java.util.function.Function; import javax.annotation.Nonnull; -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; /** Container for an API client and destination. */ @FunctionalInterface @@ -34,66 +22,7 @@ public interface AiCoreDestination { */ @Nonnull default ApiClient client() { - return client(ClientOptions.SERIALIZE_WITHOUT_NULL_VALUES); - } - - /** - * Get the API client with options. - * - * @param options the options - * @return the API client - */ - @Nonnull - default ApiClient client(@Nonnull final ClientOptions options) { - final Destination destination = destination(); - return options.getInitializer().apply(destination); - } - - /** Options for the API client. */ - @FunctionalInterface - interface ClientOptions { - - /** Serialize with null values. */ - ClientOptions SERIALIZE_WITH_NULL_VALUES = () -> ApiClient::new; - - /** Serialize without null values. */ - ClientOptions SERIALIZE_WITHOUT_NULL_VALUES = () -> ClientOptions::withoutNull; - - /** - * Get the initializer for the API client. - * - * @return the initializer - */ - @Nonnull - Function getInitializer(); - - /** - * Helper method to Serialize without null values. - * - * @param destination the destination - * @return the API client - */ - @SuppressWarnings("UnstableApiUsage") - @Nonnull - private static ApiClient withoutNull(@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()); - } + final var destination = destination(); + return new ApiClient(destination); } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index c9afa372..7125eb19 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -2,30 +2,24 @@ import static com.sap.ai.sdk.core.AiCoreDeployment.getDeploymentId; import static com.sap.ai.sdk.core.AiCoreDeployment.isDeploymentOfModel; -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.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; -import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationProperties; -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.function.Predicate; import java.util.function.Supplier; import javax.annotation.Nonnull; - -import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; /** Connectivity convenience methods for AI Core. */ @Slf4j @RequiredArgsConstructor(access = AccessLevel.PROTECTED) -public class AiCoreService implements AiCoreDestination { +public class AiCoreService extends AiCoreServiceExtension implements AiCoreDestination { // the destination to be used for AI Core service calls. @Nonnull private final Supplier destination; @@ -38,7 +32,9 @@ public AiCoreService() { @Nonnull @Override public Destination destination() { - return destination.get(); + final var dest = destination.get(); + final var newDestinationBuilder = DefaultHttpDestination.fromDestination(dest); + return createDestination(newDestinationBuilder, dest); } /** @@ -49,19 +45,7 @@ public Destination destination() { */ @Nonnull public AiCoreService withDestination(@Nonnull final Destination destination) { - - final var newDestinationBuilder = DefaultHttpDestination.fromDestination(destination); - final var newDestination = refineDestination(newDestinationBuilder, destination); - return new AiCoreService(() -> newDestination); - } - - @Nonnull - protected Destination refineDestination( - @Nonnull final DefaultHttpDestination.Builder builder, - @Nonnull final DestinationProperties properties) { - String uri = properties.get(DestinationProperty.URI).get(); - uri = StringUtils.appendIfMissing(uri, "/") + "v2/"; - return builder.uri(uri).property(AI_CLIENT_TYPE_KEY, AI_CLIENT_TYPE_VALUE).build(); + return new AiCoreService(() -> destination); } /** @@ -108,14 +92,15 @@ public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId */ @Nonnull protected static Destination getDefaultDestination() - throws DestinationAccessException, DestinationNotFoundException { + throws DestinationAccessException, DestinationNotFoundException { final var serviceKey = System.getenv("AICORE_SERVICE_KEY"); return DestinationResolver.getDestination(serviceKey); } + @Nonnull - protected static ApiClient getApiClient() - throws DestinationAccessException, DestinationNotFoundException { - final var serviceKey = System.getenv("AICORE_SERVICE_KEY"); - return DestinationResolver.getDestination(serviceKey); + @Override + public ApiClient client() { + final var destination = destination(); + return createApiClient(destination); } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceExtension.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceExtension.java new file mode 100644 index 00000000..b6409283 --- /dev/null +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceExtension.java @@ -0,0 +1,68 @@ +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.DestinationProperties; +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 javax.annotation.Nonnull; +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; + +abstract class AiCoreServiceExtension { + + @Nonnull + protected Destination createDestination( + @Nonnull final DefaultHttpDestination.Builder builder, + @Nonnull final DestinationProperties properties) { + String uri = properties.get(DestinationProperty.URI).get(); + if (!uri.endsWith("/")) { + uri = uri + "/"; + } + uri = uri + "v2/"; + return builder.uri(uri).property(AI_CLIENT_TYPE_KEY, AI_CLIENT_TYPE_VALUE).build(); + } + + /** + * 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 createApiClient(@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()); + } +} From 583fd498879b1889befe9cff615e1b0428c1d913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Mon, 14 Oct 2024 14:15:30 +0200 Subject: [PATCH 45/79] work in progress --- core/pom.xml | 4 ++ .../com/sap/ai/sdk/core/AiCoreDeployment.java | 42 +++++++++++-------- .../com/sap/ai/sdk/core/AiCoreService.java | 41 +++++++----------- .../ai/sdk/core/AiCoreServiceExtension.java | 22 ++++++++-- .../sap/ai/sdk/core/AiCoreServiceTest.java | 12 +++--- .../ai/sdk/core/DestinationResolverTest.java | 2 +- .../ai/sdk/core/client/ArtifactUnitTest.java | 10 ++--- .../core/client/ConfigurationUnitTest.java | 10 ++--- .../sdk/core/client/DeploymentUnitTest.java | 26 ++++++------ .../ai/sdk/core/client/ExecutionUnitTest.java | 22 +++++----- .../ai/sdk/core/client/ScenarioUnitTest.java | 8 ++-- .../client/OrchestrationUnitTest.java | 30 ++++++++++++- 12 files changed, 136 insertions(+), 93 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index 90a4ce5c..930df153 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -91,6 +91,10 @@ org.slf4j slf4j-api + + io.vavr + vavr + com.sap.cloud.sdk.cloudplatform diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index 2b040cf6..c404aee1 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -4,28 +4,27 @@ import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; -import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination; +import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationProperties; +import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationProperty; import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import java.util.Map; import java.util.NoSuchElementException; import java.util.function.Function; import java.util.function.Predicate; -import java.util.function.Supplier; import javax.annotation.Nonnull; import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.experimental.Delegate; /** Connectivity convenience methods for AI Core with deployment. */ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) -public class AiCoreDeployment implements AiCoreDestination { +public class AiCoreDeployment extends AiCoreServiceExtension implements AiCoreDestination { private static final String AI_RESOURCE_GROUP = "URL.headers.AI-Resource-Group"; // the delegating AI Core Service instance - @Nonnull private final AiCoreServiceExtension service; - - // the destination handler to be used - @Nonnull private final Supplier destination; + @Delegate + @Nonnull private final AiCoreServiceExtension delegate; // the deployment id handler to be used, based on resource group @Nonnull private final Function deploymentId; @@ -43,22 +42,23 @@ public class AiCoreDeployment implements AiCoreDestination { */ public AiCoreDeployment( @Nonnull final AiCoreService service, @Nonnull final Function deploymentId) { - this(service, service::destination, deploymentId, "default"); + this(service, deploymentId, "default"); } @Nonnull @Override public Destination destination() { - final var dest = destination.get().asHttp(); + final var dest = getBaseDestination(); final var builder = DefaultHttpDestination.fromDestination(dest); - return createDestination(builder, dest); + refineDestinationBuilder(builder, dest); + return builder.build(); } @Nonnull @Override public ApiClient client() { final var destination = destination(); - return service.createApiClient(destination); + return createApiClient(destination); } /** @@ -69,7 +69,7 @@ public ApiClient client() { */ @Nonnull public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { - return new AiCoreDeployment(service, destination, deploymentId, resourceGroup); + return new AiCoreDeployment(this, deploymentId, resourceGroup); } /** @@ -80,7 +80,10 @@ public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { */ @Nonnull public AiCoreDeployment withDestination(@Nonnull final Destination destination) { - return new AiCoreDeployment(service, () -> destination, deploymentId, resourceGroup); + return new AiCoreDeployment(this, deploymentId, resourceGroup) { + @Getter + private final Destination baseDestination = destination; + }; } /** @@ -90,11 +93,16 @@ public AiCoreDeployment withDestination(@Nonnull final Destination destination) * @param d The original destination. * @return The updated destination. */ - protected Destination createDestination( - @Nonnull final DefaultHttpDestination.Builder builder, @Nonnull final HttpDestination d) { - builder.uri(d.getUri().resolve("inference/deployments/%s/".formatted(getDeploymentId()))); + @Override + protected void refineDestinationBuilder( + @Nonnull final DefaultHttpDestination.Builder builder, + @Nonnull final DestinationProperties d) { + + AiCoreService.this.refineDestinationBuilder(builder, d); + + String uri = d.get(DestinationProperty.URI).get(); + builder.uri(uri+"inference/deployments/%s/".formatted(getDeploymentId()))); builder.property(AI_RESOURCE_GROUP, getResourceGroup()); - return service.createDestination(builder, d); } /** diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index 7125eb19..ef5664ef 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -6,35 +6,37 @@ import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; -import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException; -import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationNotFoundException; +import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationProperties; +import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationProperty; import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import java.util.function.Predicate; -import java.util.function.Supplier; import javax.annotation.Nonnull; import lombok.AccessLevel; +import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.experimental.Delegate; import lombok.extern.slf4j.Slf4j; /** Connectivity convenience methods for AI Core. */ @Slf4j -@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) public class AiCoreService extends AiCoreServiceExtension implements AiCoreDestination { - // the destination to be used for AI Core service calls. - @Nonnull private final Supplier destination; + @Delegate + private final AiCoreServiceExtension delegate; /** Create a new instance of the AI Core service. */ public AiCoreService() { - this(AiCoreService::getDefaultDestination); + this(new AiCoreServiceExtension()); } @Nonnull @Override public Destination destination() { - final var dest = destination.get(); - final var newDestinationBuilder = DefaultHttpDestination.fromDestination(dest); - return createDestination(newDestinationBuilder, dest); + final var dest = getBaseDestination(); + final var builder = DefaultHttpDestination.fromDestination(dest); + refineDestinationBuilder(builder, dest); + return builder.build(); } /** @@ -45,7 +47,10 @@ public Destination destination() { */ @Nonnull public AiCoreService withDestination(@Nonnull final Destination destination) { - return new AiCoreService(() -> destination); + return new AiCoreService(this) { + @Getter + private final Destination baseDestination = destination; + }; } /** @@ -83,20 +88,6 @@ public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId return new AiCoreDeployment(this, res -> getDeploymentId(client(), res, p)); } - /** - * 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 static Destination getDefaultDestination() - throws DestinationAccessException, DestinationNotFoundException { - final var serviceKey = System.getenv("AICORE_SERVICE_KEY"); - return DestinationResolver.getDestination(serviceKey); - } - @Nonnull @Override public ApiClient client() { diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceExtension.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceExtension.java index b6409283..df0d25ab 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceExtension.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceExtension.java @@ -17,24 +17,38 @@ import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationNotFoundException; import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import javax.annotation.Nonnull; + 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; -abstract class AiCoreServiceExtension { +class AiCoreServiceExtension { + + /** + * 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); + }; @Nonnull - protected Destination createDestination( + protected void refineDestinationBuilder( @Nonnull final DefaultHttpDestination.Builder builder, @Nonnull final DestinationProperties properties) { String uri = properties.get(DestinationProperty.URI).get(); if (!uri.endsWith("/")) { uri = uri + "/"; } - uri = uri + "v2/"; - return builder.uri(uri).property(AI_CLIENT_TYPE_KEY, AI_CLIENT_TYPE_VALUE).build(); + builder.uri(uri + "v2/").property(AI_CLIENT_TYPE_KEY, AI_CLIENT_TYPE_VALUE); } /** diff --git a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java index 2d1c52cd..aee0cdee 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java @@ -65,11 +65,11 @@ void testSimpleCase() { final var client = core.client(); // verification - assertThat(destination.get(DestinationProperty.URI)).contains("https://srv/v2"); + assertThat(destination.get(DestinationProperty.URI)).contains("https://srv/v2/"); assertThat(destination.get(DestinationProperty.AUTH_TYPE)).isEmpty(); assertThat(destination.get(DestinationProperty.NAME)).singleElement(STRING).contains("aicore"); assertThat(destination.get(AI_CLIENT_TYPE_KEY)).contains(AI_CLIENT_TYPE_VALUE); - assertThat(client.getBasePath()).isEqualTo("https://srv/v2"); + assertThat(client.getBasePath()).isEqualTo("https://srv/v2/"); verify(accessor, times(2)).getServiceBindings(); } @@ -85,7 +85,7 @@ void testBaseDestination() { final var client = core.client(); // verification - assertThat(destination.get(DestinationProperty.URI)).contains("https://foo.bar"); + assertThat(destination.get(DestinationProperty.URI)).contains("https://foo.bar/v2/"); assertThat(destination.get(DestinationProperty.AUTH_TYPE)).isEmpty(); assertThat(destination.get(DestinationProperty.NAME)).isEmpty(); assertThat(destination.get(AI_CLIENT_TYPE_KEY)).contains(AI_CLIENT_TYPE_VALUE); @@ -93,21 +93,21 @@ void testBaseDestination() { } @Test - void testDeploymnt() { + void testDeployment() { final var accessor = mock(ServiceBindingAccessor.class); DestinationResolver.setAccessor(accessor); doReturn(List.of(BINDING)).when(accessor).getServiceBindings(); // execution without errors final var destination = new AiCoreService().destination(); - final var ciient = new AiCoreService().client(); + final var client = new AiCoreService().client(); // verification assertThat(destination.get(DestinationProperty.URI)).contains("https://srv/v2/"); assertThat(destination.get(DestinationProperty.AUTH_TYPE)).isEmpty(); assertThat(destination.get(DestinationProperty.NAME)).singleElement(STRING).contains("aicore"); assertThat(destination.get(AI_CLIENT_TYPE_KEY)).contains(AI_CLIENT_TYPE_VALUE); - assertThat(ciient.getBasePath()).isEqualTo("https://srv/v2/"); + assertThat(client.getBasePath()).isEqualTo("https://srv/v2/"); verify(accessor, times(2)).getServiceBindings(); } } diff --git a/core/src/test/java/com/sap/ai/sdk/core/DestinationResolverTest.java b/core/src/test/java/com/sap/ai/sdk/core/DestinationResolverTest.java index 3f395a53..c6c8391a 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/DestinationResolverTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/DestinationResolverTest.java @@ -44,6 +44,6 @@ void getDestinationWithEnvVarSucceedsLocally() { } """; var result = DestinationResolver.getDestination(AICORE_SERVICE_KEY).asHttp(); - assertThat(result.getUri()).hasToString("https://api.ai.core/v2"); + assertThat(result.getUri()).hasToString("https://api.ai.core"); } } diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java index a4783ac4..dcfbf505 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java @@ -24,7 +24,7 @@ public class ArtifactUnitTest extends WireMockTestServer { @Test void getArtifacts() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/artifacts")) + get(urlPathEqualTo("/v2/lm/artifacts")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -70,7 +70,7 @@ void getArtifacts() { @Test void postArtifact() { wireMockServer.stubFor( - post(urlPathEqualTo("/lm/artifacts")) + post(urlPathEqualTo("/v2/lm/artifacts")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -102,7 +102,7 @@ void postArtifact() { assertThat(artifact.getUrl()).isEqualTo("ai://default/spam/data"); wireMockServer.verify( - postRequestedFor(urlPathEqualTo("/lm/artifacts")) + postRequestedFor(urlPathEqualTo("/v2/lm/artifacts")) .withHeader("AI-Resource-Group", equalTo("default")) .withRequestBody( equalToJson( @@ -121,7 +121,7 @@ void postArtifact() { @Test void getArtifactById() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/artifacts/777dea85-e9b1-4a7b-9bea-14769b977633")) + get(urlPathEqualTo("/v2/lm/artifacts/777dea85-e9b1-4a7b-9bea-14769b977633")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -158,7 +158,7 @@ void getArtifactById() { @Test void getArtifactCount() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/artifacts/$count")) + get(urlPathEqualTo("/v2/lm/artifacts/$count")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java index 6f419781..6f2a0a5c 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java @@ -26,7 +26,7 @@ public class ConfigurationUnitTest extends WireMockTestServer { @Test void getConfigurations() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/configurations")) + get(urlPathEqualTo("/v2/lm/configurations")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -80,7 +80,7 @@ void getConfigurations() { @Test void postConfiguration() { wireMockServer.stubFor( - post(urlPathEqualTo("/lm/configurations")) + post(urlPathEqualTo("/v2/lm/configurations")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -112,7 +112,7 @@ void postConfiguration() { assertThat(configuration.getMessage()).isEqualTo("Configuration created"); wireMockServer.verify( - postRequestedFor(urlPathEqualTo("/lm/configurations")) + postRequestedFor(urlPathEqualTo("/v2/lm/configurations")) .withHeader("AI-Resource-Group", equalTo("default")) .withRequestBody( equalToJson( @@ -135,7 +135,7 @@ void postConfiguration() { @Test void getConfigurationCount() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/configurations/$count")) + get(urlPathEqualTo("/v2/lm/configurations/$count")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -153,7 +153,7 @@ void getConfigurationCount() { @Test void getConfigurationById() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/configurations/6ff6cb80-87db-45f0-b718-4e1d96e66332")) + get(urlPathEqualTo("/v2/lm/configurations/6ff6cb80-87db-45f0-b718-4e1d96e66332")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java index fda31895..a6804b23 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java @@ -42,7 +42,7 @@ public class DeploymentUnitTest extends WireMockTestServer { @Test void getDeployments() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/deployments")) + get(urlPathEqualTo("/v2/lm/deployments")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -119,7 +119,7 @@ void getDeployments() { @Test void postDeployment() { wireMockServer.stubFor( - post(urlPathEqualTo("/lm/deployments")) + post(urlPathEqualTo("/v2/lm/deployments")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -148,7 +148,7 @@ void postDeployment() { assertThat(deployment.getStatus()).isEqualTo(AiExecutionStatus.UNKNOWN_DEFAULT_OPEN_API); wireMockServer.verify( - postRequestedFor(urlPathEqualTo("/lm/deployments")) + postRequestedFor(urlPathEqualTo("/v2/lm/deployments")) .withHeader("AI-Resource-Group", equalTo("default")) .withRequestBody( equalToJson( @@ -162,7 +162,7 @@ void postDeployment() { @Test void patchDeploymentStatus() { wireMockServer.stubFor( - patch(urlPathEqualTo("/lm/deployments/d19b998f347341aa")) + patch(urlPathEqualTo("/v2/lm/deployments/d19b998f347341aa")) .willReturn( aResponse() .withStatus(HttpStatus.SC_ACCEPTED) @@ -186,7 +186,7 @@ void patchDeploymentStatus() { // verify that null fields are absent from the sent request wireMockServer.verify( - patchRequestedFor(urlPathEqualTo("/lm/deployments/d19b998f347341aa")) + patchRequestedFor(urlPathEqualTo("/v2/lm/deployments/d19b998f347341aa")) .withHeader("AI-Resource-Group", equalTo("default")) .withRequestBody( equalToJson( @@ -200,7 +200,7 @@ void patchDeploymentStatus() { @Test void deleteDeployment() { wireMockServer.stubFor( - delete(urlPathEqualTo("/lm/deployments/d5b764fe55b3e87c")) + delete(urlPathEqualTo("/v2/lm/deployments/d5b764fe55b3e87c")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -227,7 +227,7 @@ void deleteDeployment() { @Test void getDeploymentById() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/deployments/db1d64d9f06be467")) + get(urlPathEqualTo("/v2/lm/deployments/db1d64d9f06be467")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -291,7 +291,7 @@ void getDeploymentById() { @Test void patchDeploymentConfiguration() { wireMockServer.stubFor( - patch(urlPathEqualTo("/lm/deployments/d03050a2ab7055cc")) + patch(urlPathEqualTo("/v2/lm/deployments/d03050a2ab7055cc")) .willReturn( aResponse() .withStatus(HttpStatus.SC_ACCEPTED) @@ -316,7 +316,7 @@ void patchDeploymentConfiguration() { // verify that null fields are absent from the sent request wireMockServer.verify( - patchRequestedFor(urlPathEqualTo("/lm/deployments/d03050a2ab7055cc")) + patchRequestedFor(urlPathEqualTo("/v2/lm/deployments/d03050a2ab7055cc")) .withHeader("AI-Resource-Group", equalTo("default")) .withRequestBody( equalToJson( @@ -330,7 +330,7 @@ void patchDeploymentConfiguration() { @Test void getDeploymentCount() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/deployments/$count")) + get(urlPathEqualTo("/v2/lm/deployments/$count")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -348,7 +348,7 @@ void getDeploymentCount() { @Test void getDeploymentLogs() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/deployments/d19b998f347341aa/logs")) + get(urlPathEqualTo("/v2/lm/deployments/d19b998f347341aa/logs")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -395,7 +395,7 @@ void getDeploymentLogs() { @Test void patchBulkDeployments() { wireMockServer.stubFor( - patch(urlPathEqualTo("/lm/deployments")) + patch(urlPathEqualTo("/v2/lm/deployments")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -436,7 +436,7 @@ void patchBulkDeployments() { .isEqualTo("Deployment modification scheduled"); wireMockServer.verify( - patchRequestedFor(urlPathEqualTo("/lm/deployments")) + patchRequestedFor(urlPathEqualTo("/v2/lm/deployments")) .withHeader("AI-Resource-Group", equalTo("default")) .withRequestBody( equalToJson( diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java index 5a79089b..53e6c9ad 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java @@ -40,7 +40,7 @@ public class ExecutionUnitTest extends WireMockTestServer { @Test void getExecutions() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/executions")) + get(urlPathEqualTo("/v2/lm/executions")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -119,7 +119,7 @@ void getExecutions() { @Test void postExecution() { wireMockServer.stubFor( - post(urlPathEqualTo("/lm/executions")) + post(urlPathEqualTo("/v2/lm/executions")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -145,7 +145,7 @@ void postExecution() { assertThat(execution.getCustomField("url")).isEqualTo("ai://default/eab289226fe981da"); wireMockServer.verify( - postRequestedFor(urlPathEqualTo("/lm/executions")) + postRequestedFor(urlPathEqualTo("/v2/lm/executions")) .withHeader("AI-Resource-Group", equalTo("default")) .withRequestBody( equalToJson( @@ -159,7 +159,7 @@ void postExecution() { @Test void getExecutionById() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/executions/e529e8bd58740bc9")) + get(urlPathEqualTo("/v2/lm/executions/e529e8bd58740bc9")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -232,7 +232,7 @@ void getExecutionById() { @Test void deleteExecution() { wireMockServer.stubFor( - delete(urlPathEqualTo("/lm/executions/e529e8bd58740bc9")) + delete(urlPathEqualTo("/v2/lm/executions/e529e8bd58740bc9")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -260,7 +260,7 @@ void deleteExecution() { @Test void patchExecution() { wireMockServer.stubFor( - patch(urlPathEqualTo("/lm/executions/eec3c6ea18bac6da")) + patch(urlPathEqualTo("/v2/lm/executions/eec3c6ea18bac6da")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -287,7 +287,7 @@ void patchExecution() { .isEqualTo("Execution modification scheduled"); wireMockServer.verify( - patchRequestedFor(urlPathEqualTo("/lm/executions/eec3c6ea18bac6da")) + patchRequestedFor(urlPathEqualTo("/v2/lm/executions/eec3c6ea18bac6da")) .withHeader("AI-Resource-Group", equalTo("default")) .withRequestBody(equalToJson("{\"targetStatus\":\"STOPPED\"}"))); } @@ -295,7 +295,7 @@ void patchExecution() { @Test void getExecutionCount() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/executions/$count")) + get(urlPathEqualTo("/v2/lm/executions/$count")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -313,7 +313,7 @@ void getExecutionCount() { @Test void getExecutionLogs() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/executions/ee467bea5af28adb/logs")) + get(urlPathEqualTo("/v2/lm/executions/ee467bea5af28adb/logs")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -357,7 +357,7 @@ void getExecutionLogs() { @Test void patchBulkExecutions() { wireMockServer.stubFor( - patch(urlPathEqualTo("/lm/executions")) + patch(urlPathEqualTo("/v2/lm/executions")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -398,7 +398,7 @@ void patchBulkExecutions() { .isEqualTo("Execution modification scheduled"); wireMockServer.verify( - patchRequestedFor(urlPathEqualTo("/lm/executions")) + patchRequestedFor(urlPathEqualTo("/v2/lm/executions")) .withHeader("AI-Resource-Group", equalTo("default")) .withRequestBody( equalToJson( diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ScenarioUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ScenarioUnitTest.java index d483e6d8..9ec203d4 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ScenarioUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ScenarioUnitTest.java @@ -25,7 +25,7 @@ public class ScenarioUnitTest extends WireMockTestServer { @Test void getScenarios() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/scenarios")) + get(urlPathEqualTo("/v2/lm/scenarios")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -71,7 +71,7 @@ void getScenarios() { @Test void getScenarioVersions() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/scenarios/foundation-models/versions")) + get(urlPathEqualTo("/v2/lm/scenarios/foundation-models/versions")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -110,7 +110,7 @@ void getScenarioVersions() { @Test void getScenarioById() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/scenarios/foundation-models")) + get(urlPathEqualTo("/v2/lm/scenarios/foundation-models")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -150,7 +150,7 @@ void getScenarioById() { @Test void getScenarioModels() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/scenarios/foundation-models/models")) + get(urlPathEqualTo("/v2/lm/scenarios/foundation-models/models")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java index fa4a628a..ff49a64b 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java @@ -1,7 +1,9 @@ package com.sap.ai.sdk.orchestration.client; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.jsonResponse; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; @@ -40,6 +42,8 @@ import java.util.Map; import java.util.Objects; import java.util.function.Function; + +import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.web.client.HttpClientErrorException; @@ -77,9 +81,31 @@ public class OrchestrationUnitTest { @BeforeEach void setup(WireMockRuntimeInfo server) { + + stubFor( + get(urlPathEqualTo("/v2/lm/deployments")) + .withHeader("AI-Resource-Group", equalTo("default")) + .willReturn( + aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("content-type", "application/json") + .withBody( + """ + { + "resources": [ + { + "configurationId": "7652a231-ba9b-4fcc-b473-2c355cb21b61", + "id": "d19b998f347341aa", + "scenarioId": "orchestration" + } + ] + } + """))); + + final DefaultHttpDestination destination = DefaultHttpDestination.builder(server.getHttpBaseUrl()).build(); - final var apiClient = new AiCoreService().withDestination(destination).client(); + final var apiClient = new AiCoreService().withDestination(destination).forDeploymentByScenario("orchestration").client(); client = new OrchestrationCompletionApi(apiClient); } @@ -316,7 +342,7 @@ void messagesHistory() throws IOException { @Test void maskingAnonymization() throws IOException { stubFor( - post(urlPathEqualTo("/completion")) + post(urlPathEqualTo("/v2/completion")) .willReturn( aResponse() .withBodyFile("maskingResponse.json") From dc4528282408b1f9fb1ef73485780f98fb1f4e16 Mon Sep 17 00:00:00 2001 From: I538344 Date: Tue, 15 Oct 2024 10:13:40 +0200 Subject: [PATCH 46/79] Fixed tests --- .../com/sap/ai/sdk/core/AiCoreDeployment.java | 62 ++++++- .../com/sap/ai/sdk/core/AiCoreService.java | 23 ++- .../com/sap/ai/sdk/core/DeploymentCache.java | 171 +++++++----------- .../java/com/sap/ai/sdk/core/CacheTest.java | 14 +- .../ai/sdk/core/client/ArtifactUnitTest.java | 1 - .../core/client/ConfigurationUnitTest.java | 1 - .../sdk/core/client/DeploymentUnitTest.java | 1 - .../ai/sdk/core/client/ExecutionUnitTest.java | 1 - .../core/{ => client}/WireMockTestServer.java | 3 +- .../openai/OpenAiClientTest.java | 3 - .../controllers/ConfigurationController.java | 9 +- 11 files changed, 143 insertions(+), 146 deletions(-) rename core/src/test/java/com/sap/ai/sdk/core/{ => client}/WireMockTestServer.java (94%) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index 6dc5e170..35447bff 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -3,13 +3,16 @@ 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.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination; import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; +import java.util.HashMap; import java.util.Map; import java.util.NoSuchElementException; +import java.util.function.Function; import java.util.function.Supplier; import javax.annotation.Nonnull; import lombok.AccessLevel; @@ -21,6 +24,11 @@ public class AiCoreDeployment implements AiCoreDestination { private static final String AI_RESOURCE_GROUP = "URL.headers.AI-Resource-Group"; + private static final Map DEPLOYMENT_CACHES = new HashMap<>(); + + // the deployment id handler to be used, based on resource group + @Nonnull private final Function deploymentId; + // the base destination handler to be used @Nonnull private final Supplier destination; @@ -32,10 +40,13 @@ public class AiCoreDeployment implements AiCoreDestination { /** * Create a new instance of the AI Core service with a specific deployment id and destination. * + * @param deploymentId The deployment id handler, based on resource group. * @param destination The destination handler. */ - public AiCoreDeployment(@Nonnull final Supplier destination) { - this(destination, "default"); + public AiCoreDeployment( + @Nonnull final Function deploymentId, + @Nonnull final Supplier destination) { + this(deploymentId, destination, "default"); } @Nonnull @@ -55,7 +66,7 @@ public Destination destination() { */ @Nonnull public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { - return new AiCoreDeployment(destination, resourceGroup); + return new AiCoreDeployment(deploymentId, destination, resourceGroup); } /** @@ -66,7 +77,7 @@ public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { */ @Nonnull public AiCoreDeployment withDestination(@Nonnull final Destination destination) { - return new AiCoreDeployment(() -> destination, resourceGroup); + return new AiCoreDeployment(deploymentId, () -> destination, resourceGroup); } /** @@ -129,15 +140,46 @@ protected static boolean isDeploymentOfModel( } /** - * Get the deployment id from the scenario id. If there are multiple deployments of the same - * scenario id, the first one is returned. + * Get the deployment id from the foundation model name. If there are multiple deployments of the + * same model, the first one is returned. + * + * @param resourceGroup the resource group, usually "default". + * @param modelName the name of the foundation model. + * @return the deployment id. + * @throws NoSuchElementException if no running deployment is found for the model. + */ + @Nonnull + protected static String getDeploymentIdByModel( + @Nonnull final ApiClient client, + @Nonnull final String resourceGroup, + @Nonnull final String modelName) + throws NoSuchElementException { + final var deploymentCache = + DEPLOYMENT_CACHES.computeIfAbsent( + client, c -> new DeploymentCache(new DeploymentApi(client), resourceGroup)); + System.out.println("DEPLOYMENT_CACHES size" + DEPLOYMENT_CACHES.size()); + return deploymentCache.getDeploymentIdByModel(resourceGroup, modelName); + } + + /** + * Get the deployment id from the scenario id. If there are multiple deployments of the * same + * model, the first one is returned. * - * @return the deployment id - * @throws NoSuchElementException if no deployment is found for the scenario id. + * @param client The API client to do HTTP requests to AI Core. + * @param resourceGroup the resource group, usually "default". + * @param scenarioId the scenario id, can be "orchestration". + * @return the deployment id. + * @throws NoSuchElementException if no running deployment is found for the scenario. */ @Nonnull - protected String getDeploymentId(@Nonnull final ApiClient client, @Nonnull final String name) + public static String getDeploymentIdByScenario( + @Nonnull final ApiClient client, + @Nonnull final String resourceGroup, + @Nonnull final String scenarioId) throws NoSuchElementException { - return DeploymentCache.getDeploymentId(resourceGroup, name); + final var deploymentCache = + DEPLOYMENT_CACHES.computeIfAbsent( + client, c -> new DeploymentCache(new DeploymentApi(client), resourceGroup)); + return deploymentCache.getDeploymentIdByScenario(resourceGroup, scenarioId); } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index 3119dae2..a216976c 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -1,5 +1,8 @@ package com.sap.ai.sdk.core; +import static com.sap.ai.sdk.core.AiCoreDeployment.getDeploymentIdByModel; +import static com.sap.ai.sdk.core.AiCoreDeployment.getDeploymentIdByScenario; + import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException; import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationNotFoundException; @@ -52,27 +55,31 @@ public AiCoreDeployment forDeployment(@Nonnull final String deploymentId) { } /** - * Set a specific deployment by model name. + * 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 service. + * @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::destination).getDeploymentId(client(), modelName); + public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) { + return new AiCoreDeployment( + res -> getDeploymentIdByModel(client(), res, modelName), this::destination); } /** - * Set a specific deployment by scenario id. + * 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 service. + * @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::destination).getDeploymentId(client(), scenarioId); + public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId) { + return new AiCoreDeployment( + res -> getDeploymentIdByScenario(client(), res, scenarioId), this::destination); } /** diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index b9454976..4dad1f1d 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -1,11 +1,14 @@ package com.sap.ai.sdk.core; +import static com.sap.ai.sdk.core.AiCoreDeployment.isDeploymentOfModel; + import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.cloud.sdk.services.openapi.core.OpenApiRequestException; -import java.util.HashMap; -import java.util.Map; +import java.util.ArrayList; +import java.util.List; import java.util.NoSuchElementException; +import java.util.Optional; import javax.annotation.Nonnull; import lombok.extern.slf4j.Slf4j; @@ -16,40 +19,23 @@ @Slf4j public class DeploymentCache { /** The client to use for deployment queries. */ - static DeploymentApi API; + private final DeploymentApi API; /** Cache for deployment ids. The key is the model name and the value is the deployment id. */ - private static final Map CACHE = new HashMap<>(); + private final List CACHE = new ArrayList<>(); - static boolean isEmpty() { - return API == null; - } - - /** - * Eagerly load the deployment cache with the given client. - * - * @param client the deployment client. - */ - public static void eagerlyLoaded(@Nonnull final DeploymentApi client) { - API = client; - loadCache(); - } - - /** - * Lazy load the deployment cache with the given client. - * - * @param client the deployment client. - */ - public static void lazyLoaded(@Nonnull final DeploymentApi client) { - API = client; + public DeploymentCache(DeploymentApi api, String resourceGroup) { + API = api; + loadCache(resourceGroup); } /** * Remove all entries from the cache. * *

      Call both clearCache and {@link #loadCache} method whenever a deployment is deleted. + * TODO:test */ - public static void clearCache() { + public void clearCache() { CACHE.clear(); } @@ -57,112 +43,79 @@ public static void clearCache() { * Load all deployments into the cache * *

      Call both {@link #clearCache} and loadCache method whenever a deployment is deleted. + * + * @param resourceGroup the resource group, usually "default". */ - public static void loadCache() { + public void loadCache(@Nonnull final String resourceGroup) { try { - final var deployments = API.query("default").getResources(); - deployments.forEach(deployment -> CACHE.put(getModelName(deployment), deployment.getId())); + final var deployments = API.query(resourceGroup).getResources(); + CACHE.addAll(deployments); } catch (final OpenApiRequestException e) { log.error("Failed to load deployments into cache", e); } } /** - * Get the deployment id for the orchestration scenario or any foundation model. + * Get the deployment id from the foundation model name. If there are multiple deployments of the + * same model, the first one is returned. * * @param resourceGroup the resource group, usually "default". - * @param name "orchestration" or the model name. + * @param modelName the name of the foundation model. * @return the deployment id. + * @throws NoSuchElementException if no running deployment is found for the model. */ @Nonnull - public static String getDeploymentId( - @Nonnull final String resourceGroup, @Nonnull final String name) { - if (isEmpty()) { - loadCache(); - } - return CACHE.computeIfAbsent( - name, - n -> { - if ("orchestration".equals(n)) { - return getOrchestrationDeployment(resourceGroup); - } else { - return getDeploymentForModel(resourceGroup, name); - } - }); - } - - /** - * Get the deployment id from the scenario id. If there are multiple deployments of the same - * scenario id, the first one is returned. - * - * @param resourceGroup the resource group. - * @return the deployment id - * @throws NoSuchElementException if no deployment is found for the scenario id. - */ - private static String getOrchestrationDeployment(@Nonnull final String resourceGroup) + public String getDeploymentIdByModel( + @Nonnull final String resourceGroup, @Nonnull final String modelName) throws NoSuchElementException { - final var deployments = - API.query(resourceGroup, null, null, "orchestration", "RUNNING", null, null, null); + return getDeploymentIdByModel(modelName) + .orElseGet( + () -> { + loadCache(resourceGroup); + return getDeploymentIdByModel(modelName) + .orElseThrow( + () -> + new NoSuchElementException( + "No running deployment found for model: " + modelName)); + }); + } - return deployments.getResources().stream() - .map(AiDeployment::getId) + private Optional getDeploymentIdByModel(@Nonnull final String modelName) { + return CACHE.stream() + .filter(deployment -> isDeploymentOfModel(modelName, deployment)) .findFirst() - .orElseThrow( - () -> - new NoSuchElementException( - "No running deployment found with scenario id \"orchestration\"")); + .map(AiDeployment::getId); } /** - * Get the deployment id from the model name. If there are multiple deployments of the same model, - * the first one is returned. + * Get the deployment id from the scenario id. If there are multiple deployments of the * same + * model, the first one is returned. * - * @param resourceGroup the resource group. - * @param modelName the model name. - * @return the deployment id - * @throws NoSuchElementException if no deployment is found for the model name. + * @param resourceGroup the resource group, usually "default". + * @param scenarioId the scenario id, can be "orchestration". + * @return the deployment id. + * @throws NoSuchElementException if no running deployment is found for the scenario. TODO: test */ - private static String getDeploymentForModel( - @Nonnull final String resourceGroup, @Nonnull final String modelName) + @Nonnull + public String getDeploymentIdByScenario( + @Nonnull final String resourceGroup, @Nonnull final String scenarioId) throws NoSuchElementException { - final var deployments = - API.query(resourceGroup, null, null, "foundation-models", "RUNNING", null, null, null); - - return deployments.getResources().stream() - .filter(deployment -> modelName.equals(getModelName(deployment))) - .map(AiDeployment::getId) - .findFirst() - .orElseThrow( - () -> - new NoSuchElementException( - "No running deployment found with model name " + modelName)); + return getDeploymentIdByScenario(scenarioId) + .orElseGet( + () -> { + loadCache(resourceGroup); + return getDeploymentIdByScenario(scenarioId) + .orElseThrow( + () -> + new NoSuchElementException( + "No running deployment found for scenario: " + scenarioId)); + }); } - /** This exists because getBackendDetails() is broken */ - private static String getModelName(@Nonnull final AiDeployment deployment) { - if ("orchestration".equals(deployment.getScenarioId())) { - return "orchestration"; - } - final var deploymentDetails = deployment.getDetails(); - // The AI Core specification doesn't mention that this is nullable, but it can be. - // Remove this check when the specification is fixed. - if (deploymentDetails == null) { - return ""; - } - final var resources = deploymentDetails.getResources(); - if (resources == null) { - return ""; - } - if (!resources.getCustomFieldNames().contains("backend_details")) { - return ""; - } - final var detailsObject = resources.getCustomField("backend_details"); - - if (detailsObject instanceof Map details - && details.get("model") instanceof Map model - && model.get("name") instanceof String name) { - return name; - } - return ""; + private Optional getDeploymentIdByScenario(@Nonnull final String scenarioId) { + return CACHE.stream() + .filter(deployment -> scenarioId.equals(deployment.getScenarioId())) + .findFirst() + .map(AiDeployment::getId); } } diff --git a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java index 7837322e..0e98f3d7 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java @@ -7,6 +7,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import com.sap.ai.sdk.core.client.DeploymentApi; +import com.sap.ai.sdk.core.client.WireMockTestServer; import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -15,7 +16,6 @@ class CacheTest extends WireMockTestServer { @BeforeEach void setupCache() { - DeploymentCache.lazyLoaded(new DeploymentApi(destination)); wireMockServer.resetRequests(); } @@ -94,12 +94,12 @@ private static void stubEmpty() { @Test void newDeployment() { stubGPT4(); - DeploymentCache.loadCache(); + var cache = new DeploymentCache(new DeploymentApi(client), "default"); - DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + cache.getDeploymentIdByModel("default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); - DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + cache.getDeploymentIdByModel("default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); } @@ -115,14 +115,14 @@ void newDeployment() { @Test void newDeploymentAfterReset() { stubEmpty(); - DeploymentCache.loadCache(); + var cache = new DeploymentCache(new DeploymentApi(client), "default"); stubGPT4(); - DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + cache.getDeploymentIdByModel("default", "gpt-4-32k"); // 1 reset empty and 1 cache miss wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); - DeploymentCache.getDeploymentId("default", "gpt-4-32k"); + cache.getDeploymentIdByModel("default", "gpt-4-32k"); wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); } } diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java index 366d57fb..a4783ac4 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java @@ -9,7 +9,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static org.assertj.core.api.Assertions.assertThat; -import com.sap.ai.sdk.core.WireMockTestServer; import com.sap.ai.sdk.core.client.model.AiArtifact; import com.sap.ai.sdk.core.client.model.AiArtifactCreationResponse; import com.sap.ai.sdk.core.client.model.AiArtifactList; diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java index fa0c01da..6f419781 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java @@ -9,7 +9,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static org.assertj.core.api.Assertions.assertThat; -import com.sap.ai.sdk.core.WireMockTestServer; import com.sap.ai.sdk.core.client.model.AiArtifactArgumentBinding; import com.sap.ai.sdk.core.client.model.AiConfiguration; import com.sap.ai.sdk.core.client.model.AiConfigurationBaseData; diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java index 0e862eb6..fda31895 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java @@ -12,7 +12,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static org.assertj.core.api.Assertions.assertThat; -import com.sap.ai.sdk.core.WireMockTestServer; import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.ai.sdk.core.client.model.AiDeploymentBulkModificationRequest; import com.sap.ai.sdk.core.client.model.AiDeploymentBulkModificationResponse; diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java index 9345a65a..5a79089b 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java @@ -12,7 +12,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static org.assertj.core.api.Assertions.assertThat; -import com.sap.ai.sdk.core.WireMockTestServer; import com.sap.ai.sdk.core.client.model.AiArtifact; import com.sap.ai.sdk.core.client.model.AiEnactmentCreationRequest; import com.sap.ai.sdk.core.client.model.AiExecution; diff --git a/core/src/test/java/com/sap/ai/sdk/core/WireMockTestServer.java b/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java similarity index 94% rename from core/src/test/java/com/sap/ai/sdk/core/WireMockTestServer.java rename to core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java index d94f5b93..d50ef94d 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/WireMockTestServer.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java @@ -1,9 +1,10 @@ -package com.sap.ai.sdk.core; +package com.sap.ai.sdk.core.client; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.sap.ai.sdk.core.AiCoreService; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; diff --git a/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientTest.java b/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientTest.java index 33f50118..faa84a27 100644 --- a/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientTest.java +++ b/foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClientTest.java @@ -14,8 +14,6 @@ import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import com.github.tomakehurst.wiremock.stubbing.Scenario; -import com.sap.ai.sdk.core.DeploymentCache; -import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionChoice; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionDelta; import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionOutput; @@ -61,7 +59,6 @@ void setup(WireMockRuntimeInfo server) { final DefaultHttpDestination destination = DefaultHttpDestination.builder(server.getHttpBaseUrl()).build(); client = OpenAiClient.withCustomDestination(destination); - DeploymentCache.lazyLoaded(new DeploymentApi(destination)); ApacheHttpClient5Accessor.setHttpClientCache(ApacheHttpClient5Cache.DISABLED); } diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ConfigurationController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ConfigurationController.java index 5e94ec9d..630687aa 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ConfigurationController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ConfigurationController.java @@ -1,9 +1,9 @@ package com.sap.ai.sdk.app.controllers; -import static com.sap.ai.sdk.core.Core.getClient; - +import com.sap.ai.sdk.core.AiCoreService; import com.sap.ai.sdk.core.client.ConfigurationApi; import com.sap.ai.sdk.core.client.model.AiConfigurationList; +import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -12,7 +12,8 @@ @RestController public class ConfigurationController { - private static final ConfigurationApi API = new ConfigurationApi(getClient()); + private static final ApiClient API_CLIENT = new AiCoreService().client(); + private static final ConfigurationApi API = new ConfigurationApi(API_CLIENT); /** * Get the list of configurations. @@ -21,6 +22,6 @@ public class ConfigurationController { */ @GetMapping("/configurations") AiConfigurationList getConfigurations() { - return API.configurationQuery("default"); + return API.query("default"); } } From 6684e39e2fa8ddc95b6a91b9fe28d861eebe382f Mon Sep 17 00:00:00 2001 From: I538344 Date: Tue, 15 Oct 2024 10:24:00 +0200 Subject: [PATCH 47/79] Single cache --- .../com/sap/ai/sdk/core/AiCoreDeployment.java | 16 ++++---------- .../com/sap/ai/sdk/core/DeploymentCache.java | 21 +++++++------------ 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index 35447bff..9047a20a 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -9,7 +9,6 @@ import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination; import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; -import java.util.HashMap; import java.util.Map; import java.util.NoSuchElementException; import java.util.function.Function; @@ -24,8 +23,6 @@ public class AiCoreDeployment implements AiCoreDestination { private static final String AI_RESOURCE_GROUP = "URL.headers.AI-Resource-Group"; - private static final Map DEPLOYMENT_CACHES = new HashMap<>(); - // the deployment id handler to be used, based on resource group @Nonnull private final Function deploymentId; @@ -154,11 +151,8 @@ protected static String getDeploymentIdByModel( @Nonnull final String resourceGroup, @Nonnull final String modelName) throws NoSuchElementException { - final var deploymentCache = - DEPLOYMENT_CACHES.computeIfAbsent( - client, c -> new DeploymentCache(new DeploymentApi(client), resourceGroup)); - System.out.println("DEPLOYMENT_CACHES size" + DEPLOYMENT_CACHES.size()); - return deploymentCache.getDeploymentIdByModel(resourceGroup, modelName); + DeploymentCache.API = new DeploymentApi(client); + return DeploymentCache.getDeploymentIdByModel(resourceGroup, modelName); } /** @@ -177,9 +171,7 @@ public static String getDeploymentIdByScenario( @Nonnull final String resourceGroup, @Nonnull final String scenarioId) throws NoSuchElementException { - final var deploymentCache = - DEPLOYMENT_CACHES.computeIfAbsent( - client, c -> new DeploymentCache(new DeploymentApi(client), resourceGroup)); - return deploymentCache.getDeploymentIdByScenario(resourceGroup, scenarioId); + DeploymentCache.API = new DeploymentApi(client); + return DeploymentCache.getDeploymentIdByScenario(resourceGroup, scenarioId); } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index 4dad1f1d..a567c06d 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -19,15 +19,10 @@ @Slf4j public class DeploymentCache { /** The client to use for deployment queries. */ - private final DeploymentApi API; + protected static DeploymentApi API; /** Cache for deployment ids. The key is the model name and the value is the deployment id. */ - private final List CACHE = new ArrayList<>(); - - public DeploymentCache(DeploymentApi api, String resourceGroup) { - API = api; - loadCache(resourceGroup); - } + protected static final List CACHE = new ArrayList<>(); /** * Remove all entries from the cache. @@ -35,7 +30,7 @@ public DeploymentCache(DeploymentApi api, String resourceGroup) { *

      Call both clearCache and {@link #loadCache} method whenever a deployment is deleted. * TODO:test */ - public void clearCache() { + public static void clearCache() { CACHE.clear(); } @@ -46,7 +41,7 @@ public void clearCache() { * * @param resourceGroup the resource group, usually "default". */ - public void loadCache(@Nonnull final String resourceGroup) { + public static void loadCache(@Nonnull final String resourceGroup) { try { final var deployments = API.query(resourceGroup).getResources(); CACHE.addAll(deployments); @@ -65,7 +60,7 @@ public void loadCache(@Nonnull final String resourceGroup) { * @throws NoSuchElementException if no running deployment is found for the model. */ @Nonnull - public String getDeploymentIdByModel( + public static String getDeploymentIdByModel( @Nonnull final String resourceGroup, @Nonnull final String modelName) throws NoSuchElementException { return getDeploymentIdByModel(modelName) @@ -80,7 +75,7 @@ public String getDeploymentIdByModel( }); } - private Optional getDeploymentIdByModel(@Nonnull final String modelName) { + private static Optional getDeploymentIdByModel(@Nonnull final String modelName) { return CACHE.stream() .filter(deployment -> isDeploymentOfModel(modelName, deployment)) .findFirst() @@ -97,7 +92,7 @@ private Optional getDeploymentIdByModel(@Nonnull final String modelName) * @throws NoSuchElementException if no running deployment is found for the scenario. TODO: test */ @Nonnull - public String getDeploymentIdByScenario( + public static String getDeploymentIdByScenario( @Nonnull final String resourceGroup, @Nonnull final String scenarioId) throws NoSuchElementException { return getDeploymentIdByScenario(scenarioId) @@ -112,7 +107,7 @@ public String getDeploymentIdByScenario( }); } - private Optional getDeploymentIdByScenario(@Nonnull final String scenarioId) { + private static Optional getDeploymentIdByScenario(@Nonnull final String scenarioId) { return CACHE.stream() .filter(deployment -> scenarioId.equals(deployment.getScenarioId())) .findFirst() From eb19ae5807d96e6f9576cf95d611a685c18c6e09 Mon Sep 17 00:00:00 2001 From: I538344 Date: Tue, 15 Oct 2024 10:27:48 +0200 Subject: [PATCH 48/79] Added clearCache test --- .../com/sap/ai/sdk/core/DeploymentCache.java | 3 +- .../java/com/sap/ai/sdk/core/CacheTest.java | 28 +++++++++++++++---- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index a567c06d..5a4e39a8 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -28,7 +28,6 @@ public class DeploymentCache { * Remove all entries from the cache. * *

      Call both clearCache and {@link #loadCache} method whenever a deployment is deleted. - * TODO:test */ public static void clearCache() { CACHE.clear(); @@ -89,7 +88,7 @@ private static Optional getDeploymentIdByModel(@Nonnull final String mod * @param resourceGroup the resource group, usually "default". * @param scenarioId the scenario id, can be "orchestration". * @return the deployment id. - * @throws NoSuchElementException if no running deployment is found for the scenario. TODO: test + * @throws NoSuchElementException if no running deployment is found for the scenario. */ @Nonnull public static String getDeploymentIdByScenario( diff --git a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java index 0e98f3d7..3b0f2212 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java @@ -16,6 +16,7 @@ class CacheTest extends WireMockTestServer { @BeforeEach void setupCache() { + DeploymentCache.API = new DeploymentApi(client); wireMockServer.resetRequests(); } @@ -94,15 +95,30 @@ private static void stubEmpty() { @Test void newDeployment() { stubGPT4(); - var cache = new DeploymentCache(new DeploymentApi(client), "default"); + DeploymentCache.loadCache("default"); - cache.getDeploymentIdByModel("default", "gpt-4-32k"); + DeploymentCache.getDeploymentIdByModel("default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); - cache.getDeploymentIdByModel("default", "gpt-4-32k"); + DeploymentCache.getDeploymentIdByModel("default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); } + @Test + void clearCache() { + stubGPT4(); + DeploymentCache.loadCache("default"); + + DeploymentCache.getDeploymentIdByModel("default", "gpt-4-32k"); + wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); + + DeploymentCache.clearCache(); + + DeploymentCache.getDeploymentIdByModel("default", "gpt-4-32k"); + // the deployment is not in the cache anymore, so we need to fetch it again + wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); + } + /** * The user creates a deployment after starting with an empty cache. * @@ -115,14 +131,14 @@ void newDeployment() { @Test void newDeploymentAfterReset() { stubEmpty(); - var cache = new DeploymentCache(new DeploymentApi(client), "default"); + DeploymentCache.loadCache("default"); stubGPT4(); - cache.getDeploymentIdByModel("default", "gpt-4-32k"); + DeploymentCache.getDeploymentIdByModel("default", "gpt-4-32k"); // 1 reset empty and 1 cache miss wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); - cache.getDeploymentIdByModel("default", "gpt-4-32k"); + DeploymentCache.getDeploymentIdByModel("default", "gpt-4-32k"); wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); } } From f61e5c8e4f91f7839e998b20782f1a64c9324c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 15 Oct 2024 10:45:09 +0200 Subject: [PATCH 49/79] refine work in progress --- .../com/sap/ai/sdk/core/AiCoreDeployment.java | 67 +++++------ .../com/sap/ai/sdk/core/AiCoreService.java | 111 ++++++++++++++---- .../ai/sdk/core/AiCoreServiceExtension.java | 82 ------------- 3 files changed, 121 insertions(+), 139 deletions(-) delete mode 100644 core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceExtension.java diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index c404aee1..0efc0e9c 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -4,7 +4,6 @@ import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; -import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationProperties; import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationProperty; import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import java.util.Map; @@ -15,16 +14,14 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; -import lombok.experimental.Delegate; /** Connectivity convenience methods for AI Core with deployment. */ -@RequiredArgsConstructor(access = AccessLevel.PRIVATE) -public class AiCoreDeployment extends AiCoreServiceExtension implements AiCoreDestination { +@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 - @Delegate - @Nonnull private final AiCoreServiceExtension delegate; + @Nonnull private final AiCoreService delegate; // the deployment id handler to be used, based on resource group @Nonnull private final Function deploymentId; @@ -40,7 +37,7 @@ public class AiCoreDeployment extends AiCoreServiceExtension implements AiCoreDe * @param service The AI Core Service instance. * @param deploymentId The deployment id handler, based on resource group. */ - public AiCoreDeployment( + protected AiCoreDeployment( @Nonnull final AiCoreService service, @Nonnull final Function deploymentId) { this(service, deploymentId, "default"); } @@ -48,9 +45,10 @@ public AiCoreDeployment( @Nonnull @Override public Destination destination() { - final var dest = getBaseDestination(); - final var builder = DefaultHttpDestination.fromDestination(dest); - refineDestinationBuilder(builder, dest); + final var dest = delegate.baseDestinationHandler.apply(delegate); + DefaultHttpDestination.Builder builder = delegate.builderHandler.apply(delegate, dest); + destinationSetUrl(builder, dest); + destinationSetHeaders(builder, dest); return builder.build(); } @@ -58,7 +56,28 @@ public Destination destination() { @Override public ApiClient client() { final var destination = destination(); - return createApiClient(destination); + return delegate.clientHandler.apply(delegate, destination); + } + + /** + * Update and set the URL for the destination. + * + * @param builder The destination builder. + * @param dest The original destination reference. + */ + protected void destinationSetUrl(DefaultHttpDestination.Builder builder, Destination dest) { + String uri = dest.get(DestinationProperty.URI).get(); + builder.uri(uri + "inference/deployments/%s/".formatted(getDeploymentId())); + } + + /** + * Update and set the default request headers for the destination. + * + * @param builder The destination builder. + * @param dest The original destination reference. + */ + protected void destinationSetHeaders(DefaultHttpDestination.Builder builder, Destination dest) { + builder.property(AI_RESOURCE_GROUP, getResourceGroup()); } /** @@ -69,7 +88,7 @@ public ApiClient client() { */ @Nonnull public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { - return new AiCoreDeployment(this, deploymentId, resourceGroup); + return new AiCoreDeployment(delegate, deploymentId, resourceGroup); } /** @@ -80,29 +99,7 @@ public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { */ @Nonnull public AiCoreDeployment withDestination(@Nonnull final Destination destination) { - return new AiCoreDeployment(this, deploymentId, resourceGroup) { - @Getter - private final Destination baseDestination = destination; - }; - } - - /** - * Update the destination builder. - * - * @param builder The new destination builder. - * @param d The original destination. - * @return The updated destination. - */ - @Override - protected void refineDestinationBuilder( - @Nonnull final DefaultHttpDestination.Builder builder, - @Nonnull final DestinationProperties d) { - - AiCoreService.this.refineDestinationBuilder(builder, d); - - String uri = d.get(DestinationProperty.URI).get(); - builder.uri(uri+"inference/deployments/%s/".formatted(getDeploymentId()))); - builder.property(AI_RESOURCE_GROUP, getResourceGroup()); + return new AiCoreDeployment(delegate.withDestination(destination), deploymentId, resourceGroup); } /** diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index ef5664ef..7c5a14d4 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -2,41 +2,63 @@ import static com.sap.ai.sdk.core.AiCoreDeployment.getDeploymentId; import static com.sap.ai.sdk.core.AiCoreDeployment.isDeploymentOfModel; +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.ai.sdk.core.client.model.AiDeployment; +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.DestinationProperties; 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.function.BiFunction; +import java.util.function.Function; import java.util.function.Predicate; import javax.annotation.Nonnull; -import lombok.AccessLevel; -import lombok.Getter; import lombok.RequiredArgsConstructor; -import lombok.experimental.Delegate; 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(access = AccessLevel.PRIVATE) -public class AiCoreService extends AiCoreServiceExtension implements AiCoreDestination { +@RequiredArgsConstructor +public class AiCoreService implements AiCoreDestination { - @Delegate - private final AiCoreServiceExtension delegate; + final Function baseDestinationHandler; + final BiFunction clientHandler; + final BiFunction builderHandler; - /** Create a new instance of the AI Core service. */ + /** The default constructor. */ public AiCoreService() { - this(new AiCoreServiceExtension()); + this( + AiCoreService::getBaseDestination, + AiCoreService::createApiClient, + AiCoreService::refineDestinationBuilder); + } + + @Nonnull + @Override + public ApiClient client() { + final var destination = destination(); + return clientHandler.apply(this, destination); } @Nonnull @Override public Destination destination() { - final var dest = getBaseDestination(); - final var builder = DefaultHttpDestination.fromDestination(dest); - refineDestinationBuilder(builder, dest); - return builder.build(); + final var dest = baseDestinationHandler.apply(this); + return builderHandler.apply(this, dest).build(); } /** @@ -47,10 +69,7 @@ public Destination destination() { */ @Nonnull public AiCoreService withDestination(@Nonnull final Destination destination) { - return new AiCoreService(this) { - @Getter - private final Destination baseDestination = destination; - }; + return new AiCoreService((_ignore) -> destination, clientHandler, builderHandler); } /** @@ -88,10 +107,58 @@ public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId return new AiCoreDeployment(this, res -> getDeploymentId(client(), res, p)); } + /** + * 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 - @Override - public ApiClient client() { - final var destination = destination(); - return createApiClient(destination); + protected Destination getBaseDestination() + throws DestinationAccessException, DestinationNotFoundException { + final var serviceKey = System.getenv("AICORE_SERVICE_KEY"); + return DestinationResolver.getDestination(serviceKey); + } + + @Nonnull + protected DefaultHttpDestination.Builder refineDestinationBuilder( + @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 createApiClient(@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()); } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceExtension.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceExtension.java deleted file mode 100644 index df0d25ab..00000000 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreServiceExtension.java +++ /dev/null @@ -1,82 +0,0 @@ -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.DestinationProperties; -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 javax.annotation.Nonnull; - -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; - -class AiCoreServiceExtension { - - /** - * 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); - }; - - @Nonnull - protected void refineDestinationBuilder( - @Nonnull final DefaultHttpDestination.Builder builder, - @Nonnull final DestinationProperties properties) { - String uri = properties.get(DestinationProperty.URI).get(); - if (!uri.endsWith("/")) { - uri = uri + "/"; - } - builder.uri(uri + "v2/").property(AI_CLIENT_TYPE_KEY, AI_CLIENT_TYPE_VALUE); - } - - /** - * 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 createApiClient(@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()); - } -} From d6e3f25297bd67c1be7c4cbcc2246b2c4d174be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 15 Oct 2024 11:13:41 +0200 Subject: [PATCH 50/79] Fix tests --- .../com/sap/ai/sdk/core/AiCoreDeployment.java | 30 +++++--- .../sap/ai/sdk/core/AiCoreServiceTest.java | 19 +++++ .../client/OrchestrationUnitTest.java | 73 +++++++++++-------- 3 files changed, 82 insertions(+), 40 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index 0efc0e9c..9ca04b4a 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -16,12 +16,12 @@ import lombok.RequiredArgsConstructor; /** Connectivity convenience methods for AI Core with deployment. */ -@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +@RequiredArgsConstructor(access = AccessLevel.PUBLIC) 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 delegate; + @Nonnull private final AiCoreService service; // the deployment id handler to be used, based on resource group @Nonnull private final Function deploymentId; @@ -37,16 +37,25 @@ public class AiCoreDeployment implements AiCoreDestination { * @param service The AI Core Service instance. * @param deploymentId The deployment id handler, based on resource group. */ - protected AiCoreDeployment( + public AiCoreDeployment( @Nonnull final AiCoreService service, @Nonnull final Function deploymentId) { this(service, deploymentId, "default"); } + /** + * Create a new instance of the AI Core service with a specific deployment id and destination. + * + * @param deploymentId The deployment id handler, based on resource group. + */ + public AiCoreDeployment(@Nonnull final String deploymentId) { + this(new AiCoreService(), (_ignore) -> deploymentId); + } + @Nonnull @Override public Destination destination() { - final var dest = delegate.baseDestinationHandler.apply(delegate); - DefaultHttpDestination.Builder builder = delegate.builderHandler.apply(delegate, dest); + final var dest = service.baseDestinationHandler.apply(service); + DefaultHttpDestination.Builder builder = service.builderHandler.apply(service, dest); destinationSetUrl(builder, dest); destinationSetHeaders(builder, dest); return builder.build(); @@ -56,7 +65,7 @@ public Destination destination() { @Override public ApiClient client() { final var destination = destination(); - return delegate.clientHandler.apply(delegate, destination); + return service.clientHandler.apply(service, destination); } /** @@ -67,7 +76,10 @@ public ApiClient client() { */ protected void destinationSetUrl(DefaultHttpDestination.Builder builder, Destination dest) { String uri = dest.get(DestinationProperty.URI).get(); - builder.uri(uri + "inference/deployments/%s/".formatted(getDeploymentId())); + if (!uri.endsWith("/")) { + uri = uri + "/"; + } + builder.uri(uri + "v2/inference/deployments/%s/".formatted(getDeploymentId())); } /** @@ -88,7 +100,7 @@ protected void destinationSetHeaders(DefaultHttpDestination.Builder builder, Des */ @Nonnull public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { - return new AiCoreDeployment(delegate, deploymentId, resourceGroup); + return new AiCoreDeployment(service, deploymentId, resourceGroup); } /** @@ -99,7 +111,7 @@ public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { */ @Nonnull public AiCoreDeployment withDestination(@Nonnull final Destination destination) { - return new AiCoreDeployment(delegate.withDestination(destination), deploymentId, resourceGroup); + return new AiCoreDeployment(service.withDestination(destination), deploymentId, resourceGroup); } /** diff --git a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java index aee0cdee..5a68f19d 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java @@ -110,4 +110,23 @@ void testDeployment() { assertThat(client.getBasePath()).isEqualTo("https://srv/v2/"); verify(accessor, times(2)).getServiceBindings(); } + + @Test + void testenCustomization() { + final var accessor = mock(ServiceBindingAccessor.class); + DestinationResolver.setAccessor(accessor); + doReturn(List.of(BINDING)).when(accessor).getServiceBindings(); + + // execution without errors + final var destination = new AiCoreService().destination(); + final var client = new AiCoreService().client(); + + // verification + assertThat(destination.get(DestinationProperty.URI)).contains("https://srv/v2/"); + assertThat(destination.get(DestinationProperty.AUTH_TYPE)).isEmpty(); + assertThat(destination.get(DestinationProperty.NAME)).singleElement(STRING).contains("aicore"); + assertThat(destination.get(AI_CLIENT_TYPE_KEY)).contains(AI_CLIENT_TYPE_VALUE); + assertThat(client.getBasePath()).isEqualTo("https://srv/v2/"); + verify(accessor, times(2)).getServiceBindings(); + } } diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java index ff49a64b..85ba3f5d 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java @@ -42,7 +42,6 @@ import java.util.Map; import java.util.Objects; import java.util.function.Function; - import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -83,36 +82,38 @@ public class OrchestrationUnitTest { void setup(WireMockRuntimeInfo server) { stubFor( - get(urlPathEqualTo("/v2/lm/deployments")) - .withHeader("AI-Resource-Group", equalTo("default")) - .willReturn( - aResponse() - .withStatus(HttpStatus.SC_OK) - .withHeader("content-type", "application/json") - .withBody( - """ - { - "resources": [ - { - "configurationId": "7652a231-ba9b-4fcc-b473-2c355cb21b61", - "id": "d19b998f347341aa", - "scenarioId": "orchestration" - } - ] - } - """))); - + get(urlPathEqualTo("/v2/lm/deployments")) + .withHeader("AI-Resource-Group", equalTo("default")) + .willReturn( + aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("content-type", "application/json") + .withBody( + """ + { + "resources": [ + { + "id": "abcdef0123456789", + "scenarioId": "orchestration" + } + ] + } + """))); final DefaultHttpDestination destination = DefaultHttpDestination.builder(server.getHttpBaseUrl()).build(); - final var apiClient = new AiCoreService().withDestination(destination).forDeploymentByScenario("orchestration").client(); + final var apiClient = + new AiCoreService() + .withDestination(destination) + .forDeploymentByScenario("orchestration") + .client(); client = new OrchestrationCompletionApi(apiClient); } @Test void templating() throws IOException { stubFor( - post(urlPathEqualTo("/completion")) + post(urlPathEqualTo("/v2/inference/deployments/abcdef0123456789/completion")) .willReturn( aResponse() .withBodyFile("templatingResponse.json") @@ -167,14 +168,16 @@ void templating() throws IOException { // verify that null fields are absent from the sent request try (var requestInputStream = TEST_FILE_LOADER.apply("templatingRequest.json")) { final String request = new String(requestInputStream.readAllBytes()); - verify(postRequestedFor(urlPathEqualTo("/completion")).withRequestBody(equalToJson(request))); + verify( + postRequestedFor(urlPathEqualTo("/v2/inference/deployments/abcdef0123456789/completion")) + .withRequestBody(equalToJson(request))); } } @Test void templatingBadRequest() { stubFor( - post(urlPathEqualTo("/completion")) + post(urlPathEqualTo("/v2/inference/deployments/abcdef0123456789/completion")) .willReturn( jsonResponse( """ @@ -248,7 +251,7 @@ void templatingBadRequest() { @Test void filteringLoose() throws IOException { stubFor( - post(urlPathEqualTo("/completion")) + post(urlPathEqualTo("/v2/inference/deployments/abcdef0123456789/completion")) .willReturn( aResponse() .withBodyFile("filteringLooseResponse.json") @@ -262,7 +265,9 @@ void filteringLoose() throws IOException { // verify that null fields are absent from the sent request try (var requestInputStream = TEST_FILE_LOADER.apply("filteringLooseRequest.json")) { final String request = new String(requestInputStream.readAllBytes()); - verify(postRequestedFor(urlPathEqualTo("/completion")).withRequestBody(equalToJson(request))); + verify( + postRequestedFor(urlPathEqualTo("/v2/inference/deployments/abcdef0123456789/completion")) + .withRequestBody(equalToJson(request))); } } @@ -271,7 +276,9 @@ void filteringStrict() { final String response = """ {"request_id": "bf6d6792-7adf-4d3c-9368-a73615af8c5a", "code": 400, "message": "Content filtered due to Safety violations. Please modify the prompt and try again.", "location": "Input Filter", "module_results": {"templating": [{"role": "user", "content": "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: The area surrounding the apartment is known for prostitutes and gang violence including armed conflicts, gun violence is frequent."}], "input_filtering": {"message": "Content filtered due to Safety violations. Please modify the prompt and try again.", "data": {"original_service_response": {"Hate": 0, "SelfHarm": 0, "Sexual": 0, "Violence": 2}, "checked_text": "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: The area surrounding the apartment is known for prostitutes and gang violence including armed conflicts, gun violence is frequent."}}}}"""; - stubFor(post(urlPathEqualTo("/completion")).willReturn(jsonResponse(response, SC_BAD_REQUEST))); + stubFor( + post(urlPathEqualTo("/v2/inference/deployments/abcdef0123456789/completion")) + .willReturn(jsonResponse(response, SC_BAD_REQUEST))); final var config = FILTERING_CONFIG.apply(NUMBER_0); @@ -283,7 +290,7 @@ void filteringStrict() { @Test void messagesHistory() throws IOException { stubFor( - post(urlPathEqualTo("/completion")) + post(urlPathEqualTo("/v2/inference/deployments/abcdef0123456789/completion")) .willReturn( aResponse() .withBodyFile("templatingResponse.json") @@ -308,7 +315,9 @@ void messagesHistory() throws IOException { // verify that the history is sent correctly try (var requestInputStream = TEST_FILE_LOADER.apply("messagesHistoryRequest.json")) { final String request = new String(requestInputStream.readAllBytes()); - verify(postRequestedFor(urlPathEqualTo("/completion")).withRequestBody(equalToJson(request))); + verify( + postRequestedFor(urlPathEqualTo("/v2/inference/deployments/abcdef0123456789/completion")) + .withRequestBody(equalToJson(request))); } } @@ -342,7 +351,7 @@ void messagesHistory() throws IOException { @Test void maskingAnonymization() throws IOException { stubFor( - post(urlPathEqualTo("/v2/completion")) + post(urlPathEqualTo("/v2/inference/deployments/abcdef0123456789/completion")) .willReturn( aResponse() .withBodyFile("maskingResponse.json") @@ -362,7 +371,9 @@ void maskingAnonymization() throws IOException { // verify that the request is sent correctly try (var requestInputStream = TEST_FILE_LOADER.apply("maskingRequest.json")) { final String request = new String(requestInputStream.readAllBytes()); - verify(postRequestedFor(urlPathEqualTo("/completion")).withRequestBody(equalToJson(request))); + verify( + postRequestedFor(urlPathEqualTo("/v2/inference/deployments/abcdef0123456789/completion")) + .withRequestBody(equalToJson(request))); } } } From 37495137beadf125488512d363284a39890896ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 15 Oct 2024 11:17:24 +0200 Subject: [PATCH 51/79] Update test --- .../sap/ai/sdk/core/AiCoreServiceTest.java | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java index 5a68f19d..dc096a19 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java @@ -14,13 +14,18 @@ import com.sap.cloud.environment.servicebinding.api.ServiceBindingAccessor; import com.sap.cloud.environment.servicebinding.api.ServiceIdentifier; 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 java.util.Collections; import java.util.List; import java.util.Map; + +import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import javax.annotation.Nonnull; + public class AiCoreServiceTest { // setup @@ -112,21 +117,17 @@ void testDeployment() { } @Test - void testenCustomization() { - final var accessor = mock(ServiceBindingAccessor.class); - DestinationResolver.setAccessor(accessor); - doReturn(List.of(BINDING)).when(accessor).getServiceBindings(); - - // execution without errors - final var destination = new AiCoreService().destination(); - final var client = new AiCoreService().client(); - - // verification - assertThat(destination.get(DestinationProperty.URI)).contains("https://srv/v2/"); - assertThat(destination.get(DestinationProperty.AUTH_TYPE)).isEmpty(); - assertThat(destination.get(DestinationProperty.NAME)).singleElement(STRING).contains("aicore"); - assertThat(destination.get(AI_CLIENT_TYPE_KEY)).contains(AI_CLIENT_TYPE_VALUE); - assertThat(client.getBasePath()).isEqualTo("https://srv/v2/"); - verify(accessor, times(2)).getServiceBindings(); + void testCustomization() { + final var customService = new AiCoreService() { + @Nonnull + @Override + protected ApiClient createApiClient(@Nonnull Destination destination) { + return new ApiClient().setBasePath("foo"); + }; + }; + + final var customServiceForDeployment = customService.forDeployment("deployment").withResourceGroup("group"); + ApiClient client = customServiceForDeployment.client(); + assertThat(client.getBasePath()).isEqualTo("foo"); } } From c76ad84e8f6628688d97d2f62f6e97123ec47a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 15 Oct 2024 11:23:48 +0200 Subject: [PATCH 52/79] Add annotations --- .../src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index 9ca04b4a..02de1f1f 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -74,7 +74,8 @@ public ApiClient client() { * @param builder The destination builder. * @param dest The original destination reference. */ - protected void destinationSetUrl(DefaultHttpDestination.Builder builder, Destination dest) { + protected void destinationSetUrl( + @Nonnull final DefaultHttpDestination.Builder builder, @Nonnull final Destination dest) { String uri = dest.get(DestinationProperty.URI).get(); if (!uri.endsWith("/")) { uri = uri + "/"; @@ -88,7 +89,8 @@ protected void destinationSetUrl(DefaultHttpDestination.Builder builder, Destina * @param builder The destination builder. * @param dest The original destination reference. */ - protected void destinationSetHeaders(DefaultHttpDestination.Builder builder, Destination dest) { + protected void destinationSetHeaders( + @Nonnull final DefaultHttpDestination.Builder builder, @Nonnull final Destination dest) { builder.property(AI_RESOURCE_GROUP, getResourceGroup()); } From 701d92bd604a84615857675857c45a6636d7c20c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 15 Oct 2024 11:23:57 +0200 Subject: [PATCH 53/79] Format --- .../sap/ai/sdk/core/AiCoreServiceTest.java | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java index dc096a19..2b506f42 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java @@ -16,16 +16,14 @@ 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.Collections; import java.util.List; import java.util.Map; - -import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; +import javax.annotation.Nonnull; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import javax.annotation.Nonnull; - public class AiCoreServiceTest { // setup @@ -118,15 +116,18 @@ void testDeployment() { @Test void testCustomization() { - final var customService = new AiCoreService() { - @Nonnull - @Override - protected ApiClient createApiClient(@Nonnull Destination destination) { - return new ApiClient().setBasePath("foo"); - }; - }; - - final var customServiceForDeployment = customService.forDeployment("deployment").withResourceGroup("group"); + final var customService = + new AiCoreService() { + @Nonnull + @Override + protected ApiClient createApiClient(@Nonnull Destination destination) { + return new ApiClient().setBasePath("foo"); + } + ; + }; + + final var customServiceForDeployment = + customService.forDeployment("deployment").withResourceGroup("group"); ApiClient client = customServiceForDeployment.client(); assertThat(client.getBasePath()).isEqualTo("foo"); } From a49d6c7b291fee1aa075c35968cc4fd9b3d4691d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 15 Oct 2024 12:39:52 +0200 Subject: [PATCH 54/79] Format; JavaDoc --- .../java/com/sap/ai/sdk/core/AiCoreService.java | 14 ++++++++++---- .../com/sap/ai/sdk/core/AiCoreServiceTest.java | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index 7c5a14d4..f8baa117 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -43,8 +43,8 @@ public class AiCoreService implements AiCoreDestination { public AiCoreService() { this( AiCoreService::getBaseDestination, - AiCoreService::createApiClient, - AiCoreService::refineDestinationBuilder); + AiCoreService::getApiClient, + AiCoreService::getDestinationBuilder); } @Nonnull @@ -121,8 +121,14 @@ protected Destination getBaseDestination() 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 refineDestinationBuilder( + protected DefaultHttpDestination.Builder getDestinationBuilder( @Nonnull final Destination destination) { final var builder = DefaultHttpDestination.fromDestination(destination); String uri = destination.get(DestinationProperty.URI).get(); @@ -142,7 +148,7 @@ protected DefaultHttpDestination.Builder refineDestinationBuilder( */ @SuppressWarnings("UnstableApiUsage") @Nonnull - protected ApiClient createApiClient(@Nonnull final Destination destination) { + protected ApiClient getApiClient(@Nonnull final Destination destination) { final var objectMapper = new Jackson2ObjectMapperBuilder() .modules(new JavaTimeModule()) diff --git a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java index 2b506f42..8b100e11 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java @@ -120,7 +120,7 @@ void testCustomization() { new AiCoreService() { @Nonnull @Override - protected ApiClient createApiClient(@Nonnull Destination destination) { + protected ApiClient getApiClient(@Nonnull final Destination destination) { return new ApiClient().setBasePath("foo"); } ; From 29914573a93b45d9cdf4dbf9b995b16c949faf14 Mon Sep 17 00:00:00 2001 From: I538344 Date: Tue, 15 Oct 2024 13:54:32 +0200 Subject: [PATCH 55/79] Fix tests --- .../java/com/sap/ai/sdk/core/AiCoreService.java | 11 +++-------- .../test/java/com/sap/ai/sdk/core/CacheTest.java | 16 ++++++++-------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index 62bdb320..c8de2dec 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -10,19 +10,16 @@ import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.google.common.collect.Iterables; -import com.sap.ai.sdk.core.client.model.AiDeployment; 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 java.util.NoSuchElementException; -import java.util.function.Supplier; import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; +import java.util.NoSuchElementException; import java.util.function.BiFunction; import java.util.function.Function; -import java.util.function.Predicate; import javax.annotation.Nonnull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -95,8 +92,7 @@ public AiCoreDeployment forDeployment(@Nonnull final String deploymentId) { */ @Nonnull public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) { - return new AiCoreDeployment( - res -> getDeploymentIdByModel(client(), res, modelName), this::destination); + return new AiCoreDeployment(this, res -> getDeploymentIdByModel(client(), res, modelName)); } /** @@ -109,8 +105,7 @@ public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) { */ @Nonnull public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId) { - return new AiCoreDeployment( - res -> getDeploymentIdByScenario(client(), res, scenarioId), this::destination); + return new AiCoreDeployment(this, res -> getDeploymentIdByScenario(client(), res, scenarioId)); } /** diff --git a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java index 3b0f2212..669b2a24 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java @@ -22,7 +22,7 @@ void setupCache() { private static void stubGPT4() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/deployments")) + get(urlPathEqualTo("/v2/lm/deployments")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -68,7 +68,7 @@ private static void stubGPT4() { private static void stubEmpty() { wireMockServer.stubFor( - get(urlPathEqualTo("/lm/deployments")) + get(urlPathEqualTo("/v2/lm/deployments")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() @@ -98,10 +98,10 @@ void newDeployment() { DeploymentCache.loadCache("default"); DeploymentCache.getDeploymentIdByModel("default", "gpt-4-32k"); - wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); + wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/v2/lm/deployments"))); DeploymentCache.getDeploymentIdByModel("default", "gpt-4-32k"); - wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); + wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/v2/lm/deployments"))); } @Test @@ -110,13 +110,13 @@ void clearCache() { DeploymentCache.loadCache("default"); DeploymentCache.getDeploymentIdByModel("default", "gpt-4-32k"); - wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/lm/deployments"))); + wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/v2/lm/deployments"))); DeploymentCache.clearCache(); DeploymentCache.getDeploymentIdByModel("default", "gpt-4-32k"); // the deployment is not in the cache anymore, so we need to fetch it again - wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); + wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/v2/lm/deployments"))); } /** @@ -136,9 +136,9 @@ void newDeploymentAfterReset() { DeploymentCache.getDeploymentIdByModel("default", "gpt-4-32k"); // 1 reset empty and 1 cache miss - wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); + wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/v2/lm/deployments"))); DeploymentCache.getDeploymentIdByModel("default", "gpt-4-32k"); - wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/lm/deployments"))); + wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/v2/lm/deployments"))); } } From 4dbc80474ac6f7b78e2f222b49e79eddc64335dd Mon Sep 17 00:00:00 2001 From: I538344 Date: Tue, 15 Oct 2024 14:00:21 +0200 Subject: [PATCH 56/79] Added resetCache --- .../com/sap/ai/sdk/core/DeploymentCache.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index 5a4e39a8..c28203cb 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -27,16 +27,28 @@ public class DeploymentCache { /** * Remove all entries from the cache. * - *

      Call both clearCache and {@link #loadCache} method whenever a deployment is deleted. + *

      Call {@link #resetCache} whenever a deployment is deleted. */ public static void clearCache() { CACHE.clear(); } + /** + * Remove all entries from the cache then load all deployments into the cache. + * + *

      Call this whenever a deployment is deleted. + * + * @param resourceGroup the resource group, usually "default". + */ + public static void resetCache(@Nonnull final String resourceGroup) { + clearCache(); + loadCache(resourceGroup); + } + /** * Load all deployments into the cache * - *

      Call both {@link #clearCache} and loadCache method whenever a deployment is deleted. + *

      Call {@link #resetCache} whenever a deployment is deleted. * * @param resourceGroup the resource group, usually "default". */ @@ -65,7 +77,7 @@ public static String getDeploymentIdByModel( return getDeploymentIdByModel(modelName) .orElseGet( () -> { - loadCache(resourceGroup); + resetCache(resourceGroup); return getDeploymentIdByModel(modelName) .orElseThrow( () -> @@ -97,7 +109,7 @@ public static String getDeploymentIdByScenario( return getDeploymentIdByScenario(scenarioId) .orElseGet( () -> { - loadCache(resourceGroup); + resetCache(resourceGroup); return getDeploymentIdByScenario(scenarioId) .orElseThrow( () -> From f224ec6d01cb271f9ac100d8843b2f0c9b14f804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 15 Oct 2024 15:43:51 +0200 Subject: [PATCH 57/79] Restructure code --- .../com/sap/ai/sdk/core/AiCoreDeployment.java | 62 +++++++++++++------ .../com/sap/ai/sdk/core/AiCoreService.java | 12 +--- 2 files changed, 47 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index 02de1f1f..a60e5c1e 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -2,10 +2,12 @@ import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiDeployment; +import com.sap.ai.sdk.core.client.model.AiDeploymentList; 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.LinkedHashMap; import java.util.Map; import java.util.NoSuchElementException; import java.util.function.Function; @@ -18,13 +20,16 @@ /** Connectivity convenience methods for AI Core with deployment. */ @RequiredArgsConstructor(access = AccessLevel.PUBLIC) public class AiCoreDeployment implements AiCoreDestination { + + private static Map CACHE = new LinkedHashMap<>(); + 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 resource group - @Nonnull private final Function deploymentId; + // the deployment id handler to be used, based on instance + @Nonnull private final Function deploymentId; // the resource group, "default" if null @Getter(AccessLevel.PROTECTED) @@ -32,23 +37,44 @@ public class AiCoreDeployment implements AiCoreDestination { private final String resourceGroup; /** - * Create a new instance of the AI Core service with a specific deployment id and destination. + * Create a new instance of the AI Core service with a deployment. * - * @param service The AI Core Service instance. - * @param deploymentId The deployment id handler, based on resource group. + * @param service The AI Core service. + * @param modelName The model name. + * @return A new instance of the AI Core service. */ - public AiCoreDeployment( - @Nonnull final AiCoreService service, @Nonnull final Function deploymentId) { - this(service, deploymentId, "default"); + @Nonnull + public static AiCoreDeployment forModelName( + @Nonnull final AiCoreService service, @Nonnull final String modelName) { + final Predicate p = deployment -> isDeploymentOfModel(modelName, deployment); + return new AiCoreDeployment(service, obj -> obj.getDeploymentId(p), "default"); } /** - * Create a new instance of the AI Core service with a specific deployment id and destination. + * Create a new instance of the AI Core service with a deployment. * - * @param deploymentId The deployment id handler, based on resource group. + * @param service The AI Core service. + * @param scenarioId The scenario id. + * @return A new instance of the AI Core service. */ - public AiCoreDeployment(@Nonnull final String deploymentId) { - this(new AiCoreService(), (_ignore) -> deploymentId); + @Nonnull + public static AiCoreDeployment forScenarioId( + @Nonnull final AiCoreService service, @Nonnull final String scenarioId) { + final Predicate p = deployment -> scenarioId.equals(deployment.getScenarioId()); + return new AiCoreDeployment(service, obj -> obj.getDeploymentId(p), "default"); + } + + /** + * Create a new instance of the AI Core service with a deployment. + * + * @param service The AI Core service. + * @param deploymentId The deployment id. + * @return A new instance of the AI Core service. + */ + @Nonnull + public static AiCoreDeployment forDeploymentId( + @Nonnull final AiCoreService service, @Nonnull final String deploymentId) { + return new AiCoreDeployment(service, obj -> deploymentId, "default"); } @Nonnull @@ -123,7 +149,7 @@ public AiCoreDeployment withDestination(@Nonnull final Destination destination) */ @Nonnull protected String getDeploymentId() { - return deploymentId.apply(getResourceGroup()); + return deploymentId.apply(this); } /** @@ -170,12 +196,12 @@ protected static boolean isDeploymentOfModel( * @throws NoSuchElementException if no deployment is found for the scenario id. */ @Nonnull - protected static String getDeploymentId( - @Nonnull final ApiClient client, - @Nonnull final String resourceGroup, - @Nonnull final Predicate predicate) + protected String getDeploymentId(@Nonnull final Predicate predicate) throws NoSuchElementException { - final var deployments = new DeploymentApi(client).query(resourceGroup); + + final var resourceGroup = getResourceGroup(); + final var deployments = + CACHE.computeIfAbsent(resourceGroup, rg -> new DeploymentApi(client()).query(rg)); final var first = deployments.getResources().stream().filter(predicate).map(AiDeployment::getId).findFirst(); diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index f8baa117..343294dc 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -1,7 +1,5 @@ package com.sap.ai.sdk.core; -import static com.sap.ai.sdk.core.AiCoreDeployment.getDeploymentId; -import static com.sap.ai.sdk.core.AiCoreDeployment.isDeploymentOfModel; import static com.sap.ai.sdk.core.DestinationResolver.AI_CLIENT_TYPE_KEY; import static com.sap.ai.sdk.core.DestinationResolver.AI_CLIENT_TYPE_VALUE; @@ -10,7 +8,6 @@ import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.google.common.collect.Iterables; -import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; @@ -20,7 +17,6 @@ import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import java.util.function.BiFunction; import java.util.function.Function; -import java.util.function.Predicate; import javax.annotation.Nonnull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -80,7 +76,7 @@ public AiCoreService withDestination(@Nonnull final Destination destination) { */ @Nonnull public AiCoreDeployment forDeployment(@Nonnull final String deploymentId) { - return new AiCoreDeployment(this, res -> deploymentId); + return AiCoreDeployment.forDeploymentId(this, deploymentId); } /** @@ -91,8 +87,7 @@ public AiCoreDeployment forDeployment(@Nonnull final String deploymentId) { */ @Nonnull public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) { - final Predicate p = deployment -> isDeploymentOfModel(modelName, deployment); - return new AiCoreDeployment(this, res -> getDeploymentId(client(), res, p)); + return AiCoreDeployment.forModelName(this, modelName); } /** @@ -103,8 +98,7 @@ public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) { */ @Nonnull public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId) { - final Predicate p = deployment -> scenarioId.equals(deployment.getScenarioId()); - return new AiCoreDeployment(this, res -> getDeploymentId(client(), res, p)); + return AiCoreDeployment.forScenarioId(this, scenarioId); } /** From b13b9cddb9fcea28c0dd04b2cafc31aaee9a4c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 15 Oct 2024 15:49:03 +0200 Subject: [PATCH 58/79] Fix PMD --- core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java | 2 +- core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index 343294dc..c6401d23 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -65,7 +65,7 @@ public Destination destination() { */ @Nonnull public AiCoreService withDestination(@Nonnull final Destination destination) { - return new AiCoreService((_ignore) -> destination, clientHandler, builderHandler); + return new AiCoreService((service) -> destination, clientHandler, builderHandler); } /** diff --git a/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java b/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java index 4c7c0ce1..3e2eebd8 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java @@ -109,7 +109,7 @@ public AiCoreCredentialsInvalidException( * * @param accessor The accessor to be used for service binding resolution. */ - static void setAccessor(@Nullable ServiceBindingAccessor accessor) { + static void setAccessor(@Nullable final ServiceBindingAccessor accessor) { DestinationResolver.accessor = accessor == null ? DefaultServiceBindingAccessor.getInstance() : accessor; } From 0afd536a830038097b274f73a3480d0c9e48dd49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 15 Oct 2024 15:52:15 +0200 Subject: [PATCH 59/79] Fix PMD --- core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index a60e5c1e..92b8105e 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -81,7 +81,7 @@ public static AiCoreDeployment forDeploymentId( @Override public Destination destination() { final var dest = service.baseDestinationHandler.apply(service); - DefaultHttpDestination.Builder builder = service.builderHandler.apply(service, dest); + final var builder = service.builderHandler.apply(service, dest); destinationSetUrl(builder, dest); destinationSetHeaders(builder, dest); return builder.build(); From 1503338324d7688b41acb706bbc163d66674ee7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 15 Oct 2024 16:10:43 +0200 Subject: [PATCH 60/79] Fix test --- .../sap/ai/sdk/core/AiCoreServiceTest.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java index 8b100e11..c12f77c5 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java @@ -120,15 +120,24 @@ void testCustomization() { new AiCoreService() { @Nonnull @Override - protected ApiClient getApiClient(@Nonnull final Destination destination) { - return new ApiClient().setBasePath("foo"); + protected Destination getBaseDestination() { + return DefaultHttpDestination.builder("https://ai").build(); + } + + @Nonnull + @Override + protected ApiClient getApiClient(@Nonnull Destination destination) { + return new ApiClient().setBasePath("https://fizz.buzz").setUserAgent("SAP"); } - ; }; final var customServiceForDeployment = customService.forDeployment("deployment").withResourceGroup("group"); - ApiClient client = customServiceForDeployment.client(); - assertThat(client.getBasePath()).isEqualTo("foo"); + + final var client = customServiceForDeployment.client(); + assertThat(client.getBasePath()).isEqualTo("https://fizz.buzz"); + + final var destination = customServiceForDeployment.destination().asHttp(); + assertThat(destination.getUri()).hasToString("https://ai/v2/inference/deployments/deployment/"); } } From 7c98c721ecce71eefbc4470e72068b2bc56be920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Tue, 15 Oct 2024 16:28:59 +0200 Subject: [PATCH 61/79] Fix test --- .../com/sap/ai/sdk/core/AiCoreDeployment.java | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index 92b8105e..721b9d74 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -18,7 +18,7 @@ import lombok.RequiredArgsConstructor; /** Connectivity convenience methods for AI Core with deployment. */ -@RequiredArgsConstructor(access = AccessLevel.PUBLIC) +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) public class AiCoreDeployment implements AiCoreDestination { private static Map CACHE = new LinkedHashMap<>(); @@ -36,6 +36,18 @@ public class AiCoreDeployment implements AiCoreDestination { @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 deploymentId) { + this(service, deploymentId, "default"); + } + /** * Create a new instance of the AI Core service with a deployment. * @@ -47,7 +59,7 @@ public class AiCoreDeployment implements AiCoreDestination { public static AiCoreDeployment forModelName( @Nonnull final AiCoreService service, @Nonnull final String modelName) { final Predicate p = deployment -> isDeploymentOfModel(modelName, deployment); - return new AiCoreDeployment(service, obj -> obj.getDeploymentId(p), "default"); + return new AiCoreDeployment(service, obj -> obj.getDeploymentId(service.client(), p)); } /** @@ -61,7 +73,7 @@ public static AiCoreDeployment forModelName( public static AiCoreDeployment forScenarioId( @Nonnull final AiCoreService service, @Nonnull final String scenarioId) { final Predicate p = deployment -> scenarioId.equals(deployment.getScenarioId()); - return new AiCoreDeployment(service, obj -> obj.getDeploymentId(p), "default"); + return new AiCoreDeployment(service, obj -> obj.getDeploymentId(service.client(), p)); } /** @@ -74,7 +86,7 @@ public static AiCoreDeployment forScenarioId( @Nonnull public static AiCoreDeployment forDeploymentId( @Nonnull final AiCoreService service, @Nonnull final String deploymentId) { - return new AiCoreDeployment(service, obj -> deploymentId, "default"); + return new AiCoreDeployment(service, obj -> deploymentId); } @Nonnull @@ -196,12 +208,13 @@ protected static boolean isDeploymentOfModel( * @throws NoSuchElementException if no deployment is found for the scenario id. */ @Nonnull - protected String getDeploymentId(@Nonnull final Predicate predicate) + protected String getDeploymentId( + @Nonnull final ApiClient client, @Nonnull final Predicate predicate) throws NoSuchElementException { final var resourceGroup = getResourceGroup(); final var deployments = - CACHE.computeIfAbsent(resourceGroup, rg -> new DeploymentApi(client()).query(rg)); + CACHE.computeIfAbsent(resourceGroup, rg -> new DeploymentApi(client).query(rg)); final var first = deployments.getResources().stream().filter(predicate).map(AiDeployment::getId).findFirst(); From fbc7f5b8abb24df2c65afc6a7c8c939e65401ab1 Mon Sep 17 00:00:00 2001 From: I538344 Date: Wed, 16 Oct 2024 09:31:14 +0200 Subject: [PATCH 62/79] Merged --- .../com/sap/ai/sdk/core/AiCoreDeployment.java | 40 +++++-------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index 8a1fb6df..a8e13be5 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -2,12 +2,11 @@ import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiDeployment; -import com.sap.ai.sdk.core.client.model.AiDeploymentList; 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.LinkedHashMap; + import java.util.Map; import java.util.NoSuchElementException; import java.util.function.Function; @@ -20,8 +19,6 @@ @RequiredArgsConstructor(access = AccessLevel.PROTECTED) public class AiCoreDeployment implements AiCoreDestination { - private static Map CACHE = new LinkedHashMap<>(); - private static final String AI_RESOURCE_GROUP = "URL.headers.AI-Resource-Group"; // the delegating AI Core Service instance @@ -57,8 +54,8 @@ public AiCoreDeployment( @Nonnull public static AiCoreDeployment forModelName( @Nonnull final AiCoreService service, @Nonnull final String modelName) { - final Predicate p = deployment -> isDeploymentOfModel(modelName, deployment); - return new AiCoreDeployment(service, obj -> obj.getDeploymentId(service.client(), p)); + return new AiCoreDeployment( + service, obj -> obj.getDeploymentIdByModel(service.client(), modelName)); } /** @@ -71,8 +68,8 @@ public static AiCoreDeployment forModelName( @Nonnull public static AiCoreDeployment forScenarioId( @Nonnull final AiCoreService service, @Nonnull final String scenarioId) { - final Predicate p = deployment -> scenarioId.equals(deployment.getScenarioId()); - return new AiCoreDeployment(service, obj -> obj.getDeploymentId(service.client(), p)); + return new AiCoreDeployment( + service, obj -> obj.getDeploymentIdByScenario(service.client(), scenarioId)); } /** @@ -203,47 +200,32 @@ protected static boolean isDeploymentOfModel( * Get the deployment id from the foundation model name. If there are multiple deployments of the * same model, the first one is returned. * - * @param resourceGroup the resource group, usually "default". * @param modelName the name of the foundation model. * @return the deployment id. * @throws NoSuchElementException if no running deployment is found for the model. */ @Nonnull - protected static String getDeploymentIdByModel( - @Nonnull final ApiClient client, - @Nonnull final String resourceGroup, - @Nonnull final String modelName) + protected String getDeploymentIdByModel( + @Nonnull final ApiClient client, @Nonnull final String modelName) throws NoSuchElementException { DeploymentCache.API = new DeploymentApi(client); - return DeploymentCache.getDeploymentIdByModel(resourceGroup, modelName); + return DeploymentCache.getDeploymentIdByModel(getResourceGroup(), modelName); } /** - * Get the deployment id from the scenario id. If there are multiple deployments of the * same + * Get the deployment id from the scenario id. If there are multiple deployments of the same * model, the first one is returned. * * @param client The API client to do HTTP requests to AI Core. - * @param resourceGroup the resource group, usually "default". * @param scenarioId the scenario id, can be "orchestration". * @return the deployment id. * @throws NoSuchElementException if no running deployment is found for the scenario. */ @Nonnull protected String getDeploymentIdByScenario( - @Nonnull final ApiClient client, - @Nonnull final String resourceGroup, - @Nonnull final String scenarioId) + @Nonnull final ApiClient client, @Nonnull final String scenarioId) throws NoSuchElementException { DeploymentCache.API = new DeploymentApi(client); - return DeploymentCache.getDeploymentIdByScenario(resourceGroup, scenarioId); - - final var resourceGroup = getResourceGroup(); - final var deployments = - CACHE.computeIfAbsent(resourceGroup, rg -> new DeploymentApi(client).query(rg)); - - final var first = - deployments.getResources().stream().filter(predicate).map(AiDeployment::getId).findFirst(); - return first.orElseThrow( - () -> new NoSuchElementException("No deployment found with scenario id orchestration")); + return DeploymentCache.getDeploymentIdByScenario(getResourceGroup(), scenarioId); } } From fdd163aad8a14b708e523c65c23b25253af40da4 Mon Sep 17 00:00:00 2001 From: I538344 Date: Wed, 16 Oct 2024 09:31:36 +0200 Subject: [PATCH 63/79] spotless --- core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index a8e13be5..774304d3 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -6,7 +6,6 @@ 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.Map; import java.util.NoSuchElementException; import java.util.function.Function; From c8233cd57ead8a2419e2a574fa6942bc5929e508 Mon Sep 17 00:00:00 2001 From: I538344 Date: Wed, 16 Oct 2024 09:56:20 +0200 Subject: [PATCH 64/79] Reduced code --- .../com/sap/ai/sdk/core/AiCoreDeployment.java | 76 ------------------- .../com/sap/ai/sdk/core/AiCoreService.java | 14 ++-- .../com/sap/ai/sdk/core/DeploymentCache.java | 47 +++++++----- .../java/com/sap/ai/sdk/core/CacheTest.java | 22 +++--- 4 files changed, 47 insertions(+), 112 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index 774304d3..58b1e016 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -1,13 +1,11 @@ package com.sap.ai.sdk.core; -import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiDeployment; 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.Map; -import java.util.NoSuchElementException; import java.util.function.Function; import javax.annotation.Nonnull; import lombok.AccessLevel; @@ -43,47 +41,6 @@ public AiCoreDeployment( this(service, deploymentId, "default"); } - /** - * Create a new instance of the AI Core service with a deployment. - * - * @param service The AI Core service. - * @param modelName The model name. - * @return A new instance of the AI Core service. - */ - @Nonnull - public static AiCoreDeployment forModelName( - @Nonnull final AiCoreService service, @Nonnull final String modelName) { - return new AiCoreDeployment( - service, obj -> obj.getDeploymentIdByModel(service.client(), modelName)); - } - - /** - * Create a new instance of the AI Core service with a deployment. - * - * @param service The AI Core service. - * @param scenarioId The scenario id. - * @return A new instance of the AI Core service. - */ - @Nonnull - public static AiCoreDeployment forScenarioId( - @Nonnull final AiCoreService service, @Nonnull final String scenarioId) { - return new AiCoreDeployment( - service, obj -> obj.getDeploymentIdByScenario(service.client(), scenarioId)); - } - - /** - * Create a new instance of the AI Core service with a deployment. - * - * @param service The AI Core service. - * @param deploymentId The deployment id. - * @return A new instance of the AI Core service. - */ - @Nonnull - public static AiCoreDeployment forDeploymentId( - @Nonnull final AiCoreService service, @Nonnull final String deploymentId) { - return new AiCoreDeployment(service, obj -> deploymentId); - } - @Nonnull @Override public Destination destination() { @@ -194,37 +151,4 @@ protected static boolean isDeploymentOfModel( } return false; } - - /** - * Get the deployment id from the foundation model name. If there are multiple deployments of the - * same model, the first one is returned. - * - * @param modelName the name of the foundation model. - * @return the deployment id. - * @throws NoSuchElementException if no running deployment is found for the model. - */ - @Nonnull - protected String getDeploymentIdByModel( - @Nonnull final ApiClient client, @Nonnull final String modelName) - throws NoSuchElementException { - DeploymentCache.API = new DeploymentApi(client); - return DeploymentCache.getDeploymentIdByModel(getResourceGroup(), modelName); - } - - /** - * Get the deployment id from the scenario id. If there are multiple deployments of the same - * model, the first one is returned. - * - * @param client The API client to do HTTP requests to AI Core. - * @param scenarioId the scenario id, can be "orchestration". - * @return the deployment id. - * @throws NoSuchElementException if no running deployment is found for the scenario. - */ - @Nonnull - protected String getDeploymentIdByScenario( - @Nonnull final ApiClient client, @Nonnull final String scenarioId) - throws NoSuchElementException { - DeploymentCache.API = new DeploymentApi(client); - return DeploymentCache.getDeploymentIdByScenario(getResourceGroup(), scenarioId); - } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index fe0bc4e7..f9773d6f 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -1,5 +1,7 @@ package com.sap.ai.sdk.core; +import static com.sap.ai.sdk.core.DeploymentCache.getDeploymentIdByModel; +import static com.sap.ai.sdk.core.DeploymentCache.getDeploymentIdByScenario; import static com.sap.ai.sdk.core.DestinationResolver.AI_CLIENT_TYPE_KEY; import static com.sap.ai.sdk.core.DestinationResolver.AI_CLIENT_TYPE_VALUE; @@ -77,7 +79,7 @@ public AiCoreService withDestination(@Nonnull final Destination destination) { */ @Nonnull public AiCoreDeployment forDeployment(@Nonnull final String deploymentId) { - return AiCoreDeployment.forDeploymentId(this, deploymentId); + return new AiCoreDeployment(this, obj -> deploymentId); } /** @@ -90,12 +92,13 @@ public AiCoreDeployment forDeployment(@Nonnull final String deploymentId) { */ @Nonnull public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) { - return AiCoreDeployment.forModelName(this, modelName); + return new AiCoreDeployment( + this, obj -> 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. + * 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 service. @@ -103,7 +106,8 @@ public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) { */ @Nonnull public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId) { - return AiCoreDeployment.forScenarioId(this, scenarioId); + return new AiCoreDeployment( + this, obj -> getDeploymentIdByScenario(this.client(), obj.getResourceGroup(), scenarioId)); } /** diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index c28203cb..728b74ac 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -4,6 +4,7 @@ import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiDeployment; +import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import com.sap.cloud.sdk.services.openapi.core.OpenApiRequestException; import java.util.ArrayList; import java.util.List; @@ -18,31 +19,31 @@ */ @Slf4j public class DeploymentCache { - /** The client to use for deployment queries. */ - protected static DeploymentApi API; /** Cache for deployment ids. The key is the model name and the value is the deployment id. */ protected static final List CACHE = new ArrayList<>(); - /** - * Remove all entries from the cache. - * - *

      Call {@link #resetCache} whenever a deployment is deleted. - */ - public static void clearCache() { - CACHE.clear(); - } - /** * Remove all entries from the cache then load all deployments into the cache. * *

      Call this whenever a deployment is deleted. * + * @param client the API client to query deployments. * @param resourceGroup the resource group, usually "default". */ - public static void resetCache(@Nonnull final String resourceGroup) { + public static void resetCache( + @Nonnull final ApiClient client, @Nonnull final String resourceGroup) { clearCache(); - loadCache(resourceGroup); + loadCache(client, resourceGroup); + } + + /** + * Remove all entries from the cache. + * + *

      Call {@link #resetCache} whenever a deployment is deleted. + */ + protected static void clearCache() { + CACHE.clear(); } /** @@ -50,11 +51,13 @@ public static void resetCache(@Nonnull final String resourceGroup) { * *

      Call {@link #resetCache} whenever a deployment is deleted. * + * @param client the API client to query deployments. * @param resourceGroup the resource group, usually "default". */ - public static void loadCache(@Nonnull final String resourceGroup) { + protected static void loadCache( + @Nonnull final ApiClient client, @Nonnull final String resourceGroup) { try { - final var deployments = API.query(resourceGroup).getResources(); + final var deployments = new DeploymentApi(client).query(resourceGroup).getResources(); CACHE.addAll(deployments); } catch (final OpenApiRequestException e) { log.error("Failed to load deployments into cache", e); @@ -65,6 +68,7 @@ public static void loadCache(@Nonnull final String resourceGroup) { * Get the deployment id from the foundation model name. If there are multiple deployments of the * same model, the first one is returned. * + * @param client the API client to maybe reset the cache if the deployment is not found. * @param resourceGroup the resource group, usually "default". * @param modelName the name of the foundation model. * @return the deployment id. @@ -72,12 +76,14 @@ public static void loadCache(@Nonnull final String resourceGroup) { */ @Nonnull public static String getDeploymentIdByModel( - @Nonnull final String resourceGroup, @Nonnull final String modelName) + @Nonnull final ApiClient client, + @Nonnull final String resourceGroup, + @Nonnull final String modelName) throws NoSuchElementException { return getDeploymentIdByModel(modelName) .orElseGet( () -> { - resetCache(resourceGroup); + resetCache(client, resourceGroup); return getDeploymentIdByModel(modelName) .orElseThrow( () -> @@ -97,6 +103,7 @@ private static Optional getDeploymentIdByModel(@Nonnull final String mod * Get the deployment id from the scenario id. If there are multiple deployments of the * same * model, the first one is returned. * + * @param client the API client to maybe reset the cache if the deployment is not found. * @param resourceGroup the resource group, usually "default". * @param scenarioId the scenario id, can be "orchestration". * @return the deployment id. @@ -104,12 +111,14 @@ private static Optional getDeploymentIdByModel(@Nonnull final String mod */ @Nonnull public static String getDeploymentIdByScenario( - @Nonnull final String resourceGroup, @Nonnull final String scenarioId) + @Nonnull final ApiClient client, + @Nonnull final String resourceGroup, + @Nonnull final String scenarioId) throws NoSuchElementException { return getDeploymentIdByScenario(scenarioId) .orElseGet( () -> { - resetCache(resourceGroup); + resetCache(client, resourceGroup); return getDeploymentIdByScenario(scenarioId) .orElseThrow( () -> diff --git a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java index 669b2a24..905a2969 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java @@ -6,7 +6,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; -import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.WireMockTestServer; import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.BeforeEach; @@ -16,7 +15,6 @@ class CacheTest extends WireMockTestServer { @BeforeEach void setupCache() { - DeploymentCache.API = new DeploymentApi(client); wireMockServer.resetRequests(); } @@ -95,27 +93,27 @@ private static void stubEmpty() { @Test void newDeployment() { stubGPT4(); - DeploymentCache.loadCache("default"); + DeploymentCache.loadCache(client, "default"); - DeploymentCache.getDeploymentIdByModel("default", "gpt-4-32k"); + DeploymentCache.getDeploymentIdByModel(client, "default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/v2/lm/deployments"))); - DeploymentCache.getDeploymentIdByModel("default", "gpt-4-32k"); + DeploymentCache.getDeploymentIdByModel(client, "default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/v2/lm/deployments"))); } @Test void clearCache() { stubGPT4(); - DeploymentCache.loadCache("default"); + DeploymentCache.loadCache(client, "default"); - DeploymentCache.getDeploymentIdByModel("default", "gpt-4-32k"); + DeploymentCache.getDeploymentIdByModel(client, "default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/v2/lm/deployments"))); DeploymentCache.clearCache(); - DeploymentCache.getDeploymentIdByModel("default", "gpt-4-32k"); - // the deployment is not in the cache anymore, so we need to fetch it again + DeploymentCache.getDeploymentIdByModel(client, "default", "gpt-4-32k"); + // the deployment is not in the cache anymore, so we need to query it again wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/v2/lm/deployments"))); } @@ -131,14 +129,14 @@ void clearCache() { @Test void newDeploymentAfterReset() { stubEmpty(); - DeploymentCache.loadCache("default"); + DeploymentCache.loadCache(client, "default"); stubGPT4(); - DeploymentCache.getDeploymentIdByModel("default", "gpt-4-32k"); + DeploymentCache.getDeploymentIdByModel(client, "default", "gpt-4-32k"); // 1 reset empty and 1 cache miss wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/v2/lm/deployments"))); - DeploymentCache.getDeploymentIdByModel("default", "gpt-4-32k"); + DeploymentCache.getDeploymentIdByModel(client, "default", "gpt-4-32k"); wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/v2/lm/deployments"))); } } From cabee7a7073f85820814c5a04c5b6891f72abc16 Mon Sep 17 00:00:00 2001 From: I538344 Date: Wed, 16 Oct 2024 09:59:37 +0200 Subject: [PATCH 65/79] Added throws --- core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index f9773d6f..9dfe6037 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -91,7 +91,8 @@ public AiCoreDeployment forDeployment(@Nonnull final String deploymentId) { * @throws NoSuchElementException if no running deployment is found for the model. */ @Nonnull - public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) { + public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) + throws NoSuchElementException { return new AiCoreDeployment( this, obj -> getDeploymentIdByModel(this.client(), obj.getResourceGroup(), modelName)); } @@ -105,7 +106,8 @@ public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) { * @throws NoSuchElementException if no running deployment is found for the scenario. */ @Nonnull - public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId) { + public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId) + throws NoSuchElementException { return new AiCoreDeployment( this, obj -> getDeploymentIdByScenario(this.client(), obj.getResourceGroup(), scenarioId)); } From 20b8cda963fa411e6f7eb104ecc14c68025a6333 Mon Sep 17 00:00:00 2001 From: I538344 Date: Wed, 16 Oct 2024 10:46:32 +0200 Subject: [PATCH 66/79] Added API client to application startup --- .../src/main/java/com/sap/ai/sdk/app/Application.java | 8 +++++++- .../ai/sdk/app/controllers/ConfigurationController.java | 5 ++--- .../sap/ai/sdk/app/controllers/DeploymentController.java | 5 ++--- .../sap/ai/sdk/app/controllers/ScenarioController.java | 5 +++-- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/Application.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/Application.java index 5378b970..a78dbbba 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/Application.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/Application.java @@ -1,14 +1,20 @@ package com.sap.ai.sdk.app; +import com.sap.ai.sdk.core.AiCoreService; +import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.ServletComponentScan; import org.springframework.context.annotation.ComponentScan; +/** Main class to start the Spring Boot application. */ @SpringBootApplication @ComponentScan({"com.sap.cloud.sdk", "com.sap.ai.sdk.app"}) @ServletComponentScan({"com.sap.cloud.sdk", "com.sap.ai.sdk.app"}) -class Application { +public class Application { + /** The API client connected to the AI Core service. */ + public static final ApiClient API_CLIENT = new AiCoreService().client(); + /** * Main method to start the Spring Boot application. * diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ConfigurationController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ConfigurationController.java index 630687aa..6afe81b9 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ConfigurationController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ConfigurationController.java @@ -1,9 +1,9 @@ package com.sap.ai.sdk.app.controllers; -import com.sap.ai.sdk.core.AiCoreService; +import static com.sap.ai.sdk.app.Application.API_CLIENT; + import com.sap.ai.sdk.core.client.ConfigurationApi; import com.sap.ai.sdk.core.client.model.AiConfigurationList; -import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -12,7 +12,6 @@ @RestController public class ConfigurationController { - private static final ApiClient API_CLIENT = new AiCoreService().client(); private static final ConfigurationApi API = new ConfigurationApi(API_CLIENT); /** diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java index b093d52f..99abb794 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/DeploymentController.java @@ -1,6 +1,7 @@ package com.sap.ai.sdk.app.controllers; -import com.sap.ai.sdk.core.AiCoreService; +import static com.sap.ai.sdk.app.Application.API_CLIENT; + import com.sap.ai.sdk.core.client.ConfigurationApi; import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiConfigurationBaseData; @@ -15,7 +16,6 @@ import com.sap.ai.sdk.core.client.model.AiDeploymentTargetStatus; import com.sap.ai.sdk.core.client.model.AiParameterArgumentBinding; import com.sap.ai.sdk.foundationmodels.openai.OpenAiModel; -import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import java.util.List; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -31,7 +31,6 @@ @RequestMapping("/deployments") class DeploymentController { - private static final ApiClient API_CLIENT = new AiCoreService().client(); private static final DeploymentApi API = new DeploymentApi(API_CLIENT); /** diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ScenarioController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ScenarioController.java index 7a1bdb4a..1e6df585 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ScenarioController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/ScenarioController.java @@ -1,6 +1,7 @@ package com.sap.ai.sdk.app.controllers; -import com.sap.ai.sdk.core.AiCoreService; +import static com.sap.ai.sdk.app.Application.API_CLIENT; + import com.sap.ai.sdk.core.client.ScenarioApi; import com.sap.ai.sdk.core.client.model.AiModelList; import com.sap.ai.sdk.core.client.model.AiScenarioList; @@ -13,7 +14,7 @@ @SuppressWarnings("unused") // debug method that doesn't need to be tested public class ScenarioController { - private static final ScenarioApi API = new ScenarioApi(new AiCoreService().client()); + private static final ScenarioApi API = new ScenarioApi(API_CLIENT); /** * Get the list of available scenarios From 350a555805d9bc14cf132d1b6e4e00603bbbd510 Mon Sep 17 00:00:00 2001 From: I538344 Date: Wed, 16 Oct 2024 14:09:56 +0200 Subject: [PATCH 67/79] Make DeploymentCache package private and an instance --- .../com/sap/ai/sdk/core/AiCoreService.java | 14 ++++++++---- .../com/sap/ai/sdk/core/DeploymentCache.java | 20 ++++++++--------- .../java/com/sap/ai/sdk/core/CacheTest.java | 22 ++++++++++--------- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index 9dfe6037..0db4902e 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -1,7 +1,5 @@ package com.sap.ai.sdk.core; -import static com.sap.ai.sdk.core.DeploymentCache.getDeploymentIdByModel; -import static com.sap.ai.sdk.core.DeploymentCache.getDeploymentIdByScenario; import static com.sap.ai.sdk.core.DestinationResolver.AI_CLIENT_TYPE_KEY; import static com.sap.ai.sdk.core.DestinationResolver.AI_CLIENT_TYPE_VALUE; @@ -38,6 +36,8 @@ public class AiCoreService implements AiCoreDestination { final BiFunction clientHandler; final BiFunction builderHandler; + private static final DeploymentCache DEPLOYMENT_CACHE = new DeploymentCache(); + /** The default constructor. */ public AiCoreService() { this( @@ -94,7 +94,10 @@ public AiCoreDeployment forDeployment(@Nonnull final String deploymentId) { public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) throws NoSuchElementException { return new AiCoreDeployment( - this, obj -> getDeploymentIdByModel(this.client(), obj.getResourceGroup(), modelName)); + this, + obj -> + DEPLOYMENT_CACHE.getDeploymentIdByModel( + this.client(), obj.getResourceGroup(), modelName)); } /** @@ -109,7 +112,10 @@ public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId) throws NoSuchElementException { return new AiCoreDeployment( - this, obj -> getDeploymentIdByScenario(this.client(), obj.getResourceGroup(), scenarioId)); + this, + obj -> + DEPLOYMENT_CACHE.getDeploymentIdByScenario( + this.client(), obj.getResourceGroup(), scenarioId)); } /** diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index 728b74ac..8c0e9f65 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -18,10 +18,10 @@ * scenario or for a model. */ @Slf4j -public class DeploymentCache { +class DeploymentCache { /** Cache for deployment ids. The key is the model name and the value is the deployment id. */ - protected static final List CACHE = new ArrayList<>(); + protected final List CACHE = new ArrayList<>(); /** * Remove all entries from the cache then load all deployments into the cache. @@ -31,8 +31,7 @@ public class DeploymentCache { * @param client the API client to query deployments. * @param resourceGroup the resource group, usually "default". */ - public static void resetCache( - @Nonnull final ApiClient client, @Nonnull final String resourceGroup) { + public void resetCache(@Nonnull final ApiClient client, @Nonnull final String resourceGroup) { clearCache(); loadCache(client, resourceGroup); } @@ -42,7 +41,7 @@ public static void resetCache( * *

      Call {@link #resetCache} whenever a deployment is deleted. */ - protected static void clearCache() { + protected void clearCache() { CACHE.clear(); } @@ -54,8 +53,7 @@ protected static void clearCache() { * @param client the API client to query deployments. * @param resourceGroup the resource group, usually "default". */ - protected static void loadCache( - @Nonnull final ApiClient client, @Nonnull final String resourceGroup) { + protected void loadCache(@Nonnull final ApiClient client, @Nonnull final String resourceGroup) { try { final var deployments = new DeploymentApi(client).query(resourceGroup).getResources(); CACHE.addAll(deployments); @@ -75,7 +73,7 @@ protected static void loadCache( * @throws NoSuchElementException if no running deployment is found for the model. */ @Nonnull - public static String getDeploymentIdByModel( + public String getDeploymentIdByModel( @Nonnull final ApiClient client, @Nonnull final String resourceGroup, @Nonnull final String modelName) @@ -92,7 +90,7 @@ public static String getDeploymentIdByModel( }); } - private static Optional getDeploymentIdByModel(@Nonnull final String modelName) { + private Optional getDeploymentIdByModel(@Nonnull final String modelName) { return CACHE.stream() .filter(deployment -> isDeploymentOfModel(modelName, deployment)) .findFirst() @@ -110,7 +108,7 @@ private static Optional getDeploymentIdByModel(@Nonnull final String mod * @throws NoSuchElementException if no running deployment is found for the scenario. */ @Nonnull - public static String getDeploymentIdByScenario( + public String getDeploymentIdByScenario( @Nonnull final ApiClient client, @Nonnull final String resourceGroup, @Nonnull final String scenarioId) @@ -127,7 +125,7 @@ public static String getDeploymentIdByScenario( }); } - private static Optional getDeploymentIdByScenario(@Nonnull final String scenarioId) { + private Optional getDeploymentIdByScenario(@Nonnull final String scenarioId) { return CACHE.stream() .filter(deployment -> scenarioId.equals(deployment.getScenarioId())) .findFirst() diff --git a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java index 905a2969..bd2850ac 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/CacheTest.java @@ -13,6 +13,8 @@ class CacheTest extends WireMockTestServer { + private final DeploymentCache cacheUnderTest = new DeploymentCache(); + @BeforeEach void setupCache() { wireMockServer.resetRequests(); @@ -93,26 +95,26 @@ private static void stubEmpty() { @Test void newDeployment() { stubGPT4(); - DeploymentCache.loadCache(client, "default"); + cacheUnderTest.loadCache(client, "default"); - DeploymentCache.getDeploymentIdByModel(client, "default", "gpt-4-32k"); + cacheUnderTest.getDeploymentIdByModel(client, "default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/v2/lm/deployments"))); - DeploymentCache.getDeploymentIdByModel(client, "default", "gpt-4-32k"); + cacheUnderTest.getDeploymentIdByModel(client, "default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/v2/lm/deployments"))); } @Test void clearCache() { stubGPT4(); - DeploymentCache.loadCache(client, "default"); + cacheUnderTest.loadCache(client, "default"); - DeploymentCache.getDeploymentIdByModel(client, "default", "gpt-4-32k"); + cacheUnderTest.getDeploymentIdByModel(client, "default", "gpt-4-32k"); wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/v2/lm/deployments"))); - DeploymentCache.clearCache(); + cacheUnderTest.clearCache(); - DeploymentCache.getDeploymentIdByModel(client, "default", "gpt-4-32k"); + cacheUnderTest.getDeploymentIdByModel(client, "default", "gpt-4-32k"); // the deployment is not in the cache anymore, so we need to query it again wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/v2/lm/deployments"))); } @@ -129,14 +131,14 @@ void clearCache() { @Test void newDeploymentAfterReset() { stubEmpty(); - DeploymentCache.loadCache(client, "default"); + cacheUnderTest.loadCache(client, "default"); stubGPT4(); - DeploymentCache.getDeploymentIdByModel(client, "default", "gpt-4-32k"); + cacheUnderTest.getDeploymentIdByModel(client, "default", "gpt-4-32k"); // 1 reset empty and 1 cache miss wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/v2/lm/deployments"))); - DeploymentCache.getDeploymentIdByModel(client, "default", "gpt-4-32k"); + cacheUnderTest.getDeploymentIdByModel(client, "default", "gpt-4-32k"); wireMockServer.verify(2, getRequestedFor(urlPathEqualTo("/v2/lm/deployments"))); } } From 0982a7407cc7aebe4a2d524a9a5b5b82f504f1e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 16 Oct 2024 14:59:37 +0200 Subject: [PATCH 68/79] Apply review comments --- .../com/sap/ai/sdk/core/AiCoreDeployment.java | 17 ++--------------- .../java/com/sap/ai/sdk/core/AiCoreService.java | 6 +++--- .../ai/sdk/core/client/WireMockTestServer.java | 5 ++--- .../client/OrchestrationUnitTest.java | 11 ++++------- 4 files changed, 11 insertions(+), 28 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index 721b9d74..c58d5895 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -95,7 +95,7 @@ public Destination destination() { final var dest = service.baseDestinationHandler.apply(service); final var builder = service.builderHandler.apply(service, dest); destinationSetUrl(builder, dest); - destinationSetHeaders(builder, dest); + destinationSetHeaders(builder); return builder.build(); } @@ -125,10 +125,8 @@ protected void destinationSetUrl( * Update and set the default request headers for the destination. * * @param builder The destination builder. - * @param dest The original destination reference. */ - protected void destinationSetHeaders( - @Nonnull final DefaultHttpDestination.Builder builder, @Nonnull final Destination dest) { + protected void destinationSetHeaders(@Nonnull final DefaultHttpDestination.Builder builder) { builder.property(AI_RESOURCE_GROUP, getResourceGroup()); } @@ -143,17 +141,6 @@ public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { return new AiCoreDeployment(service, deploymentId, resourceGroup); } - /** - * Set the destination. - * - * @param destination The destination. - * @return A new instance of the AI Core service. - */ - @Nonnull - public AiCoreDeployment withDestination(@Nonnull final Destination destination) { - return new AiCoreDeployment(service.withDestination(destination), deploymentId, resourceGroup); - } - /** * Get the deployment id. * diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index c6401d23..53b65a09 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -72,7 +72,7 @@ public AiCoreService withDestination(@Nonnull final Destination destination) { * 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 service. + * @return A new instance of the AI Core Deployment. */ @Nonnull public AiCoreDeployment forDeployment(@Nonnull final String deploymentId) { @@ -83,7 +83,7 @@ public AiCoreDeployment forDeployment(@Nonnull final String deploymentId) { * Set a specific deployment by model name. * * @param modelName The model name to be used for AI Core service calls. - * @return A new instance of the AI Core service. + * @return A new instance of the AI Core Deployment. */ @Nonnull public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) { @@ -94,7 +94,7 @@ public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) { * Set a specific deployment by scenario id. * * @param scenarioId The scenario id to be used for AI Core service calls. - * @return A new instance of the AI Core service. + * @return A new instance of the AI Core Deployment. */ @Nonnull public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId) { diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java b/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java index 08e5a89d..1d0a22ba 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java @@ -6,7 +6,6 @@ import com.github.tomakehurst.wiremock.core.WireMockConfiguration; import com.sap.ai.sdk.core.AiCoreService; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; -import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -17,14 +16,14 @@ abstract class WireMockTestServer { wireMockConfig().dynamicPort(); static WireMockServer wireMockServer; - static Destination destination; static ApiClient client; @BeforeAll static void setup() { wireMockServer = new WireMockServer(WIREMOCK_CONFIGURATION); wireMockServer.start(); - destination = DefaultHttpDestination.builder(wireMockServer.baseUrl()).build(); + + final var destination = DefaultHttpDestination.builder(wireMockServer.baseUrl()).build(); client = new AiCoreService().withDestination(destination).client(); } diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java index 3373f057..dcbd51de 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java @@ -5,6 +5,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.jsonResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.okJson; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; @@ -43,7 +44,6 @@ import java.util.Map; import java.util.Objects; import java.util.function.Function; -import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.web.client.HttpClientErrorException; @@ -81,16 +81,12 @@ public class OrchestrationUnitTest { @BeforeEach void setup(WireMockRuntimeInfo server) { - stubFor( get(urlPathEqualTo("/v2/lm/deployments")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( - aResponse() - .withStatus(HttpStatus.SC_OK) - .withHeader("content-type", "application/json") - .withBody( - """ + okJson( + """ { "resources": [ { @@ -103,6 +99,7 @@ void setup(WireMockRuntimeInfo server) { final DefaultHttpDestination destination = DefaultHttpDestination.builder(server.getHttpBaseUrl()).build(); + final var apiClient = new AiCoreService() .withDestination(destination) From 957510986a5d5ef499d4fcc586a60e193a4e07ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 16 Oct 2024 15:35:23 +0200 Subject: [PATCH 69/79] Apply review comments --- .../sap/ai/sdk/core/AiCoreServiceTest.java | 23 ++++--------------- .../client/OrchestrationUnitTest.java | 3 ++- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java index c12f77c5..d9e03756 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java @@ -56,7 +56,7 @@ void testLazyEvaluation() { } @Test - void testSimpleCase() { + void testDefaultCase() { // setup final var accessor = mock(ServiceBindingAccessor.class); DestinationResolver.setAccessor(accessor); @@ -95,24 +95,6 @@ void testBaseDestination() { assertThat(client.getBasePath()).isEqualTo("https://foo.bar/v2/"); } - @Test - void testDeployment() { - final var accessor = mock(ServiceBindingAccessor.class); - DestinationResolver.setAccessor(accessor); - doReturn(List.of(BINDING)).when(accessor).getServiceBindings(); - - // execution without errors - final var destination = new AiCoreService().destination(); - final var client = new AiCoreService().client(); - - // verification - assertThat(destination.get(DestinationProperty.URI)).contains("https://srv/v2/"); - assertThat(destination.get(DestinationProperty.AUTH_TYPE)).isEmpty(); - assertThat(destination.get(DestinationProperty.NAME)).singleElement(STRING).contains("aicore"); - assertThat(destination.get(AI_CLIENT_TYPE_KEY)).contains(AI_CLIENT_TYPE_VALUE); - assertThat(client.getBasePath()).isEqualTo("https://srv/v2/"); - verify(accessor, times(2)).getServiceBindings(); - } @Test void testCustomization() { @@ -139,5 +121,8 @@ protected ApiClient getApiClient(@Nonnull Destination destination) { final var destination = customServiceForDeployment.destination().asHttp(); assertThat(destination.getUri()).hasToString("https://ai/v2/inference/deployments/deployment/"); + + final var resourceGroup = customServiceForDeployment.getResourceGroup(); + assertThat(resourceGroup).isEqualTo("group"); } } diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java index dcbd51de..28272a24 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java @@ -83,7 +83,7 @@ public class OrchestrationUnitTest { void setup(WireMockRuntimeInfo server) { stubFor( get(urlPathEqualTo("/v2/lm/deployments")) - .withHeader("AI-Resource-Group", equalTo("default")) + .withHeader("AI-Resource-Group", equalTo("my-resource-group")) .willReturn( okJson( """ @@ -104,6 +104,7 @@ void setup(WireMockRuntimeInfo server) { new AiCoreService() .withDestination(destination) .forDeploymentByScenario("orchestration") + .withResourceGroup("my-resource-group") .client(); client = new OrchestrationCompletionApi(apiClient); } From 2b5221c5aa2bb08a81f42c630c8987c7d06e4d6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 16 Oct 2024 15:37:22 +0200 Subject: [PATCH 70/79] Add assertion on AI-Client-Type header --- .../sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java index 28272a24..f810db42 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/client/OrchestrationUnitTest.java @@ -84,6 +84,7 @@ void setup(WireMockRuntimeInfo server) { stubFor( get(urlPathEqualTo("/v2/lm/deployments")) .withHeader("AI-Resource-Group", equalTo("my-resource-group")) + .withHeader("AI-Client-Type", equalTo("AI SDK Java")) .willReturn( okJson( """ From 505ce61766e2761fcf380ee58e2c47a8075f9557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20D=C3=BCmont?= Date: Wed, 16 Oct 2024 15:44:30 +0200 Subject: [PATCH 71/79] Minor JavaDoc fix --- core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index 53b65a09..eee44dee 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -61,7 +61,7 @@ public Destination destination() { * 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. + * @return A new instance of the AI Core Service based on the provided destination. */ @Nonnull public AiCoreService withDestination(@Nonnull final Destination destination) { From d4d98fc921311728504989a4da04c7af0cb4197e Mon Sep 17 00:00:00 2001 From: SAP Cloud SDK Bot Date: Wed, 16 Oct 2024 13:45:18 +0000 Subject: [PATCH 72/79] Formatting --- core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java index d9e03756..7a275818 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java @@ -95,7 +95,6 @@ void testBaseDestination() { assertThat(client.getBasePath()).isEqualTo("https://foo.bar/v2/"); } - @Test void testCustomization() { final var customService = From c0af7dbc3cd3e427d57bf2fbb8a0036d39221c0c Mon Sep 17 00:00:00 2001 From: I538344 Date: Wed, 16 Oct 2024 16:01:48 +0200 Subject: [PATCH 73/79] Merged main --- .../com/sap/ai/sdk/core/AiCoreDeployment.java | 38 ------------------ .../com/sap/ai/sdk/core/DeploymentCache.java | 39 ++++++++++++++++++- 2 files changed, 37 insertions(+), 40 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index fb639fb1..b0620591 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -1,11 +1,9 @@ package com.sap.ai.sdk.core; -import com.sap.ai.sdk.core.client.model.AiDeployment; 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.Map; import java.util.function.Function; import javax.annotation.Nonnull; import lombok.AccessLevel; @@ -102,40 +100,4 @@ public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { protected String getDeploymentId() { return deploymentId.apply(this); } - - /** - * This exists because getBackendDetails() is broken - * - * @param modelName The model name. - * @param deployment The deployment. - * @return true if the deployment is of the model. - */ - protected static boolean isDeploymentOfModel( - @Nonnull final String modelName, @Nonnull final AiDeployment deployment) { - final var deploymentDetails = deployment.getDetails(); - // The AI Core specification doesn't mention that this is nullable, but it can be. - // Remove this check when the specification is fixed. - if (deploymentDetails == null) { - return false; - } - final var resources = deploymentDetails.getResources(); - if (resources == null) { - return false; - } - Object detailsObject = resources.getBackendDetails(); - // workaround for AIWDF-2124 - if (detailsObject == null) { - if (!resources.getCustomFieldNames().contains("backend_details")) { - return false; - } - detailsObject = resources.getCustomField("backend_details"); - } - - if (detailsObject instanceof Map details - && details.get("model") instanceof Map model - && model.get("name") instanceof String name) { - return modelName.equals(name); - } - return false; - } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index 8c0e9f65..911ea20d 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -1,13 +1,12 @@ package com.sap.ai.sdk.core; -import static com.sap.ai.sdk.core.AiCoreDeployment.isDeploymentOfModel; - import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import com.sap.cloud.sdk.services.openapi.core.OpenApiRequestException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; import javax.annotation.Nonnull; @@ -131,4 +130,40 @@ private Optional getDeploymentIdByScenario(@Nonnull final String scenari .findFirst() .map(AiDeployment::getId); } + + /** + * This exists because getBackendDetails() is broken + * + * @param modelName The model name. + * @param deployment The deployment. + * @return true if the deployment is of the model. + */ + protected static boolean isDeploymentOfModel( + @Nonnull final String modelName, @Nonnull final AiDeployment deployment) { + final var deploymentDetails = deployment.getDetails(); + // The AI Core specification doesn't mention that this is nullable, but it can be. + // Remove this check when the specification is fixed. + if (deploymentDetails == null) { + return false; + } + final var resources = deploymentDetails.getResources(); + if (resources == null) { + return false; + } + Object detailsObject = resources.getBackendDetails(); + // workaround for AIWDF-2124 + if (detailsObject == null) { + if (!resources.getCustomFieldNames().contains("backend_details")) { + return false; + } + detailsObject = resources.getCustomField("backend_details"); + } + + if (detailsObject instanceof Map details + && details.get("model") instanceof Map model + && model.get("name") instanceof String name) { + return modelName.equals(name); + } + return false; + } } From bb2ae4c15989f6c5dc43b2b8073acba5273e7150 Mon Sep 17 00:00:00 2001 From: I538344 Date: Wed, 16 Oct 2024 19:08:36 +0200 Subject: [PATCH 74/79] Refactor AiCoreDeployment --- .../com/sap/ai/sdk/core/AiCoreDeployment.java | 92 +++---------------- .../com/sap/ai/sdk/core/AiCoreService.java | 45 +++++++-- 2 files changed, 53 insertions(+), 84 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index b0620591..d14c0f87 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -1,103 +1,39 @@ 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.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.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) +@RequiredArgsConstructor 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; + private final AiCoreService aiCoreService; // the deployment id handler to be used, based on instance @Nonnull private final Function 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 deploymentId) { - this(service, deploymentId, "default"); + public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { + aiCoreService.setResourceGroup(resourceGroup); + return this; } @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(); + public Destination destination() throws DestinationAccessException, DestinationNotFoundException { + aiCoreService.deploymentId = deploymentId.apply(this); + return aiCoreService.destination(); } @Nonnull - @Override public ApiClient client() { - final var destination = destination(); - return service.clientHandler.apply(service, destination); + aiCoreService.deploymentId = deploymentId.apply(this); + return aiCoreService.client(); } - /** - * 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); + String getResourceGroup() { + return aiCoreService.getResourceGroup(); } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index c063b3c7..e12328e7 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -19,7 +19,10 @@ import java.util.function.BiFunction; import java.util.function.Function; import javax.annotation.Nonnull; +import lombok.AccessLevel; +import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.http.client.BufferingClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; @@ -32,18 +35,29 @@ @RequiredArgsConstructor public class AiCoreService implements AiCoreDestination { - final Function baseDestinationHandler; + Function baseDestinationHandler; final BiFunction clientHandler; final BiFunction builderHandler; private static final DeploymentCache DEPLOYMENT_CACHE = new DeploymentCache(); + private static final String AI_RESOURCE_GROUP = "URL.headers.AI-Resource-Group"; + + @Getter(AccessLevel.PROTECTED) + @Setter(AccessLevel.PROTECTED) + @Nonnull + private String resourceGroup; + + @Nonnull String deploymentId; + /** The default constructor. */ public AiCoreService() { this( - AiCoreService::getBaseDestination, AiCoreService::getApiClient, - AiCoreService::getDestinationBuilder); + AiCoreService::getDestinationBuilder, + "default", + ""); + baseDestinationHandler = AiCoreService::getBaseDestination; } @Nonnull @@ -57,18 +71,37 @@ public ApiClient client() { @Override public Destination destination() { final var dest = baseDestinationHandler.apply(this); - return builderHandler.apply(this, dest).build(); + final var builder = builderHandler.apply(this, dest); + if (!deploymentId.isEmpty()) { + destinationSetUrl(builder, dest); + destinationSetHeaders(builder); + } + return builder.build(); + } + + 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(deploymentId)); + } + + protected void destinationSetHeaders(@Nonnull final DefaultHttpDestination.Builder builder) { + builder.property(AI_RESOURCE_GROUP, getResourceGroup()); } /** * 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. + * @return The AI Core Service based on the provided destination. */ @Nonnull public AiCoreService withDestination(@Nonnull final Destination destination) { - return new AiCoreService((service) -> destination, clientHandler, builderHandler); + baseDestinationHandler = (service) -> destination; + return this; } /** From 642f388a7806b4bd858f8ef0fd6f73d123b9e482 Mon Sep 17 00:00:00 2001 From: SAP Cloud SDK Bot Date: Wed, 16 Oct 2024 17:11:36 +0000 Subject: [PATCH 75/79] Formatting --- core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index e12328e7..63e5616f 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -52,11 +52,7 @@ public class AiCoreService implements AiCoreDestination { /** The default constructor. */ public AiCoreService() { - this( - AiCoreService::getApiClient, - AiCoreService::getDestinationBuilder, - "default", - ""); + this(AiCoreService::getApiClient, AiCoreService::getDestinationBuilder, "default", ""); baseDestinationHandler = AiCoreService::getBaseDestination; } From 329b182777c5b0e977e961e8ac3bdd161ca0eae2 Mon Sep 17 00:00:00 2001 From: I538344 Date: Wed, 16 Oct 2024 19:16:25 +0200 Subject: [PATCH 76/79] Added Javadoc --- core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index 63e5616f..9ebdb89a 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -43,11 +43,15 @@ public class AiCoreService implements AiCoreDestination { private static final String AI_RESOURCE_GROUP = "URL.headers.AI-Resource-Group"; + /** The resource group is defined by AiCoreDeployment.withResourceGroup(). */ @Getter(AccessLevel.PROTECTED) @Setter(AccessLevel.PROTECTED) @Nonnull private String resourceGroup; + /** + * The deployment id is queried by AiCoreDeployment.destination() or AiCoreDeployment.client(). + */ @Nonnull String deploymentId; /** The default constructor. */ From 9f5b2cce17665b8b7d68b1ebd329b674c529840c Mon Sep 17 00:00:00 2001 From: I538344 Date: Wed, 16 Oct 2024 19:21:16 +0200 Subject: [PATCH 77/79] Added Javadoc --- .../java/com/sap/ai/sdk/core/AiCoreDeployment.java | 10 +++++++++- .../main/java/com/sap/ai/sdk/core/AiCoreService.java | 11 +++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index d14c0f87..90da8b6b 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -8,13 +8,19 @@ import javax.annotation.Nonnull; import lombok.RequiredArgsConstructor; +/** Connectivity convenience methods for AI Core with deployment. */ @RequiredArgsConstructor public class AiCoreDeployment implements AiCoreDestination { private final AiCoreService aiCoreService; - // the deployment id handler to be used, based on instance @Nonnull private final Function deploymentId; + /** + * 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) { aiCoreService.setResourceGroup(resourceGroup); @@ -22,12 +28,14 @@ public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { } @Nonnull + @Override public Destination destination() throws DestinationAccessException, DestinationNotFoundException { aiCoreService.deploymentId = deploymentId.apply(this); return aiCoreService.destination(); } @Nonnull + @Override public ApiClient client() { aiCoreService.deploymentId = deploymentId.apply(this); return aiCoreService.client(); diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index 9ebdb89a..cfb84b3b 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -79,6 +79,12 @@ public Destination destination() { return builder.build(); } + /** + * 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(); @@ -88,6 +94,11 @@ protected void destinationSetUrl( builder.uri(uri + "v2/inference/deployments/%s/".formatted(deploymentId)); } + /** + * 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()); } From dc597ee7942bb8d55d4fee2d73ad81190f844fdc Mon Sep 17 00:00:00 2001 From: I538344 Date: Thu, 17 Oct 2024 08:49:15 +0200 Subject: [PATCH 78/79] AiCoreDeployment.destinationId is a supplier --- .../com/sap/ai/sdk/core/AiCoreDeployment.java | 19 +++++++-------- .../com/sap/ai/sdk/core/AiCoreService.java | 24 +++++-------------- .../sap/ai/sdk/core/AiCoreServiceTest.java | 2 +- 3 files changed, 15 insertions(+), 30 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java index 90da8b6b..465b58d5 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreDeployment.java @@ -4,44 +4,41 @@ 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.function.Function; +import java.util.function.Supplier; import javax.annotation.Nonnull; +import lombok.AccessLevel; import lombok.RequiredArgsConstructor; /** Connectivity convenience methods for AI Core with deployment. */ -@RequiredArgsConstructor +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) public class AiCoreDeployment implements AiCoreDestination { private final AiCoreService aiCoreService; - @Nonnull private final Function deploymentId; + @Nonnull private final Supplier deploymentId; /** * Set the resource group. * - * @param resourceGroup The resource group. + * @param resourceGroup The resource group, default value "default". * @return A new instance of the AI Core service. */ @Nonnull public AiCoreDeployment withResourceGroup(@Nonnull final String resourceGroup) { - aiCoreService.setResourceGroup(resourceGroup); + aiCoreService.resourceGroup = resourceGroup; return this; } @Nonnull @Override public Destination destination() throws DestinationAccessException, DestinationNotFoundException { - aiCoreService.deploymentId = deploymentId.apply(this); + aiCoreService.deploymentId = deploymentId.get(); return aiCoreService.destination(); } @Nonnull @Override public ApiClient client() { - aiCoreService.deploymentId = deploymentId.apply(this); + aiCoreService.deploymentId = deploymentId.get(); return aiCoreService.client(); } - - String getResourceGroup() { - return aiCoreService.getResourceGroup(); - } } diff --git a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java index cfb84b3b..1d966f1c 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java +++ b/core/src/main/java/com/sap/ai/sdk/core/AiCoreService.java @@ -19,10 +19,7 @@ import java.util.function.BiFunction; import java.util.function.Function; import javax.annotation.Nonnull; -import lombok.AccessLevel; -import lombok.Getter; import lombok.RequiredArgsConstructor; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.http.client.BufferingClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; @@ -44,14 +41,9 @@ public class AiCoreService implements AiCoreDestination { private static final String AI_RESOURCE_GROUP = "URL.headers.AI-Resource-Group"; /** The resource group is defined by AiCoreDeployment.withResourceGroup(). */ - @Getter(AccessLevel.PROTECTED) - @Setter(AccessLevel.PROTECTED) - @Nonnull - private String resourceGroup; + @Nonnull String resourceGroup; - /** - * The deployment id is queried by AiCoreDeployment.destination() or AiCoreDeployment.client(). - */ + /** The deployment id is set by AiCoreDeployment.destination() or AiCoreDeployment.client(). */ @Nonnull String deploymentId; /** The default constructor. */ @@ -100,7 +92,7 @@ protected void destinationSetUrl( * @param builder The destination builder. */ protected void destinationSetHeaders(@Nonnull final DefaultHttpDestination.Builder builder) { - builder.property(AI_RESOURCE_GROUP, getResourceGroup()); + builder.property(AI_RESOURCE_GROUP, resourceGroup); } /** @@ -123,7 +115,7 @@ public AiCoreService withDestination(@Nonnull final Destination destination) { */ @Nonnull public AiCoreDeployment forDeployment(@Nonnull final String deploymentId) { - return new AiCoreDeployment(this, obj -> deploymentId); + return new AiCoreDeployment(this, () -> deploymentId); } /** @@ -139,9 +131,7 @@ public AiCoreDeployment forDeploymentByModel(@Nonnull final String modelName) throws NoSuchElementException { return new AiCoreDeployment( this, - obj -> - DEPLOYMENT_CACHE.getDeploymentIdByModel( - this.client(), obj.getResourceGroup(), modelName)); + () -> DEPLOYMENT_CACHE.getDeploymentIdByModel(this.client(), resourceGroup, modelName)); } /** @@ -157,9 +147,7 @@ public AiCoreDeployment forDeploymentByScenario(@Nonnull final String scenarioId throws NoSuchElementException { return new AiCoreDeployment( this, - obj -> - DEPLOYMENT_CACHE.getDeploymentIdByScenario( - this.client(), obj.getResourceGroup(), scenarioId)); + () -> DEPLOYMENT_CACHE.getDeploymentIdByScenario(client(), resourceGroup, scenarioId)); } /** diff --git a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java index 7a275818..cdfd5874 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/AiCoreServiceTest.java @@ -121,7 +121,7 @@ protected ApiClient getApiClient(@Nonnull Destination destination) { final var destination = customServiceForDeployment.destination().asHttp(); assertThat(destination.getUri()).hasToString("https://ai/v2/inference/deployments/deployment/"); - final var resourceGroup = customServiceForDeployment.getResourceGroup(); + final var resourceGroup = customService.resourceGroup; assertThat(resourceGroup).isEqualTo("group"); } } From 324d0c9db84974e7952374a995e021c123752b5c Mon Sep 17 00:00:00 2001 From: I538344 Date: Thu, 17 Oct 2024 09:33:04 +0200 Subject: [PATCH 79/79] Changed cache to a Set --- core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java index 911ea20d..aa54ceaf 100644 --- a/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java +++ b/core/src/main/java/com/sap/ai/sdk/core/DeploymentCache.java @@ -4,11 +4,11 @@ import com.sap.ai.sdk.core.client.model.AiDeployment; import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; import com.sap.cloud.sdk.services.openapi.core.OpenApiRequestException; -import java.util.ArrayList; -import java.util.List; +import java.util.HashSet; import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; +import java.util.Set; import javax.annotation.Nonnull; import lombok.extern.slf4j.Slf4j; @@ -20,7 +20,7 @@ class DeploymentCache { /** Cache for deployment ids. The key is the model name and the value is the deployment id. */ - protected final List CACHE = new ArrayList<>(); + protected final Set CACHE = new HashSet<>(); /** * Remove all entries from the cache then load all deployments into the cache.