Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ See [`cds-starter-ai`](cds-starter-ai/README.md) for the quickest setup.
## Prerequisites

- Java 17+
- Maven 3.6.3+
- CAP Java 5.0+
- Node.js 20+ with `@sap/cds-dk` 9+ (for CDS build tooling)
- An [SAP AI Core](https://help.sap.com/docs/sap-ai-core) service binding (for `cds-feature-ai-core` and `cds-feature-recommendations`)
- An [SAP AI Core](https://help.sap.com/docs/sap-ai-core) service binding (for production use)

Without the respective service binding, each plugin falls back to a mock or degraded mode for local development.
The build is hermetic: `cds-maven-plugin` downloads its own Node runtime and `cds-feature-ai-core/package.json` pins `@sap/cds-dk`. A globally installed `@sap/cds-dk` is **not** required.

Without an AI Core binding the plugins fall back to mock implementations for local development.

## Samples

Expand All @@ -37,7 +39,14 @@ mvn clean install # build all modules
mvn test # run unit tests
```

For per-plugin details (configuration, programmatic API, multi-tenancy behaviour) see the individual module READMEs. For integration tests against a real AI Core instance see [`integration-tests/`](integration-tests/README.md).
For integration tests against a real AI Core instance:

```bash
cds bind ai-core -2 <your-ai-core-service-instance>
cds bind --exec mvn verify
```

See [`integration-tests/README.md`](integration-tests/README.md) for the full integration-test layout, including the multi-tenancy profile.

## Support, Feedback, Contributing

Expand Down
122 changes: 70 additions & 52 deletions cds-feature-ai-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Bridges CAP Java applications to [SAP AI Core](https://help.sap.com/docs/sap-ai-

## Features

- **`AICore` CDS Service** - Exposes resource groups, deployments, and configurations as CDS entities with full CRUD support
- **`AICore` CDS Service** - Internal CDS service (annotated `@protocol: 'none'`) modelling resource groups, deployments, and configurations as CDS entities. The plugin does **not** expose this service via OData; it is consumed in-process via `RemoteService`. To expose it externally, project it from your own service or use the `@cap-js/ai` model.
- **Multi-Tenancy** - Automatic per-tenant resource group creation/deletion on subscribe/unsubscribe
- **Deployment Management** - Auto-creates configurations and deployments for AI Core models with retry and backoff
- **Inference Client Factory** - Provides ready-to-use `ApiClient` instances scoped to a deployment for downstream foundation-model SDKs
Expand All @@ -27,12 +27,14 @@ The plugin auto-registers via Java's `ServiceLoader` mechanism - no code changes

### AI Core Binding

In production, bind an SAP AI Core service instance to your application. Supported methods:
In production, bind an SAP AI Core service instance to your application via a standard service binding (Cloud Foundry / Kubernetes). For local hybrid testing against a real AI Core instance, use the CAP CLI:

- **Service binding** (Cloud Foundry / Kubernetes)
- **Environment variable** `AICORE_SERVICE_KEY` - for local hybrid testing (via `cds bind --exec`)
```bash
cds bind ai-core -2 <your-ai-core-service-instance>
cds bind --exec mvn spring-boot:run
```

Without a binding the plugin registers a mock implementation.
Without a binding the plugin registers a mock implementation suitable for local development.

## Configuration

Expand All @@ -53,75 +55,91 @@ and the presence of a `DeploymentService`. No additional configuration flag is r

## CDS Service: `AICore`

The plugin registers a CAP service named `AICore` that proxies AI Core REST APIs as CDS entities:
The plugin registers a CAP service named `AICore` that proxies AI Core REST APIs as CDS entities.
The service is internal (`@protocol: 'none'`); use a `RemoteService` lookup to interact with it.

### Entities

| Entity | Operations | Description |
| ----------------------- | -------------------- | ---------------------------------------------------------------- |
| `AICore.resourceGroups` | READ, CREATE, DELETE | Resource group lifecycle, supports label filtering by `tenantId` |
| `AICore.deployments` | READ, CREATE, DELETE | Deployment management with status tracking |
| `AICore.configurations` | READ, CREATE | Configuration management for scenarios and executables |
| Entity | Operations | Description |
| ----------------------- | ---------------------------- | ---------------------------------------------------------------- |
| `AICore.resourceGroups` | READ, CREATE, UPDATE, DELETE | Resource group lifecycle, supports label filtering by `tenantId` |
| `AICore.deployments` | READ, CREATE, UPDATE, DELETE | Deployment management with status tracking; bound action `stop` |
| `AICore.configurations` | READ, CREATE | Configuration management for scenarios and executables |

## Programmatic API

### Programmatic API
The plugin exposes its functionality through three event contexts emitted on the `AICore` `RemoteService`. This pattern decouples callers from the implementation and makes it easy to override individual steps in tests.

```java
// Get the resource group for the current tenant
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.services.cds.RemoteService;
import com.sap.cds.feature.aicore.api.ModelDeploymentSpec; // or RPT-1 instead: import com.sap.cds.feature.recommendation.api.RptModelSpec;

// 1. Obtain the AICore service as a RemoteService
RemoteService aiCore = runtime.getServiceCatalog()
.getService(RemoteService.class, AICore_.CDS_NAME);

// 2. Resolve the resource group for the current tenant
// (auto-creates the group on first use in multi-tenant mode)
ResourceGroupContext rgCtx = ResourceGroupContext.create();
aiCoreService.emit(rgCtx);
String resourceGroup = rgCtx.getResult();
aiCore.emit(rgCtx);
String resourceGroupId = rgCtx.getResult();

// Get (or auto-create) a deployment ID for a model spec in the given resource group
// 3. Resolve (or create) a deployment for a given model spec
DeploymentIdContext depCtx = DeploymentIdContext.create();
depCtx.setResourceGroupId(resourceGroup);
depCtx.setSpec(RptModelSpec.rpt1());
aiCoreService.emit(depCtx);
depCtx.setResourceGroupId(resourceGroupId);
depCtx.setSpec(new ModelDeploymentSpec(
"foundation-models", "azure-openai", "my-gpt4o-config", List.of(), d -> true));
// or RPT-1 model instead:
// depCtx.setSpec(RptModelSpec.rpt1());
aiCore.emit(depCtx);
String deploymentId = depCtx.getResult();
```

## Multi-Tenancy

When multi-tenancy is active (detected via `cds.multiTenancy.sidecar.url`):
// 4. Obtain a configured ApiClient for the deployment
InferenceClientContext infCtx = InferenceClientContext.create();
infCtx.setResourceGroupId(resourceGroupId);
infCtx.setDeploymentId(deploymentId);
aiCore.emit(infCtx);
ApiClient client = infCtx.getResult();
```

1. **Subscribe** - Creates resource group `{prefix}{tenantId}` with label `ext.ai.sap.com/CDS_TENANT_ID`
2. **Unsubscribe** - Deletes the tenant's resource group
3. **Isolation** - Each tenant's predictions use their own resource group and deployment
The `ApiClient` returned by `InferenceClientContext` is preconfigured with the AI Core
destination and the deployment URL; use it to construct foundation-model SDK
clients (for example `RptInferenceClient` from `cds-feature-recommendations`).

The lifecycle hooks are registered automatically when multi-tenancy is enabled.
See [`RecommendationConfiguration.java`](../cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RecommendationConfiguration.java) for a real-world example of this pattern.

## Programmatic Usage
Because `RemoteService` extends `CqnService`, you can also run CDS queries against
the entities directly:

```java
// Obtain the AICore service (registered as a RemoteService by the plugin)
RemoteService aiCoreService = runtime.getServiceCatalog()
.getService(RemoteService.class, "AICore");
Result rgs = aiCore.run(Select.from(AICore_.CDS_NAME + ".resourceGroups"));
```

// Use for entity operations (RemoteService extends CqnService)
Result rgs = aiCoreService.run(Select.from("AICore.resourceGroups"));
### Public API

// Resolve resource group, deployment, and inference client via CDS events
ResourceGroupContext rgCtx = ResourceGroupContext.create();
aiCoreService.emit(rgCtx);
String resourceGroup = rgCtx.getResult();
The stable public API of this plugin lives in the `com.sap.cds.feature.aicore.api` package.
Implementation classes in sibling packages may change without notice.

DeploymentIdContext depCtx = DeploymentIdContext.create();
depCtx.setResourceGroupId(resourceGroup);
depCtx.setSpec(RptModelSpec.rpt1());
aiCoreService.emit(depCtx);
String deploymentId = depCtx.getResult();
| Type | Purpose |
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| [`ResourceGroupContext`](src/main/java/com/sap/cds/feature/aicore/api/ResourceGroupContext.java) | Event context for `resourceGroup` - resolves (and auto-creates in MTX mode) the AI Core resource group for the current/explicit tenant |
| [`DeploymentIdContext`](src/main/java/com/sap/cds/feature/aicore/api/DeploymentIdContext.java) | Event context for `deploymentId` - resolves or creates a deployment matching a `ModelDeploymentSpec` inside a resource group |
| [`InferenceClientContext`](src/main/java/com/sap/cds/feature/aicore/api/InferenceClientContext.java) | Event context for `inferenceClient` - returns an `ApiClient` preconfigured with the inference destination for a given deployment |
| [`ModelDeploymentSpec`](src/main/java/com/sap/cds/feature/aicore/api/ModelDeploymentSpec.java) | Record describing a target deployment (scenario, executable, configuration name, parameter bindings, match predicate) |

InferenceClientContext infCtx = InferenceClientContext.create();
infCtx.setResourceGroupId(resourceGroup);
infCtx.setDeploymentId(deploymentId);
aiCoreService.emit(infCtx);
ApiClient client = infCtx.getResult();
```
## Multi-Tenancy

The `ApiClient` returned via `InferenceClientContext` is preconfigured with the AI Core
destination and the deployment URL; use it to construct foundation-model SDK
clients (for example `RptInferenceClient` from `cds-feature-recommendations`).
When multi-tenancy is active (detected via `cds.multiTenancy.sidecar.url`):

See [`RecommendationConfiguration.java`](../cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RecommendationConfiguration.java) for a real-world example of this pattern.
1. **Subscribe** - Creates resource group `{prefix}{tenantId}` with label `ext.ai.sap.com/CDS_TENANT_ID`
2. **Unsubscribe** - Deletes the tenant's resource group
3. **Isolation** - Each tenant's predictions use their own resource group and deployment

The lifecycle hooks are registered automatically when multi-tenancy is enabled.

## Related

Expand Down
26 changes: 21 additions & 5 deletions cds-feature-recommendations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ Add `@cap-js/ai` to your project's `package.json`:
}
```

Then run `npm install`. The plugin hooks into the CDS compiler and automatically adds the `SAP_Recommendations` navigation property to draft-enabled entities that have value-list fields.
Then run `npm install`. The plugin hooks into the CDS compiler and automatically adds the `SAP_Recommendations` navigation property to draft-enabled entities that have value-list fields.

Since the Java module `cds-feature-ai-core` already provides the `AICore` service CDS model, disable the duplicate model from `@cap-js/ai` in your `.cdsrc.json`:
Both the Java module `cds-feature-ai-core` and the `@cap-js/ai` package ship a CDS model for the `AICore` service. Pick **one** of the following patterns in your `.cdsrc.json`, depending on whether you need to expose AICore via OData:

**Option A — Java-internal use only (recommended for most apps):** the Java plugin's CDS model is sufficient because the recommendation handler consumes it in-process. Disable the duplicate model from `@cap-js/ai`:

```json
{
Expand All @@ -64,6 +66,18 @@ Since the Java module `cds-feature-ai-core` already provides the `AICore` servic
}
```

**Option B — Expose AICore via OData:** keep the `@cap-js/ai` model and project from it in your own service (see `samples/bookshop/srv/ai-core-service.cds` for an example):

```json
{
"requires": {
"AICore": {
"model": "@cap-js/ai/srv/AICoreService"
}
}
}
```

## Enabling Recommendations

For recommendations to fire on an entity:
Expand All @@ -72,7 +86,7 @@ For recommendations to fire on an entity:
- At least one field must be annotated with a **value list**
- The `SAP_Recommendations` navigation property must be present — either via the CDS plugin (see above) or added manually (see below). Without it, predictions are computed but not serialized in OData responses.

Recommendations are triggered for fields annotated with `@Common.ValueList`, `@Common.ValueListWithFixedValues`, or whose association target has `@cds.odata.valuelist`:
Recommendations are triggered for fields annotated with `@Common.ValueList` or `@Common.ValueListWithFixedValues`. The CDS compiler also derives `@Common.ValueList` from `@cds.odata.valuelist` on association targets, so annotating the target entity has the same effect:

```cds
@odata.draft.enabled
Expand Down Expand Up @@ -179,7 +193,7 @@ The following configuration applies to the RPT-1 model implementation.

```yaml
cds:
requires:
ai:
recommendations:
contextRowLimit: 2000 # Max historical rows used as training context (RPT-1)
```
Expand Down Expand Up @@ -218,11 +232,13 @@ The following field types are supported by the RPT-1 model implementation:
| Temporal | `Date`, `Time`, `DateTime`, `Timestamp` |
| Other | `Boolean` |

Equivalent CDS HANA types (e.g. `hana.SMALLINT`, `hana.TINYINT`, `hana.SMALLDECIMAL`, `hana.REAL`, `hana.CHAR`, `hana.NCHAR`, `hana.VARCHAR`, `hana.CLOB`) are also supported.

Binary, vector, and draft system fields are excluded automatically.

## Local Development

Without an AI Core binding, the plugin uses a `MockAIClient` that returns random predictions from existing context rows - useful for UI development without AI Core access. The `@cap-js/ai` CDS plugin is still required for the model enhancement.
Without an AI Core binding, the plugin uses a `MockRecommendationClient` that returns random predictions from existing context rows - useful for UI development without AI Core access. The `@cap-js/ai` CDS plugin is still required for the model enhancement.

## Related

Expand Down
9 changes: 5 additions & 4 deletions integration-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,18 @@ mvn verify -Pmtx-integration-tests

**Skipping all integration tests (source modules only):**

The `with-integration-tests` profile is active by default at the root. Deactivate it to skip both `integration-tests/` and `coverage-report/`:

```bash
mvn verify -Pskip-integration-tests
mvn install -P-with-integration-tests
```

## Profiles

| Profile | Scope | Effect |
|---------|-------|--------|
| _(default)_ | Root | Builds all modules; runs spring integration tests |
| `with-integration-tests` | Root | **Active by default**; includes `integration-tests/` and `coverage-report/`. Deactivate with `-P-with-integration-tests`. |
Comment thread
lisajulia marked this conversation as resolved.
| `mtx-integration-tests` | `integration-tests/` | Also includes the `mtx-local/srv` module |
| `skip-integration-tests` | Root | Excludes `integration-tests/` and `coverage-report/` entirely |

## Coverage

Expand All @@ -61,7 +62,7 @@ coverage-report/target/site/jacoco-aggregate/index.html

### Thresholds

Coverage thresholds are enforced by SonarQube in the CI pipeline (80% on new code).
Coverage thresholds are enforced by SonarQube's Quality Gate in the CI pipeline (80%, configured on SonarQube).

### Coverage data sources

Expand Down
Loading