From 1b06edd518b74dad3231c9abbd3830e94ae6552c Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 17 Jun 2026 17:53:50 +0200 Subject: [PATCH 01/14] refactor(ai-core): migrate AICoreService to RemoteService - Remove AICoreService interface and AICoreServiceImpl - Introduce AICore constants class with SERVICE_NAME - Convert all handlers to use RemoteService with event contexts - Update DeploymentIdContext, InferenceClientContext, ResourceGroupContext - Update AICoreServiceConfiguration to register handlers on RemoteService - Adapt all unit tests to new RemoteService-based API - Update sample .cdsrc.json and ai-core-service.cds --- .../sap/cds/feature/aicore/api/AICore.java | 32 ++++++ .../cds/feature/aicore/api/AICoreService.java | 88 ----------------- .../aicore/api/DeploymentIdContext.java | 6 +- .../aicore/api/InferenceClientContext.java | 4 +- .../aicore/api/ModelDeploymentSpec.java | 4 +- .../aicore/api/ResourceGroupContext.java | 6 +- .../core/AICoreServiceConfiguration.java | 97 +++++++++++-------- .../aicore/core/AICoreServiceImpl.java | 70 ------------- .../aicore/core/handler/AICoreApiHandler.java | 6 +- .../aicore/core/handler/ActionHandler.java | 4 +- .../core/handler/ConfigurationHandler.java | 4 +- .../core/handler/DeploymentHandler.java | 4 +- .../core/handler/MockAICoreApiHandler.java | 8 +- .../core/handler/MockEntityHandler.java | 24 ++--- .../core/handler/ResourceGroupHandler.java | 4 +- .../core/AICoreServiceConfigurationTest.java | 25 ++--- .../AICoreServiceImplDeploymentIdTest.java | 89 ++++++++--------- .../core/MockAICoreServiceImplTest.java | 68 +++++++++---- .../handler/ConfigurationHandlerTest.java | 19 ++-- .../core/handler/DeploymentHandlerTest.java | 19 ++-- .../handler/ResourceGroupHandlerTest.java | 35 ++++--- .../core/handler/TenantScopingTest.java | 19 ++-- samples/bookshop/.cdsrc.json | 4 +- samples/bookshop/srv/ai-core-service.cds | 2 +- 24 files changed, 297 insertions(+), 344 deletions(-) create mode 100644 cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/AICore.java delete mode 100644 cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/AICoreService.java delete mode 100644 cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/AICoreServiceImpl.java 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: - * - *

- * - *

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/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') { From 3305da352ea8e3a39ab58fc52f9e2af72e0b5d1a Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 17 Jun 2026 17:54:04 +0200 Subject: [PATCH 02/14] chore(itests): adapt integration tests to RemoteService - Update all integration tests to use RemoteService + event contexts - Replace AICoreService.deploymentId/resourceGroup calls with context pattern - Update BaseIntegrationTest with new service resolution approach --- .../aicore/itest/mt/MtxLifecycleTest.java | 15 ++++--- .../itest/mt/SubscribeUnsubscribeTest.java | 15 ++++--- .../aicore/itest/mt/TenantIsolationTest.java | 28 ++++++++---- .../aicore/itest/AICoreServiceTest.java | 39 +++++++++++----- .../cds/feature/aicore/itest/ActionTest.java | 44 ++++++++++++++----- .../aicore/itest/BaseIntegrationTest.java | 26 ++++++----- .../aicore/itest/ConfigurationTest.java | 10 ++--- .../feature/aicore/itest/DeploymentTest.java | 8 ++-- .../aicore/itest/MultiTenancyTest.java | 37 ++++++++++++---- .../aicore/itest/ResourceGroupTest.java | 16 +++---- 10 files changed, 162 insertions(+), 76 deletions(-) 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 = From 17835c3f8ba2c518b1faa04903d1ef5f8dbbd1c5 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 17 Jun 2026 17:55:13 +0200 Subject: [PATCH 03/14] refactor(recommendations): adapt to RemoteService API - Remove RptIndexColumns utility; inline resolveIndexColumn and addSyntheticKeyIfNeeded into RptInferenceClient - Change RecommendationClient.predict to accept keyNames as argument - Drop generic type parameter from RecommendationClientResolver; resolve now takes RemoteService directly - Remove keyNames from RptInferenceClient constructor (passed at predict time) - FioriRecommendationHandler now holds RemoteService reference and passes keyNames at prediction time - RecommendationConfiguration uses RemoteService + event context pattern (ResourceGroupContext, DeploymentIdContext, InferenceClientContext) - MockRecommendationClient simplified (no keyNames in constructor) - Update all tests to match new signatures --- .../FioriRecommendationHandler.java | 13 ++- .../MockRecommendationClient.java | 12 ++- .../RecommendationConfiguration.java | 47 +++++++---- .../RecommendationContextBuilder.java | 11 +-- .../recommendation/RptIndexColumns.java | 25 ------ .../api/RecommendationClient.java | 36 ++++---- .../api/RecommendationClientResolver.java | 12 +-- .../api/RptInferenceClient.java | 83 +++++++++++-------- .../FioriRecommendationHandlerTest.java | 20 +++-- .../RecommendationConfigurationTest.java | 10 +-- .../api/RptInferenceClientTest.java | 35 ++++++++ 11 files changed, 172 insertions(+), 132 deletions(-) delete mode 100644 cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RptIndexColumns.java 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 1498d0f..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 @@ -11,6 +11,7 @@ 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; @@ -30,7 +31,8 @@ class FioriRecommendationHandler implements EventHandler { private static final int DEFAULT_CONTEXT_ROW_LIMIT = 2000; private static final String SAP_RECOMMENDATIONS = "SAP_Recommendations"; - private final RecommendationClientResolver> clientResolver; + 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 @@ -40,7 +42,10 @@ class FioriRecommendationHandler implements EventHandler { Caffeine.newBuilder().maximumSize(10_000).build(); FioriRecommendationHandler( - RecommendationClientResolver> clientResolver, PersistenceService db) { + RemoteService aiCoreService, + RecommendationClientResolver clientResolver, + PersistenceService db) { + this.aiCoreService = aiCoreService; this.clientResolver = clientResolver; this.db = db; } @@ -129,9 +134,9 @@ public void afterRead(CdsReadEventContext context, List dataList) { List missingPredictionElementNames = builder.predictionElementNames().stream().filter(c -> row.get(c) == null).toList(); - RecommendationClient client = clientResolver.resolve(builder.keyNames()); + RecommendationClient client = clientResolver.resolve(aiCoreService); List predictions = - client.predict(predictRow, contextRows, missingPredictionElementNames); + client.predict(predictRow, contextRows, missingPredictionElementNames, builder.keyNames()); if (predictions.isEmpty()) { logger.warn("No predictions returned from AI client."); 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 c2c9157..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 @@ -18,16 +18,14 @@ 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(); - private final List keyNames; - - MockRecommendationClient(List keyNames) { - this.keyNames = keyNames; - } @Override public List predict( - CdsData predictionRow, List contextRows, List predictionColumns) { - String indexColumn = RptIndexColumns.resolveIndexColumn(keyNames, predictionRow); + 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) { 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 529c7cf..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,18 +3,21 @@ */ 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; import com.sap.cds.services.utils.environment.ServiceBindingUtils; -import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,8 +30,8 @@ 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."); @@ -45,15 +48,13 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { } boolean hasBind = hasAICoreBinding(runtime); - // The real resolver is a lambda resolved at prediction time. That's necessary because - // resource group and deployment ID are tenant-specific and are only available at - // prediction time from the request context. AICoreService is captured in the closure. - RecommendationClientResolver> clientResolver = + RecommendationClientResolver resolver = hasBind - ? keyNames -> resolveRptClient(aiCoreService, keyNames) - : keyNames -> new MockRecommendationClient(keyNames); + ? RecommendationConfiguration::resolveRptClient + : service -> new MockRecommendationClient(); - FioriRecommendationHandler handler = new FioriRecommendationHandler(clientResolver, db); + FioriRecommendationHandler handler = + new FioriRecommendationHandler(aiCoreService, resolver, db); configurer.eventHandler(handler); configurer.eventHandler(new RecommendationModelChangedHandler(handler)); } @@ -63,14 +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, List keyNames) { - String resourceGroup = service.resourceGroup(); - String deploymentId = service.deploymentId(resourceGroup, RptModelSpec.rpt1()); - return new RptInferenceClient(service.inferenceClient(resourceGroup, deploymentId), keyNames); + 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 049d6e5..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 @@ -31,7 +31,7 @@ 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 ODATA_VALUE_LIST_ANNOTATION = "@cds.odata.valuelist"; + 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 = @@ -124,10 +124,11 @@ CdsData buildPredictRow(CdsData row) { } Set allowed = new HashSet<>(contextColumns); allowed.addAll(keyNames); - Map predictRow = - allowed.stream() - .filter(row::containsKey) - .collect(HashMap::new, (m, col) -> m.put(col, row.get(col)), HashMap::putAll); + Map predictRow = new HashMap<>(); + allowed.forEach( + col -> { + if (row.containsKey(col)) predictRow.put(col, row.get(col)); + }); return CdsData.create(predictRow); } diff --git a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RptIndexColumns.java b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RptIndexColumns.java deleted file mode 100644 index 26bc7a4..0000000 --- a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RptIndexColumns.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * © 2026 SAP SE or an SAP affiliate company and cds-ai contributors. - */ -package com.sap.cds.feature.recommendation; - -import com.sap.cds.CdsData; -import java.util.List; - -public class RptIndexColumns { - - // RPT-1 requires a single string index column to identify rows in the request/response. - // When the entity has a composite or non-string key, a synthetic string column is used instead. - public static final String SYNTHETIC_INDEX_COLUMN = "SAP_RECOMMENDATIONS_ID"; - - // Returns the column name to use as the RPT-1 index column. Uses the single key directly if - // it holds a String value; falls back to the synthetic column for composite or non-string keys. - public 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; - } - - private RptIndexColumns() {} -} 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 4558ec8..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,25 +8,21 @@ public interface RecommendationClient { - /** - * Predicts values for the missing columns of a single entity row. - * - *

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. {@code FioriRecommendationHandler} bails out when the read returns more than one entity, - * so predictions only fire on single-entity reads. - *
  2. {@code 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 - * @return the predicted values as a list of result rows - */ + // 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); + 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 82b2eca..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,12 +3,12 @@ */ package com.sap.cds.feature.recommendation.api; -// A single-method interface so callers can supply a custom client via lambda. -// @FunctionalInterface enforces this and causes a compile error if a second method is ever added. -// The type parameter T allows the resolver to receive any context the client might need (e.g. key -// names). +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 { +public interface RecommendationClientResolver { - RecommendationClient resolve(T context); + 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 c8058e6..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,13 +12,11 @@ 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.feature.recommendation.RptIndexColumns; 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; import io.github.resilience4j.retry.Retry; import io.github.resilience4j.retry.RetryConfig; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -33,11 +31,20 @@ *

Example usage: * *

{@code
- * AICoreService service = ...;
- * String rg = service.resourceGroup();
- * String deploymentId = service.deploymentId(rg, RptModelSpec.rpt1());
- * RptInferenceClient client = new RptInferenceClient(service.inferenceClient(rg, deploymentId), keyNames);
- * List predictions = client.predict(predictionRow, contextRows, List.of("targetColumn"));
+ * 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 { @@ -50,23 +57,25 @@ public class RptInferenceClient implements RecommendationClient { private static final Retry INFERENCE_RETRY = buildInferenceRetry(); private final DefaultApi rpt; - private final List keyNames; - public RptInferenceClient(ApiClient apiClient, List keyNames) { + public RptInferenceClient(ApiClient apiClient) { this.rpt = new DefaultApi(apiClient.withObjectMapper(JacksonConfiguration.getDefaultObjectMapper())); - this.keyNames = keyNames; } @Override public List predict( - CdsData predictionRow, List contextRows, List predictionColumns) { - String indexColumn = RptIndexColumns.resolveIndexColumn(keyNames, predictionRow); + CdsData predictionRow, + List contextRows, + List predictionColumns, + List keyNames) { + String indexColumn = resolveIndexColumn(keyNames, predictionRow); CdsData preparedPredictRow = preparePredictRow(predictionRow, predictionColumns); - List allRows = new ArrayList<>(contextRows); + List allRows = new java.util.ArrayList<>(contextRows); allRows.add(preparedPredictRow); + addSyntheticKeyIfNeeded(allRows, keyNames, indexColumn); - PredictRequestPayload request = buildRequest(allRows, predictionColumns, indexColumn, keyNames); + PredictRequestPayload request = buildRequest(allRows, predictionColumns, indexColumn); logger.debug( "Sending prediction request for one row with {} context rows, {} target columns", contextRows.size(), @@ -84,6 +93,21 @@ 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) { @@ -97,6 +121,13 @@ static String computeSyntheticKey(Map row, List keyNames 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) { @@ -108,10 +139,7 @@ private static CdsData preparePredictRow(CdsData predictRow, List predic } private static PredictRequestPayload buildRequest( - List rows, - List predictionColumns, - String indexColumn, - List keyNames) { + List rows, List predictionColumns, String indexColumn) { var targetColumns = predictionColumns.stream() .map( @@ -122,24 +150,7 @@ private static PredictRequestPayload buildRequest( .taskType(TargetColumnConfig.TaskTypeEnum.CLASSIFICATION)) .toList(); - // RPT-1 requires exactly one string-typed index column per row to identify predictions. - // When the entity key is composite or non-string, then the index column is - // RptIndexColumns.SYNTHETIC_INDEX_COLUMN and we need to compute the sytheticKey for all rows - // before sending them to RPT-1. - boolean syntheticKeyNeeded = RptIndexColumns.SYNTHETIC_INDEX_COLUMN.equals(indexColumn); - var sdkRows = - rows.stream() - .map( - row -> { - Map sdkRow = toSdkRow(row); - if (syntheticKeyNeeded) { - sdkRow.put( - RptIndexColumns.SYNTHETIC_INDEX_COLUMN, - RowsInnerValue.create(computeSyntheticKey(row, keyNames))); - } - return sdkRow; - }) - .toList(); + var sdkRows = rows.stream().map(row -> toSdkRow(row)).toList(); return PredictRequestPayload.create() .predictionConfig(PredictionConfig.create().targetColumns(targetColumns)) 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 c21c823..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 @@ -17,6 +17,7 @@ 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; @@ -32,7 +33,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; import org.mockito.ArgumentCaptor; +import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) @@ -41,6 +44,9 @@ class FioriRecommendationHandlerTest { private static CdsRuntime runtime; private static PersistenceService db; + @Mock(answer = Answers.CALLS_REAL_METHODS) + private RemoteService aiCoreService; + private FioriRecommendationHandler cut; private RecommendationClient predictionClient; @@ -61,7 +67,7 @@ void setup() { reset(db); when(db.getName()).thenReturn(PersistenceService.DEFAULT_NAME); predictionClient = randomPickClient(); - cut = new FioriRecommendationHandler(keyNames -> predictionClient, db); + cut = new FioriRecommendationHandler(aiCoreService, (service) -> predictionClient, db); } // ── tests ────────────────────────────────────────────────────────────────── @@ -146,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 = (predictionRow, contextRows, cols) -> List.of(); + predictionClient = (predictionRow, contextRows, cols, keyNames) -> List.of(); cut.afterRead(ctx, dataList(row)); assertThat(row).doesNotContainKey("SAP_Recommendations"); }); @@ -160,7 +166,7 @@ void multiplePredictions_returnsEarlyWithoutRecommendations() { CdsReadEventContext ctx = readContext("test.Books", List.of(row)); when(db.run(any(CqnSelect.class))).thenReturn(twoContextRows()); predictionClient = - (predictionRow, contextRows, cols) -> + (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)); @@ -436,7 +442,7 @@ private static Result twoContextRows() { private static RecommendationClient rptStyleClient() { Random random = new Random(42); - return (predictionRow, contextRows, predictionColumns) -> { + return (predictionRow, contextRows, predictionColumns, keyNames) -> { Map prediction = new HashMap<>(); for (String col : predictionColumns) { List available = @@ -444,14 +450,14 @@ private static RecommendationClient rptStyleClient() { Object val = available.isEmpty() ? null : available.get(random.nextInt(available.size())); prediction.put(col, List.of(Map.of("prediction", val))); } - prediction.put("ID", predictionRow.get("ID")); + 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 (predictionRow, contextRows, predictionColumns) -> { + return (predictionRow, contextRows, predictionColumns, keyNames) -> { Map prediction = new HashMap<>(); for (String col : predictionColumns) { if (predictionRow.get(col) == null) { @@ -461,7 +467,7 @@ private static RecommendationClient randomPickClient() { prediction.put(col, List.of(Map.of("prediction", val))); } } - prediction.put("ID", predictionRow.get("ID")); + 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 8b3ebce..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,8 +5,9 @@ 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; @@ -24,7 +25,7 @@ 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 @@ -33,7 +34,7 @@ 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); @@ -47,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 index 8abce82..5784e21 100644 --- 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 @@ -5,12 +5,47 @@ 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")); From d8449026809666325b15f271f7cca09b683be4dd Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 17 Jun 2026 17:55:28 +0200 Subject: [PATCH 04/14] chore(samples): adapt bookshop sample to RemoteService - Replace AICoreService usage with RemoteService + event context pattern - Use AICore.SERVICE_NAME constant for service lookup - Demonstrate ResourceGroupContext, DeploymentIdContext, InferenceClientContext --- .../handlers/AICoreShowcaseHandler.java | 52 ++++++++++++++----- 1 file changed, 40 insertions(+), 12 deletions(-) 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 5ebfdf3..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(); } @@ -126,16 +141,29 @@ public void onPredictCategory(EventContext context) { Map.of( "ID", "ctx-5", "name", "Blender", "price", "89.99", "category", "Appliances"))); - AICoreService service = getAICoreService(); - String rg = service.resourceGroup(); - String deploymentId = service.deploymentId(rg, RptModelSpec.rpt1()); - RptInferenceClient client = - new RptInferenceClient(service.inferenceClient(rg, deploymentId), List.of("ID")); + 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 (Map product : products) { CdsData predictionRow = CdsData.create(new HashMap<>(product)); - List predictions = client.predict(predictionRow, contextRows, List.of("category")); + 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"); From e36089a399483ccf1db25d30debcf65c9d9414e9 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 17 Jun 2026 18:15:20 +0200 Subject: [PATCH 05/14] update last Cqn references --- .../com/sap/cds/feature/aicore/itest/ActionTest.java | 5 +++-- .../feature/aicore/itest/BaseIntegrationTest.java | 4 ++-- .../cds/feature/aicore/itest/ConfigurationTest.java | 8 ++++---- .../sap/cds/feature/aicore/itest/DeploymentTest.java | 6 +++--- .../cds/feature/aicore/itest/ResourceGroupTest.java | 12 ++++++------ 5 files changed, 18 insertions(+), 17 deletions(-) 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 39f430e..3c1602a 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 @@ -29,7 +29,8 @@ class ActionTest extends BaseIntegrationTest { @BeforeAll void ensureResourceGroupReady() { - ensureResourceGroupProvisioned(getAICoreCqnService(), getAICoreConfig().defaultResourceGroup()); + ensureResourceGroupProvisioned( + getAICoreRemoteService(), getAICoreConfig().defaultResourceGroup()); } @Test @@ -95,7 +96,7 @@ void deploymentId_cachedOnSecondCall() { + "re-enable once test creates its own isolated deployment") @Test void stop_deployment_changesTargetStatus() { - RemoteService service = getAICoreCqnService(); + RemoteService service = getAICoreRemoteService(); 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 38dd0ca..755b9ac 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 @@ -51,7 +51,7 @@ protected AICoreConfig getAICoreConfig() { return AICoreConfig.from(runtime.getEnvironment(), mt); } - protected RemoteService getAICoreCqnService() { + protected RemoteService getAICoreRemoteService() { return getAICoreService(); } @@ -60,7 +60,7 @@ protected String ensureRptDeploymentReady() { return CACHED_DEPLOYMENT_IDS.computeIfAbsent( resourceGroup, rg -> { - ensureResourceGroupProvisioned(getAICoreCqnService(), rg); + ensureResourceGroupProvisioned(getAICoreRemoteService(), rg); RemoteService service = getAICoreService(); DeploymentIdContext depCtx = DeploymentIdContext.create(); depCtx.setResourceGroupId(rg); 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 9b7d1af..eceb53d 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 @@ -18,7 +18,7 @@ class ConfigurationTest extends BaseIntegrationTest { @Test void readAll_returnsConfigurations() { - RemoteService service = getAICoreCqnService(); + RemoteService service = getAICoreRemoteService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); Result result = service.run( @@ -30,7 +30,7 @@ void readAll_returnsConfigurations() { @Test void readAll_filterByScenario() { - RemoteService service = getAICoreCqnService(); + RemoteService service = getAICoreRemoteService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); Result result = service.run( @@ -46,7 +46,7 @@ void readAll_filterByScenario() { @Test void create_andReadById() { - RemoteService service = getAICoreCqnService(); + RemoteService service = getAICoreRemoteService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); String configName = "itest-config-" + System.currentTimeMillis(); @@ -91,7 +91,7 @@ void create_andReadById() { @Test void create_withParameterBindings_mapsCorrectly() { - RemoteService service = getAICoreCqnService(); + RemoteService service = getAICoreRemoteService(); 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 c1111dc..7ec0f2e 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 @@ -19,7 +19,7 @@ class DeploymentTest extends BaseIntegrationTest { @Test void readAll_returnsDeployments() { - RemoteService service = getAICoreCqnService(); + RemoteService service = getAICoreRemoteService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); Result result = service.run( @@ -31,7 +31,7 @@ void readAll_returnsDeployments() { @Test void readSingle_returnsDeploymentDetails() { - RemoteService service = getAICoreCqnService(); + RemoteService service = getAICoreRemoteService(); 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() { - RemoteService service = getAICoreCqnService(); + RemoteService service = getAICoreRemoteService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); Result deployments = 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 6c00532..204c063 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 @@ -30,7 +30,7 @@ class ResourceGroupTest extends BaseIntegrationTest { void cleanup() { if (createdResourceGroupId != null) { try { - RemoteService service = getAICoreCqnService(); + RemoteService service = getAICoreRemoteService(); 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(); - RemoteService service = getAICoreCqnService(); + RemoteService service = getAICoreRemoteService(); 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; - RemoteService service = getAICoreCqnService(); + RemoteService service = getAICoreRemoteService(); service.run( Insert.into("AICore.resourceGroups") @@ -82,7 +82,7 @@ void create_withTenantLabel_andFilterByTenant() { @Test void readAll_returnsResourceGroups() { - RemoteService service = getAICoreCqnService(); + RemoteService service = getAICoreRemoteService(); 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(); - RemoteService service = getAICoreCqnService(); + RemoteService service = getAICoreRemoteService(); 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(); - RemoteService service = getAICoreCqnService(); + RemoteService service = getAICoreRemoteService(); service.run(Insert.into("AICore.resourceGroups").entry(Map.of("resourceGroupId", rgId))); From c883126ae60858ea3947d4048c022145dc7ddab3 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Thu, 18 Jun 2026 08:55:58 +0200 Subject: [PATCH 06/14] update unit-tests --- .../handler/ConfigurationHandlerTest.java | 7 +----- .../core/handler/DeploymentHandlerTest.java | 7 +----- .../aicore/core/handler/HandlerTestUtils.java | 23 +++++++++++++++++++ .../handler/ResourceGroupHandlerTest.java | 12 ++-------- .../core/handler/TenantScopingTest.java | 7 +----- 5 files changed, 28 insertions(+), 28 deletions(-) create mode 100644 cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/HandlerTestUtils.java 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 f6de01a..580cb7c 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 @@ -27,8 +27,6 @@ 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; @@ -58,10 +56,7 @@ static void bootRuntime() { resourceGroupApi = mock(ResourceGroupApi.class); DeploymentApi deploymentApi = mock(DeploymentApi.class); - 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 props = HandlerTestUtils.aicoreProperties(); var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props)); configurer.cdsModel("edmx/csn.json"); 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 44743d5..c84a4bd 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 @@ -30,8 +30,6 @@ 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; @@ -62,10 +60,7 @@ static void bootRuntime() { resourceGroupApi = mock(ResourceGroupApi.class); configurationApi = mock(ConfigurationApi.class); - 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 props = HandlerTestUtils.aicoreProperties(); var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props)); configurer.cdsModel("edmx/csn.json"); diff --git a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/HandlerTestUtils.java b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/HandlerTestUtils.java new file mode 100644 index 0000000..77db33c --- /dev/null +++ b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/HandlerTestUtils.java @@ -0,0 +1,23 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-ai contributors. + */ +package com.sap.cds.feature.aicore.core.handler; + +import com.sap.cds.feature.aicore.api.AICore; +import com.sap.cds.services.environment.CdsProperties; +import com.sap.cds.services.environment.CdsProperties.Remote.RemoteServiceConfig; + +/** Shared test utilities for handler tests that boot a CDS runtime with the AICore model. */ +final class HandlerTestUtils { + + private HandlerTestUtils() {} + + /** Creates CdsProperties with the AICore RemoteService configured. */ + static CdsProperties aicoreProperties() { + CdsProperties props = new CdsProperties(); + RemoteServiceConfig rsConfig = new RemoteServiceConfig(AICore.SERVICE_NAME); + rsConfig.setModel(AICore.SERVICE_NAME); + props.getRemote().getServices().put(AICore.SERVICE_NAME, rsConfig); + return props; + } +} 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 a635b98..9d9a3ad 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 @@ -30,8 +30,6 @@ 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; @@ -61,10 +59,7 @@ static void bootRuntime() { DeploymentApi deploymentApi = mock(DeploymentApi.class); ConfigurationApi configurationApi = mock(ConfigurationApi.class); - 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 props = HandlerTestUtils.aicoreProperties(); var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props)); configurer.cdsModel("edmx/csn.json"); @@ -215,10 +210,7 @@ static void bootMtRuntime() { DeploymentApi deploymentApi = mock(DeploymentApi.class); ConfigurationApi configurationApi = mock(ConfigurationApi.class); - 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 props = HandlerTestUtils.aicoreProperties(); var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props)); configurer.cdsModel("edmx/csn.json"); 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 168b5fc..801a14a 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 @@ -25,8 +25,6 @@ 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; @@ -55,10 +53,7 @@ static void bootRuntime() { resourceGroupApi = mock(ResourceGroupApi.class); ConfigurationApi configurationApi = mock(ConfigurationApi.class); - 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 props = HandlerTestUtils.aicoreProperties(); var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props)); configurer.cdsModel("edmx/csn.json"); From c82bcd28e7818f3e800fce01696bff97145a9a2b Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Thu, 18 Jun 2026 08:58:24 +0200 Subject: [PATCH 07/14] restore functionality --- .../FioriRecommendationHandler.java | 13 ++-- .../MockRecommendationClient.java | 12 ++-- .../RecommendationConfiguration.java | 31 ++++++--- .../RecommendationContextBuilder.java | 2 +- .../recommendation/RptIndexColumns.java | 25 +++++++ .../api/RecommendationClient.java | 36 +++++----- .../api/RecommendationClientResolver.java | 12 ++-- .../api/RptInferenceClient.java | 68 +++++++++---------- 8 files changed, 119 insertions(+), 80 deletions(-) create mode 100644 cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RptIndexColumns.java 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 f00ce7e..1498d0f 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 @@ -11,7 +11,6 @@ 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; @@ -31,8 +30,7 @@ class FioriRecommendationHandler implements EventHandler { private static final int DEFAULT_CONTEXT_ROW_LIMIT = 2000; private static final String SAP_RECOMMENDATIONS = "SAP_Recommendations"; - private final RemoteService aiCoreService; - private final RecommendationClientResolver clientResolver; + 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 @@ -42,10 +40,7 @@ class FioriRecommendationHandler implements EventHandler { Caffeine.newBuilder().maximumSize(10_000).build(); FioriRecommendationHandler( - RemoteService aiCoreService, - RecommendationClientResolver clientResolver, - PersistenceService db) { - this.aiCoreService = aiCoreService; + RecommendationClientResolver> clientResolver, PersistenceService db) { this.clientResolver = clientResolver; this.db = db; } @@ -134,9 +129,9 @@ public void afterRead(CdsReadEventContext context, List dataList) { List missingPredictionElementNames = builder.predictionElementNames().stream().filter(c -> row.get(c) == null).toList(); - RecommendationClient client = clientResolver.resolve(aiCoreService); + RecommendationClient client = clientResolver.resolve(builder.keyNames()); List predictions = - client.predict(predictRow, contextRows, missingPredictionElementNames, builder.keyNames()); + client.predict(predictRow, contextRows, missingPredictionElementNames); if (predictions.isEmpty()) { logger.warn("No predictions returned from AI client."); 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 cb2b64c..c2c9157 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 @@ -18,14 +18,16 @@ 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(); + private final List keyNames; + + MockRecommendationClient(List keyNames) { + this.keyNames = keyNames; + } @Override public List predict( - CdsData predictionRow, - List contextRows, - List predictionColumns, - List keyNames) { - String indexColumn = keyNames.size() == 1 ? keyNames.get(0) : "SAP_RECOMMENDATIONS_ID"; + CdsData predictionRow, List contextRows, List predictionColumns) { + String indexColumn = RptIndexColumns.resolveIndexColumn(keyNames, predictionRow); Map prediction = new HashMap<>(); for (String col : predictionColumns) { if (predictionRow.get(col) == null) { 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 1d3df83..01995de 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 @@ -18,6 +18,7 @@ import com.sap.cds.services.runtime.CdsRuntimeConfiguration; import com.sap.cds.services.runtime.CdsRuntimeConfigurer; import com.sap.cds.services.utils.environment.ServiceBindingUtils; +import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,13 +49,15 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { } boolean hasBind = hasAICoreBinding(runtime); - RecommendationClientResolver resolver = + // The real resolver is a lambda resolved at prediction time. That's necessary because + // resource group and deployment ID are tenant-specific and are only available at + // prediction time from the request context. The RemoteService is captured in the closure. + RecommendationClientResolver> clientResolver = hasBind - ? RecommendationConfiguration::resolveRptClient - : service -> new MockRecommendationClient(); + ? keyNames -> resolveRptClient(aiCoreService, keyNames) + : keyNames -> new MockRecommendationClient(keyNames); - FioriRecommendationHandler handler = - new FioriRecommendationHandler(aiCoreService, resolver, db); + FioriRecommendationHandler handler = new FioriRecommendationHandler(clientResolver, db); configurer.eventHandler(handler); configurer.eventHandler(new RecommendationModelChangedHandler(handler)); } @@ -64,26 +67,38 @@ private static boolean hasAICoreBinding(CdsRuntime runtime) { .getEnvironment() .getServiceBindings() .filter(b -> ServiceBindingUtils.matches(b, "aicore")) - .findAny() + .findFirst() .isPresent(); } - private static RecommendationClient resolveRptClient(RemoteService service) { + private static RecommendationClient resolveRptClient( + RemoteService service, List keyNames) { ResourceGroupContext rgCtx = ResourceGroupContext.create(); service.emit(rgCtx); String resourceGroup = rgCtx.getResult(); + if (resourceGroup == null) { + throw new IllegalStateException("Failed to resolve resource group from AI Core service"); + } DeploymentIdContext depCtx = DeploymentIdContext.create(); depCtx.setResourceGroupId(resourceGroup); depCtx.setSpec(RptModelSpec.rpt1()); service.emit(depCtx); String deploymentId = depCtx.getResult(); + if (deploymentId == null) { + throw new IllegalStateException( + "Failed to resolve deployment ID for resource group: " + resourceGroup); + } InferenceClientContext infCtx = InferenceClientContext.create(); infCtx.setResourceGroupId(resourceGroup); infCtx.setDeploymentId(deploymentId); service.emit(infCtx); + if (infCtx.getResult() == null) { + throw new IllegalStateException( + "Failed to create inference client for deployment: " + deploymentId); + } - return new RptInferenceClient(infCtx.getResult()); + return new RptInferenceClient(infCtx.getResult(), keyNames); } } 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 4c335e2..d0c105c 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 @@ -31,7 +31,7 @@ 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 ODATA_VALUE_LIST_ANNOTATION = "cds.odata.valuelist"; + 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 = diff --git a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RptIndexColumns.java b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RptIndexColumns.java new file mode 100644 index 0000000..26bc7a4 --- /dev/null +++ b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RptIndexColumns.java @@ -0,0 +1,25 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-ai contributors. + */ +package com.sap.cds.feature.recommendation; + +import com.sap.cds.CdsData; +import java.util.List; + +public class RptIndexColumns { + + // RPT-1 requires a single string index column to identify rows in the request/response. + // When the entity has a composite or non-string key, a synthetic string column is used instead. + public static final String SYNTHETIC_INDEX_COLUMN = "SAP_RECOMMENDATIONS_ID"; + + // Returns the column name to use as the RPT-1 index column. Uses the single key directly if + // it holds a String value; falls back to the synthetic column for composite or non-string keys. + public 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; + } + + private RptIndexColumns() {} +} 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 1170780..4558ec8 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,21 +8,25 @@ public interface RecommendationClient { - // 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 + /** + * Predicts values for the missing columns of a single entity row. + * + *

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. {@code FioriRecommendationHandler} bails out when the read returns more than one entity, + * so predictions only fire on single-entity reads. + *
  2. {@code 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 + * @return the predicted values as a list of result rows + */ List predict( - CdsData predictionRow, - List contextRows, - List predictionColumns, - List keyNames); + CdsData predictionRow, List contextRows, List predictionColumns); } 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 fdd71c6..82b2eca 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,12 +3,12 @@ */ package com.sap.cds.feature.recommendation.api; -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. +// A single-method interface so callers can supply a custom client via lambda. +// @FunctionalInterface enforces this and causes a compile error if a second method is ever added. +// The type parameter T allows the resolver to receive any context the client might need (e.g. key +// names). @FunctionalInterface -public interface RecommendationClientResolver { +public interface RecommendationClientResolver { - RecommendationClient resolve(RemoteService aiCoreService); + RecommendationClient resolve(T context); } 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 ed7d238..06f6f4d 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,11 +12,13 @@ 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.feature.recommendation.RptIndexColumns; 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; import io.github.resilience4j.retry.Retry; import io.github.resilience4j.retry.RetryConfig; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -43,8 +45,8 @@ * 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")); + * RptInferenceClient client = new RptInferenceClient(infCtx.getResult(), keyNames); + * List predictions = client.predict(predictionRow, contextRows, List.of("targetColumn")); * } */ public class RptInferenceClient implements RecommendationClient { @@ -57,25 +59,23 @@ public class RptInferenceClient implements RecommendationClient { private static final Retry INFERENCE_RETRY = buildInferenceRetry(); private final DefaultApi rpt; + private final List keyNames; - public RptInferenceClient(ApiClient apiClient) { + public RptInferenceClient(ApiClient apiClient, List keyNames) { this.rpt = new DefaultApi(apiClient.withObjectMapper(JacksonConfiguration.getDefaultObjectMapper())); + this.keyNames = keyNames; } @Override public List predict( - CdsData predictionRow, - List contextRows, - List predictionColumns, - List keyNames) { - String indexColumn = resolveIndexColumn(keyNames, predictionRow); + CdsData predictionRow, List contextRows, List predictionColumns) { + String indexColumn = RptIndexColumns.resolveIndexColumn(keyNames, predictionRow); CdsData preparedPredictRow = preparePredictRow(predictionRow, predictionColumns); - List allRows = new java.util.ArrayList<>(contextRows); + List allRows = new ArrayList<>(contextRows); allRows.add(preparedPredictRow); - addSyntheticKeyIfNeeded(allRows, keyNames, indexColumn); - PredictRequestPayload request = buildRequest(allRows, predictionColumns, indexColumn); + PredictRequestPayload request = buildRequest(allRows, predictionColumns, indexColumn, keyNames); logger.debug( "Sending prediction request for one row with {} context rows, {} target columns", contextRows.size(), @@ -93,21 +93,6 @@ 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) { @@ -121,13 +106,6 @@ static String computeSyntheticKey(Map row, List keyNames 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) { @@ -139,7 +117,10 @@ private static CdsData preparePredictRow(CdsData predictRow, List predic } private static PredictRequestPayload buildRequest( - List rows, List predictionColumns, String indexColumn) { + List rows, + List predictionColumns, + String indexColumn, + List keyNames) { var targetColumns = predictionColumns.stream() .map( @@ -150,7 +131,24 @@ private static PredictRequestPayload buildRequest( .taskType(TargetColumnConfig.TaskTypeEnum.CLASSIFICATION)) .toList(); - var sdkRows = rows.stream().map(row -> toSdkRow(row)).toList(); + // RPT-1 requires exactly one string-typed index column per row to identify predictions. + // When the entity key is composite or non-string, then the index column is + // RptIndexColumns.SYNTHETIC_INDEX_COLUMN and we need to compute the syntheticKey for all rows + // before sending them to RPT-1. + boolean syntheticKeyNeeded = RptIndexColumns.SYNTHETIC_INDEX_COLUMN.equals(indexColumn); + var sdkRows = + rows.stream() + .map( + row -> { + Map sdkRow = toSdkRow(row); + if (syntheticKeyNeeded) { + sdkRow.put( + RptIndexColumns.SYNTHETIC_INDEX_COLUMN, + RowsInnerValue.create(computeSyntheticKey(row, keyNames))); + } + return sdkRow; + }) + .toList(); return PredictRequestPayload.create() .predictionConfig(PredictionConfig.create().targetColumns(targetColumns)) From 3cb2163e3954862dd0d5c55b273e092bfb6c852d Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Thu, 18 Jun 2026 08:58:31 +0200 Subject: [PATCH 08/14] adapt tests --- .../FioriRecommendationHandlerTest.java | 23 +++++----------- .../api/RptInferenceClientTest.java | 27 +++++++------------ 2 files changed, 17 insertions(+), 33 deletions(-) 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 34b9972..b25faa9 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 @@ -17,7 +17,6 @@ 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; @@ -32,21 +31,13 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Answers; import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -@ExtendWith(MockitoExtension.class) class FioriRecommendationHandlerTest { private static CdsRuntime runtime; private static PersistenceService db; - @Mock(answer = Answers.CALLS_REAL_METHODS) - private RemoteService aiCoreService; - private FioriRecommendationHandler cut; private RecommendationClient predictionClient; @@ -67,7 +58,7 @@ void setup() { reset(db); when(db.getName()).thenReturn(PersistenceService.DEFAULT_NAME); predictionClient = randomPickClient(); - cut = new FioriRecommendationHandler(aiCoreService, (service) -> predictionClient, db); + cut = new FioriRecommendationHandler((keyNames) -> predictionClient, db); } // ── tests ────────────────────────────────────────────────────────────────── @@ -152,7 +143,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 = (predictionRow, contextRows, cols, keyNames) -> List.of(); + predictionClient = (predictionRow, contextRows, cols) -> List.of(); cut.afterRead(ctx, dataList(row)); assertThat(row).doesNotContainKey("SAP_Recommendations"); }); @@ -166,7 +157,7 @@ void multiplePredictions_returnsEarlyWithoutRecommendations() { CdsReadEventContext ctx = readContext("test.Books", List.of(row)); when(db.run(any(CqnSelect.class))).thenReturn(twoContextRows()); predictionClient = - (predictionRow, contextRows, cols, idx) -> + (predictionRow, contextRows, cols) -> List.of( CdsData.create(Map.of("ID", "id-1")), CdsData.create(Map.of("ID", "id-2"))); cut.afterRead(ctx, dataList(row)); @@ -442,7 +433,7 @@ private static Result twoContextRows() { private static RecommendationClient rptStyleClient() { Random random = new Random(42); - return (predictionRow, contextRows, predictionColumns, keyNames) -> { + return (predictionRow, contextRows, predictionColumns) -> { Map prediction = new HashMap<>(); for (String col : predictionColumns) { List available = @@ -450,14 +441,14 @@ private static RecommendationClient rptStyleClient() { Object val = available.isEmpty() ? null : available.get(random.nextInt(available.size())); prediction.put(col, List.of(Map.of("prediction", val))); } - prediction.put(keyNames.get(0), predictionRow.get(keyNames.get(0))); + prediction.put("ID", predictionRow.get("ID")); return List.of(CdsData.create(prediction)); }; } private static RecommendationClient randomPickClient() { Random random = new Random(42); - return (predictionRow, contextRows, predictionColumns, keyNames) -> { + return (predictionRow, contextRows, predictionColumns) -> { Map prediction = new HashMap<>(); for (String col : predictionColumns) { if (predictionRow.get(col) == null) { @@ -467,7 +458,7 @@ private static RecommendationClient randomPickClient() { prediction.put(col, List.of(Map.of("prediction", val))); } } - prediction.put(keyNames.get(0), predictionRow.get(keyNames.get(0))); + prediction.put("ID", predictionRow.get("ID")); return List.of(CdsData.create(prediction)); }; } 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 index 5784e21..54586bb 100644 --- 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 @@ -6,43 +6,36 @@ import static org.assertj.core.api.Assertions.assertThat; import com.sap.cds.CdsData; -import java.lang.reflect.Method; +import com.sap.cds.feature.recommendation.RptIndexColumns; 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 { + void resolveIndexColumn_singleStringKey_usesItDirectly() { CdsData row = CdsData.create(Map.of("isbn", "978-3-16")); - assertThat(resolveIndexColumn(List.of("isbn"), row)).isEqualTo("isbn"); + assertThat(RptIndexColumns.resolveIndexColumn(List.of("isbn"), row)).isEqualTo("isbn"); } @Test - void resolveIndexColumn_singleUuidKey_usesItDirectly() throws Exception { + void resolveIndexColumn_singleUuidKey_usesItDirectly() { CdsData row = CdsData.create(Map.of("ID", "a009c640-434a-4542-ac68-51b400c880ec")); - assertThat(resolveIndexColumn(List.of("ID"), row)).isEqualTo("ID"); + assertThat(RptIndexColumns.resolveIndexColumn(List.of("ID"), row)).isEqualTo("ID"); } @Test - void resolveIndexColumn_singleIntegerKey_usesSyntheticColumn() throws Exception { + void resolveIndexColumn_singleIntegerKey_usesSyntheticColumn() { CdsData row = CdsData.create(Map.of("order_ID", 42)); - assertThat(resolveIndexColumn(List.of("order_ID"), row)).isEqualTo("SAP_RECOMMENDATIONS_ID"); + assertThat(RptIndexColumns.resolveIndexColumn(List.of("order_ID"), row)) + .isEqualTo("SAP_RECOMMENDATIONS_ID"); } @Test - void resolveIndexColumn_compositeKey_usesSyntheticColumn() throws Exception { + void resolveIndexColumn_compositeKey_usesSyntheticColumn() { CdsData row = CdsData.create(Map.of("order_ID", 1, "item_no", 10)); - assertThat(resolveIndexColumn(List.of("order_ID", "item_no"), row)) + assertThat(RptIndexColumns.resolveIndexColumn(List.of("order_ID", "item_no"), row)) .isEqualTo("SAP_RECOMMENDATIONS_ID"); } From 1b714baaa500df56290d8bfd8496fe1641b6fd5e Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Thu, 18 Jun 2026 08:58:39 +0200 Subject: [PATCH 09/14] simplify itests --- .../com/sap/cds/feature/aicore/itest/ActionTest.java | 4 ++-- .../feature/aicore/itest/BaseIntegrationTest.java | 6 +----- .../cds/feature/aicore/itest/ConfigurationTest.java | 8 ++++---- .../sap/cds/feature/aicore/itest/DeploymentTest.java | 6 +++--- .../cds/feature/aicore/itest/ResourceGroupTest.java | 12 ++++++------ 5 files changed, 16 insertions(+), 20 deletions(-) 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 3c1602a..ab6308e 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 @@ -30,7 +30,7 @@ class ActionTest extends BaseIntegrationTest { @BeforeAll void ensureResourceGroupReady() { ensureResourceGroupProvisioned( - getAICoreRemoteService(), getAICoreConfig().defaultResourceGroup()); + getAICoreService(), getAICoreConfig().defaultResourceGroup()); } @Test @@ -96,7 +96,7 @@ void deploymentId_cachedOnSecondCall() { + "re-enable once test creates its own isolated deployment") @Test void stop_deployment_changesTargetStatus() { - RemoteService service = getAICoreRemoteService(); + RemoteService service = getAICoreService(); 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 755b9ac..48dfcfb 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 @@ -51,16 +51,12 @@ protected AICoreConfig getAICoreConfig() { return AICoreConfig.from(runtime.getEnvironment(), mt); } - protected RemoteService getAICoreRemoteService() { - return getAICoreService(); - } - protected String ensureRptDeploymentReady() { String resourceGroup = getAICoreConfig().defaultResourceGroup(); return CACHED_DEPLOYMENT_IDS.computeIfAbsent( resourceGroup, rg -> { - ensureResourceGroupProvisioned(getAICoreRemoteService(), rg); + ensureResourceGroupProvisioned(getAICoreService(), rg); RemoteService service = getAICoreService(); DeploymentIdContext depCtx = DeploymentIdContext.create(); depCtx.setResourceGroupId(rg); 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 eceb53d..bdc62fb 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 @@ -18,7 +18,7 @@ class ConfigurationTest extends BaseIntegrationTest { @Test void readAll_returnsConfigurations() { - RemoteService service = getAICoreRemoteService(); + RemoteService service = getAICoreService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); Result result = service.run( @@ -30,7 +30,7 @@ void readAll_returnsConfigurations() { @Test void readAll_filterByScenario() { - RemoteService service = getAICoreRemoteService(); + RemoteService service = getAICoreService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); Result result = service.run( @@ -46,7 +46,7 @@ void readAll_filterByScenario() { @Test void create_andReadById() { - RemoteService service = getAICoreRemoteService(); + RemoteService service = getAICoreService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); String configName = "itest-config-" + System.currentTimeMillis(); @@ -91,7 +91,7 @@ void create_andReadById() { @Test void create_withParameterBindings_mapsCorrectly() { - RemoteService service = getAICoreRemoteService(); + RemoteService service = getAICoreService(); 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 7ec0f2e..cc9ef53 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 @@ -19,7 +19,7 @@ class DeploymentTest extends BaseIntegrationTest { @Test void readAll_returnsDeployments() { - RemoteService service = getAICoreRemoteService(); + RemoteService service = getAICoreService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); Result result = service.run( @@ -31,7 +31,7 @@ void readAll_returnsDeployments() { @Test void readSingle_returnsDeploymentDetails() { - RemoteService service = getAICoreRemoteService(); + RemoteService service = getAICoreService(); 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() { - RemoteService service = getAICoreRemoteService(); + RemoteService service = getAICoreService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); Result deployments = 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 204c063..bc63a28 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 @@ -30,7 +30,7 @@ class ResourceGroupTest extends BaseIntegrationTest { void cleanup() { if (createdResourceGroupId != null) { try { - RemoteService service = getAICoreRemoteService(); + RemoteService service = getAICoreService(); 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(); - RemoteService service = getAICoreRemoteService(); + RemoteService service = getAICoreService(); 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; - RemoteService service = getAICoreRemoteService(); + RemoteService service = getAICoreService(); service.run( Insert.into("AICore.resourceGroups") @@ -82,7 +82,7 @@ void create_withTenantLabel_andFilterByTenant() { @Test void readAll_returnsResourceGroups() { - RemoteService service = getAICoreRemoteService(); + RemoteService service = getAICoreService(); 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(); - RemoteService service = getAICoreRemoteService(); + RemoteService service = getAICoreService(); 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(); - RemoteService service = getAICoreRemoteService(); + RemoteService service = getAICoreService(); service.run(Insert.into("AICore.resourceGroups").entry(Map.of("resourceGroupId", rgId))); From 90c495d85e004ff31544ba28419b9f23b785f59f Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Thu, 18 Jun 2026 08:58:53 +0200 Subject: [PATCH 10/14] simplification --- .../handlers/AICoreShowcaseHandler.java | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) 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 d2cfaf6..f62e320 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 @@ -142,28 +142,13 @@ public void onPredictCategory(EventContext context) { "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()); + RptInferenceClient client = createRptClient(service, List.of("ID")); List> results = new ArrayList<>(); for (Map product : products) { CdsData predictionRow = CdsData.create(new HashMap<>(product)); List predictions = - client.predict(predictionRow, contextRows, List.of("category"), List.of("ID")); + client.predict(predictionRow, contextRows, List.of("category")); for (CdsData prediction : predictions) { String id = (String) prediction.get("ID"); Object categoryObj = prediction.get("category"); @@ -186,4 +171,24 @@ private String extractPrediction(List predictionList) { } return predictionList.get(0).toString(); } + + /** Helper to resolve a ready-to-use RptInferenceClient from the AI Core RemoteService. */ + private static RptInferenceClient createRptClient( + RemoteService service, List keyNames) { + 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); + + return new RptInferenceClient(infCtx.getResult(), keyNames); + } } From e9acf176945a921371037780c3dbb7669a165db2 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Thu, 18 Jun 2026 09:00:35 +0200 Subject: [PATCH 11/14] spotless --- .../feature/aicore/core/handler/ResourceGroupHandlerTest.java | 2 +- .../test/java/com/sap/cds/feature/aicore/itest/ActionTest.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) 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 9d9a3ad..71fa0f8 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 @@ -210,7 +210,7 @@ static void bootMtRuntime() { DeploymentApi deploymentApi = mock(DeploymentApi.class); ConfigurationApi configurationApi = mock(ConfigurationApi.class); - var props = HandlerTestUtils.aicoreProperties(); + var props = HandlerTestUtils.aicoreProperties(); var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props)); configurer.cdsModel("edmx/csn.json"); 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 ab6308e..9f29c7d 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 @@ -29,8 +29,7 @@ class ActionTest extends BaseIntegrationTest { @BeforeAll void ensureResourceGroupReady() { - ensureResourceGroupProvisioned( - getAICoreService(), getAICoreConfig().defaultResourceGroup()); + ensureResourceGroupProvisioned(getAICoreService(), getAICoreConfig().defaultResourceGroup()); } @Test From a51b0cb7b640ebefda67998e8b2f7769ea969d63 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Thu, 18 Jun 2026 09:17:47 +0200 Subject: [PATCH 12/14] big blunder now fixed whoops --- .../sap/cds/feature/aicore/api/AICore.java | 32 ------------------- .../core/AICoreServiceConfiguration.java | 10 +++--- .../aicore/core/handler/AICoreApiHandler.java | 4 +-- .../aicore/core/handler/ActionHandler.java | 4 +-- .../core/handler/ConfigurationHandler.java | 4 +-- .../core/handler/DeploymentHandler.java | 4 +-- .../core/handler/MockAICoreApiHandler.java | 4 +-- .../core/handler/MockEntityHandler.java | 27 +++++++++------- .../core/handler/ResourceGroupHandler.java | 4 +-- .../core/AICoreServiceConfigurationTest.java | 8 ++--- .../AICoreServiceImplDeploymentIdTest.java | 10 +++--- .../core/MockAICoreServiceImplTest.java | 4 +-- .../handler/ConfigurationHandlerTest.java | 11 ++++--- .../core/handler/DeploymentHandlerTest.java | 15 +++++---- .../aicore/core/handler/HandlerTestUtils.java | 8 ++--- .../handler/ResourceGroupHandlerTest.java | 19 +++++------ .../core/handler/TenantScopingTest.java | 17 +++++----- .../RecommendationConfiguration.java | 4 +-- .../api/RptInferenceClient.java | 2 +- .../RecommendationConfigurationTest.java | 6 ++-- .../aicore/itest/mt/MtxLifecycleTest.java | 4 +-- .../itest/mt/SubscribeUnsubscribeTest.java | 4 +-- .../aicore/itest/mt/TenantIsolationTest.java | 4 +-- .../aicore/itest/BaseIntegrationTest.java | 4 +-- .../handlers/AICoreShowcaseHandler.java | 12 ++++--- 25 files changed, 101 insertions(+), 124 deletions(-) delete mode 100644 cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/AICore.java 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 deleted file mode 100644 index 764bb18..0000000 --- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/AICore.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * © 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/core/AICoreServiceConfiguration.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/AICoreServiceConfiguration.java index 976cd24..8ad1bda 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,6 @@ 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.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; @@ -17,6 +16,7 @@ import com.sap.cds.feature.aicore.core.handler.MockAICoreSetupHandler; import com.sap.cds.feature.aicore.core.handler.MockEntityHandler; import com.sap.cds.feature.aicore.core.handler.ResourceGroupHandler; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; import com.sap.cds.services.environment.CdsProperties; import com.sap.cds.services.environment.CdsProperties.Remote.RemoteServiceConfig; import com.sap.cds.services.mt.DeploymentService; @@ -55,15 +55,15 @@ public class AICoreServiceConfiguration implements CdsRuntimeConfiguration { */ @Override public void environment(CdsRuntimeConfigurer configurer) { - RemoteServiceConfig remoteConfig = new RemoteServiceConfig(AICore.SERVICE_NAME); - remoteConfig.setModel(AICore.SERVICE_NAME); + RemoteServiceConfig remoteConfig = new RemoteServiceConfig(AICore_.CDS_NAME); + remoteConfig.setModel(AICore_.CDS_NAME); configurer .getCdsRuntime() .getEnvironment() .getCdsProperties() .getRemote() .getServices() - .putIfAbsent(AICore.SERVICE_NAME, remoteConfig); + .putIfAbsent(AICore_.CDS_NAME, remoteConfig); } @Override @@ -127,7 +127,7 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { } private static boolean hasAICoreModel(CdsRuntime runtime) { - return runtime.getCdsModel().findService(AICore.SERVICE_NAME).isPresent(); + return runtime.getCdsModel().findService(AICore_.CDS_NAME).isPresent(); } private static boolean hasAICoreBinding(CdsRuntime runtime) { 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 613cdd1..d7c9f13 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.AICore; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.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; @@ -38,7 +38,7 @@ *

Contains the business logic for deployment discovery/creation and inference client * construction. Resource-group resolution is delegated to {@link DeploymentResolver}. */ -@ServiceName(AICore.SERVICE_NAME) +@ServiceName(AICore_.CDS_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 8fb95f5..f2ea8ef 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,10 +5,10 @@ import com.sap.ai.sdk.core.model.AiDeploymentModificationRequest; import com.sap.ai.sdk.core.model.AiDeploymentTargetStatus; -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; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; import com.sap.cds.feature.aicore.generated.cds4j.aicore.Deployments; import com.sap.cds.feature.aicore.generated.cds4j.aicore.DeploymentsStopContext; import com.sap.cds.feature.aicore.generated.cds4j.aicore.Deployments_; @@ -19,7 +19,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@ServiceName(AICore.SERVICE_NAME) +@ServiceName(AICore_.CDS_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 76c6642..9ca2de4 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.AICore; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.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(AICore.SERVICE_NAME) +@ServiceName(AICore_.CDS_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 193a5ae..23a90ed 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.AICore; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.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(AICore.SERVICE_NAME) +@ServiceName(AICore_.CDS_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 133ebd1..05b4bfd 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.AICore; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.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; @@ -23,7 +23,7 @@ * 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(AICore.SERVICE_NAME) +@ServiceName(AICore_.CDS_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 4680b7a..a8ea0c9 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,10 @@ package com.sap.cds.feature.aicore.core.handler; import com.sap.cds.CdsData; -import com.sap.cds.feature.aicore.api.AICore; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.Configurations_; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.Deployments_; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.ResourceGroups_; import com.sap.cds.ql.cqn.AnalysisResult; import com.sap.cds.ql.cqn.CqnAnalyzer; import com.sap.cds.ql.cqn.CqnDelete; @@ -25,7 +28,7 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -@ServiceName(AICore.SERVICE_NAME) +@ServiceName(AICore_.CDS_NAME) public class MockEntityHandler implements EventHandler { private final Map> resourceGroups = new ConcurrentHashMap<>(); @@ -34,7 +37,7 @@ public class MockEntityHandler implements EventHandler { // --- Resource Groups --- - @On(entity = AICore.RESOURCE_GROUPS) + @On(entity = ResourceGroups_.CDS_NAME) public void readResourceGroups(CdsReadEventContext context) { CqnSelect select = context.getCqn(); CdsModel model = context.getModel(); @@ -61,7 +64,7 @@ public void readResourceGroups(CdsReadEventContext context) { } } - @On(entity = AICore.RESOURCE_GROUPS) + @On(entity = ResourceGroups_.CDS_NAME) public void createResourceGroups(CdsCreateEventContext context) { CqnInsert insert = context.getCqn(); List> results = new ArrayList<>(); @@ -76,7 +79,7 @@ public void createResourceGroups(CdsCreateEventContext context) { context.setResult(results); } - @On(entity = AICore.RESOURCE_GROUPS) + @On(entity = ResourceGroups_.CDS_NAME) public void updateResourceGroups(CdsUpdateEventContext context) { CqnUpdate update = context.getCqn(); CdsModel model = context.getModel(); @@ -92,7 +95,7 @@ public void updateResourceGroups(CdsUpdateEventContext context) { context.setResult(List.of(CdsData.create(existing))); } - @On(entity = AICore.RESOURCE_GROUPS) + @On(entity = ResourceGroups_.CDS_NAME) public void deleteResourceGroups(CdsDeleteEventContext context) { CqnDelete delete = context.getCqn(); CdsModel model = context.getModel(); @@ -105,7 +108,7 @@ public void deleteResourceGroups(CdsDeleteEventContext context) { // --- Deployments --- - @On(entity = AICore.DEPLOYMENTS) + @On(entity = Deployments_.CDS_NAME) public void readDeployments(CdsReadEventContext context) { CqnSelect select = context.getCqn(); CdsModel model = context.getModel(); @@ -121,7 +124,7 @@ public void readDeployments(CdsReadEventContext context) { } } - @On(entity = AICore.DEPLOYMENTS) + @On(entity = Deployments_.CDS_NAME) public void createDeployments(CdsCreateEventContext context) { CqnInsert insert = context.getCqn(); List> results = new ArrayList<>(); @@ -136,7 +139,7 @@ public void createDeployments(CdsCreateEventContext context) { context.setResult(results); } - @On(entity = AICore.DEPLOYMENTS) + @On(entity = Deployments_.CDS_NAME) public void updateDeployments(CdsUpdateEventContext context) { CqnUpdate update = context.getCqn(); CdsModel model = context.getModel(); @@ -152,7 +155,7 @@ public void updateDeployments(CdsUpdateEventContext context) { context.setResult(List.of(CdsData.create(existing))); } - @On(entity = AICore.DEPLOYMENTS) + @On(entity = Deployments_.CDS_NAME) public void deleteDeployments(CdsDeleteEventContext context) { CqnDelete delete = context.getCqn(); CdsModel model = context.getModel(); @@ -165,7 +168,7 @@ public void deleteDeployments(CdsDeleteEventContext context) { // --- Configurations --- - @On(entity = AICore.CONFIGURATIONS) + @On(entity = Configurations_.CDS_NAME) public void readConfigurations(CdsReadEventContext context) { CqnSelect select = context.getCqn(); CdsModel model = context.getModel(); @@ -181,7 +184,7 @@ public void readConfigurations(CdsReadEventContext context) { } } - @On(entity = AICore.CONFIGURATIONS) + @On(entity = Configurations_.CDS_NAME) 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 016b833..fcf42eb 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.AICore; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.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(AICore.SERVICE_NAME) +@ServiceName(AICore_.CDS_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 42cb7a5..fcea483 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,7 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.sap.cds.feature.aicore.api.AICore; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; import com.sap.cds.services.cds.RemoteService; import com.sap.cds.services.environment.CdsProperties; import com.sap.cds.services.impl.environment.SimplePropertiesProvider; @@ -34,7 +34,7 @@ void noBinding_noMultiTenancy_registersService() { .complete(); RemoteService service = - runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME); + runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); assertThat(service).isNotNull(); } @@ -57,7 +57,7 @@ void noBinding_withSidecarUrl_registersService() { .complete(); RemoteService service = - runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME); + runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); assertThat(service).isNotNull(); } @@ -71,7 +71,7 @@ void noModel_skipsServiceRegistration() { .complete(); RemoteService service = - runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME); + runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_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 84979cf..77c800f 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,7 +23,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.AICore; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.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; @@ -72,9 +72,9 @@ private String cacheKey() { */ 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); + RemoteServiceConfig rsConfig = new RemoteServiceConfig(AICore_.CDS_NAME); + rsConfig.setModel(AICore_.CDS_NAME); + props.getRemote().getServices().put(AICore_.CDS_NAME, rsConfig); CdsRuntimeConfigurer configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props)); @@ -91,7 +91,7 @@ private RemoteService createService(boolean multiTenancy) { configurer.eventHandler(new AICoreApiHandler(config, clients, resolver)); configurer.complete(); - return runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME); + return runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); } @BeforeEach 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 056e0fc..6c4e5bb 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,7 +5,7 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.sap.cds.feature.aicore.api.AICore; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.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; @@ -42,7 +42,7 @@ private RemoteService createMockService(boolean multiTenancy) { .eventHandlerConfigurations() .complete(); - return runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME); + return runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); } @Test 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 580cb7c..03d360a 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,10 +20,11 @@ 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.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; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.Configurations_; import com.sap.cds.ql.Insert; import com.sap.cds.ql.Select; import com.sap.cds.services.cds.RemoteService; @@ -73,7 +74,7 @@ static void bootRuntime() { configurer.eventHandler(new ConfigurationHandler(config, clients, resolver)); configurer.complete(); - service = runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME); + service = runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); } @BeforeEach @@ -101,7 +102,7 @@ void onRead_returnsConfigurationsForResourceGroup() { (Function) ctx -> service.run( - Select.from("AICore.configurations") + Select.from(Configurations_.CDS_NAME) .where(c -> c.get("resourceGroup_resourceGroupId").eq("default")))); verify(configurationApi).query(eq("default"), any(), any(), any(), any(), any(), any(), any()); @@ -124,7 +125,7 @@ void onRead_nullResources_returnsEmptyList() { (Function) ctx -> service.run( - Select.from("AICore.configurations") + Select.from(Configurations_.CDS_NAME) .where(c -> c.get("resourceGroup_resourceGroupId").eq("default")))); assertThat(result.list()).isEmpty(); @@ -144,7 +145,7 @@ void onCreate_createsConfiguration() { (Function) ctx -> service.run( - Insert.into("AICore.configurations") + Insert.into(Configurations_.CDS_NAME) .entry( Map.of( "name", "test-config", 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 c84a4bd..c4fad6c 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,10 +21,11 @@ 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.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; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.Deployments_; import com.sap.cds.ql.Insert; import com.sap.cds.ql.Update; import com.sap.cds.services.ErrorStatuses; @@ -77,7 +78,7 @@ static void bootRuntime() { configurer.eventHandler(new DeploymentHandler(config, clients, resolver)); configurer.complete(); - service = runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME); + service = runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); } @BeforeEach @@ -100,7 +101,7 @@ void onCreate_createsDeploymentWithConfigurationId() { (Function) ctx -> service.run( - Insert.into("AICore.deployments") + Insert.into(Deployments_.CDS_NAME) .entry( Map.of( "configurationId", "cfg-1", @@ -127,7 +128,7 @@ void onCreate_withTtl_setsTtlOnRequest() { (Function) ctx -> service.run( - Insert.into("AICore.deployments") + Insert.into(Deployments_.CDS_NAME) .entry( Map.of( "configurationId", "cfg-2", @@ -148,7 +149,7 @@ void onUpdate_withTargetStatus_callsModifyWithTargetStatus() { (Function) ctx -> service.run( - Update.entity("AICore.deployments") + Update.entity(Deployments_.CDS_NAME) .where(d -> d.get("id").eq("dep-123")) .data("targetStatus", "STOPPED"))); @@ -166,7 +167,7 @@ void onUpdate_withConfigurationId_callsModifyWithConfigurationId() { (Function) ctx -> service.run( - Update.entity("AICore.deployments") + Update.entity(Deployments_.CDS_NAME) .where(d -> d.get("id").eq("dep-789")) .data("configurationId", "config-456"))); @@ -186,7 +187,7 @@ void onUpdate_withoutTargetStatusOrConfigurationId_throwsBadRequest() { (Function) ctx -> service.run( - Update.entity("AICore.deployments") + Update.entity(Deployments_.CDS_NAME) .where(d -> d.get("id").eq("dep-x")) .data("ttl", "1d")))) .isInstanceOfSatisfying( diff --git a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/HandlerTestUtils.java b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/HandlerTestUtils.java index 77db33c..a8ac72c 100644 --- a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/HandlerTestUtils.java +++ b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/HandlerTestUtils.java @@ -3,7 +3,7 @@ */ package com.sap.cds.feature.aicore.core.handler; -import com.sap.cds.feature.aicore.api.AICore; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; import com.sap.cds.services.environment.CdsProperties; import com.sap.cds.services.environment.CdsProperties.Remote.RemoteServiceConfig; @@ -15,9 +15,9 @@ private HandlerTestUtils() {} /** Creates CdsProperties with the AICore RemoteService configured. */ static CdsProperties aicoreProperties() { CdsProperties props = new CdsProperties(); - RemoteServiceConfig rsConfig = new RemoteServiceConfig(AICore.SERVICE_NAME); - rsConfig.setModel(AICore.SERVICE_NAME); - props.getRemote().getServices().put(AICore.SERVICE_NAME, rsConfig); + RemoteServiceConfig rsConfig = new RemoteServiceConfig(AICore_.CDS_NAME); + rsConfig.setModel(AICore_.CDS_NAME); + props.getRemote().getServices().put(AICore_.CDS_NAME, rsConfig); return props; } } 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 71fa0f8..0a1f23d 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,10 +22,11 @@ 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.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; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.ResourceGroups_; import com.sap.cds.ql.Insert; import com.sap.cds.ql.Select; import com.sap.cds.ql.Update; @@ -76,7 +77,7 @@ static void bootRuntime() { configurer.eventHandler(new ResourceGroupHandler(config, clients, resolver)); configurer.complete(); - service = runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME); + service = runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); } @BeforeEach @@ -99,7 +100,7 @@ void onRead_returnsAllResourceGroups() { .requestContext() .run( (Function) - ctx -> service.run(Select.from("AICore.resourceGroups"))); + ctx -> service.run(Select.from(ResourceGroups_.CDS_NAME))); verify(resourceGroupApi).getAll(any(), any(), any(), any(), any(), any(), any()); assertThat(result.list()).hasSize(1); @@ -114,7 +115,7 @@ void onCreate_createsResourceGroup() { (Function) ctx -> service.run( - Insert.into("AICore.resourceGroups") + Insert.into(ResourceGroups_.CDS_NAME) .entry(Map.of("resourceGroupId", "rg-new")))); ArgumentCaptor captor = @@ -131,7 +132,7 @@ void onCreate_withTenantId_setsTenantLabel() { (Function) ctx -> service.run( - Insert.into("AICore.resourceGroups") + Insert.into(ResourceGroups_.CDS_NAME) .entry( Map.of( "resourceGroupId", "rg-tenant", @@ -158,7 +159,7 @@ void onUpdate_withLabels_callsPatchWithLabels() { (Function) ctx -> service.run( - Update.entity("AICore.resourceGroups") + Update.entity(ResourceGroups_.CDS_NAME) .where(d -> d.get("resourceGroupId").eq("rg-upd")) .data("labels", List.of(Map.of("key", "env", "value", "staging"))))); @@ -183,7 +184,7 @@ void onUpdate_withoutLabels_callsPatchWithoutLabels() { (Function) ctx -> service.run( - Update.entity("AICore.resourceGroups") + Update.entity(ResourceGroups_.CDS_NAME) .where(d -> d.get("resourceGroupId").eq("rg-nolabel")) .data("statusMessage", "updated"))); @@ -229,7 +230,7 @@ static void bootMtRuntime() { configurer.complete(); mtService = - mtRuntime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME); + mtRuntime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); } @BeforeEach @@ -256,7 +257,7 @@ void readAll_multiTenancy_nonProviderUser_restrictsByCurrentTenant() { .setIsAuthenticated(true)) .run( (Function) - ctx -> mtService.run(Select.from("AICore.resourceGroups"))); + ctx -> mtService.run(Select.from(ResourceGroups_.CDS_NAME))); ArgumentCaptor> selectorCaptor = ArgumentCaptor.forClass(List.class); verify(mtResourceGroupApi) 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 801a14a..a79326f 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,10 +18,11 @@ 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.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; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.Deployments_; import com.sap.cds.ql.Select; import com.sap.cds.services.ServiceException; import com.sap.cds.services.cds.RemoteService; @@ -70,7 +71,7 @@ static void bootRuntime() { configurer.eventHandler(new DeploymentHandler(config, clients, resolver)); configurer.complete(); - service = runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME); + service = runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); } @BeforeEach @@ -98,7 +99,7 @@ void matchingTenant_allowsAccess() { (Function) ctx -> service.run( - Select.from("AICore.deployments") + Select.from(Deployments_.CDS_NAME) .where( d -> d.get("resourceGroup_resourceGroupId") @@ -125,7 +126,7 @@ void nonMatchingTenant_throws404() { (Function) ctx -> service.run( - Select.from("AICore.deployments") + Select.from(Deployments_.CDS_NAME) .where( d -> d.get("resourceGroup_resourceGroupId") @@ -149,7 +150,7 @@ void providerUser_bypassesTenantCheck() { (Function) ctx -> service.run( - Select.from("AICore.deployments") + Select.from(Deployments_.CDS_NAME) .where( d -> d.get("resourceGroup_resourceGroupId") @@ -177,7 +178,7 @@ void nullTenantUser_bypassesTenantCheck() { (Function) ctx -> service.run( - Select.from("AICore.deployments") + Select.from(Deployments_.CDS_NAME) .where( d -> d.get("resourceGroup_resourceGroupId") @@ -206,7 +207,7 @@ void noLabelsOnResourceGroup_throws404() { (Function) ctx -> service.run( - Select.from("AICore.deployments") + Select.from(Deployments_.CDS_NAME) .where( d -> d.get("resourceGroup_resourceGroupId") @@ -236,7 +237,7 @@ void emptyLabelsOnResourceGroup_throws404() { (Function) ctx -> service.run( - Select.from("AICore.deployments") + Select.from(Deployments_.CDS_NAME) .where( d -> d.get("resourceGroup_resourceGroupId") 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 01995de..10852f0 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,7 +3,7 @@ */ package com.sap.cds.feature.recommendation; -import com.sap.cds.feature.aicore.api.AICore; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.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; @@ -32,7 +32,7 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { ServiceCatalog serviceCatalog = runtime.getServiceCatalog(); RemoteService aiCoreService = - serviceCatalog.getService(RemoteService.class, AICore.SERVICE_NAME); + serviceCatalog.getService(RemoteService.class, AICore_.CDS_NAME); if (aiCoreService == null) { logger.info("No AICoreService found, skipping Fiori recommendation handler registration."); 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 06f6f4d..c3e3c43 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 @@ -33,7 +33,7 @@ *

Example usage: * *

{@code
- * RemoteService service = runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
+ * RemoteService service = runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME);
  * ResourceGroupContext rgCtx = ResourceGroupContext.create();
  * service.emit(rgCtx);
  * String rg = rgCtx.getResult();
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 4ab68d8..ee9ee52 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,7 +5,7 @@
 
 import static org.mockito.Mockito.*;
 
-import com.sap.cds.feature.aicore.api.AICore;
+import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_;
 import com.sap.cds.services.ServiceCatalog;
 import com.sap.cds.services.cds.RemoteService;
 import com.sap.cds.services.environment.CdsEnvironment;
@@ -34,7 +34,7 @@ void aiCoreServiceFound_registersHandler() {
     when(runtime.getServiceCatalog()).thenReturn(serviceCatalog);
     when(runtime.getEnvironment()).thenReturn(environment);
     when(environment.getServiceBindings()).thenReturn(Stream.empty());
-    when(serviceCatalog.getService(RemoteService.class, AICore.SERVICE_NAME))
+    when(serviceCatalog.getService(RemoteService.class, AICore_.CDS_NAME))
         .thenReturn(aiCoreService);
     when(serviceCatalog.getService(PersistenceService.class, PersistenceService.DEFAULT_NAME))
         .thenReturn(persistenceService);
@@ -48,7 +48,7 @@ void aiCoreServiceFound_registersHandler() {
   void aiCoreServiceNull_doesNotRegisterHandler() {
     when(configurer.getCdsRuntime()).thenReturn(runtime);
     when(runtime.getServiceCatalog()).thenReturn(serviceCatalog);
-    when(serviceCatalog.getService(RemoteService.class, AICore.SERVICE_NAME)).thenReturn(null);
+    when(serviceCatalog.getService(RemoteService.class, AICore_.CDS_NAME)).thenReturn(null);
 
     new RecommendationConfiguration().eventHandlers(configurer);
 
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 1ad8aa5..30e5a4d 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,7 +7,7 @@
 import static org.assertj.core.api.Assertions.assertThatCode;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.sap.cds.feature.aicore.api.AICore;
+import com.sap.cds.feature.aicore.generated.cds4j.aicore.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;
@@ -75,6 +75,6 @@ void subscribeUnsubscribe_repeatedTwice_completesCleanly() throws Exception {
   }
 
   private RemoteService getService() {
-    return runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
+    return runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_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 fa53769..185d387 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,7 +9,7 @@
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.sap.cds.feature.aicore.api.AICore;
+import com.sap.cds.feature.aicore.generated.cds4j.aicore.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;
@@ -88,6 +88,6 @@ void tearDown() {
   }
 
   private RemoteService getService() {
-    return runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
+    return runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_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 5e44cfc..d30830f 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,7 +6,7 @@
 import static org.assertj.core.api.Assertions.assertThat;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.sap.cds.feature.aicore.api.AICore;
+import com.sap.cds.feature.aicore.generated.cds4j.aicore.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;
@@ -81,7 +81,7 @@ void resourceGroupPrefix_applied() throws Exception {
   }
 
   private RemoteService getService() {
-    return runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
+    return runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME);
   }
 
   private AICoreConfig getConfig() {
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 48dfcfb..0d9860c 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,7 +5,7 @@
 
 import com.sap.cds.Result;
 import com.sap.cds.Row;
-import com.sap.cds.feature.aicore.api.AICore;
+import com.sap.cds.feature.aicore.generated.cds4j.aicore.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;
@@ -41,7 +41,7 @@ public abstract class BaseIntegrationTest {
   @Autowired protected CdsRuntime runtime;
 
   protected RemoteService getAICoreService() {
-    return runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
+    return runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME);
   }
 
   protected AICoreConfig getAICoreConfig() {
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 f62e320..e7ce877 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,9 @@
 
 import com.sap.cds.CdsData;
 import com.sap.cds.Result;
-import com.sap.cds.feature.aicore.api.AICore;
+import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_;
+import com.sap.cds.feature.aicore.generated.cds4j.aicore.Configurations_;
+import com.sap.cds.feature.aicore.generated.cds4j.aicore.Deployments_;
 import com.sap.cds.feature.aicore.api.DeploymentIdContext;
 import com.sap.cds.feature.aicore.api.InferenceClientContext;
 import com.sap.cds.feature.aicore.api.ResourceGroupContext;
@@ -33,7 +35,7 @@ public class AICoreShowcaseHandler implements EventHandler {
   @Autowired private CdsRuntime runtime;
 
   private RemoteService getAICoreService() {
-    return runtime.getServiceCatalog().getService(RemoteService.class, AICore.SERVICE_NAME);
+    return runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME);
   }
 
   // This handler is NOT required - the plugin automatically delegates reads on projections
@@ -41,7 +43,7 @@ private RemoteService getAICoreService() {
   // programmatically, e.g. for custom filtering or post-processing.
   @On(event = CqnService.EVENT_READ, entity = "AICoreShowcaseService.Configurations")
   public void onReadConfigurations(CdsReadEventContext context) {
-    context.setResult(getAICoreService().run(Select.from("AICore.configurations")));
+    context.setResult(getAICoreService().run(Select.from(Configurations_.CDS_NAME)));
   }
 
   @On(event = "setupTenantResources")
@@ -84,7 +86,7 @@ public void onStopDeployment(EventContext context) {
 
     getAICoreService()
         .run(
-            Update.entity("AICore.deployments")
+            Update.entity(Deployments_.CDS_NAME)
                 .where(d -> d.get("id").eq(deploymentId))
                 .data(
                     Map.of(
@@ -105,7 +107,7 @@ public void onCreateConfiguration(EventContext context) {
     Result result =
         getAICoreService()
             .run(
-                Insert.into("AICore.configurations")
+                Insert.into(Configurations_.CDS_NAME)
                     .entry(
                         Map.of(
                             "name", name,

From 2b8bb3789f4fb8d0fe22decdf4a344e35c6daf51 Mon Sep 17 00:00:00 2001
From: Marvin Lindner 
Date: Thu, 18 Jun 2026 09:23:37 +0200
Subject: [PATCH 13/14] last occurences

---
 .../cds/feature/aicore/itest/ActionTest.java  |  7 +++---
 .../aicore/itest/BaseIntegrationTest.java     |  7 +++---
 .../aicore/itest/ConfigurationTest.java       | 13 ++++++-----
 .../feature/aicore/itest/DeploymentTest.java  | 13 ++++++-----
 .../aicore/itest/ResourceGroupTest.java       | 23 ++++++++++---------
 5 files changed, 34 insertions(+), 29 deletions(-)

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 9f29c7d..42ae5d3 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
@@ -12,6 +12,7 @@
 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.aicore.generated.cds4j.aicore.Deployments_;
 import com.sap.cds.feature.recommendation.api.RptModelSpec;
 import com.sap.cds.ql.Select;
 import com.sap.cds.ql.Update;
@@ -100,7 +101,7 @@ void stop_deployment_changesTargetStatus() {
 
     Result deployments =
         service.run(
-            Select.from("AICore.deployments")
+            Select.from(Deployments_.CDS_NAME)
                 .where(d -> d.get("resourceGroup_resourceGroupId").eq(resourceGroup)));
 
     String deploymentId = null;
@@ -116,14 +117,14 @@ void stop_deployment_changesTargetStatus() {
     final String targetId = deploymentId;
 
     service.run(
-        Update.entity("AICore.deployments")
+        Update.entity(Deployments_.CDS_NAME)
             .where(d -> d.get("id").eq(targetId))
             .data(
                 Map.of("targetStatus", "STOPPED", "resourceGroup_resourceGroupId", resourceGroup)));
 
     Result readResult =
         service.run(
-            Select.from("AICore.deployments")
+            Select.from(Deployments_.CDS_NAME)
                 .where(
                     d ->
                         d.get("id")
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 0d9860c..5377883 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
@@ -6,6 +6,7 @@
 import com.sap.cds.Result;
 import com.sap.cds.Row;
 import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_;
+import com.sap.cds.feature.aicore.generated.cds4j.aicore.ResourceGroups_;
 import com.sap.cds.feature.aicore.api.DeploymentIdContext;
 import com.sap.cds.feature.aicore.core.AICoreConfig;
 import com.sap.cds.feature.recommendation.api.RptModelSpec;
@@ -70,7 +71,7 @@ protected void ensureResourceGroupProvisioned(RemoteService service, String reso
     if (!resourceGroupExists(service, resourceGroup)) {
       logger.info("Creating resource group {} with itest owner label", resourceGroup);
       service.run(
-          Insert.into("AICore.resourceGroups")
+          Insert.into(ResourceGroups_.CDS_NAME)
               .entry(
                   Map.of(
                       "resourceGroupId",
@@ -82,7 +83,7 @@ protected void ensureResourceGroupProvisioned(RemoteService service, String reso
   }
 
   private boolean resourceGroupExists(RemoteService service, String resourceGroup) {
-    Result all = service.run(Select.from("AICore.resourceGroups"));
+    Result all = service.run(Select.from(ResourceGroups_.CDS_NAME));
     for (Row row : all) {
       if (resourceGroup.equals(row.get("resourceGroupId"))) {
         return true;
@@ -93,7 +94,7 @@ private boolean resourceGroupExists(RemoteService 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"));
+      Result all = service.run(Select.from(ResourceGroups_.CDS_NAME));
       for (Row row : all) {
         if (resourceGroup.equals(row.get("resourceGroupId"))) {
           String status = (String) row.get("status");
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 bdc62fb..db4a823 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
@@ -7,6 +7,7 @@
 
 import com.sap.cds.Result;
 import com.sap.cds.Row;
+import com.sap.cds.feature.aicore.generated.cds4j.aicore.Configurations_;
 import com.sap.cds.ql.Insert;
 import com.sap.cds.ql.Select;
 import com.sap.cds.services.cds.RemoteService;
@@ -22,7 +23,7 @@ void readAll_returnsConfigurations() {
     String resourceGroup = getAICoreConfig().defaultResourceGroup();
     Result result =
         service.run(
-            Select.from("AICore.configurations")
+            Select.from(Configurations_.CDS_NAME)
                 .where(c -> c.get("resourceGroup_resourceGroupId").eq(resourceGroup)));
 
     assertThat(result.list()).isNotNull();
@@ -34,7 +35,7 @@ void readAll_filterByScenario() {
     String resourceGroup = getAICoreConfig().defaultResourceGroup();
     Result result =
         service.run(
-            Select.from("AICore.configurations")
+            Select.from(Configurations_.CDS_NAME)
                 .where(
                     c ->
                         c.get("scenarioId")
@@ -52,7 +53,7 @@ void create_andReadById() {
     String configName = "itest-config-" + System.currentTimeMillis();
     Result created =
         service.run(
-            Insert.into("AICore.configurations")
+            Insert.into(Configurations_.CDS_NAME)
                 .entry(
                     Map.of(
                         "name",
@@ -75,7 +76,7 @@ void create_andReadById() {
     // Read back by ID
     Result readResult =
         service.run(
-            Select.from("AICore.configurations")
+            Select.from(Configurations_.CDS_NAME)
                 .where(
                     c ->
                         c.get("id")
@@ -97,7 +98,7 @@ void create_withParameterBindings_mapsCorrectly() {
     String configName = "itest-params-" + System.currentTimeMillis();
     Result created =
         service.run(
-            Insert.into("AICore.configurations")
+            Insert.into(Configurations_.CDS_NAME)
                 .entry(
                     Map.of(
                         "name",
@@ -117,7 +118,7 @@ void create_withParameterBindings_mapsCorrectly() {
 
     Result readResult =
         service.run(
-            Select.from("AICore.configurations")
+            Select.from(Configurations_.CDS_NAME)
                 .where(
                     c ->
                         c.get("id")
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 cc9ef53..e4fbba8 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
@@ -8,6 +8,7 @@
 
 import com.sap.cds.Result;
 import com.sap.cds.Row;
+import com.sap.cds.feature.aicore.generated.cds4j.aicore.Deployments_;
 import com.sap.cds.ql.Select;
 import com.sap.cds.ql.Update;
 import com.sap.cds.services.cds.RemoteService;
@@ -23,7 +24,7 @@ void readAll_returnsDeployments() {
     String resourceGroup = getAICoreConfig().defaultResourceGroup();
     Result result =
         service.run(
-            Select.from("AICore.deployments")
+            Select.from(Deployments_.CDS_NAME)
                 .where(d -> d.get("resourceGroup_resourceGroupId").eq(resourceGroup)));
 
     assertThat(result.list()).isNotNull();
@@ -35,7 +36,7 @@ void readSingle_returnsDeploymentDetails() {
     String resourceGroup = getAICoreConfig().defaultResourceGroup();
     Result all =
         service.run(
-            Select.from("AICore.deployments")
+            Select.from(Deployments_.CDS_NAME)
                 .where(d -> d.get("resourceGroup_resourceGroupId").eq(resourceGroup)));
 
     assumeFalse(all.list().isEmpty(), "No deployments available");
@@ -43,7 +44,7 @@ void readSingle_returnsDeploymentDetails() {
     String id = (String) all.list().get(0).get("id");
     Result single =
         service.run(
-            Select.from("AICore.deployments")
+            Select.from(Deployments_.CDS_NAME)
                 .where(
                     d ->
                         d.get("id")
@@ -67,7 +68,7 @@ void update_targetStatus_stopsRunningDeployment() {
 
     Result deployments =
         service.run(
-            Select.from("AICore.deployments")
+            Select.from(Deployments_.CDS_NAME)
                 .where(d -> d.get("resourceGroup_resourceGroupId").eq(resourceGroup)));
 
     String deploymentId = null;
@@ -83,14 +84,14 @@ void update_targetStatus_stopsRunningDeployment() {
     final String targetId = deploymentId;
 
     service.run(
-        Update.entity("AICore.deployments")
+        Update.entity(Deployments_.CDS_NAME)
             .where(d -> d.get("id").eq(targetId))
             .data(
                 Map.of("targetStatus", "STOPPED", "resourceGroup_resourceGroupId", resourceGroup)));
 
     Result readResult =
         service.run(
-            Select.from("AICore.deployments")
+            Select.from(Deployments_.CDS_NAME)
                 .where(
                     d ->
                         d.get("id")
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 bc63a28..fea4ff2 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
@@ -8,6 +8,7 @@
 
 import com.sap.cds.Result;
 import com.sap.cds.Row;
+import com.sap.cds.feature.aicore.generated.cds4j.aicore.ResourceGroups_;
 import com.sap.cds.ql.Delete;
 import com.sap.cds.ql.Insert;
 import com.sap.cds.ql.Select;
@@ -33,7 +34,7 @@ void cleanup() {
         RemoteService service = getAICoreService();
         waitForResourceGroupProvisioned(service, createdResourceGroupId);
         service.run(
-            Delete.from("AICore.resourceGroups")
+            Delete.from(ResourceGroups_.CDS_NAME)
                 .where(r -> r.get("resourceGroupId").eq(createdResourceGroupId)));
       } catch (Exception ignored) {
       }
@@ -47,12 +48,12 @@ void create_andRead_resourceGroup() {
     RemoteService service = getAICoreService();
 
     service.run(
-        Insert.into("AICore.resourceGroups")
+        Insert.into(ResourceGroups_.CDS_NAME)
             .entry(Map.of("resourceGroupId", createdResourceGroupId)));
 
     Result result =
         service.run(
-            Select.from("AICore.resourceGroups")
+            Select.from(ResourceGroups_.CDS_NAME)
                 .where(r -> r.get("resourceGroupId").eq(createdResourceGroupId)));
 
     assertThat(result.list()).hasSize(1);
@@ -68,12 +69,12 @@ void create_withTenantLabel_andFilterByTenant() {
     RemoteService service = getAICoreService();
 
     service.run(
-        Insert.into("AICore.resourceGroups")
+        Insert.into(ResourceGroups_.CDS_NAME)
             .entry(Map.of("resourceGroupId", createdResourceGroupId, "tenantId", tenantId)));
 
     Result result =
         service.run(
-            Select.from("AICore.resourceGroups").where(r -> r.get("tenantId").eq(tenantId)));
+            Select.from(ResourceGroups_.CDS_NAME).where(r -> r.get("tenantId").eq(tenantId)));
 
     assertThat(result.list()).isNotEmpty();
     Row row = result.first().orElseThrow();
@@ -83,7 +84,7 @@ void create_withTenantLabel_andFilterByTenant() {
   @Test
   void readAll_returnsResourceGroups() {
     RemoteService service = getAICoreService();
-    Result result = service.run(Select.from("AICore.resourceGroups"));
+    Result result = service.run(Select.from(ResourceGroups_.CDS_NAME));
     assertThat(result.list()).isNotNull();
   }
 
@@ -93,7 +94,7 @@ void create_withLabels() {
     RemoteService service = getAICoreService();
 
     service.run(
-        Insert.into("AICore.resourceGroups")
+        Insert.into(ResourceGroups_.CDS_NAME)
             .entry(
                 Map.of(
                     "resourceGroupId",
@@ -106,7 +107,7 @@ void create_withLabels() {
 
     Result result =
         service.run(
-            Select.from("AICore.resourceGroups")
+            Select.from(ResourceGroups_.CDS_NAME)
                 .where(r -> r.get("resourceGroupId").eq(createdResourceGroupId)));
 
     assertThat(result.list()).hasSize(1);
@@ -121,14 +122,14 @@ void delete_resourceGroup() throws InterruptedException {
     String rgId = TEST_RG_PREFIX + "del-" + System.currentTimeMillis();
     RemoteService service = getAICoreService();
 
-    service.run(Insert.into("AICore.resourceGroups").entry(Map.of("resourceGroupId", rgId)));
+    service.run(Insert.into(ResourceGroups_.CDS_NAME).entry(Map.of("resourceGroupId", rgId)));
 
     waitForResourceGroupProvisioned(service, rgId);
 
     assertThatCode(
             () ->
                 service.run(
-                    Delete.from("AICore.resourceGroups")
+                    Delete.from(ResourceGroups_.CDS_NAME)
                         .where(r -> r.get("resourceGroupId").eq(rgId))))
         .doesNotThrowAnyException();
 
@@ -140,7 +141,7 @@ private void waitForResourceGroupProvisioned(RemoteService service, String rgId)
     for (int i = 0; i < 30; i++) {
       Result result =
           service.run(
-              Select.from("AICore.resourceGroups").where(r -> r.get("resourceGroupId").eq(rgId)));
+              Select.from(ResourceGroups_.CDS_NAME).where(r -> r.get("resourceGroupId").eq(rgId)));
       if (!result.list().isEmpty()) {
         String status = (String) result.single().get("status");
         if ("PROVISIONED".equals(status)) {

From 5bcba6bb3b1f40ab5c8fa4055aab78146e2edf66 Mon Sep 17 00:00:00 2001
From: Marvin Lindner 
Date: Thu, 18 Jun 2026 09:39:24 +0200
Subject: [PATCH 14/14] spotless

---
 .../cds/feature/aicore/core/handler/AICoreApiHandler.java    | 2 +-
 .../feature/aicore/core/handler/ConfigurationHandler.java    | 2 +-
 .../cds/feature/aicore/core/handler/DeploymentHandler.java   | 2 +-
 .../feature/aicore/core/handler/MockAICoreApiHandler.java    | 2 +-
 .../feature/aicore/core/handler/ResourceGroupHandler.java    | 2 +-
 .../aicore/core/AICoreServiceImplDeploymentIdTest.java       | 2 +-
 .../cds/feature/aicore/core/MockAICoreServiceImplTest.java   | 2 +-
 .../aicore/core/handler/ResourceGroupHandlerTest.java        | 3 +--
 .../feature/recommendation/RecommendationConfiguration.java  | 5 ++---
 .../sap/cds/feature/aicore/itest/BaseIntegrationTest.java    | 4 ++--
 10 files changed, 12 insertions(+), 14 deletions(-)

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 d7c9f13..2ab2ee7 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,6 @@
 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.generated.cds4j.aicore.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;
@@ -18,6 +17,7 @@
 import com.sap.cds.feature.aicore.core.AICoreClients;
 import com.sap.cds.feature.aicore.core.AICoreConfig;
 import com.sap.cds.feature.aicore.core.DeploymentResolver;
+import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_;
 import com.sap.cds.services.ErrorStatuses;
 import com.sap.cds.services.ServiceException;
 import com.sap.cds.services.handler.EventHandler;
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 9ca2de4..01a9f4b 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,10 +7,10 @@
 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.generated.cds4j.aicore.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;
+import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_;
 import com.sap.cds.feature.aicore.generated.cds4j.aicore.ArtifactArgumentBinding;
 import com.sap.cds.feature.aicore.generated.cds4j.aicore.Configurations;
 import com.sap.cds.feature.aicore.generated.cds4j.aicore.Configurations_;
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 23a90ed..c16f166 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,10 +9,10 @@
 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.generated.cds4j.aicore.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;
+import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_;
 import com.sap.cds.feature.aicore.generated.cds4j.aicore.Deployments;
 import com.sap.cds.feature.aicore.generated.cds4j.aicore.Deployments_;
 import com.sap.cds.feature.aicore.generated.cds4j.aicore.ResourceGroups;
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 05b4bfd..b8916c1 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,12 +3,12 @@
  */
 package com.sap.cds.feature.aicore.core.handler;
 
-import com.sap.cds.feature.aicore.generated.cds4j.aicore.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;
 import com.sap.cds.feature.aicore.api.ResourceGroupContext;
 import com.sap.cds.feature.aicore.core.AICoreConfig;
+import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_;
 import com.sap.cds.services.ErrorStatuses;
 import com.sap.cds.services.ServiceException;
 import com.sap.cds.services.handler.EventHandler;
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 fcf42eb..567f292 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,10 +9,10 @@
 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.generated.cds4j.aicore.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;
+import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_;
 import com.sap.cds.feature.aicore.generated.cds4j.aicore.ResourceGroups;
 import com.sap.cds.feature.aicore.generated.cds4j.aicore.ResourceGroups_;
 import com.sap.cds.ql.cqn.AnalysisResult;
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 77c800f..e43f64b 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,11 +23,11 @@
 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.generated.cds4j.aicore.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.feature.aicore.generated.cds4j.aicore.AICore_;
 import com.sap.cds.services.cds.RemoteService;
 import com.sap.cds.services.environment.CdsProperties;
 import com.sap.cds.services.environment.CdsProperties.Remote.RemoteServiceConfig;
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 6c4e5bb..00919b7 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,11 +5,11 @@
 
 import static org.assertj.core.api.Assertions.assertThat;
 
-import com.sap.cds.feature.aicore.generated.cds4j.aicore.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.feature.aicore.generated.cds4j.aicore.AICore_;
 import com.sap.cds.services.cds.RemoteService;
 import com.sap.cds.services.environment.CdsProperties;
 import com.sap.cds.services.impl.environment.SimplePropertiesProvider;
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 0a1f23d..abe4335 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
@@ -229,8 +229,7 @@ static void bootMtRuntime() {
       configurer.eventHandler(new ResourceGroupHandler(config, clients, resolver));
       configurer.complete();
 
-      mtService =
-          mtRuntime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME);
+      mtService = mtRuntime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME);
     }
 
     @BeforeEach
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 10852f0..773526e 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,10 +3,10 @@
  */
 package com.sap.cds.feature.recommendation;
 
-import com.sap.cds.feature.aicore.generated.cds4j.aicore.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.aicore.generated.cds4j.aicore.AICore_;
 import com.sap.cds.feature.recommendation.api.RecommendationClient;
 import com.sap.cds.feature.recommendation.api.RecommendationClientResolver;
 import com.sap.cds.feature.recommendation.api.RptInferenceClient;
@@ -31,8 +31,7 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) {
     CdsRuntime runtime = configurer.getCdsRuntime();
     ServiceCatalog serviceCatalog = runtime.getServiceCatalog();
 
-    RemoteService aiCoreService =
-        serviceCatalog.getService(RemoteService.class, AICore_.CDS_NAME);
+    RemoteService aiCoreService = serviceCatalog.getService(RemoteService.class, AICore_.CDS_NAME);
 
     if (aiCoreService == null) {
       logger.info("No AICoreService found, skipping Fiori recommendation handler registration.");
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 5377883..ea356aa 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,10 +5,10 @@
 
 import com.sap.cds.Result;
 import com.sap.cds.Row;
-import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_;
-import com.sap.cds.feature.aicore.generated.cds4j.aicore.ResourceGroups_;
 import com.sap.cds.feature.aicore.api.DeploymentIdContext;
 import com.sap.cds.feature.aicore.core.AICoreConfig;
+import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_;
+import com.sap.cds.feature.aicore.generated.cds4j.aicore.ResourceGroups_;
 import com.sap.cds.feature.recommendation.api.RptModelSpec;
 import com.sap.cds.ql.Insert;
 import com.sap.cds.ql.Select;