diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/AICore.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/AICore.java
new file mode 100644
index 0000000..764bb18
--- /dev/null
+++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/AICore.java
@@ -0,0 +1,32 @@
+/*
+ * © 2026 SAP SE or an SAP affiliate company and cds-ai contributors.
+ */
+package com.sap.cds.feature.aicore.api;
+
+/**
+ * Constants for the AI Core plugin.
+ *
+ *
Use {@link #SERVICE_NAME} for service catalog lookups and {@code @ServiceName} annotations.
+ * Use the entity constants for {@code @On(entity = ...)} handler annotations.
+ *
+ *
The service is a {@link com.sap.cds.services.cds.RemoteService RemoteService} auto-created by
+ * the CAP Java runtime from the CDS model. Callers interact with it by emitting typed {@link
+ * com.sap.cds.services.EventContext EventContext} instances ({@link ResourceGroupContext}, {@link
+ * DeploymentIdContext}, {@link InferenceClientContext}) or via CQL on the defined entities.
+ */
+public final class AICore {
+
+ private AICore() {}
+
+ /** Service name matching the CDS service definition, used for catalog lookup. */
+ public static final String SERVICE_NAME = "AICore";
+
+ /** Qualified name of the {@code resourceGroups} entity. */
+ public static final String RESOURCE_GROUPS = "AICore.resourceGroups";
+
+ /** Qualified name of the {@code deployments} entity. */
+ public static final String DEPLOYMENTS = "AICore.deployments";
+
+ /** Qualified name of the {@code configurations} entity. */
+ public static final String CONFIGURATIONS = "AICore.configurations";
+}
diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/AICoreService.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/AICoreService.java
deleted file mode 100644
index 8a2e24c..0000000
--- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/AICoreService.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * © 2026 SAP SE or an SAP affiliate company and cds-ai contributors.
- */
-package com.sap.cds.feature.aicore.api;
-
-import com.sap.cds.services.cds.RemoteService;
-import com.sap.cloud.sdk.services.openapi.apache.apiclient.ApiClient;
-
-/**
- * CAP service contract for SAP AI Core integration.
- *
- *
The service exposes resource-group, configuration and deployment lifecycle as CDS entities
- * (see {@link #RESOURCE_GROUPS}, {@link #DEPLOYMENTS}, {@link #CONFIGURATIONS}) and additionally
- * provides programmatic helpers to:
- *
- *
- * Resolve the resource group ID for the current tenant ({@link #resourceGroup()}), creating
- * it on-demand when multi-tenancy is enabled.
- * Resolve (or create) a deployment matching a {@link ModelDeploymentSpec} ({@link
- * #deploymentId(String, ModelDeploymentSpec)}).
- * Build an {@link ApiClient} preconfigured for inference against a specific deployment
- * ({@link #inferenceClient(String, String)}).
- *
- *
- * The implementation is tenant-aware: it reads the current tenant from the {@code
- * RequestContext}. Callers do not need to pass tenant identifiers explicitly.
- */
-public interface AICoreService extends RemoteService {
-
- /** Default service name under which an instance is registered in the service catalog. */
- String DEFAULT_NAME = "AICoreService$Default";
-
- /** Qualified name of the {@code resourceGroups} entity exposed by this service. */
- String RESOURCE_GROUPS = "AICore.resourceGroups";
-
- /** Qualified name of the {@code deployments} entity exposed by this service. */
- String DEPLOYMENTS = "AICore.deployments";
-
- /** Qualified name of the {@code configurations} entity exposed by this service. */
- String CONFIGURATIONS = "AICore.configurations";
-
- /**
- * Returns the AI Core resource group ID associated with the current tenant.
- *
- *
When multi-tenancy is disabled the configured default resource group is returned. When
- * enabled, the resource group is looked up by the {@code ext.ai.sap.com/CDS_TENANT_ID} label and
- * created on first call if it does not exist.
- *
- * @return the AI Core resource group ID for the current tenant
- */
- String resourceGroup();
-
- /**
- * Returns the AI Core resource group ID associated with the given tenant.
- *
- *
This variant is used during subscribe/unsubscribe flows where the tenant ID is explicitly
- * available from the context rather than the current request.
- *
- * @param tenantId the CDS tenant identifier
- * @return the AI Core resource group ID for the specified tenant
- */
- String resourceGroupForTenant(String tenantId);
-
- /**
- * Returns the deployment ID for the given model spec inside the given resource group.
- *
- *
Looks up an existing RUNNING/PENDING deployment that matches the spec, otherwise creates a
- * configuration (if missing) and a new deployment, then polls until the deployment reaches
- * RUNNING. Results are cached per {@code (resourceGroupId, configurationName)} pair.
- *
- * @param resourceGroupId the AI Core resource group to operate in
- * @param spec the deployment specification (scenario, executable, configuration name and
- * existing-match predicate)
- * @return the deployment ID
- */
- String deploymentId(String resourceGroupId, ModelDeploymentSpec spec);
-
- /**
- * Returns an {@link ApiClient} preconfigured with the inference destination for the given
- * deployment, suitable for constructing foundation-model SDK clients.
- *
- * @param resourceGroupId the AI Core resource group containing the deployment
- * @param deploymentId the deployment ID returned by {@link #deploymentId(String,
- * ModelDeploymentSpec)}
- * @return a configured {@link ApiClient} pointing at the deployment's inference endpoint
- */
- ApiClient inferenceClient(String resourceGroupId, String deploymentId);
-}
diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/DeploymentIdContext.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/DeploymentIdContext.java
index 4fa04d9..04857c0 100644
--- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/DeploymentIdContext.java
+++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/DeploymentIdContext.java
@@ -9,9 +9,9 @@
/**
* Typed {@link EventContext} for the {@code deploymentId} event.
*
- *
Emitted by {@link AICoreService#deploymentId(String, ModelDeploymentSpec)} to resolve (or
- * create) a deployment matching the given spec inside the given resource group. The ON handler
- * performs cache lookup, retry, configuration creation, deployment creation and polling.
+ *
Emitted on the AI Core service to resolve (or create) a deployment matching the given spec
+ * inside the given resource group. The ON handler performs cache lookup, retry, configuration
+ * creation, deployment creation and polling.
*/
@EventName(DeploymentIdContext.EVENT)
public interface DeploymentIdContext extends EventContext {
diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/InferenceClientContext.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/InferenceClientContext.java
index 1b3095e..8ed8710 100644
--- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/InferenceClientContext.java
+++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/InferenceClientContext.java
@@ -10,8 +10,8 @@
/**
* Typed {@link EventContext} for the {@code inferenceClient} event.
*
- *
Emitted by {@link AICoreService#inferenceClient(String, String)} to build an {@link ApiClient}
- * preconfigured with the inference destination for the given deployment.
+ *
Emitted on the AI Core service to build an {@link ApiClient} preconfigured with the inference
+ * destination for the given deployment.
*/
@EventName(InferenceClientContext.EVENT)
public interface InferenceClientContext extends EventContext {
diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/ModelDeploymentSpec.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/ModelDeploymentSpec.java
index a51d1b6..72b29f3 100644
--- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/ModelDeploymentSpec.java
+++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/ModelDeploymentSpec.java
@@ -9,8 +9,8 @@
import java.util.function.Predicate;
/**
- * Describes a target AI Core deployment used by {@link AICoreService#deploymentId(String,
- * ModelDeploymentSpec)} to look up or create a deployment inside a resource group.
+ * Describes a target AI Core deployment used by the {@code deploymentId} event to look up or create
+ * a deployment inside a resource group.
*
*
The spec carries the AI Core scenario/executable identification, the human-readable
* configuration name (used as a stable key for caching and idempotent reuse), the parameter
diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/ResourceGroupContext.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/ResourceGroupContext.java
index 19a2a82..9d08328 100644
--- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/ResourceGroupContext.java
+++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/ResourceGroupContext.java
@@ -9,9 +9,9 @@
/**
* Typed {@link EventContext} for the {@code resourceGroup} event.
*
- *
Emitted by {@link AICoreService#resourceGroup()} to resolve the AI Core resource group ID for
- * the current tenant. In multi-tenancy mode, the resource group is created on-demand if it does not
- * exist. In single-tenancy mode, the configured default resource group is returned.
+ *
Emitted on the AI Core service to resolve the AI Core resource group ID for the current
+ * tenant. In multi-tenancy mode, the resource group is created on-demand if it does not exist. In
+ * single-tenancy mode, the configured default resource group is returned.
*
*
If {@link #getTenantId()} is non-null, the handler uses the explicit tenant ID. Otherwise, the
* current tenant is read from the {@code RequestContext}.
diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/AICoreServiceConfiguration.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/AICoreServiceConfiguration.java
index 202ed8d..976cd24 100644
--- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/AICoreServiceConfiguration.java
+++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/AICoreServiceConfiguration.java
@@ -7,7 +7,7 @@
import com.sap.ai.sdk.core.client.ConfigurationApi;
import com.sap.ai.sdk.core.client.DeploymentApi;
import com.sap.ai.sdk.core.client.ResourceGroupApi;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
import com.sap.cds.feature.aicore.core.handler.AICoreApiHandler;
import com.sap.cds.feature.aicore.core.handler.AICoreSetupHandler;
import com.sap.cds.feature.aicore.core.handler.ActionHandler;
@@ -18,6 +18,7 @@
import com.sap.cds.feature.aicore.core.handler.MockEntityHandler;
import com.sap.cds.feature.aicore.core.handler.ResourceGroupHandler;
import com.sap.cds.services.environment.CdsProperties;
+import com.sap.cds.services.environment.CdsProperties.Remote.RemoteServiceConfig;
import com.sap.cds.services.mt.DeploymentService;
import com.sap.cds.services.runtime.CdsRuntime;
import com.sap.cds.services.runtime.CdsRuntimeConfiguration;
@@ -27,13 +28,16 @@
import org.slf4j.LoggerFactory;
/**
- * {@link CdsRuntimeConfiguration} that wires the {@code AICore} service and its event handlers into
- * the CAP Java runtime.
+ * {@link CdsRuntimeConfiguration} that wires the {@code AICore} remote service and its event
+ * handlers into the CAP Java runtime.
*
- *
Detects the presence of an SAP AI Core service binding (either a regular service binding or
- * the {@code AICORE_SERVICE_KEY} environment variable used for hybrid local testing) and registers
- * the appropriate handlers. Picked up automatically through {@code ServiceLoader}; applications do
- * not need to instantiate this class directly.
+ *
In the {@link #environment} phase, a {@link RemoteServiceConfig} entry for "AICore" is
+ * injected into the runtime properties so the framework's {@code RemoteServiceConfiguration}
+ * auto-creates the service instance from the CDS model. This follows the same pattern used by
+ * {@code cds-feature-notifications}.
+ *
+ *
In the {@link #eventHandlers} phase, production or mock handlers are registered depending on
+ * whether an AI Core service binding is present.
*/
public class AICoreServiceConfiguration implements CdsRuntimeConfiguration {
@@ -43,34 +47,23 @@ public class AICoreServiceConfiguration implements CdsRuntimeConfiguration {
private AICoreClients clients;
private DeploymentResolver resolver;
- private static boolean hasAICoreModel(CdsRuntime runtime) {
- return runtime.getCdsModel().findService("AICore").isPresent();
- }
-
- private static boolean hasAICoreBinding(CdsRuntime runtime) {
- return runtime
- .getEnvironment()
- .getServiceBindings()
- .filter(b -> ServiceBindingUtils.matches(b, "aicore"))
- .findFirst()
- .isPresent();
- }
-
/**
- * Detects multi-tenancy by checking the standard CAP Java {@code cds.multiTenancy.sidecar.url}
- * property or the presence of a {@link DeploymentService} in the service catalog. This aligns
- * with the standard CAP Java convention — no custom property flag is needed.
+ * Injects a {@link RemoteServiceConfig} for "AICore" into the runtime properties. This runs
+ * before all {@code services()} methods, ensuring the framework's {@code
+ * RemoteServiceConfiguration} will auto-create a {@code RemoteServiceImpl} for the AICore CDS
+ * service definition.
*/
- private static boolean detectMultiTenancy(CdsRuntime runtime) {
- CdsProperties props = runtime.getEnvironment().getCdsProperties();
- String sidecarUrl = props.getMultiTenancy().getSidecar().getUrl();
- if (sidecarUrl != null && !sidecarUrl.isBlank()) {
- return true;
- }
- return runtime
- .getServiceCatalog()
- .getService(DeploymentService.class, DeploymentService.DEFAULT_NAME)
- != null;
+ @Override
+ public void environment(CdsRuntimeConfigurer configurer) {
+ RemoteServiceConfig remoteConfig = new RemoteServiceConfig(AICore.SERVICE_NAME);
+ remoteConfig.setModel(AICore.SERVICE_NAME);
+ configurer
+ .getCdsRuntime()
+ .getEnvironment()
+ .getCdsProperties()
+ .getRemote()
+ .getServices()
+ .putIfAbsent(AICore.SERVICE_NAME, remoteConfig);
}
@Override
@@ -78,7 +71,7 @@ public void services(CdsRuntimeConfigurer configurer) {
CdsRuntime runtime = configurer.getCdsRuntime();
if (!hasAICoreModel(runtime)) {
- logger.debug("AICore CDS model not found in runtime model — skipping service registration.");
+ logger.debug("AICore CDS model not found in runtime model - skipping handler setup.");
return;
}
@@ -96,23 +89,19 @@ public void services(CdsRuntimeConfigurer configurer) {
this.clients =
new AICoreClients(deploymentApi, configurationApi, resourceGroupApi, sdkService);
this.resolver = new DeploymentResolver(config, deploymentApi, resourceGroupApi);
- logger.info("Registered AICoreService backed by AI Core binding.");
+ logger.info("AI Core binding detected - production handlers will be registered.");
} else {
- logger.info(
- "Registered AICoreService (no AI Core binding found — mock handlers will be used).");
+ logger.info("No AI Core binding found - mock handlers will be registered.");
}
-
- configurer.service(new AICoreServiceImpl(AICoreService.DEFAULT_NAME, runtime));
}
@Override
public void eventHandlers(CdsRuntimeConfigurer configurer) {
if (config == null) {
- return; // No AICore model — services() skipped registration
+ return; // No AICore model - services() skipped
}
if (clients != null) {
- // Production path: real AI Core binding
configurer.eventHandler(new AICoreApiHandler(config, clients, resolver));
configurer.eventHandler(new ResourceGroupHandler(config, clients, resolver));
configurer.eventHandler(new DeploymentHandler(config, clients, resolver));
@@ -125,7 +114,6 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) {
logger.debug("Registered AI Core setup handler for MTX subscribe/unsubscribe.");
}
} else {
- // Mock path: no AI Core binding
MockAICoreApiHandler mockApiHandler = new MockAICoreApiHandler(config);
configurer.eventHandler(new MockEntityHandler());
configurer.eventHandler(mockApiHandler);
@@ -137,4 +125,29 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) {
}
}
}
+
+ private static boolean hasAICoreModel(CdsRuntime runtime) {
+ return runtime.getCdsModel().findService(AICore.SERVICE_NAME).isPresent();
+ }
+
+ private static boolean hasAICoreBinding(CdsRuntime runtime) {
+ return runtime
+ .getEnvironment()
+ .getServiceBindings()
+ .filter(b -> ServiceBindingUtils.matches(b, "aicore"))
+ .findFirst()
+ .isPresent();
+ }
+
+ private static boolean detectMultiTenancy(CdsRuntime runtime) {
+ CdsProperties props = runtime.getEnvironment().getCdsProperties();
+ String sidecarUrl = props.getMultiTenancy().getSidecar().getUrl();
+ if (sidecarUrl != null && !sidecarUrl.isBlank()) {
+ return true;
+ }
+ return runtime
+ .getServiceCatalog()
+ .getService(DeploymentService.class, DeploymentService.DEFAULT_NAME)
+ != null;
+ }
}
diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/AICoreServiceImpl.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/AICoreServiceImpl.java
deleted file mode 100644
index b1c226c..0000000
--- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/AICoreServiceImpl.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * © 2026 SAP SE or an SAP affiliate company and cds-ai contributors.
- */
-package com.sap.cds.feature.aicore.core;
-
-import com.sap.cds.feature.aicore.api.AICoreService;
-import com.sap.cds.feature.aicore.api.DeploymentIdContext;
-import com.sap.cds.feature.aicore.api.InferenceClientContext;
-import com.sap.cds.feature.aicore.api.ModelDeploymentSpec;
-import com.sap.cds.feature.aicore.api.ResourceGroupContext;
-import com.sap.cds.services.impl.cds.AbstractCdsDefinedService;
-import com.sap.cds.services.runtime.CdsRuntime;
-import com.sap.cloud.sdk.services.openapi.apache.apiclient.ApiClient;
-
-/**
- * Production implementation of {@link AICoreService}.
- *
- *
This class is a pure delegation layer: each API method creates a typed {@link
- * com.sap.cds.services.EventContext EventContext}, emits it via the CAP event mechanism, and
- * returns the handler's result. All business logic (caching, locking, API calls) lives in the
- * registered ON handlers which receive their dependencies via constructor injection.
- *
- *
Implementation note: This class extends {@code AbstractCdsDefinedService}
- * from the CAP Java runtime's internal {@code impl} package. This is necessary because the public
- * API ({@code ServiceDelegator}) does not provide CQN execution capabilities or CDS model binding.
- * The semi-public {@code AbstractCqnService} (from {@code cds-services-utils}) provides CQN but not
- * {@code getDefinition()}. Until a public API alternative exists, this coupling is accepted and
- * version-compatibility is verified through integration tests against the CAP Java runtime.
- */
-public class AICoreServiceImpl extends AbstractCdsDefinedService implements AICoreService {
-
- private static final String CDS_DEFINITION_NAME = "AICore";
-
- public AICoreServiceImpl(String name, CdsRuntime runtime) {
- super(name, CDS_DEFINITION_NAME, runtime);
- }
-
- @Override
- public String resourceGroup() {
- ResourceGroupContext ctx = ResourceGroupContext.create();
- emit(ctx);
- return ctx.getResult();
- }
-
- @Override
- public String resourceGroupForTenant(String tenantId) {
- ResourceGroupContext ctx = ResourceGroupContext.create();
- ctx.setTenantId(tenantId);
- emit(ctx);
- return ctx.getResult();
- }
-
- @Override
- public String deploymentId(String resourceGroupId, ModelDeploymentSpec spec) {
- DeploymentIdContext ctx = DeploymentIdContext.create();
- ctx.setResourceGroupId(resourceGroupId);
- ctx.setSpec(spec);
- emit(ctx);
- return ctx.getResult();
- }
-
- @Override
- public ApiClient inferenceClient(String resourceGroupId, String deploymentId) {
- InferenceClientContext ctx = InferenceClientContext.create();
- ctx.setResourceGroupId(resourceGroupId);
- ctx.setDeploymentId(deploymentId);
- emit(ctx);
- return ctx.getResult();
- }
-}
diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/AICoreApiHandler.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/AICoreApiHandler.java
index 3ecf8e7..613cdd1 100644
--- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/AICoreApiHandler.java
+++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/AICoreApiHandler.java
@@ -10,7 +10,7 @@
import com.sap.ai.sdk.core.model.AiDeploymentList;
import com.sap.ai.sdk.core.model.AiDeploymentResponseWithDetails;
import com.sap.ai.sdk.core.model.AiDeploymentStatus;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
import com.sap.cds.feature.aicore.api.DeploymentIdContext;
import com.sap.cds.feature.aicore.api.InferenceClientContext;
import com.sap.cds.feature.aicore.api.ModelDeploymentSpec;
@@ -32,13 +32,13 @@
import org.slf4j.LoggerFactory;
/**
- * ON handler for the {@link AICoreService} API events ({@code resourceGroup}, {@code deploymentId},
+ * ON handler for the AI Core service API events ({@code resourceGroup}, {@code deploymentId},
* {@code inferenceClient}).
*
*
Contains the business logic for deployment discovery/creation and inference client
* construction. Resource-group resolution is delegated to {@link DeploymentResolver}.
*/
-@ServiceName(AICoreService.DEFAULT_NAME)
+@ServiceName(AICore.SERVICE_NAME)
public class AICoreApiHandler implements EventHandler {
private static final Logger logger = LoggerFactory.getLogger(AICoreApiHandler.class);
diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ActionHandler.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ActionHandler.java
index 2c9d1e7..8fb95f5 100644
--- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ActionHandler.java
+++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ActionHandler.java
@@ -5,7 +5,7 @@
import com.sap.ai.sdk.core.model.AiDeploymentModificationRequest;
import com.sap.ai.sdk.core.model.AiDeploymentTargetStatus;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
import com.sap.cds.feature.aicore.core.AICoreClients;
import com.sap.cds.feature.aicore.core.AICoreConfig;
import com.sap.cds.feature.aicore.core.DeploymentResolver;
@@ -19,7 +19,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-@ServiceName(AICoreService.DEFAULT_NAME)
+@ServiceName(AICore.SERVICE_NAME)
public class ActionHandler extends AbstractCrudHandler {
private static final Logger logger = LoggerFactory.getLogger(ActionHandler.class);
diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ConfigurationHandler.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ConfigurationHandler.java
index bf4e1d8..76c6642 100644
--- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ConfigurationHandler.java
+++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ConfigurationHandler.java
@@ -7,7 +7,7 @@
import com.sap.ai.sdk.core.model.AiConfigurationBaseData;
import com.sap.ai.sdk.core.model.AiConfigurationList;
import com.sap.ai.sdk.core.model.AiParameterArgumentBinding;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
import com.sap.cds.feature.aicore.core.AICoreClients;
import com.sap.cds.feature.aicore.core.AICoreConfig;
import com.sap.cds.feature.aicore.core.DeploymentResolver;
@@ -32,7 +32,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-@ServiceName(AICoreService.DEFAULT_NAME)
+@ServiceName(AICore.SERVICE_NAME)
public class ConfigurationHandler extends AbstractCrudHandler {
private static final Logger logger = LoggerFactory.getLogger(ConfigurationHandler.class);
diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/DeploymentHandler.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/DeploymentHandler.java
index 4657bc4..193a5ae 100644
--- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/DeploymentHandler.java
+++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/DeploymentHandler.java
@@ -9,7 +9,7 @@
import com.sap.ai.sdk.core.model.AiDeploymentModificationRequest;
import com.sap.ai.sdk.core.model.AiDeploymentResponseWithDetails;
import com.sap.ai.sdk.core.model.AiDeploymentTargetStatus;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
import com.sap.cds.feature.aicore.core.AICoreClients;
import com.sap.cds.feature.aicore.core.AICoreConfig;
import com.sap.cds.feature.aicore.core.DeploymentResolver;
@@ -36,7 +36,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-@ServiceName(AICoreService.DEFAULT_NAME)
+@ServiceName(AICore.SERVICE_NAME)
public class DeploymentHandler extends AbstractCrudHandler {
private static final Logger logger = LoggerFactory.getLogger(DeploymentHandler.class);
diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/MockAICoreApiHandler.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/MockAICoreApiHandler.java
index f5155a2..133ebd1 100644
--- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/MockAICoreApiHandler.java
+++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/MockAICoreApiHandler.java
@@ -3,7 +3,7 @@
*/
package com.sap.cds.feature.aicore.core.handler;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
import com.sap.cds.feature.aicore.api.DeploymentIdContext;
import com.sap.cds.feature.aicore.api.InferenceClientContext;
import com.sap.cds.feature.aicore.api.ModelDeploymentSpec;
@@ -20,10 +20,10 @@
import org.slf4j.LoggerFactory;
/**
- * Mock ON handler for the {@link AICoreService} API events when no AI Core binding is available.
- * Uses in-memory maps instead of real API calls.
+ * Mock ON handler for the AI Core service API events when no AI Core binding is available. Uses
+ * in-memory maps instead of real API calls.
*/
-@ServiceName(AICoreService.DEFAULT_NAME)
+@ServiceName(AICore.SERVICE_NAME)
public class MockAICoreApiHandler implements EventHandler {
private static final Logger logger = LoggerFactory.getLogger(MockAICoreApiHandler.class);
diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/MockEntityHandler.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/MockEntityHandler.java
index 2ac76ab..4680b7a 100644
--- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/MockEntityHandler.java
+++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/MockEntityHandler.java
@@ -4,7 +4,7 @@
package com.sap.cds.feature.aicore.core.handler;
import com.sap.cds.CdsData;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
import com.sap.cds.ql.cqn.AnalysisResult;
import com.sap.cds.ql.cqn.CqnAnalyzer;
import com.sap.cds.ql.cqn.CqnDelete;
@@ -25,7 +25,7 @@
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
-@ServiceName(AICoreService.DEFAULT_NAME)
+@ServiceName(AICore.SERVICE_NAME)
public class MockEntityHandler implements EventHandler {
private final Map> resourceGroups = new ConcurrentHashMap<>();
@@ -34,7 +34,7 @@ public class MockEntityHandler implements EventHandler {
// --- Resource Groups ---
- @On(entity = AICoreService.RESOURCE_GROUPS)
+ @On(entity = AICore.RESOURCE_GROUPS)
public void readResourceGroups(CdsReadEventContext context) {
CqnSelect select = context.getCqn();
CdsModel model = context.getModel();
@@ -61,7 +61,7 @@ public void readResourceGroups(CdsReadEventContext context) {
}
}
- @On(entity = AICoreService.RESOURCE_GROUPS)
+ @On(entity = AICore.RESOURCE_GROUPS)
public void createResourceGroups(CdsCreateEventContext context) {
CqnInsert insert = context.getCqn();
List> results = new ArrayList<>();
@@ -76,7 +76,7 @@ public void createResourceGroups(CdsCreateEventContext context) {
context.setResult(results);
}
- @On(entity = AICoreService.RESOURCE_GROUPS)
+ @On(entity = AICore.RESOURCE_GROUPS)
public void updateResourceGroups(CdsUpdateEventContext context) {
CqnUpdate update = context.getCqn();
CdsModel model = context.getModel();
@@ -92,7 +92,7 @@ public void updateResourceGroups(CdsUpdateEventContext context) {
context.setResult(List.of(CdsData.create(existing)));
}
- @On(entity = AICoreService.RESOURCE_GROUPS)
+ @On(entity = AICore.RESOURCE_GROUPS)
public void deleteResourceGroups(CdsDeleteEventContext context) {
CqnDelete delete = context.getCqn();
CdsModel model = context.getModel();
@@ -105,7 +105,7 @@ public void deleteResourceGroups(CdsDeleteEventContext context) {
// --- Deployments ---
- @On(entity = AICoreService.DEPLOYMENTS)
+ @On(entity = AICore.DEPLOYMENTS)
public void readDeployments(CdsReadEventContext context) {
CqnSelect select = context.getCqn();
CdsModel model = context.getModel();
@@ -121,7 +121,7 @@ public void readDeployments(CdsReadEventContext context) {
}
}
- @On(entity = AICoreService.DEPLOYMENTS)
+ @On(entity = AICore.DEPLOYMENTS)
public void createDeployments(CdsCreateEventContext context) {
CqnInsert insert = context.getCqn();
List> results = new ArrayList<>();
@@ -136,7 +136,7 @@ public void createDeployments(CdsCreateEventContext context) {
context.setResult(results);
}
- @On(entity = AICoreService.DEPLOYMENTS)
+ @On(entity = AICore.DEPLOYMENTS)
public void updateDeployments(CdsUpdateEventContext context) {
CqnUpdate update = context.getCqn();
CdsModel model = context.getModel();
@@ -152,7 +152,7 @@ public void updateDeployments(CdsUpdateEventContext context) {
context.setResult(List.of(CdsData.create(existing)));
}
- @On(entity = AICoreService.DEPLOYMENTS)
+ @On(entity = AICore.DEPLOYMENTS)
public void deleteDeployments(CdsDeleteEventContext context) {
CqnDelete delete = context.getCqn();
CdsModel model = context.getModel();
@@ -165,7 +165,7 @@ public void deleteDeployments(CdsDeleteEventContext context) {
// --- Configurations ---
- @On(entity = AICoreService.CONFIGURATIONS)
+ @On(entity = AICore.CONFIGURATIONS)
public void readConfigurations(CdsReadEventContext context) {
CqnSelect select = context.getCqn();
CdsModel model = context.getModel();
@@ -181,7 +181,7 @@ public void readConfigurations(CdsReadEventContext context) {
}
}
- @On(entity = AICoreService.CONFIGURATIONS)
+ @On(entity = AICore.CONFIGURATIONS)
public void createConfigurations(CdsCreateEventContext context) {
CqnInsert insert = context.getCqn();
List> results = new ArrayList<>();
diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ResourceGroupHandler.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ResourceGroupHandler.java
index 9471662..016b833 100644
--- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ResourceGroupHandler.java
+++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ResourceGroupHandler.java
@@ -9,7 +9,7 @@
import com.sap.ai.sdk.core.model.BckndResourceGroupPatchRequest;
import com.sap.ai.sdk.core.model.BckndResourceGroupsPostRequest;
import com.sap.cds.CdsData;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
import com.sap.cds.feature.aicore.core.AICoreClients;
import com.sap.cds.feature.aicore.core.AICoreConfig;
import com.sap.cds.feature.aicore.core.DeploymentResolver;
@@ -36,7 +36,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-@ServiceName(AICoreService.DEFAULT_NAME)
+@ServiceName(AICore.SERVICE_NAME)
public class ResourceGroupHandler extends AbstractCrudHandler {
private static final Logger logger = LoggerFactory.getLogger(ResourceGroupHandler.class);
diff --git a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceConfigurationTest.java b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceConfigurationTest.java
index c6612a6..42cb7a5 100644
--- a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceConfigurationTest.java
+++ b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceConfigurationTest.java
@@ -5,7 +5,8 @@
import static org.assertj.core.api.Assertions.assertThat;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
+import com.sap.cds.services.cds.RemoteService;
import com.sap.cds.services.environment.CdsProperties;
import com.sap.cds.services.impl.environment.SimplePropertiesProvider;
import com.sap.cds.services.runtime.CdsRuntime;
@@ -17,8 +18,8 @@
* CDS model. This verifies the full service registration and handler wiring lifecycle without heavy
* Mockito mocks.
*
- * Since the test runtime has no service bindings, the configuration always registers an {@link
- * AICoreServiceImpl} with mock handlers regardless of environment variables.
+ *
Since the test runtime has no service bindings, the configuration always registers mock
+ * handlers regardless of environment variables.
*/
class AICoreServiceConfigurationTest {
@@ -26,15 +27,16 @@ class AICoreServiceConfigurationTest {
void noBinding_noMultiTenancy_registersService() {
CdsRuntime runtime =
CdsRuntimeConfigurer.create(new SimplePropertiesProvider(new CdsProperties()))
+ .environmentConfigurations()
.cdsModel("edmx/csn.json")
.serviceConfigurations()
.eventHandlerConfigurations()
.complete();
- AICoreService service =
- runtime.getServiceCatalog().getService(AICoreService.class, AICoreService.DEFAULT_NAME);
+ RemoteService service =
+ runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
- assertThat(service).isNotNull().isInstanceOf(AICoreServiceImpl.class);
+ assertThat(service).isNotNull();
}
@Test
@@ -48,15 +50,16 @@ void noBinding_withSidecarUrl_registersService() {
CdsRuntime runtime =
CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props))
+ .environmentConfigurations()
.cdsModel("edmx/csn.json")
.serviceConfigurations()
.eventHandlerConfigurations()
.complete();
- AICoreService service =
- runtime.getServiceCatalog().getService(AICoreService.class, AICoreService.DEFAULT_NAME);
+ RemoteService service =
+ runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
- assertThat(service).isNotNull().isInstanceOf(AICoreServiceImpl.class);
+ assertThat(service).isNotNull();
}
@Test
@@ -67,8 +70,8 @@ void noModel_skipsServiceRegistration() {
.eventHandlerConfigurations()
.complete();
- AICoreService service =
- runtime.getServiceCatalog().getService(AICoreService.class, AICoreService.DEFAULT_NAME);
+ RemoteService service =
+ runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
assertThat(service).isNull();
}
diff --git a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceImplDeploymentIdTest.java b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceImplDeploymentIdTest.java
index 9c66431..84979cf 100644
--- a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceImplDeploymentIdTest.java
+++ b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceImplDeploymentIdTest.java
@@ -23,16 +23,19 @@
import com.sap.ai.sdk.core.model.AiDeploymentList;
import com.sap.ai.sdk.core.model.AiDeploymentResponseWithDetails;
import com.sap.ai.sdk.core.model.AiDeploymentStatus;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
+import com.sap.cds.feature.aicore.api.DeploymentIdContext;
import com.sap.cds.feature.aicore.api.ModelDeploymentSpec;
+import com.sap.cds.feature.aicore.api.ResourceGroupContext;
import com.sap.cds.feature.aicore.core.handler.AICoreApiHandler;
+import com.sap.cds.services.cds.RemoteService;
import com.sap.cds.services.environment.CdsProperties;
+import com.sap.cds.services.environment.CdsProperties.Remote.RemoteServiceConfig;
import com.sap.cds.services.impl.environment.SimplePropertiesProvider;
import com.sap.cds.services.runtime.CdsRuntime;
import com.sap.cds.services.runtime.CdsRuntimeConfigurer;
import com.sap.cloud.sdk.services.openapi.apache.core.OpenApiRequestException;
import java.lang.reflect.Field;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
@@ -53,7 +56,7 @@ class AICoreServiceImplDeploymentIdTest {
private DeploymentApi deploymentApi;
private ConfigurationApi configurationApi;
private ResourceGroupApi resourceGroupApi;
- private AICoreServiceImpl service;
+ private RemoteService service;
private DeploymentResolver resolver;
private final ModelDeploymentSpec spec =
@@ -64,16 +67,19 @@ private String cacheKey() {
}
/**
- * Creates an {@link AICoreServiceImpl} properly registered with a CDS runtime and the {@link
+ * Creates a {@link RemoteService} properly registered with a CDS runtime and the {@link
* AICoreApiHandler} so that {@code emit()} dispatches to the handler.
*/
- private AICoreServiceImpl createService(boolean multiTenancy) {
- TestPropertiesProvider props = new TestPropertiesProvider();
- props.setProperty("cds.ai.core.maxRetries", 1);
- props.setProperty("cds.ai.core.initialDelayMs", 1L);
-
- CdsRuntimeConfigurer configurer = CdsRuntimeConfigurer.create(props);
+ private RemoteService createService(boolean multiTenancy) {
+ CdsProperties props = new CdsProperties();
+ RemoteServiceConfig rsConfig = new RemoteServiceConfig(AICore.SERVICE_NAME);
+ rsConfig.setModel(AICore.SERVICE_NAME);
+ props.getRemote().getServices().put(AICore.SERVICE_NAME, rsConfig);
+
+ CdsRuntimeConfigurer configurer =
+ CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props));
configurer.cdsModel("edmx/csn.json");
+ configurer.serviceConfigurations();
CdsRuntime runtime = configurer.getCdsRuntime();
AICoreConfig config = new AICoreConfig("default", "cds-", 1, 1L, multiTenancy);
@@ -82,11 +88,10 @@ private AICoreServiceImpl createService(boolean multiTenancy) {
deploymentApi, configurationApi, resourceGroupApi, mock(AiCoreService.class));
resolver = new DeploymentResolver(config, deploymentApi, resourceGroupApi);
- AICoreServiceImpl svc = new AICoreServiceImpl(AICoreService.DEFAULT_NAME, runtime);
- configurer.service(svc);
configurer.eventHandler(new AICoreApiHandler(config, clients, resolver));
configurer.complete();
- return svc;
+
+ return runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
}
@BeforeEach
@@ -105,7 +110,7 @@ void cacheHit_runningDeployment_returnsCachedIdWithoutQuery() throws Exception {
when(running.getStatus()).thenReturn(AiDeploymentStatus.RUNNING);
when(deploymentApi.get(RG, DEPLOYMENT_ID)).thenReturn(running);
- String result = service.deploymentId(RG, spec);
+ String result = emitDeploymentId(service, RG, spec);
assertThat(result).isEqualTo(DEPLOYMENT_ID);
verify(deploymentApi).get(RG, DEPLOYMENT_ID);
@@ -130,7 +135,7 @@ void cacheStale_404OnGet_invalidatesAndReturnsExistingFromQuery() throws Excepti
when(deploymentApi.query(eq(RG), any(), any(), eq(SCENARIO), any(), any(), any(), any()))
.thenReturn(list);
- String result = service.deploymentId(RG, spec);
+ String result = emitDeploymentId(service, RG, spec);
assertThat(result).isEqualTo(otherDeployment);
assertThat(getDeploymentCache(resolver)).containsEntry(cacheKey(), otherDeployment);
@@ -144,7 +149,7 @@ void cacheStale_5xxOnGet_propagatesAndPreservesCacheEntry() throws Exception {
OpenApiRequestException serverError = new OpenApiRequestException("boom").statusCode(503);
when(deploymentApi.get(RG, "still-valid-id")).thenThrow(serverError);
- assertThatThrownBy(() -> service.deploymentId(RG, spec)).rootCause().isSameAs(serverError);
+ assertThatThrownBy(() -> emitDeploymentId(service, RG, spec)).rootCause().isSameAs(serverError);
assertThat(getDeploymentCache(resolver)).containsEntry(cacheKey(), "still-valid-id");
verify(deploymentApi, never()).query(any(), any(), any(), any(), any(), any(), any(), any());
@@ -162,7 +167,7 @@ void noCache_existingMatchingDeployment_isReusedAndCached() throws Exception {
when(deploymentApi.query(eq(RG), any(), any(), eq(SCENARIO), any(), any(), any(), any()))
.thenReturn(list);
- String result = service.deploymentId(RG, spec);
+ String result = emitDeploymentId(service, RG, spec);
assertThat(result).isEqualTo(DEPLOYMENT_ID);
assertThat(getDeploymentCache(resolver)).containsEntry(cacheKey(), DEPLOYMENT_ID);
@@ -185,8 +190,8 @@ void secondCallUsesCachedResult_singleQueryToApi() {
when(running.getStatus()).thenReturn(AiDeploymentStatus.RUNNING);
when(deploymentApi.get(RG, DEPLOYMENT_ID)).thenReturn(running);
- String first = service.deploymentId(RG, spec);
- String second = service.deploymentId(RG, spec);
+ String first = emitDeploymentId(service, RG, spec);
+ String second = emitDeploymentId(service, RG, spec);
assertThat(first).isEqualTo(DEPLOYMENT_ID);
assertThat(second).isEqualTo(DEPLOYMENT_ID);
@@ -197,15 +202,15 @@ void secondCallUsesCachedResult_singleQueryToApi() {
@Test
void resourceGroupForTenant_nullTenantId_returnsDefault() {
- AICoreServiceImpl mtService = createService(true);
+ RemoteService mtService = createService(true);
- String result = mtService.resourceGroupForTenant(null);
+ String result = emitResourceGroup(mtService, null);
assertThat(result).isEqualTo("default");
}
@Test
void resourceGroupForTenant_multiTenancyDisabled_returnsDefault() {
- String result = service.resourceGroupForTenant("any-tenant");
+ String result = emitResourceGroup(service, "any-tenant");
assertThat(result).isEqualTo("default");
}
@@ -232,7 +237,7 @@ void noCacheNoExistingDeployment_createsNewDeploymentWhenConfigExists() throws E
when(runningPoll.getStatus()).thenReturn(AiDeploymentStatus.RUNNING);
when(deploymentApi.get(RG, DEPLOYMENT_ID)).thenReturn(runningPoll);
- String result = service.deploymentId(RG, spec);
+ String result = emitDeploymentId(service, RG, spec);
assertThat(result).isEqualTo(DEPLOYMENT_ID);
assertThat(getDeploymentCache(resolver)).containsEntry(cacheKey(), DEPLOYMENT_ID);
@@ -244,6 +249,21 @@ void noCacheNoExistingDeployment_createsNewDeploymentWhenConfigExists() throws E
// Helpers
// ──────────────────────────────────────────────────────────────────────────
+ private static String emitDeploymentId(RemoteService svc, String rg, ModelDeploymentSpec spec) {
+ DeploymentIdContext ctx = DeploymentIdContext.create();
+ ctx.setResourceGroupId(rg);
+ ctx.setSpec(spec);
+ svc.emit(ctx);
+ return ctx.getResult();
+ }
+
+ private static String emitResourceGroup(RemoteService svc, String tenantId) {
+ ResourceGroupContext ctx = ResourceGroupContext.create();
+ ctx.setTenantId(tenantId);
+ svc.emit(ctx);
+ return ctx.getResult();
+ }
+
@SuppressWarnings("unchecked")
private static void putInDeploymentCache(DeploymentResolver resolver, String key, String value)
throws Exception {
@@ -260,27 +280,4 @@ private static Map getDeploymentCache(DeploymentResolver resolve
field.setAccessible(true);
return ((com.github.benmanes.caffeine.cache.Cache) field.get(resolver)).asMap();
}
-
- /** Properties provider that allows overriding specific keys for test configuration. */
- private static class TestPropertiesProvider extends SimplePropertiesProvider {
- private final Map properties = new HashMap<>();
-
- TestPropertiesProvider() {
- super(new CdsProperties());
- }
-
- void setProperty(String key, Object value) {
- properties.put(key, value);
- }
-
- @Override
- @SuppressWarnings("unchecked")
- public T getProperty(String key, Class asClazz, T defaultValue) {
- Object value = properties.get(key);
- if (value != null && asClazz.isInstance(value)) {
- return (T) value;
- }
- return defaultValue;
- }
- }
}
diff --git a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/MockAICoreServiceImplTest.java b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/MockAICoreServiceImplTest.java
index c555b58..056e0fc 100644
--- a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/MockAICoreServiceImplTest.java
+++ b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/MockAICoreServiceImplTest.java
@@ -5,9 +5,12 @@
import static org.assertj.core.api.Assertions.assertThat;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
+import com.sap.cds.feature.aicore.api.DeploymentIdContext;
import com.sap.cds.feature.aicore.api.ModelDeploymentSpec;
+import com.sap.cds.feature.aicore.api.ResourceGroupContext;
import com.sap.cds.feature.aicore.core.handler.MockAICoreApiHandler;
+import com.sap.cds.services.cds.RemoteService;
import com.sap.cds.services.environment.CdsProperties;
import com.sap.cds.services.impl.environment.SimplePropertiesProvider;
import com.sap.cds.services.runtime.CdsRuntime;
@@ -21,7 +24,7 @@
*/
class MockAICoreServiceImplTest {
- private AICoreService createMockService(boolean multiTenancy) {
+ private RemoteService createMockService(boolean multiTenancy) {
CdsProperties props = new CdsProperties();
if (multiTenancy) {
CdsProperties.MultiTenancy mt = new CdsProperties.MultiTenancy();
@@ -33,55 +36,84 @@ private AICoreService createMockService(boolean multiTenancy) {
CdsRuntime runtime =
CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props))
+ .environmentConfigurations()
.cdsModel("edmx/csn.json")
.serviceConfigurations()
.eventHandlerConfigurations()
.complete();
- return runtime.getServiceCatalog().getService(AICoreService.class, AICoreService.DEFAULT_NAME);
+ return runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
}
@Test
void noMultiTenancy_resourceGroupReturnsDefault() {
- AICoreService service = createMockService(false);
- assertThat(service.resourceGroup()).isEqualTo("default");
+ RemoteService service = createMockService(false);
+ ResourceGroupContext rgCtx = ResourceGroupContext.create();
+ service.emit(rgCtx);
+ assertThat(rgCtx.getResult()).isEqualTo("default");
}
@Test
void noMultiTenancy_resourceGroupForTenant_returnsDefault() {
- AICoreService service = createMockService(false);
- assertThat(service.resourceGroupForTenant("any-tenant")).isEqualTo("default");
+ RemoteService service = createMockService(false);
+ ResourceGroupContext rgCtx = ResourceGroupContext.create();
+ rgCtx.setTenantId("any-tenant");
+ service.emit(rgCtx);
+ assertThat(rgCtx.getResult()).isEqualTo("default");
}
@Test
void multiTenancy_resourceGroupForTenant_returnsPrefixed() {
- AICoreService service = createMockService(true);
- String rg = service.resourceGroupForTenant("my-tenant");
- assertThat(rg).isEqualTo("cds-my-tenant");
+ RemoteService service = createMockService(true);
+ ResourceGroupContext rgCtx = ResourceGroupContext.create();
+ rgCtx.setTenantId("my-tenant");
+ service.emit(rgCtx);
+ assertThat(rgCtx.getResult()).isEqualTo("cds-my-tenant");
}
@Test
void multiTenancy_resourceGroupForTenant_cachesResult() {
- AICoreService service = createMockService(true);
- String first = service.resourceGroupForTenant("t1");
- String second = service.resourceGroupForTenant("t1");
+ RemoteService service = createMockService(true);
+ ResourceGroupContext rgCtx1 = ResourceGroupContext.create();
+ rgCtx1.setTenantId("t1");
+ service.emit(rgCtx1);
+ String first = rgCtx1.getResult();
+
+ ResourceGroupContext rgCtx2 = ResourceGroupContext.create();
+ rgCtx2.setTenantId("t1");
+ service.emit(rgCtx2);
+ String second = rgCtx2.getResult();
assertThat(first).isEqualTo(second);
}
@Test
void deploymentId_returnsMockId() {
- AICoreService service = createMockService(false);
+ RemoteService service = createMockService(false);
var spec = new ModelDeploymentSpec("scenario", "exec", "cfg1", List.of(), d -> true);
- String id = service.deploymentId("default", spec);
+ DeploymentIdContext depCtx = DeploymentIdContext.create();
+ depCtx.setResourceGroupId("default");
+ depCtx.setSpec(spec);
+ service.emit(depCtx);
+ String id = depCtx.getResult();
assertThat(id).startsWith("mock-deployment-");
}
@Test
void deploymentId_cachesSameResult() {
- AICoreService service = createMockService(false);
+ RemoteService service = createMockService(false);
var spec = new ModelDeploymentSpec("scenario", "exec", "cfg1", List.of(), d -> true);
- String first = service.deploymentId("default", spec);
- String second = service.deploymentId("default", spec);
+
+ DeploymentIdContext depCtx1 = DeploymentIdContext.create();
+ depCtx1.setResourceGroupId("default");
+ depCtx1.setSpec(spec);
+ service.emit(depCtx1);
+ String first = depCtx1.getResult();
+
+ DeploymentIdContext depCtx2 = DeploymentIdContext.create();
+ depCtx2.setResourceGroupId("default");
+ depCtx2.setSpec(spec);
+ service.emit(depCtx2);
+ String second = depCtx2.getResult();
assertThat(first).isEqualTo(second);
}
}
diff --git a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/ConfigurationHandlerTest.java b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/ConfigurationHandlerTest.java
index 7fdb3e2..f6de01a 100644
--- a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/ConfigurationHandlerTest.java
+++ b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/ConfigurationHandlerTest.java
@@ -20,14 +20,15 @@
import com.sap.ai.sdk.core.model.AiConfigurationCreationResponse;
import com.sap.ai.sdk.core.model.AiConfigurationList;
import com.sap.cds.Result;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
import com.sap.cds.feature.aicore.core.AICoreClients;
import com.sap.cds.feature.aicore.core.AICoreConfig;
-import com.sap.cds.feature.aicore.core.AICoreServiceImpl;
import com.sap.cds.feature.aicore.core.DeploymentResolver;
import com.sap.cds.ql.Insert;
import com.sap.cds.ql.Select;
+import com.sap.cds.services.cds.RemoteService;
import com.sap.cds.services.environment.CdsProperties;
+import com.sap.cds.services.environment.CdsProperties.Remote.RemoteServiceConfig;
import com.sap.cds.services.impl.environment.SimplePropertiesProvider;
import com.sap.cds.services.request.RequestContext;
import com.sap.cds.services.runtime.CdsRuntime;
@@ -47,7 +48,7 @@
class ConfigurationHandlerTest {
private static CdsRuntime runtime;
- private static AICoreServiceImpl service;
+ private static RemoteService service;
private static ConfigurationApi configurationApi;
private static ResourceGroupApi resourceGroupApi;
@@ -57,8 +58,14 @@ static void bootRuntime() {
resourceGroupApi = mock(ResourceGroupApi.class);
DeploymentApi deploymentApi = mock(DeploymentApi.class);
- var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(new CdsProperties()));
+ CdsProperties props = new CdsProperties();
+ RemoteServiceConfig rsConfig = new RemoteServiceConfig(AICore.SERVICE_NAME);
+ rsConfig.setModel(AICore.SERVICE_NAME);
+ props.getRemote().getServices().put(AICore.SERVICE_NAME, rsConfig);
+
+ var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props));
configurer.cdsModel("edmx/csn.json");
+ configurer.serviceConfigurations();
runtime = configurer.getCdsRuntime();
AICoreConfig config = new AICoreConfig("default", "cds-", 10, 300, false);
@@ -67,11 +74,11 @@ static void bootRuntime() {
deploymentApi, configurationApi, resourceGroupApi, mock(AiCoreService.class));
DeploymentResolver resolver = new DeploymentResolver(config, deploymentApi, resourceGroupApi);
- service = new AICoreServiceImpl(AICoreService.DEFAULT_NAME, runtime);
- configurer.service(service);
configurer.eventHandler(new AICoreApiHandler(config, clients, resolver));
configurer.eventHandler(new ConfigurationHandler(config, clients, resolver));
configurer.complete();
+
+ service = runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
}
@BeforeEach
diff --git a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/DeploymentHandlerTest.java b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/DeploymentHandlerTest.java
index 0e47302..44743d5 100644
--- a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/DeploymentHandlerTest.java
+++ b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/DeploymentHandlerTest.java
@@ -21,16 +21,17 @@
import com.sap.ai.sdk.core.model.AiDeploymentModificationRequest;
import com.sap.ai.sdk.core.model.AiExecutionStatus;
import com.sap.cds.Result;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
import com.sap.cds.feature.aicore.core.AICoreClients;
import com.sap.cds.feature.aicore.core.AICoreConfig;
-import com.sap.cds.feature.aicore.core.AICoreServiceImpl;
import com.sap.cds.feature.aicore.core.DeploymentResolver;
import com.sap.cds.ql.Insert;
import com.sap.cds.ql.Update;
import com.sap.cds.services.ErrorStatuses;
import com.sap.cds.services.ServiceException;
+import com.sap.cds.services.cds.RemoteService;
import com.sap.cds.services.environment.CdsProperties;
+import com.sap.cds.services.environment.CdsProperties.Remote.RemoteServiceConfig;
import com.sap.cds.services.impl.environment.SimplePropertiesProvider;
import com.sap.cds.services.request.RequestContext;
import com.sap.cds.services.runtime.CdsRuntime;
@@ -50,7 +51,7 @@
class DeploymentHandlerTest {
private static CdsRuntime runtime;
- private static AICoreServiceImpl service;
+ private static RemoteService service;
private static DeploymentApi deploymentApi;
private static ResourceGroupApi resourceGroupApi;
private static ConfigurationApi configurationApi;
@@ -61,8 +62,14 @@ static void bootRuntime() {
resourceGroupApi = mock(ResourceGroupApi.class);
configurationApi = mock(ConfigurationApi.class);
- var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(new CdsProperties()));
+ CdsProperties props = new CdsProperties();
+ RemoteServiceConfig rsConfig = new RemoteServiceConfig(AICore.SERVICE_NAME);
+ rsConfig.setModel(AICore.SERVICE_NAME);
+ props.getRemote().getServices().put(AICore.SERVICE_NAME, rsConfig);
+
+ var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props));
configurer.cdsModel("edmx/csn.json");
+ configurer.serviceConfigurations();
runtime = configurer.getCdsRuntime();
AICoreConfig config = new AICoreConfig("default", "cds-", 10, 300, false);
@@ -71,11 +78,11 @@ static void bootRuntime() {
deploymentApi, configurationApi, resourceGroupApi, mock(AiCoreService.class));
DeploymentResolver resolver = new DeploymentResolver(config, deploymentApi, resourceGroupApi);
- service = new AICoreServiceImpl(AICoreService.DEFAULT_NAME, runtime);
- configurer.service(service);
configurer.eventHandler(new AICoreApiHandler(config, clients, resolver));
configurer.eventHandler(new DeploymentHandler(config, clients, resolver));
configurer.complete();
+
+ service = runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
}
@BeforeEach
diff --git a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/ResourceGroupHandlerTest.java b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/ResourceGroupHandlerTest.java
index 4dfc443..a635b98 100644
--- a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/ResourceGroupHandlerTest.java
+++ b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/ResourceGroupHandlerTest.java
@@ -22,15 +22,16 @@
import com.sap.ai.sdk.core.model.BckndResourceGroupPatchRequest;
import com.sap.ai.sdk.core.model.BckndResourceGroupsPostRequest;
import com.sap.cds.Result;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
import com.sap.cds.feature.aicore.core.AICoreClients;
import com.sap.cds.feature.aicore.core.AICoreConfig;
-import com.sap.cds.feature.aicore.core.AICoreServiceImpl;
import com.sap.cds.feature.aicore.core.DeploymentResolver;
import com.sap.cds.ql.Insert;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.Update;
+import com.sap.cds.services.cds.RemoteService;
import com.sap.cds.services.environment.CdsProperties;
+import com.sap.cds.services.environment.CdsProperties.Remote.RemoteServiceConfig;
import com.sap.cds.services.impl.environment.SimplePropertiesProvider;
import com.sap.cds.services.request.RequestContext;
import com.sap.cds.services.runtime.CdsRuntime;
@@ -51,7 +52,7 @@
class ResourceGroupHandlerTest {
private static CdsRuntime runtime;
- private static AICoreServiceImpl service;
+ private static RemoteService service;
private static ResourceGroupApi resourceGroupApi;
@BeforeAll
@@ -60,8 +61,14 @@ static void bootRuntime() {
DeploymentApi deploymentApi = mock(DeploymentApi.class);
ConfigurationApi configurationApi = mock(ConfigurationApi.class);
- var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(new CdsProperties()));
+ CdsProperties props = new CdsProperties();
+ RemoteServiceConfig rsConfig = new RemoteServiceConfig(AICore.SERVICE_NAME);
+ rsConfig.setModel(AICore.SERVICE_NAME);
+ props.getRemote().getServices().put(AICore.SERVICE_NAME, rsConfig);
+
+ var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props));
configurer.cdsModel("edmx/csn.json");
+ configurer.serviceConfigurations();
runtime = configurer.getCdsRuntime();
AICoreConfig config = new AICoreConfig("default", "cds-", 10, 300, false);
@@ -70,11 +77,11 @@ static void bootRuntime() {
deploymentApi, configurationApi, resourceGroupApi, mock(AiCoreService.class));
DeploymentResolver resolver = new DeploymentResolver(config, deploymentApi, resourceGroupApi);
- service = new AICoreServiceImpl(AICoreService.DEFAULT_NAME, runtime);
- configurer.service(service);
configurer.eventHandler(new AICoreApiHandler(config, clients, resolver));
configurer.eventHandler(new ResourceGroupHandler(config, clients, resolver));
configurer.complete();
+
+ service = runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
}
@BeforeEach
@@ -199,7 +206,7 @@ void onUpdate_withoutLabels_callsPatchWithoutLabels() {
class MultiTenancyTests {
private static CdsRuntime mtRuntime;
- private static AICoreServiceImpl mtService;
+ private static RemoteService mtService;
private static ResourceGroupApi mtResourceGroupApi;
@BeforeAll
@@ -208,9 +215,14 @@ static void bootMtRuntime() {
DeploymentApi deploymentApi = mock(DeploymentApi.class);
ConfigurationApi configurationApi = mock(ConfigurationApi.class);
- var configurer =
- CdsRuntimeConfigurer.create(new SimplePropertiesProvider(new CdsProperties()));
+ CdsProperties props = new CdsProperties();
+ RemoteServiceConfig rsConfig = new RemoteServiceConfig(AICore.SERVICE_NAME);
+ rsConfig.setModel(AICore.SERVICE_NAME);
+ props.getRemote().getServices().put(AICore.SERVICE_NAME, rsConfig);
+
+ var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props));
configurer.cdsModel("edmx/csn.json");
+ configurer.serviceConfigurations();
mtRuntime = configurer.getCdsRuntime();
AICoreConfig config = new AICoreConfig("default", "cds-", 10, 300, true);
@@ -220,11 +232,12 @@ static void bootMtRuntime() {
DeploymentResolver resolver =
new DeploymentResolver(config, deploymentApi, mtResourceGroupApi);
- mtService = new AICoreServiceImpl(AICoreService.DEFAULT_NAME, mtRuntime);
- configurer.service(mtService);
configurer.eventHandler(new AICoreApiHandler(config, clients, resolver));
configurer.eventHandler(new ResourceGroupHandler(config, clients, resolver));
configurer.complete();
+
+ mtService =
+ mtRuntime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
}
@BeforeEach
diff --git a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/TenantScopingTest.java b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/TenantScopingTest.java
index 078779c..168b5fc 100644
--- a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/TenantScopingTest.java
+++ b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/TenantScopingTest.java
@@ -18,14 +18,15 @@
import com.sap.ai.sdk.core.model.BckndResourceGroup;
import com.sap.ai.sdk.core.model.BckndResourceGroupLabel;
import com.sap.cds.Result;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
import com.sap.cds.feature.aicore.core.AICoreClients;
import com.sap.cds.feature.aicore.core.AICoreConfig;
-import com.sap.cds.feature.aicore.core.AICoreServiceImpl;
import com.sap.cds.feature.aicore.core.DeploymentResolver;
import com.sap.cds.ql.Select;
import com.sap.cds.services.ServiceException;
+import com.sap.cds.services.cds.RemoteService;
import com.sap.cds.services.environment.CdsProperties;
+import com.sap.cds.services.environment.CdsProperties.Remote.RemoteServiceConfig;
import com.sap.cds.services.impl.environment.SimplePropertiesProvider;
import com.sap.cds.services.request.RequestContext;
import com.sap.cds.services.runtime.CdsRuntime;
@@ -44,7 +45,7 @@
class TenantScopingTest {
private static CdsRuntime runtime;
- private static AICoreServiceImpl service;
+ private static RemoteService service;
private static DeploymentApi deploymentApi;
private static ResourceGroupApi resourceGroupApi;
@@ -54,8 +55,14 @@ static void bootRuntime() {
resourceGroupApi = mock(ResourceGroupApi.class);
ConfigurationApi configurationApi = mock(ConfigurationApi.class);
- var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(new CdsProperties()));
+ CdsProperties props = new CdsProperties();
+ RemoteServiceConfig rsConfig = new RemoteServiceConfig(AICore.SERVICE_NAME);
+ rsConfig.setModel(AICore.SERVICE_NAME);
+ props.getRemote().getServices().put(AICore.SERVICE_NAME, rsConfig);
+
+ var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props));
configurer.cdsModel("edmx/csn.json");
+ configurer.serviceConfigurations();
runtime = configurer.getCdsRuntime();
AICoreConfig config = new AICoreConfig("default", "cds-", 10, 300, true);
@@ -64,11 +71,11 @@ static void bootRuntime() {
deploymentApi, configurationApi, resourceGroupApi, mock(AiCoreService.class));
DeploymentResolver resolver = new DeploymentResolver(config, deploymentApi, resourceGroupApi);
- service = new AICoreServiceImpl(AICoreService.DEFAULT_NAME, runtime);
- configurer.service(service);
configurer.eventHandler(new AICoreApiHandler(config, clients, resolver));
configurer.eventHandler(new DeploymentHandler(config, clients, resolver));
configurer.complete();
+
+ service = runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
}
@BeforeEach
diff --git a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/FioriRecommendationHandler.java b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/FioriRecommendationHandler.java
index 36e9634..f00ce7e 100644
--- a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/FioriRecommendationHandler.java
+++ b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/FioriRecommendationHandler.java
@@ -6,12 +6,12 @@
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.sap.cds.CdsData;
-import com.sap.cds.feature.aicore.api.AICoreService;
import com.sap.cds.feature.recommendation.api.RecommendationClient;
import com.sap.cds.feature.recommendation.api.RecommendationClientResolver;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.services.cds.ApplicationService;
import com.sap.cds.services.cds.CdsReadEventContext;
+import com.sap.cds.services.cds.RemoteService;
import com.sap.cds.services.draft.Drafts;
import com.sap.cds.services.handler.EventHandler;
import com.sap.cds.services.handler.annotations.After;
@@ -29,9 +29,11 @@ class FioriRecommendationHandler implements EventHandler {
private static final Logger logger = LoggerFactory.getLogger(FioriRecommendationHandler.class);
private static final int DEFAULT_CONTEXT_ROW_LIMIT = 2000;
+ private static final String SAP_RECOMMENDATIONS = "SAP_Recommendations";
- private final AICoreService aiCoreService;
+ private final RemoteService aiCoreService;
private final RecommendationClientResolver clientResolver;
+ private final PersistenceService db;
private final RecommendationResultParser resultParser = new RecommendationResultParser();
// Avoids re-evaluating the CDS model on every read to check whether an entity has prediction
// columns. Keys are ":" because if an entity needs a prediction can be
@@ -40,9 +42,12 @@ class FioriRecommendationHandler implements EventHandler {
Caffeine.newBuilder().maximumSize(10_000).build();
FioriRecommendationHandler(
- AICoreService aiCoreService, RecommendationClientResolver clientResolver) {
+ RemoteService aiCoreService,
+ RecommendationClientResolver clientResolver,
+ PersistenceService db) {
this.aiCoreService = aiCoreService;
this.clientResolver = clientResolver;
+ this.db = db;
}
void invalidateTenant(String tenantId) {
@@ -77,10 +82,13 @@ public void afterRead(CdsReadEventContext context, List dataList) {
return;
}
- if (!Boolean.FALSE.equals(row.get(Drafts.IS_ACTIVE_ENTITY))) {
+ if (row.containsKey(Drafts.IS_ACTIVE_ENTITY)
+ && !Boolean.FALSE.equals(row.get(Drafts.IS_ACTIVE_ENTITY))) {
return;
}
+ // rowType reflects the projected shape (columns actually selected); target is the full entity.
+ // Fall back to target when rowType is absent, e.g. when the result carries no type metadata.
CdsStructuredType rowType = context.getResult().rowType();
if (rowType == null) {
rowType = target;
@@ -100,18 +108,13 @@ public void afterRead(CdsReadEventContext context, List dataList) {
return;
}
- if (builder.contextColumns().isEmpty()) {
- logger.debug("No suitable context columns found, skipping predictions.");
+ if (builder.keyNames().isEmpty()) {
+ logger.debug("Entity has no key elements, skipping predictions.");
return;
}
- PersistenceService db =
- context
- .getServiceCatalog()
- .getService(PersistenceService.class, PersistenceService.DEFAULT_NAME);
- List contextRows = new ArrayList<>(db.run(builder.buildContextQuery()).list());
- if (contextRows.size() < 2) {
- logger.debug("Not enough context rows (minimum 2), skipping predictions.");
+ if (builder.contextColumns().isEmpty()) {
+ logger.trace("No suitable context columns found, skipping predictions.");
return;
}
@@ -121,11 +124,19 @@ public void afterRead(CdsReadEventContext context, List dataList) {
return;
}
- List allRows = builder.assembleRows(contextRows, predictRow, row);
+ // Result.list() returns List; the ArrayList copy also converts it to List.
+ List contextRows = new ArrayList<>(db.run(builder.buildContextQuery()).list());
+ if (contextRows.size() < 2) {
+ logger.debug("Not enough context rows (minimum 2), skipping predictions.");
+ return;
+ }
+
+ List missingPredictionElementNames =
+ builder.predictionElementNames().stream().filter(c -> row.get(c) == null).toList();
RecommendationClient client = clientResolver.resolve(aiCoreService);
List predictions =
- client.predict(allRows, builder.predictionElementNames(), builder.indexColumn());
+ client.predict(predictRow, contextRows, missingPredictionElementNames, builder.keyNames());
if (predictions.isEmpty()) {
logger.warn("No predictions returned from AI client.");
@@ -136,11 +147,9 @@ public void afterRead(CdsReadEventContext context, List dataList) {
return;
}
- List missingPredictionElementNames =
- builder.predictionElementNames().stream().filter(c -> row.get(c) == null).toList();
Map recommendations =
resultParser.buildRecommendations(
db, predictions.get(0), missingPredictionElementNames, context, rowType);
- row.put("SAP_Recommendations", recommendations);
+ row.put(SAP_RECOMMENDATIONS, recommendations);
}
}
diff --git a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/MockRecommendationClient.java b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/MockRecommendationClient.java
index 27498bb..cb2b64c 100644
--- a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/MockRecommendationClient.java
+++ b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/MockRecommendationClient.java
@@ -5,45 +5,46 @@
import com.sap.cds.CdsData;
import com.sap.cds.feature.recommendation.api.RecommendationClient;
-import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
+// Mock implementation used when no AI Core binding is present. For each prediction column that
+// is null in the predict row, it picks a random non-null value from the same column across the
+// context rows and returns it as the prediction. Columns already filled are left unchanged.
class MockRecommendationClient implements RecommendationClient {
+ // We use random here so you can see a difference in the UI. The actual value returned here is not
+ // relevant for tests.
private final Random random = new Random();
@Override
public List predict(
- List rows, List predictionColumns, String indexColumn) {
- List predictions = new ArrayList<>();
- for (CdsData row : rows) {
- Map prediction = new HashMap<>();
- boolean addPrediction = false;
- for (String col : predictionColumns) {
- if ("[PREDICT]".equals(row.get(col))) {
- addPrediction = true;
- List availableValues =
- rows.stream()
- .filter(r -> r.get(col) != null && !"[PREDICT]".equals(r.get(col)))
- .map(r -> r.get(col))
- .toList();
- Object contextValue =
- availableValues.isEmpty()
- ? null
- : availableValues.get(random.nextInt(availableValues.size()));
- Map predictionEntry = new HashMap<>();
- predictionEntry.put("prediction", contextValue);
- prediction.put(col, List.of(predictionEntry));
- }
- }
- if (addPrediction) {
- prediction.put(indexColumn, row.get(indexColumn));
- predictions.add(CdsData.create(prediction));
+ CdsData predictionRow,
+ List contextRows,
+ List predictionColumns,
+ List keyNames) {
+ String indexColumn = keyNames.size() == 1 ? keyNames.get(0) : "SAP_RECOMMENDATIONS_ID";
+ Map prediction = new HashMap<>();
+ for (String col : predictionColumns) {
+ if (predictionRow.get(col) == null) {
+ List availableValues =
+ contextRows.stream().filter(r -> r.get(col) != null).map(r -> r.get(col)).toList();
+ Object contextValue =
+ availableValues.isEmpty()
+ ? null
+ : availableValues.get(random.nextInt(availableValues.size()));
+ Map predictionEntry = new HashMap<>();
+ // Replace the empty entry in col with a randomly picked value of entries in the
+ // contextRows.
+ predictionEntry.put("prediction", contextValue);
+ prediction.put(col, List.of(predictionEntry));
}
}
- return predictions;
+ if (!keyNames.isEmpty()) {
+ prediction.put(indexColumn, predictionRow.get(keyNames.get(0)));
+ }
+ return List.of(CdsData.create(prediction));
}
}
diff --git a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RecommendationConfiguration.java b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RecommendationConfiguration.java
index afde35c..1d3df83 100644
--- a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RecommendationConfiguration.java
+++ b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RecommendationConfiguration.java
@@ -3,12 +3,17 @@
*/
package com.sap.cds.feature.recommendation;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
+import com.sap.cds.feature.aicore.api.DeploymentIdContext;
+import com.sap.cds.feature.aicore.api.InferenceClientContext;
+import com.sap.cds.feature.aicore.api.ResourceGroupContext;
import com.sap.cds.feature.recommendation.api.RecommendationClient;
import com.sap.cds.feature.recommendation.api.RecommendationClientResolver;
import com.sap.cds.feature.recommendation.api.RptInferenceClient;
import com.sap.cds.feature.recommendation.api.RptModelSpec;
import com.sap.cds.services.ServiceCatalog;
+import com.sap.cds.services.cds.RemoteService;
+import com.sap.cds.services.persistence.PersistenceService;
import com.sap.cds.services.runtime.CdsRuntime;
import com.sap.cds.services.runtime.CdsRuntimeConfiguration;
import com.sap.cds.services.runtime.CdsRuntimeConfigurer;
@@ -25,21 +30,31 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) {
CdsRuntime runtime = configurer.getCdsRuntime();
ServiceCatalog serviceCatalog = runtime.getServiceCatalog();
- AICoreService aiCoreService =
- serviceCatalog.getService(AICoreService.class, AICoreService.DEFAULT_NAME);
+ RemoteService aiCoreService =
+ serviceCatalog.getService(RemoteService.class, AICore.SERVICE_NAME);
if (aiCoreService == null) {
logger.info("No AICoreService found, skipping Fiori recommendation handler registration.");
return;
}
+ PersistenceService db =
+ serviceCatalog.getService(PersistenceService.class, PersistenceService.DEFAULT_NAME);
+
+ if (db == null) {
+ logger.info(
+ "No PersistenceService found, skipping Fiori recommendation handler registration.");
+ return;
+ }
+
boolean hasBind = hasAICoreBinding(runtime);
RecommendationClientResolver resolver =
hasBind
? RecommendationConfiguration::resolveRptClient
: service -> new MockRecommendationClient();
- FioriRecommendationHandler handler = new FioriRecommendationHandler(aiCoreService, resolver);
+ FioriRecommendationHandler handler =
+ new FioriRecommendationHandler(aiCoreService, resolver, db);
configurer.eventHandler(handler);
configurer.eventHandler(new RecommendationModelChangedHandler(handler));
}
@@ -49,13 +64,26 @@ private static boolean hasAICoreBinding(CdsRuntime runtime) {
.getEnvironment()
.getServiceBindings()
.filter(b -> ServiceBindingUtils.matches(b, "aicore"))
- .findFirst()
+ .findAny()
.isPresent();
}
- private static RecommendationClient resolveRptClient(AICoreService service) {
- String resourceGroup = service.resourceGroup();
- String deploymentId = service.deploymentId(resourceGroup, RptModelSpec.rpt1());
- return new RptInferenceClient(service.inferenceClient(resourceGroup, deploymentId));
+ private static RecommendationClient resolveRptClient(RemoteService service) {
+ ResourceGroupContext rgCtx = ResourceGroupContext.create();
+ service.emit(rgCtx);
+ String resourceGroup = rgCtx.getResult();
+
+ DeploymentIdContext depCtx = DeploymentIdContext.create();
+ depCtx.setResourceGroupId(resourceGroup);
+ depCtx.setSpec(RptModelSpec.rpt1());
+ service.emit(depCtx);
+ String deploymentId = depCtx.getResult();
+
+ InferenceClientContext infCtx = InferenceClientContext.create();
+ infCtx.setResourceGroupId(resourceGroup);
+ infCtx.setDeploymentId(deploymentId);
+ service.emit(infCtx);
+
+ return new RptInferenceClient(infCtx.getResult());
}
}
diff --git a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RecommendationContextBuilder.java b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RecommendationContextBuilder.java
index a158123..4c335e2 100644
--- a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RecommendationContextBuilder.java
+++ b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RecommendationContextBuilder.java
@@ -14,23 +14,26 @@
import com.sap.cds.reflect.CdsSimpleType;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.services.draft.Drafts;
-import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Builds the context data needed for prediction: determines which elements to predict, which
- * columns provide context, builds the context query, and prepares rows for the AI model.
+ * columns provide context and builds the context query. This class is cds-model aware, but does not
+ * know about which client will be used for the predictions.
*/
class RecommendationContextBuilder {
private static final String VALUE_LIST_ANNOTATION = "@Common.ValueList";
private static final String VALUE_LIST_WITH_FIXED_VALUES_ANNOTATION =
"@Common.ValueListWithFixedValues";
- private static final String SYNTHETIC_KEY_COLUMN = "SAP_RECOMMENDATIONS_ID";
+ private static final String ODATA_VALUE_LIST_ANNOTATION = "cds.odata.valuelist";
+ private static final String COMPUTED_ANNOTATION = "@Core.Computed";
+ private static final String READONLY_ANNOTATION = "@readonly";
private static final Set SUPPORTED_CONTEXT_TYPES =
EnumSet.of(
CdsBaseType.STRING,
@@ -64,8 +67,6 @@ class RecommendationContextBuilder {
private final List predictionElementNames;
private final List contextColumns;
private final List keyNames;
- private final boolean syntheticKeyNeeded;
- private final String indexColumn;
RecommendationContextBuilder(CdsStructuredType target, CdsStructuredType rowType, int limit) {
this.target = target;
@@ -74,10 +75,6 @@ class RecommendationContextBuilder {
this.predictionElementNames = computePredictionElements();
this.contextColumns = computeContextColumns();
this.keyNames = target.keyElements().map(CdsElement::getName).toList();
- this.syntheticKeyNeeded =
- keyNames.size() > 1 || (keyNames.size() == 1 && !"ID".equals(keyNames.get(0)));
- this.indexColumn =
- syntheticKeyNeeded ? SYNTHETIC_KEY_COLUMN : keyNames.stream().findFirst().orElse("ID");
}
List predictionElementNames() {
@@ -88,30 +85,27 @@ List contextColumns() {
return contextColumns;
}
- String indexColumn() {
- return indexColumn;
- }
-
- boolean syntheticKeyNeeded() {
- return syntheticKeyNeeded;
+ List keyNames() {
+ return keyNames;
}
CqnSelect buildContextQuery() {
- List selectColumns = new ArrayList<>(contextColumns);
- for (String key : keyNames) {
- if (!selectColumns.contains(key)) {
- selectColumns.add(key);
- }
- }
+ Set selectColumns = new HashSet<>(contextColumns);
+ selectColumns.addAll(keyNames);
+
var select =
Select.from(target.getQualifiedName())
.columns(selectColumns.toArray(String[]::new))
.where(
predictionElementNames.stream()
+ // the row for which we want to do predictions is automatically
+ // excluded by this isNotNull check
.map(col -> CQL.get(col).isNotNull())
.collect(CQL.withAnd()))
.limit(contextRowLimit);
target
+ // ensure there is some stable ordering of the contextRows, if possible order by
+ // "most recently changed" so the model gets the most up-to-date data
.concreteNonAssociationElements()
.filter(byAnnotation("cds.on.update"))
.map(CdsElement::getName)
@@ -121,49 +115,23 @@ CqnSelect buildContextQuery() {
return select;
}
+ // Builds the predict row from only the allowed columns (same set used in buildContextQuery),
+ // so draft, computed, and readonly fields are excluded by construction rather than explicit
+ // removal.
CdsData buildPredictRow(CdsData row) {
if (predictionElementNames.stream().noneMatch(c -> row.get(c) == null)) {
return null;
}
- Map predictRow = new HashMap<>(row);
- Drafts.ELEMENTS.forEach(predictRow::remove);
- for (String col : predictionElementNames) {
- predictRow.putIfAbsent(col, "[PREDICT]");
- }
+ Set allowed = new HashSet<>(contextColumns);
+ allowed.addAll(keyNames);
+ Map predictRow = new HashMap<>();
+ allowed.forEach(
+ col -> {
+ if (row.containsKey(col)) predictRow.put(col, row.get(col));
+ });
return CdsData.create(predictRow);
}
- private String computeSyntheticKey(Map row) {
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < keyNames.size(); i++) {
- if (i > 0) {
- sb.append('\0');
- }
- sb.append(keyNames.get(i));
- sb.append('\0');
- Object value = row.get(keyNames.get(i));
- if (value != null) {
- sb.append(value);
- }
- }
- return sb.toString();
- }
-
- List assembleRows(List contextRows, CdsData predictRow, CdsData currentRow) {
- List allRows = new ArrayList<>();
- if (syntheticKeyNeeded) {
- for (CdsData contextRow : contextRows) {
- contextRow.put(SYNTHETIC_KEY_COLUMN, computeSyntheticKey(contextRow));
- allRows.add(contextRow);
- }
- predictRow.put(SYNTHETIC_KEY_COLUMN, computeSyntheticKey(currentRow));
- } else {
- allRows.addAll(contextRows);
- }
- allRows.add(predictRow);
- return allRows;
- }
-
private List computePredictionElements() {
return rowType
.elements()
@@ -171,6 +139,7 @@ private List computePredictionElements() {
byAnnotation(VALUE_LIST_ANNOTATION)
.or(byAnnotation(VALUE_LIST_WITH_FIXED_VALUES_ANNOTATION)))
.filter(e -> !e.getType().isAssociation())
+ .filter(e -> !Boolean.FALSE.equals(e.getAnnotationValue(ODATA_VALUE_LIST_ANNOTATION, null)))
.map(CdsElement::getName)
.toList();
}
@@ -178,10 +147,13 @@ private List computePredictionElements() {
private List computeContextColumns() {
return rowType
.concreteNonAssociationElements()
- .filter(e -> e.getType().isSimple())
.filter(
- e -> SUPPORTED_CONTEXT_TYPES.contains(e.getType().as(CdsSimpleType.class).getType()))
+ e ->
+ e.getType() instanceof CdsSimpleType st
+ && SUPPORTED_CONTEXT_TYPES.contains(st.getType()))
.filter(e -> !Drafts.ELEMENTS.contains(e.getName()))
+ .filter(byAnnotation(COMPUTED_ANNOTATION).negate())
+ .filter(byAnnotation(READONLY_ANNOTATION).negate())
.map(CdsElement::getName)
.toList();
}
diff --git a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/api/RecommendationClient.java b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/api/RecommendationClient.java
index 8694745..1170780 100644
--- a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/api/RecommendationClient.java
+++ b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/api/RecommendationClient.java
@@ -8,5 +8,21 @@
public interface RecommendationClient {
- List predict(List rows, List predictionColumns, String indexColumn);
+ // Currently limited to a single prediction row. Multiple prediction rows may be supported in the
+ // future via a separate overload, but are ruled out at two points for now:
+ // (1) FioriRecommendationHandler bails out when the read returns more than one entity,
+ // so predictions only fire on single-entity reads.
+ // (2) FioriRecommendationHandler also rejects responses with more than one prediction back from
+ // the model, treating it as an unexpected state.
+ //
+ // @param predictionRow the single entity row to predict values for; prediction columns contain
+ // null for missing values that the model should fill
+ // @param contextRows historical rows from the same entity used as training context
+ // @param predictionColumns names of the columns the model should predict
+ // @param keyNames names of the entity's key columns, used to identify rows in the response
+ List predict(
+ CdsData predictionRow,
+ List contextRows,
+ List predictionColumns,
+ List keyNames);
}
diff --git a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/api/RecommendationClientResolver.java b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/api/RecommendationClientResolver.java
index ecc6837..fdd71c6 100644
--- a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/api/RecommendationClientResolver.java
+++ b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/api/RecommendationClientResolver.java
@@ -3,10 +3,12 @@
*/
package com.sap.cds.feature.recommendation.api;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.services.cds.RemoteService;
+// The annotation @FunctionalInterface ensures this interface has only one method, such that
+// callers can supply a custom client by providing this one method e.g. via a lambda.
@FunctionalInterface
public interface RecommendationClientResolver {
- RecommendationClient resolve(AICoreService aiCoreService);
+ RecommendationClient resolve(RemoteService aiCoreService);
}
diff --git a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/api/RptInferenceClient.java b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/api/RptInferenceClient.java
index c6e4c3a..ed7d238 100644
--- a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/api/RptInferenceClient.java
+++ b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/api/RptInferenceClient.java
@@ -12,7 +12,6 @@
import com.sap.ai.sdk.foundationmodels.rpt.generated.model.RowsInnerValue;
import com.sap.ai.sdk.foundationmodels.rpt.generated.model.TargetColumnConfig;
import com.sap.cds.CdsData;
-import com.sap.cds.services.draft.Drafts;
import com.sap.cloud.sdk.services.openapi.apache.apiclient.ApiClient;
import com.sap.cloud.sdk.services.openapi.apache.core.OpenApiRequestException;
import io.github.resilience4j.core.IntervalFunction;
@@ -21,7 +20,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,41 +31,59 @@
* Example usage:
*
*
{@code
- * AICoreService service = ...;
- * String rg = service.resourceGroup();
- * String deploymentId = service.deploymentId(rg, RptModelSpec.rpt1());
- * RptInferenceClient client = new RptInferenceClient(service.inferenceClient(rg, deploymentId));
- * List predictions = client.predict(rows, List.of("targetColumn"), "ID");
+ * RemoteService service = runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
+ * ResourceGroupContext rgCtx = ResourceGroupContext.create();
+ * service.emit(rgCtx);
+ * String rg = rgCtx.getResult();
+ * DeploymentIdContext depCtx = DeploymentIdContext.create();
+ * depCtx.setResourceGroupId(rg);
+ * depCtx.setSpec(RptModelSpec.rpt1());
+ * service.emit(depCtx);
+ * InferenceClientContext infCtx = InferenceClientContext.create();
+ * infCtx.setResourceGroupId(rg);
+ * infCtx.setDeploymentId(depCtx.getResult());
+ * service.emit(infCtx);
+ * RptInferenceClient client = new RptInferenceClient(infCtx.getResult());
+ * List predictions = client.predict(predictionRow, contextRows, List.of("targetColumn"), List.of("ID"));
* }
*/
public class RptInferenceClient implements RecommendationClient {
private static final Logger logger = LoggerFactory.getLogger(RptInferenceClient.class);
- private static final Set MANAGED_FIELDS =
- Set.of("createdBy", "modifiedBy", "createdAt", "modifiedAt");
+ // RPT-1 specific: the placeholder value that marks a column as a prediction target in the request
+ public static final String PREDICT = "[PREDICT]";
private static final Retry INFERENCE_RETRY = buildInferenceRetry();
- private final DefaultApi api;
+ private final DefaultApi rpt;
public RptInferenceClient(ApiClient apiClient) {
- this.api =
+ this.rpt =
new DefaultApi(apiClient.withObjectMapper(JacksonConfiguration.getDefaultObjectMapper()));
}
@Override
public List predict(
- List rows, List predictionColumns, String indexColumn) {
- PredictRequestPayload request = buildRequest(rows, predictionColumns, indexColumn);
+ CdsData predictionRow,
+ List contextRows,
+ List predictionColumns,
+ List keyNames) {
+ String indexColumn = resolveIndexColumn(keyNames, predictionRow);
+ CdsData preparedPredictRow = preparePredictRow(predictionRow, predictionColumns);
+ List allRows = new java.util.ArrayList<>(contextRows);
+ allRows.add(preparedPredictRow);
+ addSyntheticKeyIfNeeded(allRows, keyNames, indexColumn);
+
+ PredictRequestPayload request = buildRequest(allRows, predictionColumns, indexColumn);
logger.debug(
- "Sending prediction request for {} rows, {} target columns",
- rows.size(),
+ "Sending prediction request for one row with {} context rows, {} target columns",
+ contextRows.size(),
predictionColumns.size());
return Retry.decorateSupplier(
INFERENCE_RETRY,
() -> {
- var response = api.predict(request);
+ var response = rpt.predict(request);
logger.debug("Prediction response id: {}", response.getId());
List> raw =
JacksonConfiguration.getDefaultObjectMapper()
@@ -77,6 +93,51 @@ public List predict(
.get();
}
+ // RPT-1 specific: when the entity has a composite or non-ID key, a synthetic string index column
+ // is computed by concatenating all key fields and injected into each row before sending.
+ private static final String SYNTHETIC_INDEX_COLUMN = "SAP_RECOMMENDATIONS_ID";
+
+ // If there is one string-typed key, use it directly; for composite keys or non-string keys a
+ // synthetic string column is needed since RPT-1 requires a single string index column.
+ // Non-string single keys fall back to synthetic rather than just converting to a string.
+ // RPT-1 may reject a column declared as the index if its values are not strings.
+ private static String resolveIndexColumn(List keyNames, CdsData sampleRow) {
+ if (keyNames.size() == 1 && sampleRow.get(keyNames.get(0)) instanceof String) {
+ return keyNames.get(0);
+ }
+ return SYNTHETIC_INDEX_COLUMN;
+ }
+
+ // '\0' is used as separator because it cannot appear in database string values
+ // (VARCHAR/NVARCHAR), so concatenation of any composite key values is guaranteed collision-free.
+ static String computeSyntheticKey(Map row, List keyNames) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < keyNames.size(); i++) {
+ if (i > 0) sb.append('\0');
+ sb.append(keyNames.get(i)).append('\0');
+ Object value = row.get(keyNames.get(i));
+ if (value != null) sb.append(value);
+ }
+ return sb.toString();
+ }
+
+ private static void addSyntheticKeyIfNeeded(
+ List rows, List keyNames, String indexColumn) {
+ if (SYNTHETIC_INDEX_COLUMN.equals(indexColumn)) {
+ rows.forEach(r -> r.put(SYNTHETIC_INDEX_COLUMN, computeSyntheticKey(r, keyNames)));
+ }
+ }
+
+ // Returns a copy of the predictRow with a prediction placeholder replacing empty values
+ // in the predictionColumns - these will get filled by the predict method.
+ private static CdsData preparePredictRow(CdsData predictRow, List predictionColumns) {
+ Map preparedPredictRowMap = new HashMap<>(predictRow);
+ for (String col : predictionColumns) {
+ preparedPredictRowMap.putIfAbsent(col, PREDICT);
+ }
+ return CdsData.create(preparedPredictRowMap);
+ }
+
private static PredictRequestPayload buildRequest(
List rows, List predictionColumns, String indexColumn) {
var targetColumns =
@@ -85,31 +146,11 @@ private static PredictRequestPayload buildRequest(
col ->
TargetColumnConfig.create()
.name(col)
- .predictionPlaceholder(PredictionPlaceholder.create("[PREDICT]"))
+ .predictionPlaceholder(PredictionPlaceholder.create(PREDICT))
.taskType(TargetColumnConfig.TaskTypeEnum.CLASSIFICATION))
.toList();
- var sdkRows =
- rows.stream()
- .map(
- row -> {
- Map sdkRow = new HashMap<>();
- row.forEach(
- (k, v) -> {
- if (v != null
- && !Drafts.ELEMENTS.contains(k)
- && !MANAGED_FIELDS.contains(k)) {
- sdkRow.put(k, RowsInnerValue.create(v.toString()));
- }
- });
- for (String target : predictionColumns) {
- if (!row.containsKey(target) || row.get(target) == null) {
- sdkRow.put(target, RowsInnerValue.create("[PREDICT]"));
- }
- }
- return sdkRow;
- })
- .toList();
+ var sdkRows = rows.stream().map(row -> toSdkRow(row)).toList();
return PredictRequestPayload.create()
.predictionConfig(PredictionConfig.create().targetColumns(targetColumns))
@@ -117,6 +158,18 @@ private static PredictRequestPayload buildRequest(
.indexColumn(indexColumn);
}
+ // Converts a CdsData row to the RPT SDK row format, i.e., into Map
+ private static Map toSdkRow(CdsData row) {
+ Map sdkRow = new HashMap<>();
+ row.forEach(
+ (k, v) -> {
+ if (v != null) {
+ sdkRow.put(k, RowsInnerValue.create(v.toString()));
+ }
+ });
+ return sdkRow;
+ }
+
private static Retry buildInferenceRetry() {
RetryConfig config =
RetryConfig.custom()
diff --git a/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/FioriRecommendationHandlerTest.java b/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/FioriRecommendationHandlerTest.java
index c7f1a22..34b9972 100644
--- a/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/FioriRecommendationHandlerTest.java
+++ b/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/FioriRecommendationHandlerTest.java
@@ -13,11 +13,11 @@
import com.sap.cds.CdsData;
import com.sap.cds.Result;
import com.sap.cds.ResultBuilder;
-import com.sap.cds.feature.aicore.api.AICoreService;
import com.sap.cds.feature.recommendation.api.RecommendationClient;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.services.Service;
import com.sap.cds.services.cds.CdsReadEventContext;
+import com.sap.cds.services.cds.RemoteService;
import com.sap.cds.services.impl.utils.CdsServiceUtils;
import com.sap.cds.services.persistence.PersistenceService;
import com.sap.cds.services.request.RequestContext;
@@ -45,7 +45,7 @@ class FioriRecommendationHandlerTest {
private static PersistenceService db;
@Mock(answer = Answers.CALLS_REAL_METHODS)
- private AICoreService aiCoreService;
+ private RemoteService aiCoreService;
private FioriRecommendationHandler cut;
private RecommendationClient predictionClient;
@@ -67,7 +67,7 @@ void setup() {
reset(db);
when(db.getName()).thenReturn(PersistenceService.DEFAULT_NAME);
predictionClient = randomPickClient();
- cut = new FioriRecommendationHandler(aiCoreService, (service) -> predictionClient);
+ cut = new FioriRecommendationHandler(aiCoreService, (service) -> predictionClient, db);
}
// ── tests ──────────────────────────────────────────────────────────────────
@@ -152,7 +152,7 @@ void emptyPredictions_returnsEarlyWithoutRecommendations() {
Map row = draftRow("genre_ID", null);
CdsReadEventContext ctx = readContext("test.Books", List.of(row));
when(db.run(any(CqnSelect.class))).thenReturn(twoContextRows());
- predictionClient = (rows, cols, idx) -> List.of();
+ predictionClient = (predictionRow, contextRows, cols, keyNames) -> List.of();
cut.afterRead(ctx, dataList(row));
assertThat(row).doesNotContainKey("SAP_Recommendations");
});
@@ -166,7 +166,7 @@ void multiplePredictions_returnsEarlyWithoutRecommendations() {
CdsReadEventContext ctx = readContext("test.Books", List.of(row));
when(db.run(any(CqnSelect.class))).thenReturn(twoContextRows());
predictionClient =
- (rows, cols, idx) ->
+ (predictionRow, contextRows, cols, idx) ->
List.of(
CdsData.create(Map.of("ID", "id-1")), CdsData.create(Map.of("ID", "id-2")));
cut.afterRead(ctx, dataList(row));
@@ -203,6 +203,55 @@ void draftRow_withGenreAndCurrency_addsSapRecommendations() {
});
}
+ @Test
+ void contextQuery_excludesPredictionRowByRequiringNonNullPredictionColumns() {
+ runIn(
+ () -> {
+ Map row = draftRow("genre_ID", null);
+ CdsReadEventContext ctx = readContext("test.Books", List.of(row));
+ ArgumentCaptor selectCaptor = ArgumentCaptor.forClass(CqnSelect.class);
+ when(db.run(selectCaptor.capture())).thenReturn(twoContextRows());
+ cut.afterRead(ctx, dataList(row));
+ // The WHERE clause requires all prediction columns to be non-null, so the current row
+ // (which has genre_ID = null) is automatically excluded from the context.
+ String selectSql = selectCaptor.getAllValues().get(0).toString();
+ assertThat(selectSql).contains("\"is not\",\"null\"");
+ assertThat(selectSql).contains("genre_ID");
+ });
+ }
+
+ @Test
+ void cdsoDataValueListFalse_fieldIsExcludedFromPredictions() {
+ runIn(
+ () -> {
+ Map row = new HashMap<>();
+ row.put("ID", "a009c640-434a-4542-ac68-51b400c880ec");
+ row.put("IsActiveEntity", false);
+ row.put("genre_ID", null);
+ row.put("suppressed_ID", null);
+ CdsReadEventContext ctx = readContext("test.BooksWithDisabledValueList", List.of(row));
+ when(db.run(any(CqnSelect.class)))
+ .thenReturn(
+ ResultBuilder.selectedRows(
+ new ArrayList<>(
+ List.of(
+ new HashMap<>(
+ Map.of("ID", "x1", "genre_ID", 1, "suppressed_ID", 10)),
+ new HashMap<>(
+ Map.of("ID", "x2", "genre_ID", 2, "suppressed_ID", 20)))))
+ .result(),
+ ResultBuilder.selectedRows(List.of()).result());
+ cut.afterRead(ctx, dataList(row));
+ // genre_ID has @Common.ValueListWithFixedValues → predicted
+ // suppressed_ID has @cds.odata.valuelist: false → excluded
+ assertThat(row).containsKey("SAP_Recommendations");
+ @SuppressWarnings("unchecked")
+ Map recs = (Map) row.get("SAP_Recommendations");
+ assertThat(recs).containsKey("genre_ID");
+ assertThat(recs).doesNotContainKey("suppressed_ID");
+ });
+ }
+
@Test
void blobAndVectorFields_areExcludedFromContextSelect() {
runIn(
@@ -393,55 +442,33 @@ private static Result twoContextRows() {
private static RecommendationClient rptStyleClient() {
Random random = new Random(42);
- return (rows, predictionColumns, indexColumn) -> {
- List predictions = new ArrayList<>();
- for (CdsData row : rows) {
- if (predictionColumns.stream().noneMatch(col -> "[PREDICT]".equals(row.get(col)))) {
- continue;
- }
- Map prediction = new HashMap<>();
- for (String col : predictionColumns) {
- List available =
- rows.stream()
- .filter(r -> r.get(col) != null && !"[PREDICT]".equals(r.get(col)))
- .map(r -> r.get(col))
- .toList();
- Object val = available.isEmpty() ? null : available.get(random.nextInt(available.size()));
- prediction.put(col, List.of(Map.of("prediction", val)));
- }
- prediction.put(indexColumn, row.get(indexColumn));
- predictions.add(CdsData.create(prediction));
+ return (predictionRow, contextRows, predictionColumns, keyNames) -> {
+ Map prediction = new HashMap<>();
+ for (String col : predictionColumns) {
+ List available =
+ contextRows.stream().filter(r -> r.get(col) != null).map(r -> r.get(col)).toList();
+ Object val = available.isEmpty() ? null : available.get(random.nextInt(available.size()));
+ prediction.put(col, List.of(Map.of("prediction", val)));
}
- return predictions;
+ prediction.put(keyNames.get(0), predictionRow.get(keyNames.get(0)));
+ return List.of(CdsData.create(prediction));
};
}
private static RecommendationClient randomPickClient() {
Random random = new Random(42);
- return (rows, predictionColumns, indexColumn) -> {
- List predictions = new ArrayList<>();
- for (CdsData row : rows) {
- Map prediction = new HashMap<>();
- boolean addPrediction = false;
- for (String col : predictionColumns) {
- if ("[PREDICT]".equals(row.get(col))) {
- addPrediction = true;
- List available =
- rows.stream()
- .filter(r -> r.get(col) != null && !"[PREDICT]".equals(r.get(col)))
- .map(r -> r.get(col))
- .toList();
- Object val =
- available.isEmpty() ? null : available.get(random.nextInt(available.size()));
- prediction.put(col, List.of(Map.of("prediction", val)));
- }
- }
- if (addPrediction) {
- prediction.put(indexColumn, row.get(indexColumn));
- predictions.add(CdsData.create(prediction));
+ return (predictionRow, contextRows, predictionColumns, keyNames) -> {
+ Map prediction = new HashMap<>();
+ for (String col : predictionColumns) {
+ if (predictionRow.get(col) == null) {
+ List available =
+ contextRows.stream().filter(r -> r.get(col) != null).map(r -> r.get(col)).toList();
+ Object val = available.isEmpty() ? null : available.get(random.nextInt(available.size()));
+ prediction.put(col, List.of(Map.of("prediction", val)));
}
}
- return predictions;
+ prediction.put(keyNames.get(0), predictionRow.get(keyNames.get(0)));
+ return List.of(CdsData.create(prediction));
};
}
}
diff --git a/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/RecommendationConfigurationTest.java b/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/RecommendationConfigurationTest.java
index 69b5062..4ab68d8 100644
--- a/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/RecommendationConfigurationTest.java
+++ b/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/RecommendationConfigurationTest.java
@@ -5,9 +5,11 @@
import static org.mockito.Mockito.*;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
import com.sap.cds.services.ServiceCatalog;
+import com.sap.cds.services.cds.RemoteService;
import com.sap.cds.services.environment.CdsEnvironment;
+import com.sap.cds.services.persistence.PersistenceService;
import com.sap.cds.services.runtime.CdsRuntime;
import com.sap.cds.services.runtime.CdsRuntimeConfigurer;
import java.util.stream.Stream;
@@ -23,7 +25,8 @@ class RecommendationConfigurationTest {
@Mock private CdsRuntime runtime;
@Mock private ServiceCatalog serviceCatalog;
@Mock private CdsEnvironment environment;
- @Mock private AICoreService aiCoreService;
+ @Mock private RemoteService aiCoreService;
+ @Mock private PersistenceService persistenceService;
@Test
void aiCoreServiceFound_registersHandler() {
@@ -31,8 +34,10 @@ void aiCoreServiceFound_registersHandler() {
when(runtime.getServiceCatalog()).thenReturn(serviceCatalog);
when(runtime.getEnvironment()).thenReturn(environment);
when(environment.getServiceBindings()).thenReturn(Stream.empty());
- when(serviceCatalog.getService(AICoreService.class, AICoreService.DEFAULT_NAME))
+ when(serviceCatalog.getService(RemoteService.class, AICore.SERVICE_NAME))
.thenReturn(aiCoreService);
+ when(serviceCatalog.getService(PersistenceService.class, PersistenceService.DEFAULT_NAME))
+ .thenReturn(persistenceService);
new RecommendationConfiguration().eventHandlers(configurer);
@@ -43,8 +48,7 @@ void aiCoreServiceFound_registersHandler() {
void aiCoreServiceNull_doesNotRegisterHandler() {
when(configurer.getCdsRuntime()).thenReturn(runtime);
when(runtime.getServiceCatalog()).thenReturn(serviceCatalog);
- when(serviceCatalog.getService(AICoreService.class, AICoreService.DEFAULT_NAME))
- .thenReturn(null);
+ when(serviceCatalog.getService(RemoteService.class, AICore.SERVICE_NAME)).thenReturn(null);
new RecommendationConfiguration().eventHandlers(configurer);
diff --git a/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/api/RptInferenceClientTest.java b/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/api/RptInferenceClientTest.java
new file mode 100644
index 0000000..5784e21
--- /dev/null
+++ b/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/api/RptInferenceClientTest.java
@@ -0,0 +1,83 @@
+/*
+ * © 2026 SAP SE or an SAP affiliate company and cds-ai contributors.
+ */
+package com.sap.cds.feature.recommendation.api;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.sap.cds.CdsData;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class RptInferenceClientTest {
+
+ private static String resolveIndexColumn(List keyNames, CdsData sampleRow)
+ throws Exception {
+ Method m =
+ RptInferenceClient.class.getDeclaredMethod("resolveIndexColumn", List.class, CdsData.class);
+ m.setAccessible(true);
+ return (String) m.invoke(null, keyNames, sampleRow);
+ }
+
+ @Test
+ void resolveIndexColumn_singleStringKey_usesItDirectly() throws Exception {
+ CdsData row = CdsData.create(Map.of("isbn", "978-3-16"));
+ assertThat(resolveIndexColumn(List.of("isbn"), row)).isEqualTo("isbn");
+ }
+
+ @Test
+ void resolveIndexColumn_singleUuidKey_usesItDirectly() throws Exception {
+ CdsData row = CdsData.create(Map.of("ID", "a009c640-434a-4542-ac68-51b400c880ec"));
+ assertThat(resolveIndexColumn(List.of("ID"), row)).isEqualTo("ID");
+ }
+
+ @Test
+ void resolveIndexColumn_singleIntegerKey_usesSyntheticColumn() throws Exception {
+ CdsData row = CdsData.create(Map.of("order_ID", 42));
+ assertThat(resolveIndexColumn(List.of("order_ID"), row)).isEqualTo("SAP_RECOMMENDATIONS_ID");
+ }
+
+ @Test
+ void resolveIndexColumn_compositeKey_usesSyntheticColumn() throws Exception {
+ CdsData row = CdsData.create(Map.of("order_ID", 1, "item_no", 10));
+ assertThat(resolveIndexColumn(List.of("order_ID", "item_no"), row))
+ .isEqualTo("SAP_RECOMMENDATIONS_ID");
+ }
+
+ @Test
+ void computeSyntheticKey_singleKey() {
+ String key = RptInferenceClient.computeSyntheticKey(Map.of("ID", "abc"), List.of("ID"));
+ assertThat(key).isEqualTo("ID" + '\0' + "abc");
+ }
+
+ @Test
+ void computeSyntheticKey_compositeKey() {
+ String key =
+ RptInferenceClient.computeSyntheticKey(
+ Map.of("order_ID", 1, "item_no", 10), List.of("order_ID", "item_no"));
+ assertThat(key).isEqualTo("order_ID" + '\0' + "1" + '\0' + "item_no" + '\0' + "10");
+ }
+
+ @Test
+ void computeSyntheticKey_noCollision_betweenDifferentCompositions() {
+ // "1" + "0" must not produce the same key as "10" + ""
+ String key1 =
+ RptInferenceClient.computeSyntheticKey(
+ Map.of("order_ID", "1", "item_no", "0"), List.of("order_ID", "item_no"));
+ String key2 =
+ RptInferenceClient.computeSyntheticKey(
+ Map.of("order_ID", "10", "item_no", ""), List.of("order_ID", "item_no"));
+ assertThat(key1).isNotEqualTo(key2);
+ }
+
+ @Test
+ void computeSyntheticKey_nullValue_doesNotCrash() {
+ Map row = new java.util.HashMap<>();
+ row.put("order_ID", 1);
+ row.put("item_no", null);
+ String key = RptInferenceClient.computeSyntheticKey(row, List.of("order_ID", "item_no"));
+ assertThat(key).isEqualTo("order_ID" + '\0' + "1" + '\0' + "item_no" + '\0');
+ }
+}
diff --git a/cds-feature-recommendations/src/test/resources/model/recommendations-test.cds b/cds-feature-recommendations/src/test/resources/model/recommendations-test.cds
index bfcd611..5c30916 100644
--- a/cds-feature-recommendations/src/test/resources/model/recommendations-test.cds
+++ b/cds-feature-recommendations/src/test/resources/model/recommendations-test.cds
@@ -1,5 +1,7 @@
namespace test;
+// genre_ID and currency_code are declared as plain scalars here because this is a test model.
+// In a real CDS model these would be generated foreign key columns from annotated associations.
@odata.draft.enabled
entity Books {
key ID : UUID;
@@ -46,6 +48,16 @@ entity PlainEntity {
title : String;
}
+@odata.draft.enabled
+entity BooksWithDisabledValueList {
+ key ID : UUID;
+ @Common.ValueListWithFixedValues
+ genre_ID : Integer;
+ @Common.ValueListWithFixedValues
+ @cds.odata.valuelist: false
+ suppressed_ID : Integer;
+}
+
service TestService {
entity Books as projection on test.Books;
entity Genres as projection on test.Genres;
@@ -53,4 +65,5 @@ service TestService {
entity OrderItems as projection on test.OrderItems;
entity IsbnBooks as projection on test.IsbnBooks;
entity PlainEntity as projection on test.PlainEntity;
+ entity BooksWithDisabledValueList as projection on test.BooksWithDisabledValueList;
}
diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/MtxLifecycleTest.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/MtxLifecycleTest.java
index 6e88c40..1ad8aa5 100644
--- a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/MtxLifecycleTest.java
+++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/MtxLifecycleTest.java
@@ -7,8 +7,10 @@
import static org.assertj.core.api.Assertions.assertThatCode;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
+import com.sap.cds.feature.aicore.api.ResourceGroupContext;
import com.sap.cds.feature.aicore.itest.mt.utils.SubscriptionEndpointClient;
+import com.sap.cds.services.cds.RemoteService;
import com.sap.cds.services.runtime.CdsRuntime;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -57,19 +59,22 @@ void unsubscribe_isIdempotent() throws Exception {
@Test
void subscribeUnsubscribe_repeatedTwice_completesCleanly() throws Exception {
- AICoreService service = getService();
+ RemoteService service = getService();
for (int i = 0; i < 2; i++) {
subscriptionEndpointClient.subscribeTenant(TENANT);
// After subscribe, the service should resolve a resource group for this tenant
- String rg = service.resourceGroupForTenant(TENANT);
+ ResourceGroupContext rgCtx = ResourceGroupContext.create();
+ rgCtx.setTenantId(TENANT);
+ service.emit(rgCtx);
+ String rg = rgCtx.getResult();
assertThat(rg).isNotNull().isNotBlank();
subscriptionEndpointClient.unsubscribeTenant(TENANT);
}
}
- private AICoreService getService() {
- return runtime.getServiceCatalog().getService(AICoreService.class, AICoreService.DEFAULT_NAME);
+ private RemoteService getService() {
+ return runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
}
}
diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/SubscribeUnsubscribeTest.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/SubscribeUnsubscribeTest.java
index eb1b9d2..fa53769 100644
--- a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/SubscribeUnsubscribeTest.java
+++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/SubscribeUnsubscribeTest.java
@@ -9,8 +9,10 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
+import com.sap.cds.feature.aicore.api.ResourceGroupContext;
import com.sap.cds.feature.aicore.itest.mt.utils.SubscriptionEndpointClient;
+import com.sap.cds.services.cds.RemoteService;
import com.sap.cds.services.runtime.CdsRuntime;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -50,12 +52,15 @@ void subscribeTenant_thenServiceIsReachable() throws Exception {
@Test
void subscribeTenant_createsResourceGroup() throws Exception {
- AICoreService service = getService();
+ RemoteService service = getService();
subscriptionEndpointClient.subscribeTenant("tenant-3");
// After subscription, the service should be able to resolve a resource group for the tenant
- String resourceGroup = service.resourceGroupForTenant("tenant-3");
+ ResourceGroupContext rgCtx = ResourceGroupContext.create();
+ rgCtx.setTenantId("tenant-3");
+ service.emit(rgCtx);
+ String resourceGroup = rgCtx.getResult();
assertThat(resourceGroup).isNotNull().isNotBlank();
}
@@ -82,7 +87,7 @@ void tearDown() {
}
}
- private AICoreService getService() {
- return runtime.getServiceCatalog().getService(AICoreService.class, AICoreService.DEFAULT_NAME);
+ private RemoteService getService() {
+ return runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
}
}
diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/TenantIsolationTest.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/TenantIsolationTest.java
index 7aa06b8..5e44cfc 100644
--- a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/TenantIsolationTest.java
+++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/TenantIsolationTest.java
@@ -6,9 +6,11 @@
import static org.assertj.core.api.Assertions.assertThat;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
+import com.sap.cds.feature.aicore.api.ResourceGroupContext;
import com.sap.cds.feature.aicore.core.AICoreConfig;
import com.sap.cds.feature.aicore.itest.mt.utils.SubscriptionEndpointClient;
+import com.sap.cds.services.cds.RemoteService;
import com.sap.cds.services.environment.CdsProperties;
import com.sap.cds.services.runtime.CdsRuntime;
import org.junit.jupiter.api.AfterEach;
@@ -44,13 +46,20 @@ void multiTenancyEnabled() {
@Test
void differentTenants_getDifferentResourceGroups() throws Exception {
- AICoreService service = getService();
+ RemoteService service = getService();
subscriptionEndpointClient.subscribeTenant("tenant-1");
subscriptionEndpointClient.subscribeTenant("tenant-2");
- String rg1 = service.resourceGroupForTenant("tenant-1");
- String rg2 = service.resourceGroupForTenant("tenant-2");
+ ResourceGroupContext rgCtx1 = ResourceGroupContext.create();
+ rgCtx1.setTenantId("tenant-1");
+ service.emit(rgCtx1);
+ String rg1 = rgCtx1.getResult();
+
+ ResourceGroupContext rgCtx2 = ResourceGroupContext.create();
+ rgCtx2.setTenantId("tenant-2");
+ service.emit(rgCtx2);
+ String rg2 = rgCtx2.getResult();
assertThat(rg1).isNotNull();
assertThat(rg2).isNotNull();
@@ -60,16 +69,19 @@ void differentTenants_getDifferentResourceGroups() throws Exception {
@Test
void resourceGroupPrefix_applied() throws Exception {
AICoreConfig config = getConfig();
- AICoreService service = getService();
+ RemoteService service = getService();
subscriptionEndpointClient.subscribeTenant("tenant-1");
- String rg = service.resourceGroupForTenant("tenant-1");
+ ResourceGroupContext rgCtx = ResourceGroupContext.create();
+ rgCtx.setTenantId("tenant-1");
+ service.emit(rgCtx);
+ String rg = rgCtx.getResult();
assertThat(rg).startsWith(config.resourceGroupPrefix());
}
- private AICoreService getService() {
- return runtime.getServiceCatalog().getService(AICoreService.class, AICoreService.DEFAULT_NAME);
+ private RemoteService getService() {
+ return runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
}
private AICoreConfig getConfig() {
diff --git a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/AICoreServiceTest.java b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/AICoreServiceTest.java
index ce0c89b..795d6f8 100644
--- a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/AICoreServiceTest.java
+++ b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/AICoreServiceTest.java
@@ -5,9 +5,11 @@
import static org.assertj.core.api.Assertions.assertThat;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.DeploymentIdContext;
+import com.sap.cds.feature.aicore.api.ResourceGroupContext;
import com.sap.cds.feature.aicore.core.AICoreConfig;
import com.sap.cds.feature.recommendation.api.RptModelSpec;
+import com.sap.cds.services.cds.RemoteService;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
@@ -23,15 +25,18 @@ void prepareDeployment() {
@Test
void service_isRegistered() {
assertThat(getAICoreService()).isNotNull();
- assertThat(getAICoreService()).isInstanceOf(AICoreService.class);
+ assertThat(getAICoreService()).isInstanceOf(RemoteService.class);
}
@Test
void resourceGroupForTenant_singleTenancy_returnsDefault() {
AICoreConfig config = getAICoreConfig();
- AICoreService service = getAICoreService();
+ RemoteService service = getAICoreService();
if (!config.multiTenancyEnabled()) {
- String result = service.resourceGroupForTenant("any-tenant");
+ ResourceGroupContext rgCtx = ResourceGroupContext.create();
+ rgCtx.setTenantId("any-tenant");
+ service.emit(rgCtx);
+ String result = rgCtx.getResult();
assertThat(result).isEqualTo(config.defaultResourceGroup());
}
}
@@ -39,29 +44,43 @@ void resourceGroupForTenant_singleTenancy_returnsDefault() {
@Test
void resourceGroupForTenant_multiTenancy_createsOrFindsGroup() {
AICoreConfig config = getAICoreConfig();
- AICoreService service = getAICoreService();
+ RemoteService service = getAICoreService();
if (config.multiTenancyEnabled()) {
String tenantId = "itest-svc-tenant-" + System.currentTimeMillis();
- String resourceGroupId = service.resourceGroupForTenant(tenantId);
+ ResourceGroupContext rgCtx = ResourceGroupContext.create();
+ rgCtx.setTenantId(tenantId);
+ service.emit(rgCtx);
+ String resourceGroupId = rgCtx.getResult();
assertThat(resourceGroupId).startsWith(config.resourceGroupPrefix());
assertThat(resourceGroupId).contains(tenantId);
// Second call should return cached value
- String cached = service.resourceGroupForTenant(tenantId);
+ ResourceGroupContext rgCtx2 = ResourceGroupContext.create();
+ rgCtx2.setTenantId(tenantId);
+ service.emit(rgCtx2);
+ String cached = rgCtx2.getResult();
assertThat(cached).isEqualTo(resourceGroupId);
}
}
@Test
void deploymentId_returnsDeploymentId() {
- AICoreService service = getAICoreService();
+ RemoteService service = getAICoreService();
String resourceGroup = getAICoreConfig().defaultResourceGroup();
- String deploymentId = service.deploymentId(resourceGroup, RptModelSpec.rpt1());
+ DeploymentIdContext depCtx = DeploymentIdContext.create();
+ depCtx.setResourceGroupId(resourceGroup);
+ depCtx.setSpec(RptModelSpec.rpt1());
+ service.emit(depCtx);
+ String deploymentId = depCtx.getResult();
assertThat(deploymentId).isNotNull().isNotBlank();
// Second call should use cache
- String cached = service.deploymentId(resourceGroup, RptModelSpec.rpt1());
+ DeploymentIdContext depCtx2 = DeploymentIdContext.create();
+ depCtx2.setResourceGroupId(resourceGroup);
+ depCtx2.setSpec(RptModelSpec.rpt1());
+ service.emit(depCtx2);
+ String cached = depCtx2.getResult();
assertThat(cached).isEqualTo(deploymentId);
}
diff --git a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ActionTest.java b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ActionTest.java
index 0e7bf34..39f430e 100644
--- a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ActionTest.java
+++ b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ActionTest.java
@@ -9,12 +9,13 @@
import com.sap.cds.Result;
import com.sap.cds.Row;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.DeploymentIdContext;
+import com.sap.cds.feature.aicore.api.ResourceGroupContext;
import com.sap.cds.feature.aicore.core.AICoreConfig;
import com.sap.cds.feature.recommendation.api.RptModelSpec;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.Update;
-import com.sap.cds.services.cds.CqnService;
+import com.sap.cds.services.cds.RemoteService;
import java.util.Map;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
@@ -34,39 +35,58 @@ void ensureResourceGroupReady() {
@Test
void resourceGroupForTenant_singleTenancy_returnsDefault() {
AICoreConfig config = getAICoreConfig();
- AICoreService service = getAICoreService();
+ RemoteService service = getAICoreService();
assumeFalse(config.multiTenancyEnabled(), "Multi-tenancy is enabled");
- String result = service.resourceGroupForTenant("any-tenant-id");
+ ResourceGroupContext rgCtx = ResourceGroupContext.create();
+ rgCtx.setTenantId("any-tenant-id");
+ service.emit(rgCtx);
+ String result = rgCtx.getResult();
assertThat(result).isEqualTo(config.defaultResourceGroup());
}
@Test
void resourceGroupForTenant_multiTenancy_createsGroup() {
AICoreConfig config = getAICoreConfig();
- AICoreService service = getAICoreService();
+ RemoteService service = getAICoreService();
assumeTrue(config.multiTenancyEnabled(), "Multi-tenancy is not enabled");
String tenantId = "itest-action-tenant-" + System.currentTimeMillis();
- String resourceGroupId = service.resourceGroupForTenant(tenantId);
+ ResourceGroupContext rgCtx = ResourceGroupContext.create();
+ rgCtx.setTenantId(tenantId);
+ service.emit(rgCtx);
+ String resourceGroupId = rgCtx.getResult();
assertThat(resourceGroupId).startsWith(config.resourceGroupPrefix());
assertThat(resourceGroupId).contains(tenantId);
}
@Test
void deploymentId_returnsValidDeployment() {
- AICoreService service = getAICoreService();
+ RemoteService service = getAICoreService();
String resourceGroup = getAICoreConfig().defaultResourceGroup();
- String deploymentId = service.deploymentId(resourceGroup, RptModelSpec.rpt1());
+ DeploymentIdContext depCtx = DeploymentIdContext.create();
+ depCtx.setResourceGroupId(resourceGroup);
+ depCtx.setSpec(RptModelSpec.rpt1());
+ service.emit(depCtx);
+ String deploymentId = depCtx.getResult();
assertThat(deploymentId).isNotNull().isNotBlank();
}
@Test
void deploymentId_cachedOnSecondCall() {
- AICoreService service = getAICoreService();
+ RemoteService service = getAICoreService();
String resourceGroup = getAICoreConfig().defaultResourceGroup();
- String first = service.deploymentId(resourceGroup, RptModelSpec.rpt1());
- String second = service.deploymentId(resourceGroup, RptModelSpec.rpt1());
+ DeploymentIdContext depCtx1 = DeploymentIdContext.create();
+ depCtx1.setResourceGroupId(resourceGroup);
+ depCtx1.setSpec(RptModelSpec.rpt1());
+ service.emit(depCtx1);
+ String first = depCtx1.getResult();
+
+ DeploymentIdContext depCtx2 = DeploymentIdContext.create();
+ depCtx2.setResourceGroupId(resourceGroup);
+ depCtx2.setSpec(RptModelSpec.rpt1());
+ service.emit(depCtx2);
+ String second = depCtx2.getResult();
assertThat(second).isEqualTo(first);
}
@@ -75,7 +95,7 @@ void deploymentId_cachedOnSecondCall() {
+ "re-enable once test creates its own isolated deployment")
@Test
void stop_deployment_changesTargetStatus() {
- CqnService service = getAICoreCqnService();
+ RemoteService service = getAICoreCqnService();
String resourceGroup = getAICoreConfig().defaultResourceGroup();
Result deployments =
diff --git a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/BaseIntegrationTest.java b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/BaseIntegrationTest.java
index 06a09af..38dd0ca 100644
--- a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/BaseIntegrationTest.java
+++ b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/BaseIntegrationTest.java
@@ -5,12 +5,13 @@
import com.sap.cds.Result;
import com.sap.cds.Row;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
+import com.sap.cds.feature.aicore.api.DeploymentIdContext;
import com.sap.cds.feature.aicore.core.AICoreConfig;
import com.sap.cds.feature.recommendation.api.RptModelSpec;
import com.sap.cds.ql.Insert;
import com.sap.cds.ql.Select;
-import com.sap.cds.services.cds.CqnService;
+import com.sap.cds.services.cds.RemoteService;
import com.sap.cds.services.environment.CdsProperties;
import com.sap.cds.services.runtime.CdsRuntime;
import java.util.List;
@@ -39,8 +40,8 @@ public abstract class BaseIntegrationTest {
@Autowired protected CdsRuntime runtime;
- protected AICoreService getAICoreService() {
- return runtime.getServiceCatalog().getService(AICoreService.class, AICoreService.DEFAULT_NAME);
+ protected RemoteService getAICoreService() {
+ return runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
}
protected AICoreConfig getAICoreConfig() {
@@ -50,8 +51,8 @@ protected AICoreConfig getAICoreConfig() {
return AICoreConfig.from(runtime.getEnvironment(), mt);
}
- protected CqnService getAICoreCqnService() {
- return (CqnService) getAICoreService();
+ protected RemoteService getAICoreCqnService() {
+ return getAICoreService();
}
protected String ensureRptDeploymentReady() {
@@ -60,11 +61,16 @@ protected String ensureRptDeploymentReady() {
resourceGroup,
rg -> {
ensureResourceGroupProvisioned(getAICoreCqnService(), rg);
- return getAICoreService().deploymentId(rg, RptModelSpec.rpt1());
+ RemoteService service = getAICoreService();
+ DeploymentIdContext depCtx = DeploymentIdContext.create();
+ depCtx.setResourceGroupId(rg);
+ depCtx.setSpec(RptModelSpec.rpt1());
+ service.emit(depCtx);
+ return depCtx.getResult();
});
}
- protected void ensureResourceGroupProvisioned(CqnService service, String resourceGroup) {
+ protected void ensureResourceGroupProvisioned(RemoteService service, String resourceGroup) {
if (!resourceGroupExists(service, resourceGroup)) {
logger.info("Creating resource group {} with itest owner label", resourceGroup);
service.run(
@@ -79,7 +85,7 @@ protected void ensureResourceGroupProvisioned(CqnService service, String resourc
waitForResourceGroupProvisioned(service, resourceGroup);
}
- private boolean resourceGroupExists(CqnService service, String resourceGroup) {
+ private boolean resourceGroupExists(RemoteService service, String resourceGroup) {
Result all = service.run(Select.from("AICore.resourceGroups"));
for (Row row : all) {
if (resourceGroup.equals(row.get("resourceGroupId"))) {
@@ -89,7 +95,7 @@ private boolean resourceGroupExists(CqnService service, String resourceGroup) {
return false;
}
- private void waitForResourceGroupProvisioned(CqnService service, String resourceGroup) {
+ private void waitForResourceGroupProvisioned(RemoteService service, String resourceGroup) {
for (int i = 0; i < 30; i++) {
Result all = service.run(Select.from("AICore.resourceGroups"));
for (Row row : all) {
diff --git a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ConfigurationTest.java b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ConfigurationTest.java
index 2b1e5b5..9b7d1af 100644
--- a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ConfigurationTest.java
+++ b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ConfigurationTest.java
@@ -9,7 +9,7 @@
import com.sap.cds.Row;
import com.sap.cds.ql.Insert;
import com.sap.cds.ql.Select;
-import com.sap.cds.services.cds.CqnService;
+import com.sap.cds.services.cds.RemoteService;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
@@ -18,7 +18,7 @@ class ConfigurationTest extends BaseIntegrationTest {
@Test
void readAll_returnsConfigurations() {
- CqnService service = getAICoreCqnService();
+ RemoteService service = getAICoreCqnService();
String resourceGroup = getAICoreConfig().defaultResourceGroup();
Result result =
service.run(
@@ -30,7 +30,7 @@ void readAll_returnsConfigurations() {
@Test
void readAll_filterByScenario() {
- CqnService service = getAICoreCqnService();
+ RemoteService service = getAICoreCqnService();
String resourceGroup = getAICoreConfig().defaultResourceGroup();
Result result =
service.run(
@@ -46,7 +46,7 @@ void readAll_filterByScenario() {
@Test
void create_andReadById() {
- CqnService service = getAICoreCqnService();
+ RemoteService service = getAICoreCqnService();
String resourceGroup = getAICoreConfig().defaultResourceGroup();
String configName = "itest-config-" + System.currentTimeMillis();
@@ -91,7 +91,7 @@ void create_andReadById() {
@Test
void create_withParameterBindings_mapsCorrectly() {
- CqnService service = getAICoreCqnService();
+ RemoteService service = getAICoreCqnService();
String resourceGroup = getAICoreConfig().defaultResourceGroup();
String configName = "itest-params-" + System.currentTimeMillis();
diff --git a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/DeploymentTest.java b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/DeploymentTest.java
index d73fd50..c1111dc 100644
--- a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/DeploymentTest.java
+++ b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/DeploymentTest.java
@@ -10,7 +10,7 @@
import com.sap.cds.Row;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.Update;
-import com.sap.cds.services.cds.CqnService;
+import com.sap.cds.services.cds.RemoteService;
import java.util.Map;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -19,7 +19,7 @@ class DeploymentTest extends BaseIntegrationTest {
@Test
void readAll_returnsDeployments() {
- CqnService service = getAICoreCqnService();
+ RemoteService service = getAICoreCqnService();
String resourceGroup = getAICoreConfig().defaultResourceGroup();
Result result =
service.run(
@@ -31,7 +31,7 @@ void readAll_returnsDeployments() {
@Test
void readSingle_returnsDeploymentDetails() {
- CqnService service = getAICoreCqnService();
+ RemoteService service = getAICoreCqnService();
String resourceGroup = getAICoreConfig().defaultResourceGroup();
Result all =
service.run(
@@ -62,7 +62,7 @@ void readSingle_returnsDeploymentDetails() {
+ "re-enable once test creates its own isolated deployment")
@Test
void update_targetStatus_stopsRunningDeployment() {
- CqnService service = getAICoreCqnService();
+ RemoteService service = getAICoreCqnService();
String resourceGroup = getAICoreConfig().defaultResourceGroup();
Result deployments =
diff --git a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/MultiTenancyTest.java b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/MultiTenancyTest.java
index 2716d06..9d88af9 100644
--- a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/MultiTenancyTest.java
+++ b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/MultiTenancyTest.java
@@ -7,8 +7,9 @@
import static org.junit.jupiter.api.Assumptions.assumeFalse;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.ResourceGroupContext;
import com.sap.cds.feature.aicore.core.AICoreConfig;
+import com.sap.cds.services.cds.RemoteService;
import org.junit.jupiter.api.Test;
class MultiTenancyTest extends BaseIntegrationTest {
@@ -16,13 +17,20 @@ class MultiTenancyTest extends BaseIntegrationTest {
@Test
void differentTenants_getDifferentResourceGroups() {
AICoreConfig config = getAICoreConfig();
- AICoreService service = getAICoreService();
+ RemoteService service = getAICoreService();
assumeTrue(config.multiTenancyEnabled(), "Multi-tenancy is not enabled");
String tenantA = "itest-mt-a-" + System.currentTimeMillis();
String tenantB = "itest-mt-b-" + System.currentTimeMillis();
- String rgA = service.resourceGroupForTenant(tenantA);
- String rgB = service.resourceGroupForTenant(tenantB);
+ ResourceGroupContext rgCtxA = ResourceGroupContext.create();
+ rgCtxA.setTenantId(tenantA);
+ service.emit(rgCtxA);
+ String rgA = rgCtxA.getResult();
+
+ ResourceGroupContext rgCtxB = ResourceGroupContext.create();
+ rgCtxB.setTenantId(tenantB);
+ service.emit(rgCtxB);
+ String rgB = rgCtxB.getResult();
assertThat(rgA).isNotEqualTo(rgB);
assertThat(rgA).contains(tenantA);
@@ -32,21 +40,32 @@ void differentTenants_getDifferentResourceGroups() {
@Test
void resourceGroupPrefix_appliedCorrectly() {
AICoreConfig config = getAICoreConfig();
- AICoreService service = getAICoreService();
+ RemoteService service = getAICoreService();
assumeTrue(config.multiTenancyEnabled(), "Multi-tenancy is not enabled");
String tenantA = "itest-prefix-" + System.currentTimeMillis();
- String rg = service.resourceGroupForTenant(tenantA);
+ ResourceGroupContext rgCtx = ResourceGroupContext.create();
+ rgCtx.setTenantId(tenantA);
+ service.emit(rgCtx);
+ String rg = rgCtx.getResult();
assertThat(rg).startsWith(config.resourceGroupPrefix());
}
@Test
void singleTenancy_alwaysReturnsDefault() {
AICoreConfig config = getAICoreConfig();
- AICoreService service = getAICoreService();
+ RemoteService service = getAICoreService();
assumeFalse(config.multiTenancyEnabled(), "Multi-tenancy is enabled");
- String rg1 = service.resourceGroupForTenant("tenant-x");
- String rg2 = service.resourceGroupForTenant("tenant-y");
+
+ ResourceGroupContext rgCtx1 = ResourceGroupContext.create();
+ rgCtx1.setTenantId("tenant-x");
+ service.emit(rgCtx1);
+ String rg1 = rgCtx1.getResult();
+
+ ResourceGroupContext rgCtx2 = ResourceGroupContext.create();
+ rgCtx2.setTenantId("tenant-y");
+ service.emit(rgCtx2);
+ String rg2 = rgCtx2.getResult();
assertThat(rg1).isEqualTo(config.defaultResourceGroup());
assertThat(rg2).isEqualTo(config.defaultResourceGroup());
diff --git a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ResourceGroupTest.java b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ResourceGroupTest.java
index e6ca394..6c00532 100644
--- a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ResourceGroupTest.java
+++ b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ResourceGroupTest.java
@@ -11,7 +11,7 @@
import com.sap.cds.ql.Delete;
import com.sap.cds.ql.Insert;
import com.sap.cds.ql.Select;
-import com.sap.cds.services.cds.CqnService;
+import com.sap.cds.services.cds.RemoteService;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.AfterEach;
@@ -30,7 +30,7 @@ class ResourceGroupTest extends BaseIntegrationTest {
void cleanup() {
if (createdResourceGroupId != null) {
try {
- CqnService service = getAICoreCqnService();
+ RemoteService service = getAICoreCqnService();
waitForResourceGroupProvisioned(service, createdResourceGroupId);
service.run(
Delete.from("AICore.resourceGroups")
@@ -44,7 +44,7 @@ void cleanup() {
@Test
void create_andRead_resourceGroup() {
createdResourceGroupId = TEST_RG_PREFIX + System.currentTimeMillis();
- CqnService service = getAICoreCqnService();
+ RemoteService service = getAICoreCqnService();
service.run(
Insert.into("AICore.resourceGroups")
@@ -65,7 +65,7 @@ void create_andRead_resourceGroup() {
void create_withTenantLabel_andFilterByTenant() {
String tenantId = "itest-tenant-" + System.currentTimeMillis();
createdResourceGroupId = TEST_RG_PREFIX + tenantId;
- CqnService service = getAICoreCqnService();
+ RemoteService service = getAICoreCqnService();
service.run(
Insert.into("AICore.resourceGroups")
@@ -82,7 +82,7 @@ void create_withTenantLabel_andFilterByTenant() {
@Test
void readAll_returnsResourceGroups() {
- CqnService service = getAICoreCqnService();
+ RemoteService service = getAICoreCqnService();
Result result = service.run(Select.from("AICore.resourceGroups"));
assertThat(result.list()).isNotNull();
}
@@ -90,7 +90,7 @@ void readAll_returnsResourceGroups() {
@Test
void create_withLabels() {
createdResourceGroupId = TEST_RG_PREFIX + "labels-" + System.currentTimeMillis();
- CqnService service = getAICoreCqnService();
+ RemoteService service = getAICoreCqnService();
service.run(
Insert.into("AICore.resourceGroups")
@@ -119,7 +119,7 @@ void create_withLabels() {
@Test
void delete_resourceGroup() throws InterruptedException {
String rgId = TEST_RG_PREFIX + "del-" + System.currentTimeMillis();
- CqnService service = getAICoreCqnService();
+ RemoteService service = getAICoreCqnService();
service.run(Insert.into("AICore.resourceGroups").entry(Map.of("resourceGroupId", rgId)));
@@ -135,7 +135,7 @@ void delete_resourceGroup() throws InterruptedException {
createdResourceGroupId = null; // already deleted
}
- private void waitForResourceGroupProvisioned(CqnService service, String rgId)
+ private void waitForResourceGroupProvisioned(RemoteService service, String rgId)
throws InterruptedException {
for (int i = 0; i < 30; i++) {
Result result =
diff --git a/samples/bookshop/.cdsrc.json b/samples/bookshop/.cdsrc.json
index 94e9eda..f419d38 100644
--- a/samples/bookshop/.cdsrc.json
+++ b/samples/bookshop/.cdsrc.json
@@ -2,13 +2,13 @@
"requires": {
"db": "hana",
"AICore": {
- "model": "com.sap.cds/ai"
+ "model": "@cap-js/ai/srv/AICoreService"
},
"[production]": {
"auth": "xsuaa"
}
},
"cdsc": {
- "moduleLookupDirectories": ["node_modules/", "target/cds/"]
+ "moduleLookupDirectories": ["node_modules/"]
}
}
diff --git a/samples/bookshop/srv/ai-core-service.cds b/samples/bookshop/srv/ai-core-service.cds
index 7124cc4..1fc24c8 100644
--- a/samples/bookshop/srv/ai-core-service.cds
+++ b/samples/bookshop/srv/ai-core-service.cds
@@ -1,4 +1,4 @@
-using { AICore } from 'com.sap.cds/ai';
+using { AICore } from '@cap-js/ai/srv/AICoreService';
service AICoreShowcaseService @(requires: 'any') {
diff --git a/samples/bookshop/srv/src/main/java/customer/bookshop/handlers/AICoreShowcaseHandler.java b/samples/bookshop/srv/src/main/java/customer/bookshop/handlers/AICoreShowcaseHandler.java
index ebaa19f..d2cfaf6 100644
--- a/samples/bookshop/srv/src/main/java/customer/bookshop/handlers/AICoreShowcaseHandler.java
+++ b/samples/bookshop/srv/src/main/java/customer/bookshop/handlers/AICoreShowcaseHandler.java
@@ -2,7 +2,10 @@
import com.sap.cds.CdsData;
import com.sap.cds.Result;
-import com.sap.cds.feature.aicore.api.AICoreService;
+import com.sap.cds.feature.aicore.api.AICore;
+import com.sap.cds.feature.aicore.api.DeploymentIdContext;
+import com.sap.cds.feature.aicore.api.InferenceClientContext;
+import com.sap.cds.feature.aicore.api.ResourceGroupContext;
import com.sap.cds.feature.recommendation.api.RptInferenceClient;
import com.sap.cds.feature.recommendation.api.RptModelSpec;
import com.sap.cds.ql.Insert;
@@ -11,6 +14,7 @@
import com.sap.cds.services.EventContext;
import com.sap.cds.services.cds.CdsReadEventContext;
import com.sap.cds.services.cds.CqnService;
+import com.sap.cds.services.cds.RemoteService;
import com.sap.cds.services.handler.EventHandler;
import com.sap.cds.services.handler.annotations.On;
import com.sap.cds.services.handler.annotations.ServiceName;
@@ -28,8 +32,8 @@ public class AICoreShowcaseHandler implements EventHandler {
@Autowired private CdsRuntime runtime;
- private AICoreService getAICoreService() {
- return runtime.getServiceCatalog().getService(AICoreService.class, AICoreService.DEFAULT_NAME);
+ private RemoteService getAICoreService() {
+ return runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
}
// This handler is NOT required - the plugin automatically delegates reads on projections
@@ -42,14 +46,20 @@ public void onReadConfigurations(CdsReadEventContext context) {
@On(event = "setupTenantResources")
public void onSetupTenantResources(EventContext context) {
- String rgId = getAICoreService().resourceGroup();
+ RemoteService service = getAICoreService();
+ ResourceGroupContext rgCtx = ResourceGroupContext.create();
+ service.emit(rgCtx);
+ String rgId = rgCtx.getResult();
context.put("result", rgId);
context.setCompleted();
}
@On(event = "getMyResourceGroup")
public void onGetMyResourceGroup(EventContext context) {
- String rgId = getAICoreService().resourceGroup();
+ RemoteService service = getAICoreService();
+ ResourceGroupContext rgCtx = ResourceGroupContext.create();
+ service.emit(rgCtx);
+ String rgId = rgCtx.getResult();
context.put("result", rgId);
context.setCompleted();
}
@@ -57,7 +67,12 @@ public void onGetMyResourceGroup(EventContext context) {
@On(event = "provisionRpt1")
public void onProvisionRpt1(EventContext context) {
String resourceGroupId = (String) context.get("resourceGroupId");
- String deploymentId = getAICoreService().deploymentId(resourceGroupId, RptModelSpec.rpt1());
+ RemoteService service = getAICoreService();
+ DeploymentIdContext depCtx = DeploymentIdContext.create();
+ depCtx.setResourceGroupId(resourceGroupId);
+ depCtx.setSpec(RptModelSpec.rpt1());
+ service.emit(depCtx);
+ String deploymentId = depCtx.getResult();
context.put("result", deploymentId);
context.setCompleted();
}
@@ -112,45 +127,52 @@ public void onCreateConfiguration(EventContext context) {
public void onPredictCategory(EventContext context) {
List> products = (List>) context.get("products");
- List rows = new ArrayList<>();
- rows.add(
- CdsData.create(
- Map.of("ID", "ctx-1", "name", "Laptop", "price", "999.99", "category", "Electronics")));
- rows.add(
- CdsData.create(
- Map.of("ID", "ctx-2", "name", "Mouse", "price", "29.99", "category", "Electronics")));
- rows.add(
- CdsData.create(
- Map.of("ID", "ctx-3", "name", "Shirt", "price", "49.99", "category", "Clothing")));
- rows.add(
- CdsData.create(
- Map.of("ID", "ctx-4", "name", "Novel", "price", "14.99", "category", "Books")));
- rows.add(
- CdsData.create(
- Map.of("ID", "ctx-5", "name", "Blender", "price", "89.99", "category", "Appliances")));
-
- for (Map product : products) {
- Map row = new HashMap<>(product);
- row.put("category", "[PREDICT]");
- rows.add(CdsData.create(row));
- }
-
- AICoreService service = getAICoreService();
- String rg = service.resourceGroup();
- String deploymentId = service.deploymentId(rg, RptModelSpec.rpt1());
- RptInferenceClient client =
- new RptInferenceClient(service.inferenceClient(rg, deploymentId));
- List predictions = client.predict(rows, List.of("category"), "ID");
+ List contextRows =
+ List.of(
+ CdsData.create(
+ Map.of("ID", "ctx-1", "name", "Laptop", "price", "999.99", "category", "Electronics")),
+ CdsData.create(
+ Map.of("ID", "ctx-2", "name", "Mouse", "price", "29.99", "category", "Electronics")),
+ CdsData.create(
+ Map.of("ID", "ctx-3", "name", "Shirt", "price", "49.99", "category", "Clothing")),
+ CdsData.create(
+ Map.of("ID", "ctx-4", "name", "Novel", "price", "14.99", "category", "Books")),
+ CdsData.create(
+ Map.of(
+ "ID", "ctx-5", "name", "Blender", "price", "89.99", "category", "Appliances")));
+
+ RemoteService service = getAICoreService();
+
+ ResourceGroupContext rgCtx = ResourceGroupContext.create();
+ service.emit(rgCtx);
+ String rg = rgCtx.getResult();
+
+ DeploymentIdContext depCtx = DeploymentIdContext.create();
+ depCtx.setResourceGroupId(rg);
+ depCtx.setSpec(RptModelSpec.rpt1());
+ service.emit(depCtx);
+ String deploymentId = depCtx.getResult();
+
+ InferenceClientContext infCtx = InferenceClientContext.create();
+ infCtx.setResourceGroupId(rg);
+ infCtx.setDeploymentId(deploymentId);
+ service.emit(infCtx);
+ RptInferenceClient client = new RptInferenceClient(infCtx.getResult());
List> results = new ArrayList<>();
- for (CdsData prediction : predictions) {
- String id = (String) prediction.get("ID");
- Object categoryObj = prediction.get("category");
- String category =
- categoryObj instanceof List> list && !list.isEmpty()
- ? extractPrediction(list)
- : String.valueOf(categoryObj);
- results.add(Map.of("ID", id, "category", category));
+ for (Map product : products) {
+ CdsData predictionRow = CdsData.create(new HashMap<>(product));
+ List predictions =
+ client.predict(predictionRow, contextRows, List.of("category"), List.of("ID"));
+ for (CdsData prediction : predictions) {
+ String id = (String) prediction.get("ID");
+ Object categoryObj = prediction.get("category");
+ String category =
+ categoryObj instanceof List> list && !list.isEmpty()
+ ? extractPrediction(list)
+ : String.valueOf(categoryObj);
+ results.add(Map.of("ID", id, "category", category));
+ }
}
context.put("result", results);