-
Notifications
You must be signed in to change notification settings - Fork 0
Final review #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Final review #60
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,9 +40,12 @@ Or use the starter that bundles this with `cds-feature-ai-core`: | |
| - An [SAP AI Core](https://help.sap.com/docs/sap-ai-core) service binding (see [`cds-feature-ai-core`](../cds-feature-ai-core/README.md)) | ||
| - Entity must be **draft-enabled** (`@odata.draft.enabled`) | ||
| - At least one field annotated with a **value list** | ||
| - The `@cap-js/ai` CDS plugin must be installed (provides the model enhancement that adds `SAP_Recommendations` as a navigation property) | ||
| - The `SAP_Recommendations` navigation property must be added to the entities that should receive recommendations by | ||
| - either installing the `@cap-js/ai` CDS plugin (automatically provides the model enhancement that adds `SAP_Recommendations` as a navigation property) | ||
| - or adding the `SAP_Recommendations`property manually. | ||
| Without the `SAP_Recommendations` navigation property, the predictions will be computed but not serialized in OData responses. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clarity: The warning about missing Consider placing it as a standalone note below the two sub-bullets, or dedenting it to the parent bullet level, e.g.: Please provide feedback on the review comment by checking the appropriate box:
|
||
|
|
||
| ### CDS Plugin | ||
| #### CDS Plugin | ||
|
|
||
| Add `@cap-js/ai` to your project's `package.json`: | ||
|
|
||
|
|
@@ -55,7 +58,7 @@ 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. Without this plugin, predictions will be computed but not serialized in OData responses. | ||
| 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`: | ||
|
|
||
|
|
@@ -68,6 +71,41 @@ Since the Java module `cds-feature-ai-core` already provides the `AICore` servic | |
| } | ||
| } | ||
| ``` | ||
| #### Adding the SAP_Recommendations navigation property manually | ||
|
|
||
| If you cannot use the CDS plugin, add the `SAP_Recommendations` navigation property directly in your CDS model. You need to: | ||
|
|
||
| 1. **Define a `RecommendationItem_*` type** for each CDS primitive type used by your value-list fields. Each type must contain the four fixed fields shown below — only `RecommendedFieldValue` varies by type. | ||
| 2. **Extend each target entity** with a `SAP_Recommendations` composition that has one entry per value-list field, using the field name as the property name and the matching `RecommendationItem_*` type. | ||
|
|
||
| The property names inside `SAP_Recommendations` must exactly match the field names on the entity (e.g. `genre_ID`, `author_ID`). | ||
|
|
||
| ```cds | ||
| // Define one type per CDS primitive used by your value-list fields | ||
| type RecommendationItem_Integer { | ||
| RecommendedFieldValue : Integer; | ||
| RecommendedFieldDescription : String; | ||
| RecommendedFieldScoreValue : Decimal; | ||
| RecommendedFieldIsSuggestion: Boolean; | ||
| } | ||
|
|
||
| type RecommendationItem_UUID { | ||
| RecommendedFieldValue : UUID; | ||
| RecommendedFieldDescription : String; | ||
| RecommendedFieldScoreValue : Decimal; | ||
| RecommendedFieldIsSuggestion: Boolean; | ||
| } | ||
|
|
||
| // Extend your entity — one entry per value-list field | ||
| extend my.Books with { | ||
| SAP_Recommendations: Composition of one { | ||
| genre_ID : many RecommendationItem_Integer; | ||
| author_ID: many RecommendationItem_UUID; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| See also the [SAP Fiori Elements – Recommendations documentation](https://help.sap.com/docs/SAPUI5/b2f662dd9d7a4ec680056733050b4d34/1a6324d5ad7f4034a93f911b4e53e080.html). | ||
|
|
||
| ## Enabling Recommendations | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,7 +31,10 @@ class FioriRecommendationHandler implements EventHandler { | |
| private final AICoreService aiCoreService; | ||
| private final RecommendationClientResolver clientResolver; | ||
| private final RecommendationResultParser resultParser = new RecommendationResultParser(); | ||
| private final Cache<String, Boolean> entitiesWithoutPredictions = | ||
| // Avoids re-evaluating the CDS model on every read to check whether an entity has prediction | ||
| // columns. Keys are "<tenantId>:<entityName>" because if an entity needs a prediction can be | ||
| // different across tenants. | ||
| private final Cache<String, Boolean> entitiesWithoutPredictionsPerTenant = | ||
| Caffeine.newBuilder().maximumSize(10_000).build(); | ||
|
|
||
| FioriRecommendationHandler( | ||
|
|
@@ -40,14 +43,25 @@ class FioriRecommendationHandler implements EventHandler { | |
| this.clientResolver = clientResolver; | ||
| } | ||
|
|
||
| void invalidateTenant(String tenantId) { | ||
| String prefix = tenantKey(tenantId) + ":"; | ||
|
Comment on lines
+46
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: For example, if two tenants are named The real remaining risk is that a malicious or misconfigured tenant ID that itself contains Consider using a separator that cannot appear in tenant IDs and entity names, or use a Please provide feedback on the review comment by checking the appropriate box:
|
||
| entitiesWithoutPredictionsPerTenant.asMap().keySet().removeIf(k -> k.startsWith(prefix)); | ||
| } | ||
|
|
||
| private static String tenantKey(String tenantId) { | ||
| return tenantId != null ? tenantId : ""; | ||
| } | ||
|
|
||
| @After(entity = "*") | ||
| public void afterRead(CdsReadEventContext context, List<CdsData> dataList) { | ||
| CdsStructuredType target = context.getTarget(); | ||
| if (target == null) { | ||
| return; | ||
| } | ||
| String tenantId = context.getUserInfo().getTenant(); | ||
| String entityName = target.getQualifiedName(); | ||
| if (entitiesWithoutPredictions.getIfPresent(entityName) != null) { | ||
| String cacheKey = tenantKey(tenantId) + ":" + entityName; | ||
| if (entitiesWithoutPredictionsPerTenant.getIfPresent(cacheKey) != null) { | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -82,7 +96,7 @@ public void afterRead(CdsReadEventContext context, List<CdsData> dataList) { | |
| var builder = new RecommendationContextBuilder(target, rowType, limit); | ||
|
|
||
| if (builder.predictionElementNames().isEmpty()) { | ||
| entitiesWithoutPredictions.put(entityName, Boolean.TRUE); | ||
| entitiesWithoutPredictionsPerTenant.put(cacheKey, Boolean.TRUE); | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -109,7 +123,6 @@ public void afterRead(CdsReadEventContext context, List<CdsData> dataList) { | |
|
|
||
| List<CdsData> allRows = builder.assembleRows(contextRows, predictRow, row); | ||
|
|
||
| String tenantId = context.getUserInfo().getTenant(); | ||
| RecommendationClient client = clientResolver.resolve(aiCoreService, tenantId); | ||
| List<CdsData> predictions = | ||
| client.predict(allRows, builder.predictionElementNames(), builder.indexColumn()); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| /* | ||
| * © 2026 SAP SE or an SAP affiliate company and cds-ai contributors. | ||
| */ | ||
| package com.sap.cds.feature.recommendation; | ||
|
|
||
| import com.sap.cds.services.handler.EventHandler; | ||
| import com.sap.cds.services.handler.annotations.On; | ||
| import com.sap.cds.services.handler.annotations.ServiceName; | ||
| import com.sap.cds.services.mt.ExtensibilityService; | ||
| import com.sap.cds.services.mt.ModelChangedEventContext; | ||
|
|
||
| @ServiceName(value = ExtensibilityService.DEFAULT_NAME, type = ExtensibilityService.class) | ||
| class RecommendationModelChangedHandler implements EventHandler { | ||
|
|
||
| private final FioriRecommendationHandler recommendationHandler; | ||
|
|
||
| RecommendationModelChangedHandler(FioriRecommendationHandler recommendationHandler) { | ||
| this.recommendationHandler = recommendationHandler; | ||
| } | ||
|
|
||
| @On(event = ExtensibilityService.EVENT_MODEL_CHANGED) | ||
| public void onModelChanged(ModelChangedEventContext context) { | ||
| String tenantId = context.getUserInfo().getTenant(); | ||
| recommendationHandler.invalidateTenant(tenantId); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: Missing space before "property" in the bullet point.
SAP_Recommendationspropertyshould beSAP_Recommendationsproperty(space before "property").Double-check suggestion before committing. Edit this comment for amendments.
Please provide feedback on the review comment by checking the appropriate box: