Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e7e925f
Separate the predictionRow from the contextRows and add a constant fo…
lisajulia Jun 15, 2026
c2b35ed
Add comment on why we the @FunctionalInterface annotation is useful
lisajulia Jun 15, 2026
5437c97
Add comment to FioriRecommendationHandler
lisajulia Jun 16, 2026
3bd12c7
Moved everthing RPT-1 specific into RptInferenceClient
lisajulia Jun 16, 2026
28046c7
Rename RptInferenceClient.api -> RptInferenceClient.rpt and extract c…
lisajulia Jun 16, 2026
9c1abbe
Moved everthing RPT-1 specific into RptInferenceClient
lisajulia Jun 16, 2026
59d11ea
Replace MANAGED_FIELDS set with annotation-driven exclusion: @Core.Co…
lisajulia Jun 16, 2026
e0f491f
Change level of log statement when no suitable context columns are fo…
lisajulia Jun 16, 2026
7c2a8cb
Move 'return early if predictRow == null' to earlier in afterRead of …
lisajulia Jun 16, 2026
d6c8f00
Extract 'SAP_Recommendations' into a constant
lisajulia Jun 16, 2026
e39677f
Get Persistence Service db via Dependency Injection
lisajulia Jun 16, 2026
fc3926c
Move missingPredictionElementNames to earlier in the afterRead of the…
lisajulia Jun 16, 2026
f790bbf
Add comment about conversion from List<Row> to List<CdsData>
lisajulia Jun 16, 2026
68bb6e1
Change check for active Entity: only return early if isActiveEntity i…
lisajulia Jun 16, 2026
b60c3c1
Add comments to MockRecommendationClient
lisajulia Jun 16, 2026
bcf16de
Change type of slectcolumns to Set
lisajulia Jun 16, 2026
3d40bec
Add test: row for which we want to do predictions is automatically ex…
lisajulia Jun 16, 2026
5339f52
Add comment about ordering in the select query returned by buildConte…
lisajulia Jun 16, 2026
aad972d
Add comment computeSyntheticKey method and add test
lisajulia Jun 16, 2026
1833485
Make sure we dont react on @cds.odata.valuelist : false and add a tes…
lisajulia Jun 16, 2026
ec4deea
Simplify filters in computeContextColumn
lisajulia Jun 16, 2026
fa7e510
Use any single key as RPT-1 index column, not just fields called ID a…
lisajulia Jun 16, 2026
1e51867
Adjust sample to changes in cds-feature-recomendations
lisajulia Jun 16, 2026
793d144
Add comment about books entity in test model
lisajulia Jun 16, 2026
23e644c
Update javadoc
lisajulia Jun 16, 2026
45f45de
Do not register FioriRecommendationHandler if no PersistenceService i…
lisajulia Jun 16, 2026
fa4966e
In RptInferenceClient:resolveIndexColumn: fall back to synthetic inde…
lisajulia Jun 16, 2026
1cfc6cd
Add checks for the presence of keyNames
lisajulia Jun 16, 2026
93586d5
remove aicoreservice interface and impl and introduce constants
Schmarvinius Jun 17, 2026
2f33a48
update tests
Schmarvinius Jun 17, 2026
972ea66
update recommendations
Schmarvinius Jun 17, 2026
e2cff87
update itests
Schmarvinius Jun 17, 2026
0c50d81
update sample
Schmarvinius Jun 17, 2026
91a2580
fix sample
Schmarvinius Jun 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Use {@link #SERVICE_NAME} for service catalog lookups and {@code @ServiceName} annotations.
* Use the entity constants for {@code @On(entity = ...)} handler annotations.
*
* <p>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";
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
/**
* Typed {@link EventContext} for the {@code deploymentId} event.
*
* <p>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.
* <p>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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
/**
* Typed {@link EventContext} for the {@code inferenceClient} event.
*
* <p>Emitted by {@link AICoreService#inferenceClient(String, String)} to build an {@link ApiClient}
* preconfigured with the inference destination for the given deployment.
* <p>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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
/**
* Typed {@link EventContext} for the {@code resourceGroup} event.
*
* <p>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.
* <p>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.
*
* <p>If {@link #getTenantId()} is non-null, the handler uses the explicit tenant ID. Otherwise, the
* current tenant is read from the {@code RequestContext}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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.
*
* <p>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.
* <p>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}.
*
* <p>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 {

Expand All @@ -43,42 +47,31 @@ 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
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;
}

Expand All @@ -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));
Expand All @@ -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);
Expand All @@ -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;
}
}
Loading
Loading