Review: Full prototype implementation#49
Conversation
tenant scenarios and update README
…tup.java Co-authored-by: Matthias Braun <matthia.braun@sap.com>
Co-authored-by: Simon Engel <simon.engel01@sap.com>
Co-authored-by: Simon Engel <simon.engel01@sap.com>
Replaces the single-module srv/ codebase with the multi-module Maven layout developed on the internal SAP fork. Adds LICENSE at root (per OSPO finding), pom.xml SCM section, and fixes the broken .reuse/dep5 link in CONTRIBUTING.md. The .github/ workflows and gh_ruleset.json from this repo are preserved. Note: integration-tests/spring tests fail on clean clone because the build does not invoke npm install before cds build, so the @cap-js/ai node plugin is not available when the csn is generated. Pre-existing issue on the fork's main; tracked separately.
Co-authored-by: hyperspace-insights[bot] <209611008+hyperspace-insights[bot]@users.noreply.github.com>
* chore: bootstrap ci/cd workflows * chore: address pr bot review findings * fix: specify ai-core service instance and key * rename blackduck identifier
* fix: run npm ci before cds build in integration-tests (#17) Without `npm ci`, `cds build` ran in `integration-tests/spring/` without the `@cap-js/ai` Node plugin, so the generated CSN was missing the synthetic `SAP_Recommendations` association and 8 RecommendationTest cases failed on clean clone with `JsonPath $['SAP_Recommendations']: PathNotFoundException`. Add `cds.install-node` and `cds.npm-ci` executions to the cds-maven-plugin block in `integration-tests/spring/pom.xml`, mirroring the pattern already used in `samples/bookshop/srv/pom.xml`. CI continues to skip the Node download via `-Dcds.install-node.skip`; clean local clones get a self-contained Node + `npm ci` into `integration-tests/node_modules/`. Commit `package-lock.json` for `integration-tests/` and `samples/bookshop/` so `npm ci` is reproducible, and remove the `**/package-lock.json` entry from `.gitignore`. Closes #17 * fix: regenerate lockfiles against public npm registry The @cap-js/ai entry in the previous lockfiles pointed at int.repositories.cloud.sap (SAP-internal Artifactory), which is unreachable from GitHub-hosted runners and broke `npm ci` in CI with EAI_AGAIN. Regenerated both lockfiles with the public registry forced for the @cap-js scope so they resolve from registry.npmjs.org. * fix: invert integration-test modules profile and suppress AICore kinds models Two fixes wrapped together since they both unblock CI on PR #21. 1. Maven `<modules>` is additive in profiles, so `skip-integration-tests` was a no-op — it re-listed the source modules instead of removing the integration ones. Invert the structure: default modules are source- only; a `with-integration-tests` profile (active by default) adds `integration-tests` and `coverage-report`. CI consumers switch from `-P skip-integration-tests` to `-P '!with-integration-tests'`. 2. In CI, `cds bind --exec` writes a `[hybrid]` profile entry to `.cdsrc-private.json` for the `ai-core` binding, kind `AICore-btp`. `cds build` then expands the kind's `model: "@cap-js/ai/srv/AICoreService"` into the build's model list, which collides with the `AICore` service shipped by `cds-feature-ai-core` (loaded via `using { AICore } from 'com.sap.cds/ai'`). The existing `requires.AICore.model = false` override only suppresses the named-require model, not the kind's. Add `requires.kinds.AICore-btp.model = false` (and the same for `AICore-mocked`) so the kind never contributes a model to cds build. * fix: build root modules before sonar scan; dispatch event handlers on registered service type - SonarQube scan job ran `mvn clean verify` from `integration-tests/` without first installing the source modules, so the integration-tests build couldn't resolve `cds-feature-ai-core` etc. as JAR dependencies. Mirror the integration-tests action: install the three source modules with `-pl ... -am -DskipTests` first, then run the scan build. - `AICoreServiceConfiguration.eventHandlers` recomputed `hasAICoreBinding` separately from `services()` and unconditionally cast the registered service to `AICoreServiceImpl`. If the two calls disagreed (or if another configurer registered a Mock first), the cast threw `ClassCastException: MockAICoreServiceImpl cannot be cast to AICoreServiceImpl`. Read the actually-registered service from the catalog and dispatch via `instanceof` patterns so the handler set always matches the live registration.
The Fiori recommendation handler used to emit a SAP_Recommendations entry for every column in the prediction response, regardless of whether the user had already filled that column. This worked locally because the mock client only returns predictions for cells marked [PREDICT], but it broke in CI where the real RPT model returns predictions for every target column. Filter the response to columns that were null in the input row so behaviour is consistent across both clients. Adds a regression unit test that simulates the RPT-style response shape.
#25) * test(itest): disable ResourceGroup mutating tests pending admin scopes (#24) The 4 CREATE/DELETE tests in ResourceGroupTest fail in CI with HTTP 403 because /v2/admin/resourceGroups requires AI Core tenant-admin scopes (see SAP AI Core service guide §3.2 / §6.3), which the bound CI service key (sap-internal plan, technical user cap.calesi.munich@sap.com) does not carry. Disable them with @disabled until the IAM situation is sorted; read-only readAll_returnsResourceGroups stays enabled. Refs #24 * fix(itest): repair resource-group cleanup so quota does not leak (#24) The test extension's pre-cleanup never ran in CI because it gated on AICORE_SERVICE_KEY, but the bound credentials are exposed via VCAP_SERVICES. 48 itest-rg-* groups had leaked into the cdsmunich tenant, hitting the 50-group hard quota and turning every CREATE into a 400/quota error. - Remove the env-var guard from ResourceGroupCleanupExtension so the ResourceGroupApi auto-discovers credentials from VCAP_SERVICES (same pattern as production AICoreServiceImpl). - Add cleanup-resource-groups.cjs and invoke it with if: always() in the integration-tests action, mirroring cap-js/ai. Survives JVM crashes. - Re-enable the four ResourceGroupTest CREATE/DELETE tests. * chore(itest): drop unused .cjs cleanup script The JUnit ResourceGroupCleanupExtension already handles cleanup exhaustively in @BeforeAll/@afterall — verified green in the previous CI run. The .cjs script failed with 'No impl configured for cds.requires.kinds.AICore-btp' anyway and was wrapped in `|| true`, so it provided no real safety net.
Bumps the minor-patch group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [com.github.ben-manes.caffeine:caffeine](https://github.com/ben-manes/caffeine) | `3.2.0` | `3.2.4` | | [org.apache.maven.plugins:maven-enforcer-plugin](https://github.com/apache/maven-enforcer) | `3.6.2` | `3.6.3` | | [com.diffplug.spotless:spotless-maven-plugin](https://github.com/diffplug/spotless) | `3.4.0` | `3.5.0` | | [com.sap.ai.sdk:core](https://github.com/SAP/ai-sdk-java) | `1.18.0` | `1.19.0` | | [com.sap.ai.sdk.foundationmodels:sap-rpt](https://github.com/SAP/ai-sdk-java) | `1.18.0` | `1.19.0` | Updates `com.github.ben-manes.caffeine:caffeine` from 3.2.0 to 3.2.4 - [Release notes](https://github.com/ben-manes/caffeine/releases) - [Commits](ben-manes/caffeine@v3.2.0...v3.2.4) Updates `org.apache.maven.plugins:maven-enforcer-plugin` from 3.6.2 to 3.6.3 - [Release notes](https://github.com/apache/maven-enforcer/releases) - [Commits](apache/maven-enforcer@enforcer-3.6.2...enforcer-3.6.3) Updates `com.diffplug.spotless:spotless-maven-plugin` from 3.4.0 to 3.5.0 - [Release notes](https://github.com/diffplug/spotless/releases) - [Changelog](https://github.com/diffplug/spotless/blob/main/CHANGES.md) - [Commits](diffplug/spotless@maven/3.4.0...maven/3.5.0) Updates `com.sap.ai.sdk:core` from 1.18.0 to 1.19.0 - [Release notes](https://github.com/SAP/ai-sdk-java/releases) - [Changelog](https://github.com/SAP/ai-sdk-java/blob/main/docs/release_notes.md) - [Commits](SAP/ai-sdk-java@rel/1.18.0...rel/1.19.0) Updates `com.sap.ai.sdk.foundationmodels:sap-rpt` from 1.18.0 to 1.19.0 - [Release notes](https://github.com/SAP/ai-sdk-java/releases) - [Changelog](https://github.com/SAP/ai-sdk-java/blob/main/docs/release_notes.md) - [Commits](SAP/ai-sdk-java@rel/1.18.0...rel/1.19.0) Updates `com.sap.ai.sdk.foundationmodels:sap-rpt` from 1.18.0 to 1.19.0 - [Release notes](https://github.com/SAP/ai-sdk-java/releases) - [Changelog](https://github.com/SAP/ai-sdk-java/blob/main/docs/release_notes.md) - [Commits](SAP/ai-sdk-java@rel/1.18.0...rel/1.19.0) --- updated-dependencies: - dependency-name: com.github.ben-manes.caffeine:caffeine dependency-version: 3.2.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: org.apache.maven.plugins:maven-enforcer-plugin dependency-version: 3.6.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: com.diffplug.spotless:spotless-maven-plugin dependency-version: 3.5.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: com.sap.ai.sdk:core dependency-version: 1.19.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: com.sap.ai.sdk.foundationmodels:sap-rpt dependency-version: 1.19.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: com.sap.ai.sdk.foundationmodels:sap-rpt dependency-version: 1.19.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…#27) * fix(mtx): wire MTX subscribe lifecycle for mock AI Core service (#26) The local-with-tenants profile didn't set cds.requires.AICore.multiTenancy, so AICoreServiceImpl.isMultiTenancyEnabled() returned false and seven MTX integration tests failed. Even with the flag, the mock service had no SubscribeEventContext handler, so the tenant resource-group cache was never populated and the cache-based assertions still failed. - Enable cds.requires.AICore.multiTenancy under the local-with-tenants profile so MT registers in mtx-local integration tests. - Add MockAICoreSetupHandler mirroring AICoreSetupHandler's subscribe/ unsubscribe lifecycle in-memory (no AI Core API calls). - Register the mock setup handler from AICoreServiceConfiguration when the mock service runs with MT enabled. - Use AICoreService interface in mtx-local integration tests; drop the AICoreServiceImpl casts and the assumeTrue skips that those casts forced. * ci: split mtx-local into its own job without AI Core binding (#26) Running spring + mtx-local under one cds-bind made mtx-local inherit the AI Core binding, so AICoreServiceConfiguration registered the real AICoreServiceImpl and AICoreSetupHandler hit the live API on every subscribe/unsubscribe (HTTP 409 on resource-group delete in CI). The mtx-local module is intended to run in-memory against the mock service. - Add a Local MTX Tests job (matrix Java 17/21, no cf-bind) that runs mvn -pl integration-tests/mtx-local/srv -am -P mtx-integration-tests. - Drop the mtx-integration-tests profile from the integration-tests composite action so its cds-bind invocation runs only the spring reactor (the default). * change default rg and add provisioning to itest * fix(itest): avoid 404 on keyed RG read by listing then filtering A keyed READ on AICore.resourceGroups routes through ResourceGroupHandler.onRead -> resourceGroupApi.get(rgId), which returns 404 for a not-yet-existing RG instead of an empty list. Use a non-keyed Select and filter client-side so the helper can detect absence and INSERT the RG. * fix(itest): increase RPT deployment poll budget to ~6 min Prior 10-attempt cap (~145 s sleep budget) was right at the edge of the observed RPT-1 deployment time (~150 s) and timed out in CI. Bumping to 18 attempts gives a ~6 min ceiling so cold deployments have headroom without making genuinely failed runs hang indefinitely. * fix(itest): tune RPT poll budget to 15 attempts (~4.7 min) 18 attempts gave too much headroom; 15 is enough margin over the observed ~150 s RPT-1 deployment time without making genuinely stuck runs hang for too long. * fix(itest): scope cleanup to label-owned RGs to protect other CF instances AI Core resource groups are shared across all CF service instances bound to the same AI Core tenant, so listing every RG and stopping its deployments before delete was destroying work owned by other instances. Adopt cap-js/ai's pattern (srv/ai-core/resourceGroups.js) of stamping each RG with an owner label at create time and filtering by it on cleanup, but use a distinct key (CDS_FEATURE_AI_ITEST_OWNER) so the itest cleanup never collides with prod tenant cleanup. Cleanup also short-circuits when the owner is unset or equal to the local-dev RG name, so dev runs reuse their RG across invocations. While here, drop the test scaffold's hand-rolled config/deployment helpers and call the existing AICoreServiceImpl.deploymentId(...) which already does lookup-or-create-with-polling. The RG insert + provisioned poll stay in test scope per design. * fix(itest): drop redundant null-check on rg.getLabels() BckndResourceGroup.getLabels() is @nonnull, so the null guard was flagged by SpotBugs (RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE). * fix(ci): resolve integration test and SonarQube pipeline failures - ResourceGroupCleanupExtension: defer afterAll cleanup to end of test suite using CloseableResource pattern, preventing premature deletion of the resource group before all test classes have completed - TenantIsolationTest: catch Throwable (not just Exception) in tearDown so MockMvc AssertionError does not escape the safety catch blocks - SonarQube scan: remove -P mtx-integration-tests from the verify step; mtx-local tests already run in their dedicated local-mtx-tests job and including them under cds bind causes real AI Core API calls that conflict with the mock-based test design - BaseIntegrationTest: expose clearDeploymentIdCache() so the cleanup extension can invalidate stale cached deployment IDs after deletion * fix(itest): prevent beforeAll cleanup from deleting active resource group The ResourceGroupCleanupExtension.beforeAll() was deleting the resource group that AICoreServiceTest (which runs first) had just created, causing ActionTest and all Recommendation tests to fail with 400/500 from AI Core. - Remove beforeAll/cleanupOnce from ResourceGroupCleanupExtension; the deferred afterAll (CloseableResource at suite end) is sufficient - Expose ensureResourceGroupProvisioned() as protected in BaseIntegrationTest - Add @BeforeAll to ActionTest to guarantee the resource group is provisioned before deploymentId tests run, regardless of class order * fix(ci): generate JaCoCo XML reports for SonarQube scan - Add jacoco report goal to cds-feature-ai-core and integration-tests/spring pom.xml (they only had prepare-agent, no report generation) - Move mtx-local dependency in coverage-report to mtx-integration-tests profile so the aggregate report can be built without mtx-local - Update scan-with-sonar action: remove -DskipTests so unit tests run and generate coverage; add install to integration tests step; add step to build coverage-report aggregate; point sonar at the aggregate report * fix(itest): prevent ResourceGroupTest from leaking resource groups ResourceGroupTest creates itest-rg-* groups during tests but AfterEach cleanup silently failed (409 Conflict) because the RG wasn't provisioned yet. Additionally, the ResourceGroupCleanupExtension only cleaned label-owned groups, missing the itest-rg-* prefix groups entirely. - ResourceGroupTest: wait for RG to be provisioned before delete in AfterEach, matching the pattern used in delete_resourceGroup test - ResourceGroupCleanupExtension: add prefix-based safety-net cleanup that always deletes any itest-rg-* groups at suite end, regardless of whether CDS_AICORE_TEST_RESOURCE_GROUP is set - scan-with-sonar: set CDS_AICORE_TEST_RESOURCE_GROUP env var so the cleanup extension actually runs in the SonarQube scan job * fix(itest): stop_deployment test must use own RG, not 'default' The test was hardcoded to query and stop the first RUNNING deployment in the 'default' resource group. Since AI Core is scoped at the subaccount level, this stopped deployments from other CF spaces sharing the same subaccount (e.g. orchestration deployments). Use getDefaultResourceGroup() which resolves to the CI-specific itest-* resource group, ensuring only test-owned deployments are affected. * fix(itest): remove all hardcoded 'default' resource group references DeploymentTest.update_targetStatus_stopsRunningDeployment was still hardcoded to query and stop deployments in the 'default' resource group, which stopped the user's orchestration deployment in the shared subaccount. Also fix ConfigurationTest to avoid creating orphan configs in 'default'. All test classes now use getDefaultResourceGroup() which resolves to the CI-specific itest-* resource group via CDS_AICORE_TEST_RESOURCE_GROUP. * fix: retry on HTTP 404 from AI Core inference endpoint AI Core deployments report RUNNING status before the inference gateway has fully propagated the route. This causes 404 responses for a short window after deployment creation. The retry logic already handled 403 and 412 as transient 'not ready yet' states but was missing 404. Adding 404 to the retryable status codes makes inference calls resilient to this AI Core eventual-consistency window. * fix: cap exponential backoff at 30s and increase itest retry budget The exponential backoff with no cap resulted in extremely long final intervals (77s, 154s) leaving only ~153s of total retry window across 10 attempts. For fresh deployments where inference needs up to 5 min to become stable, this was insufficient. - Cap max interval at 30s so retries stay frequent - Increase integration test maxRetries to 15 (gives ~5.5 min window) - Together with the 404 retry fix, this handles AI Core's deployment warm-up period reliably * fix(ci): move RG cleanup to dedicated job after all itests complete When Java 17 and Java 21 integration tests run in parallel, they create separate deployments of the same RPT model. When the first-finishing job cleaned up (stopped its deployment + deleted its RG), AI Core tore down shared model serving infrastructure causing the still-running job's inference calls to fail with 404. - ResourceGroupCleanupExtension: afterAll now only cleans itest-rg-* leaked groups (safety net), no longer stops/deletes the main test RG - pipeline.yml: add dedicated 'integration-tests-cleanup' job that runs with if:always() after BOTH integration test jobs complete, then deletes all resource groups for the current run * fix(ci): remove dead code flagged by PMD PMD caught unused deleteOwnedResourceGroups, deleteLabeledResourceGroups, stopDeploymentsInResourceGroup, waitForDeploymentsStopped, and ownsResourceGroup methods left over from moving RG cleanup to CI job. Also remove unused clearDeploymentIdCache(). * fix(itest): disable stop_deployment tests that kill shared RPT deployment ActionTest.stop_deployment_changesTargetStatus and DeploymentTest. update_targetStatus_stopsRunningDeployment stop the RPT deployment created by AICoreServiceTest. Subsequent Recommendation tests then hit 404 from the inference endpoint for the full retry budget (~250s) because the stopped deployment's inference route is gone and the newly created replacement deployment needs warmup time. Disable both tests until they are refactored to create and stop their own isolated deployment instead of the shared one. * fix(ci): cleanup job reads credentials from VCAP_SERVICES not AICORE_SERVICE_KEY cds bind --exec injects credentials via VCAP_SERVICES, not AICORE_SERVICE_KEY. The cleanup script was trying to parse AICORE_SERVICE_KEY which was undefined, causing a JSON parse error.
…40) - Replace string concatenation in ServiceException messages with SLF4J-style {} placeholders in AICoreSetupHandler (3 locations) - Replace Collectors.toList() with .toList() in AbstractCrudHandler - Replace new ArrayList<>() with List.of() for null case - Remove unused imports (ArrayList, Collectors) Closes #32
…ler with record (#41) * fix: use idiomatic patterns for error messages and stream collection - Replace string concatenation in ServiceException messages with SLF4J-style {} placeholders in AICoreSetupHandler (3 locations) - Replace Collectors.toList() with .toList() in AbstractCrudHandler - Replace new ArrayList<>() with List.of() for null case - Remove unused imports (ArrayList, Collectors) Closes #32 * refactor: use cds-maven-plugin generated interfaces for type-safe entity access Replace manual AICoreElements constants class with generated typed interfaces from cds-maven-plugin (following cds-feature-attachments pattern): - Add cds-maven-plugin generate goal to cds-feature-ai-core/pom.xml - Generated interfaces: Deployments, Configurations, ResourceGroups, BckndResourceGroupLabel (with constants + typed getters/setters) - Update all handlers to use generated constants (e.g., Deployments.ID) and typed factory methods (e.g., Deployments.create(), ResourceGroups.create(id)) - Remove manual AICoreElements.java (replaced by code generation) - DeploymentHandler: use Deployments.create() with typed setters instead of 17-parameter buildDeploymentData method Closes #33 * fix: use explicit types and remove unnecessary cast in ConfigurationHandler - Use List<ParameterArgumentBinding> instead of var for bindings - Use ParameterArgumentBinding instead of var for bm - Remove unnecessary (CdsData) cast since ParameterArgumentBinding extends CdsData * fix: use ArtifactArgumentBinding generated interface for inputArtifactBindings Replace raw CdsData with typed ArtifactArgumentBinding.create() and setters for the inputArtifactBindings mapping in ConfigurationHandler. * refactor: use typed data parameters in handler method signatures Use CAP Java's handler data parameter injection for onCreate/onUpdate: - DeploymentHandler.onCreate(ctx, List<Deployments>) - DeploymentHandler.onUpdate(ctx, List<Deployments>) - ConfigurationHandler.onCreate(ctx, List<Configurations>) - ResourceGroupHandler.onCreate(ctx, List<ResourceGroups>) This eliminates manual CQN entry extraction (context.getCqn().entries()) and enables typed getters (entry.getName() vs entry.get("name")). Update tests to pass data as the second parameter directly. * fix: remove unused CdsData import in ConfigurationHandler
* fix: use idiomatic patterns for error messages and stream collection
- Replace string concatenation in ServiceException messages with SLF4J-style
{} placeholders in AICoreSetupHandler (3 locations)
- Replace Collectors.toList() with .toList() in AbstractCrudHandler
- Replace new ArrayList<>() with List.of() for null case
- Remove unused imports (ArrayList, Collectors)
Closes #32
* refactor: use cds-maven-plugin generated interfaces for type-safe entity access
Replace manual AICoreElements constants class with generated typed
interfaces from cds-maven-plugin (following cds-feature-attachments pattern):
- Add cds-maven-plugin generate goal to cds-feature-ai-core/pom.xml
- Generated interfaces: Deployments, Configurations, ResourceGroups,
BckndResourceGroupLabel (with constants + typed getters/setters)
- Update all handlers to use generated constants (e.g., Deployments.ID)
and typed factory methods (e.g., Deployments.create(), ResourceGroups.create(id))
- Remove manual AICoreElements.java (replaced by code generation)
- DeploymentHandler: use Deployments.create() with typed setters instead
of 17-parameter buildDeploymentData method
Closes #33
* refactor: slim AICoreService interface to public API surface
Remove implementation-detail methods from AICoreService interface:
- getDefaultResourceGroup()
- getResourceGroupPrefix()
- getTenantResourceGroupCache()
- getResourceGroupDeploymentCache()
- clearTenantCache()
- resolveResourceGroupFromKeys()
These methods remain public on AICoreServiceImpl and MockAICoreServiceImpl
but are no longer part of the service contract. Only domain-level API
(resourceGroupForTenant, deploymentId, inferenceClient, isMultiTenancyEnabled)
and getRetry() (cross-module consumer) remain on the interface.
Closes #34
* refactor: introduce AbstractAICoreService base class for shared internal methods
Extract getDefaultResourceGroup(), getResourceGroupPrefix(),
getTenantResourceGroupCache(), getResourceGroupDeploymentCache(),
clearTenantCache(), and resolveResourceGroupFromKeys() into an abstract
base class that both AICoreServiceImpl and MockAICoreServiceImpl extend.
This keeps the public AICoreService interface slim (domain API only)
while providing a typed common base for tests and internal consumers
that need access to cache/config methods.
Update all integration and MTX test files to use the new
getAICoreServiceImpl() helper that returns AbstractAICoreService.
Closes #34
) * fix: use idiomatic patterns for error messages and stream collection - Replace string concatenation in ServiceException messages with SLF4J-style {} placeholders in AICoreSetupHandler (3 locations) - Replace Collectors.toList() with .toList() in AbstractCrudHandler - Replace new ArrayList<>() with List.of() for null case - Remove unused imports (ArrayList, Collectors) Closes #32 * refactor: use cds-maven-plugin generated interfaces for type-safe entity access Replace manual AICoreElements constants class with generated typed interfaces from cds-maven-plugin (following cds-feature-attachments pattern): - Add cds-maven-plugin generate goal to cds-feature-ai-core/pom.xml - Generated interfaces: Deployments, Configurations, ResourceGroups, BckndResourceGroupLabel (with constants + typed getters/setters) - Update all handlers to use generated constants (e.g., Deployments.ID) and typed factory methods (e.g., Deployments.create(), ResourceGroups.create(id)) - Remove manual AICoreElements.java (replaced by code generation) - DeploymentHandler: use Deployments.create() with typed setters instead of 17-parameter buildDeploymentData method Closes #33 * refactor: slim AICoreService interface to public API surface Remove implementation-detail methods from AICoreService interface: - getDefaultResourceGroup() - getResourceGroupPrefix() - getTenantResourceGroupCache() - getResourceGroupDeploymentCache() - clearTenantCache() - resolveResourceGroupFromKeys() These methods remain public on AICoreServiceImpl and MockAICoreServiceImpl but are no longer part of the service contract. Only domain-level API (resourceGroupForTenant, deploymentId, inferenceClient, isMultiTenancyEnabled) and getRetry() (cross-module consumer) remain on the interface. Closes #34 * refactor: decompose FioriRecommendationHandler into focused helpers Extract two helper classes from the 400-line handler: - RecommendationContextBuilder: entity analysis, prediction element discovery, context query building, synthetic key computation, and row assembly - RecommendationResultParser: prediction value type coercion, text path resolution from annotations, description DB lookups, and final recommendations map assembly The handler is now a slim orchestrator (~110 lines) that delegates complex logic to the helpers. Also replace unbounded ConcurrentHashMap.newKeySet() with a bounded Caffeine cache (max 10,000 entries) for the entity negative-result cache. Closes #35 * fix: add explicit caffeine dependency; make computeSyntheticKey private - Add caffeine dependency to cds-feature-recommendations/pom.xml (previously relied on transitive from cds-feature-ai-core) - Make computeSyntheticKey private in RecommendationContextBuilder (only called internally from assembleRows)
* fix: use idiomatic patterns for error messages and stream collection
- Replace string concatenation in ServiceException messages with SLF4J-style
{} placeholders in AICoreSetupHandler (3 locations)
- Replace Collectors.toList() with .toList() in AbstractCrudHandler
- Replace new ArrayList<>() with List.of() for null case
- Remove unused imports (ArrayList, Collectors)
Closes #32
* refactor: use cds-maven-plugin generated interfaces for type-safe entity access
Replace manual AICoreElements constants class with generated typed
interfaces from cds-maven-plugin (following cds-feature-attachments pattern):
- Add cds-maven-plugin generate goal to cds-feature-ai-core/pom.xml
- Generated interfaces: Deployments, Configurations, ResourceGroups,
BckndResourceGroupLabel (with constants + typed getters/setters)
- Update all handlers to use generated constants (e.g., Deployments.ID)
and typed factory methods (e.g., Deployments.create(), ResourceGroups.create(id))
- Remove manual AICoreElements.java (replaced by code generation)
- DeploymentHandler: use Deployments.create() with typed setters instead
of 17-parameter buildDeploymentData method
Closes #33
* refactor: slim AICoreService interface to public API surface
Remove implementation-detail methods from AICoreService interface:
- getDefaultResourceGroup()
- getResourceGroupPrefix()
- getTenantResourceGroupCache()
- getResourceGroupDeploymentCache()
- clearTenantCache()
- resolveResourceGroupFromKeys()
These methods remain public on AICoreServiceImpl and MockAICoreServiceImpl
but are no longer part of the service contract. Only domain-level API
(resourceGroupForTenant, deploymentId, inferenceClient, isMultiTenancyEnabled)
and getRetry() (cross-module consumer) remain on the interface.
Closes #34
* refactor: decompose FioriRecommendationHandler into focused helpers
Extract two helper classes from the 400-line handler:
- RecommendationContextBuilder: entity analysis, prediction element
discovery, context query building, synthetic key computation, and
row assembly
- RecommendationResultParser: prediction value type coercion, text path
resolution from annotations, description DB lookups, and final
recommendations map assembly
The handler is now a slim orchestrator (~110 lines) that delegates
complex logic to the helpers.
Also replace unbounded ConcurrentHashMap.newKeySet() with a bounded
Caffeine cache (max 10,000 entries) for the entity negative-result cache.
Closes #35
* refactor: accept SDK API clients as constructor parameters
Add overloaded constructor to AICoreServiceImpl that accepts
DeploymentApi, ConfigurationApi, ResourceGroupApi, and AiCoreService
as parameters. The existing convenience constructor delegates to it
with default instances.
This enables direct injection of mocked clients in unit tests without
needing to mock getter methods on the service itself.
Closes #36
* refactor: remove convenience constructor, pass API clients explicitly
Remove the delegating convenience constructor from AICoreServiceImpl.
The only caller (AICoreServiceConfiguration) now passes DeploymentApi,
ConfigurationApi, ResourceGroupApi, and AiCoreService explicitly,
making the dependency wiring fully transparent at the creation site.
Closes #36
- Add -am flag to coverage-report generation so dependency modules are in the reactor (required by report-aggregate to resolve classes) - Add -DskipTests to avoid re-running tests - Include individual per-module JaCoCo reports in sonar paths as fallback
SummaryThe following content is AI-generated and provides a summary of the pull request: Feat: Initial Prototype for CAP Java AI IntegrationNew FeatureThis pull request introduces the initial prototype for a comprehensive AI feature integration for CAP Java applications. It includes two main libraries, a full testing suite, CI/CD pipelines, and a sample application. ✨
✨
Note: This PR is for code review only and should be closed once the review is complete. ChangesThis is a large initial commit that scaffolds the entire project. The changes are summarized by module:
PR Bot InformationVersion:
💌 Have ideas or want to contribute? Create an issue and share your thoughts with us! Made with ❤️ by Hyperspace. |
There was a problem hiding this comment.
This pull request is a large but well-structured initial implementation of a new feature, including comprehensive CI/CD pipelines and documentation. I've identified a few areas for improvement regarding maintainability and a minor bug in the cache-clearing logic. Overall, the quality is high.
PR Bot Information
Version: 1.21.0 | 📖 Documentation | 🚨 Create Incident | 💬 Feedback
- File Content Strategy: Diffs only
- Event Trigger:
pull_request.opened - Correlation ID:
ea28e160-ae3d-46b6-8a69-51c04ea3ba2b - LLM:
gemini-2.5-pro
* fix: add assertion to delete_resourceGroup test case SonarQube flagged the test for having no assertions. After deletion, verify the resource group is either removed or no longer in PROVISIONED status. * fix: use assertThatCode for async delete operation The AI Core delete is asynchronous - the resource group remains in PROVISIONED status immediately after deletion. Assert that the delete call itself completes without throwing instead.
* fix: add assertion to delete_resourceGroup test case SonarQube flagged the test for having no assertions. After deletion, verify the resource group is either removed or no longer in PROVISIONED status. * fix: use assertThatCode for async delete operation The AI Core delete is asynchronous - the resource group remains in PROVISIONED status immediately after deletion. Assert that the delete call itself completes without throwing instead. * chore: update repo references from cds-feature-ai to cds-ai Rename all standalone references to the old repository name 'cds-feature-ai' to the new name 'cds-ai'. This includes: - GitHub URLs (SCM, issues, security policy) - REUSE.toml package metadata - Copyright headers in all source files - License header template in pom.xml Submodule artifact names (cds-feature-ai-core, etc.) are intentionally left unchanged.
Signed-off-by: Marvin L <marvin.lindner@sap.com>
…ates (#48) Bumps the minor-patch group with 2 updates in the / directory: [org.junit:junit-bom](https://github.com/junit-team/junit-framework) and [com.diffplug.spotless:spotless-maven-plugin](https://github.com/diffplug/spotless). Updates `org.junit:junit-bom` from 6.0.3 to 6.1.0 - [Release notes](https://github.com/junit-team/junit-framework/releases) - [Commits](junit-team/junit-framework@r6.0.3...r6.1.0) Updates `com.diffplug.spotless:spotless-maven-plugin` from 3.5.0 to 3.5.1 - [Release notes](https://github.com/diffplug/spotless/releases) - [Changelog](https://github.com/diffplug/spotless/blob/main/CHANGES.md) - [Commits](diffplug/spotless@maven/3.5.0...maven/3.5.1) --- updated-dependencies: - dependency-name: com.diffplug.spotless:spotless-maven-plugin dependency-version: 3.5.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: org.junit:junit-bom dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Marvin L <marvin.lindner@sap.com>
Bumps the minor-patch group with 1 update: [actions/stale](https://github.com/actions/stale). Updates `actions/stale` from 10.2.0 to 10.3.0 - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](actions/stale@b5d41d4...eb5cf3a) --- updated-dependencies: - dependency-name: actions/stale dependency-version: 10.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Marvin L <marvin.lindner@sap.com>
Replace local composite actions (build, deploy-release, scan-with-codeql, scan-with-blackduck) and reusable workflows (issue, stale, prevent-issue-labeling) with their shared equivalents from cap-java/.github. Refactor cf-bind to delegate CF CLI install and login to the shared cf-login action. Closes #54
* chore: code review cleanup before senior review - fix(AICoreServiceImpl): move deploymentLocks from a Caffeine cache to ConcurrentHashMap so two threads asking for the same key always synchronize on the same monitor; eviction could otherwise cause duplicate AI Core deployments. - fix(AICoreServiceImpl): wrap deploymentApi.get on cached deployment in try/catch so a 404 (deployment deleted out-of-band) invalidates the stale cache entry instead of failing forever. - refactor(DeploymentHandler): extract a shared applyTo + DeploymentValues record so the two toDeployments overloads share the field-set logic; remaining duplication is the unavoidable getter-chain on two SDK types with no common interface. - docs: README updates - replace nonexistent AIClient / AICoreClient / fetchPredictions / rpt1DeploymentId references with the real API, align recommendations contextRowLimit config key with the code. - docs: full Javadoc on AICoreService, ModelDeploymentSpec, AICoreServiceConfiguration, and class-level Javadoc on AICoreServiceImpl. - chore: add @OverRide annotations across AICoreServiceImpl and MockAICoreServiceImpl; promote getRuntime() to AbstractAICoreService. - chore(cds-starter-ai): keep packaging=jar (matches CAP starter convention) but document the empty-jar starter pattern explicitly. - docs(RecommendationResultParser): explain the placeholder 0.5 score emitted to Fiori Elements until the AI SDK exposes prediction_proba. - test: new AICoreServiceImplDeploymentIdTest covering cache-hit, stale-cache 404 invalidation, existing-deployment reuse, second-call caching, and create-when-config-exists happy paths. - chore: mvn spotless:apply (Google Java Format + sort-pom). * build: make mvn install hermetic via npm ci (no global @sap/cds-dk) Introduce a package.json + package-lock.json at cds-feature-ai-core root pinning @sap/cds-dk@9.9.1, and bind a new cds-maven-plugin 'cds.npm-ci' execution that runs 'npm ci' before the cds.build goal. A fresh checkout can now run 'mvn install' without any prerequisite beyond Maven + JDK; cds-maven-plugin's bundled Node downloads @sap/cds-dk into a local node_modules at module root, outside the cds source tree (so 'cds build --src ./ --dest ../../../../../../gen/srv' no longer self-copies node_modules). Drop -Dcds.install-node.skip from every Maven invocation in CI (.github/workflows/pipeline.yml, .github/actions/integration-tests, .github/actions/scan-with-sonar) since the bundled Node is now the canonical execution environment for the cds CLI inside Maven. Drop the standalone 'npm i -g @sap/cds-dk' steps from the 'tests' and 'local-mtx-tests' jobs (Maven-only paths). The cf-bind composite action keeps its global cds-dk install because cds bind --exec is invoked as a shell command in subsequent steps. * fix: narrow stale-cache catch to 404 only; use deploymentCacheKey helper in tests Address two of three review comments from the PR bot on PR #57: 1. (AICoreServiceImpl#deploymentId) The previous catch on OpenApiRequestException swallowed all OpenApi failures — including transient 5xx and network errors — and unconditionally invalidated the cache entry. That would silently discard a still-valid cached deployment and trigger a duplicate creation. Narrow the catch to only treat HTTP 404 as the 'deployment deleted out-of-band' signal; propagate everything else so the caller's retry/backoff policy handles it. 2. (AICoreServiceImplDeploymentIdTest) The tests seeded the cache with the raw string 'RG + "::" + CONFIG_NAME', duplicating the cache-key format with the production code. A future change to the deploymentCacheKey() format would silently make the tests pass for the wrong reason. Promote deploymentCacheKey() from private to package-private and have the tests derive the key through it. 3. Add a regression test (cacheStale_5xxOnGet_propagatesAndPreservesCacheEntry) that exercises the narrowed catch: a 503 from deploymentApi.get must propagate AND leave the cache entry intact. * build: add hermetic npm ci for cds-feature-recommendations The previous commit (67da453) made cds-feature-ai-core hermetic by binding 'npm ci' to its Maven build, but cds-feature-recommendations also runs 'cds compile' (via cds.compile execution) and was still relying on a globally installed @sap/cds-dk that we removed from CI. Mirror the same pattern: introduce a package.json + package-lock.json at the cds-feature-recommendations module root pinning @sap/cds-dk@9.9.1, and add 'cds.install-node' + 'cds.npm-ci' executions before 'cds.compile'.
agoerler
left a comment
There was a problem hiding this comment.
Some feedback. (I have not looked at the ai core feature in detail so far).
Some general findings:
- I like the separation into two plugins very much :-)
- please move the APIs to dedicated
apipackages - looks very tidy overall
AI Core
- the AICoreService interface could be possibly segragated into
- the part to manage resource groups and deployment IDs
- the inference API - maybe you don't need this part at all but could directly register
ApiClientas a technical service or Spring bean.
- the tenant must not appear in the
AICoreServiceAPI. The user of theAICoreServiceshould be tenant-agnostic but the implementation should be tenant-aware - you should provide a mock implementation of the
APIClientthat can use langchain4j to use local models - ideally you externalize the mapping of symbolic inference model names to actual model names so that we can use differently expensive models for testing and in production
Recommendations
- make sure the annotations can also be added manually
- consider to register the
RecommendationClientas a service so that the application could overwrite theRecommendationClient's on-handler to compute recommendations somehow differently (could be done later) - design the
RecommendationClientAPI so that it's not too tightly coupled to RPT-1
| 3. Returns predictions as `SAP_Recommendations` in the OData response | ||
| 4. Fiori Elements renders the recommendations as suggestions in form fields | ||
|
|
||
| ## Setup |
|
|
||
| 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. | ||
|
|
||
| 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`: |
There was a problem hiding this comment.
Please consider to offer a slimmed-down version of @cap-js/ai that does not contain the model artifacts. Or exclude the model artifacts from cds-feature-recommendations.
|
|
||
| | Entity | Operations | Description | | ||
| | ----------------------- | -------------------- | ---------------------------------------------------------------- | | ||
| | `AICore.resourceGroups` | READ, CREATE, DELETE | Resource group lifecycle, supports label filtering by `tenantId` | |
There was a problem hiding this comment.
| | `AICore.resourceGroups` | READ, CREATE, DELETE | Resource group lifecycle, supports label filtering by `tenantId` | | |
| | `AICore.resourceGroups` | READ, CREATE, DELETE | Resource group lifecycle, supports label filtering | |
make sure that the API is always restricted to the current tenant and does not expose other tenants' resource groups
| // Get the resource group ID for a CDS tenant | ||
| String rgId = aiCoreService.resourceGroupForTenant(tenantId); |
There was a problem hiding this comment.
| // Get the resource group ID for a CDS tenant | |
| String rgId = aiCoreService.resourceGroupForTenant(tenantId); | |
| // Get the resource group ID for the current tenant | |
| String rgId = aiCoreService.resourceGroup(); |
|
|
||
| ## Multi-Tenancy | ||
|
|
||
| When `cds.requires.AICore.multiTenancy=true`: |
There was a problem hiding this comment.
Ideally you do without this config but evaluate the properties under https://pages.github.tools.sap/cap/docs/java/developing-applications/properties#cds-multiTenancy
| // Resolve a deployment and obtain a configured ApiClient for it | ||
| String resourceGroupId = aiCore.resourceGroupForTenant(tenantId); | ||
| String deploymentId = aiCore.deploymentId(resourceGroupId, RptModelSpec.rpt1()); | ||
| ApiClient client = aiCore.inferenceClient(resourceGroupId, deploymentId); |
There was a problem hiding this comment.
The implementation can obtain the tenantId from the tenant provider service. As there seems to be exactly one resource group per tenant we could simplify this:
// Resolve a deployment and obtain a configured ApiClient for it
String resourceGroupId = aiCore.resourceGroup();
String deploymentId = aiCore.deploymentId(resourceGroupId, RptModelSpec.rpt1());
ApiClient client = aiCore.inferenceClient(resourceGroupId, deploymentId);the only variable appears to the model for which there appears to be exactly one deployment -> we can further simplify the API to
AICoreService aiCore = runtime.getServiceCatalog()
.getService(AICoreService.class, AICoreService.DEFAULT_NAME);
ApiClient client = aiCore.inferenceClient(RptModelSpec.rpt1());We could also use dependency injection:
@Autowired
AICoreService aiCore;
ApiClient client = aiCore.inferenceClient(RptModelSpec.rpt1());and if we add an option to configure service instances for the ApiClient in the application.yaml (sketchy!):
cds.api.core.client:
-name : myRpt1Client
model: RPT1then we could use qualified dependency injection
@Autowired
@Qualifier("myRpt1Client")
ApiClient rpt1;If we externalize the model config for the inference client we can have different models in production as well as on localhost/CI so that we don't have to burn expensive tokens for testing but can rather use something cheap for this purpose.
| /** | ||
| * Returns the shared {@link Retry} used internally for transient AI Core errors. Exposed so | ||
| * downstream inference clients can reuse the same backoff policy. | ||
| */ | ||
| Retry getRetry(); |
agoerler
left a comment
There was a problem hiding this comment.
Some feedback. (I have not looked at the ai core feature in detail so far).
Some general findings:
- I like the separation into two plugins very much :-)
- please move the APIs to dedicated
apipackages - looks very tidy overall
AI Core
- the AICoreService interface could be possibly segragated into
- the part to manage resource groups and deployment IDs
- the inference API - maybe you don't need this part at all but could directly register
ApiClientas a technical service or Spring bean.
- the tenant must not appear in the
AICoreServiceAPI. The user of theAICoreServiceshould be tenant-agnostic but the implementation should be tenant-aware - you should provide a mock implementation of the
APIClientthat can use langchain4j to use local models - ideally you externalize the mapping of symbolic inference model names to actual model names so that we can use differently expensive models for testing and in production
Recommendations
- make sure the annotations can also be added manually
- consider to register the
RecommendationClientas a service so that the application could overwrite theRecommendationClient's on-handler to compute recommendations somehow differently (could be done later) - design the
RecommendationClientAPI so that it's not too tightly coupled to RPT-1
Thanks! |
* refactor(ai-core): move public API to dedicated api package Move AICoreService and ModelDeploymentSpec from com.sap.cds.feature.aicore.core to com.sap.cds.feature.aicore.api so the public contract is clearly separated from the internal implementation. Add a package-info.java describing the api package. Internal classes (AbstractAICoreService, AICoreServiceImpl, MockAICoreServiceImpl, AICoreServiceConfiguration, handlers) and the unit test for AICoreServiceImpl get explicit imports for the relocated types. Implementation references in AICoreService javadoc use FQCN @link to avoid coupling the interface to internals. Addresses review feedback on PR #49 (move APIs to api package). * refactor(recommendations): move public API to dedicated api package Move RecommendationClient, RecommendationClientResolver, RptInferenceClient and RptModelSpec from com.sap.cds.feature.recommendation to com.sap.cds.feature.recommendation.api so the public contract is clearly separated from the internal implementation. Add a package-info.java for the api package. RecommendationClient and RecommendationClientResolver are promoted from package-private to public to be usable from outside the plugin. Internal classes (FioriRecommendationHandler, MockRecommendationClient, RecommendationConfiguration) and tests get explicit imports for the relocated types. Addresses review feedback on PR #49 (move APIs to api package). * test(integration): update imports for relocated api packages Update test imports in integration-tests/spring and integration-tests/mtx-local to reference AICoreService, ModelDeploymentSpec and RptModelSpec from their new api packages. Addresses review feedback on PR #49 (move APIs to api package). * samples(bookshop): update imports for relocated api packages Update AICoreShowcaseHandler imports to reference AICoreService, RptInferenceClient and RptModelSpec from their new api packages. Addresses review feedback on PR #49 (move APIs to api package).
- Replace resourceGroupForTenant(String) with resourceGroup() on the public AICoreService interface. The implementation reads the tenant from the current RequestContext internally. - Remove isMultiTenancyEnabled() and getRetry() from the public interface; they remain accessible on AbstractAICoreService for internal callers. - Remove the CDS function 'resourceGroupForTenant' from index.cds and its action handler. - Detect multi-tenancy via standard CAP Java cds.multiTenancy.sidecar.url property and DeploymentService presence instead of custom flag. - Update RecommendationClientResolver to drop tenantId parameter. - Update samples, tests, and javadoc accordingly. Addresses review comments from PR #49 (Issue 2).
- Add tenant ownership verification on ResourceGroupHandler for READ (by-key), UPDATE, and DELETE operations. Returns 404 if the resource group belongs to a different tenant. - Scope list queries (READ without key) to the current tenant's resource groups via the tenant label filter in multi-tenancy mode. - Add ensureResourceGroupAccessible() guard to DeploymentHandler and ConfigurationHandler, validating the addressed resource group belongs to the current tenant before forwarding to AI Core. - Provider/system users are exempt from tenant restrictions and can access all resource groups (useful for ops/debug scenarios). - Add isProviderUser() and currentTenantId() as public helpers on AbstractAICoreService for use by handler classes. Addresses review comments from PR #49 (Issue 3a).
- Rename all configuration properties from cds.requires.AICore.* to cds.ai.core.* to align with CAP Java property naming conventions. - Rename cds.requires.recommendations.contextRowLimit to cds.ai.recommendations.contextRowLimit. - Drop the cds.requires.AICore.multiTenancy flag entirely; multi- tenancy is now auto-detected from standard CAP Java properties. - Update README with new configuration namespace and examples. Addresses review comments from PR #49 (Issue 3b).
| public interface AICoreService extends CqnService { | ||
|
|
||
| /** Default service name under which an instance is registered in the service catalog. */ | ||
| String DEFAULT_NAME = "AICore"; |
There was a problem hiding this comment.
| String DEFAULT_NAME = "AICore"; | |
| String DEFAULT_NAME = "AICoreService$Default"; |
*Service$Default is standard pattern for CAP provided services
|
|
||
| private static final Logger logger = LoggerFactory.getLogger(ActionHandler.class); | ||
|
|
||
| public ActionHandler(AICoreServiceImpl service) { |
There was a problem hiding this comment.
handlers are registered to concrete services and the called service is always always propagated via EventContext. Hence there is no need to store the ServiceImpl as reference. Also the handler should be fully decoupled from the concrete service implementation.
| super(service); | ||
| } | ||
|
|
||
| @On(event = "resourceGroupForTenant") |
| context.setCompleted(); | ||
| } | ||
|
|
||
| @On(event = "stop", entity = AICoreService.DEPLOYMENTS) |
| } | ||
|
|
||
| @On(event = "resourceGroupForTenant") | ||
| public void onResourceGroupForTenant(EventContext context) { |
There was a problem hiding this comment.
better switch to typesafe handler APIs
| .filter(q -> q.from().isRef()) | ||
| .map(q -> CqnAnalyzer.create(model).analyze(q).targetEntity()) | ||
| .map(CdsEntity::getQualifiedName) | ||
| .filter(name -> name.startsWith("AICore.")) |
There was a problem hiding this comment.
Shouldn't be the indicator rather be an annotation instead of a string prefix?
There was a problem hiding this comment.
As discussed: AICoreApplicationServiceHandler will be deleted since it introduces too many edge cases
Consumers should write the projection use case themselves!
| this.multiTenancyEnabled = multiTenancyEnabled; | ||
| CdsEnvironment env = runtime.getEnvironment(); | ||
| this.maxRetries = | ||
| env.getProperty("cds.requires.AICore.maxRetries", Integer.class, DEFAULT_MAX_RETRIES); |
There was a problem hiding this comment.
proposal:
"cds.AICore.client.maxRetries"
"cds.AICore.client.initialDelayMs"
"cds.AICore.resourceGroup.default"
"cds.AICore.resourceGroup.prefix"
There was a problem hiding this comment.
I named them cds.ai.core.* in my branch
for recommendation, it then would be cds.ai.recommendations.*
There was a problem hiding this comment.
For embeddings I could imagine cds.ai.embeddings with different profiles, e.g. using HANA NLP or AI Core models:
cds.ai:
embeddings:
# Local HANA model - no remote source
default:
model: SAP_NEB.20240715
large:
model: text-embedding-3-large
remoteSource: AICore
options:
execution_params:
timeout: 60000We could (later) add cds-feature-embeddings that uses feature-ai-core to provide a CDS4j EmbeddingService integration and does the HANA setup for using embedding models from AI Core.
| this.service = service; | ||
| } | ||
|
|
||
| @After(event = DeploymentService.EVENT_SUBSCRIBE) |
There was a problem hiding this comment.
| @After(event = DeploymentService.EVENT_SUBSCRIBE) | |
| @After |
| } | ||
|
|
||
| @Override | ||
| public String deploymentId(String resourceGroupId, ModelDeploymentSpec spec) { |
There was a problem hiding this comment.
I'd expect all this implemented actions in handlers (same for mock variant)
|
|
||
| private static final long MAX_INTERVAL_MS = 30_000L; | ||
|
|
||
| private static Retry buildRetry(int maxAttempts, long initialDelayMs) { |
| this.multiTenancyEnabled = multiTenancyEnabled; | ||
| CdsEnvironment env = runtime.getEnvironment(); | ||
| this.maxRetries = | ||
| env.getProperty("cds.requires.AICore.maxRetries", Integer.class, DEFAULT_MAX_RETRIES); |
There was a problem hiding this comment.
For embeddings I could imagine cds.ai.embeddings with different profiles, e.g. using HANA NLP or AI Core models:
cds.ai:
embeddings:
# Local HANA model - no remote source
default:
model: SAP_NEB.20240715
large:
model: text-embedding-3-large
remoteSource: AICore
options:
execution_params:
timeout: 60000We could (later) add cds-feature-embeddings that uses feature-ai-core to provide a CDS4j EmbeddingService integration and does the HANA setup for using embedding models from AI Core.
| } | ||
|
|
||
| @Override | ||
| public ApiClient inferenceClient(String resourceGroupId, String deploymentId) { |
There was a problem hiding this comment.
Do we want to have an integration point for the ai-sdk-java and other higher-level APIs like LangChain4j? How would this look like?
| throw new UnsupportedOperationException( | ||
| "MockAICoreServiceImpl does not provide an inference client; tests should stub inference."); |
There was a problem hiding this comment.
ApiClient seems to be a low level technical API. On top of that we could (later) have integrations with the ai-sdk and other higher level APIs like LangChain4j.
* Make cache for entitiesWithoutPredictionsPerTenant tenant specific * refactor(ai-core): make AICoreService tenant-agnostic and DI-friendly - Replace resourceGroupForTenant(String) with resourceGroup() on the public AICoreService interface. The implementation reads the tenant from the current RequestContext internally. - Remove isMultiTenancyEnabled() and getRetry() from the public interface; they remain accessible on AbstractAICoreService for internal callers. - Remove the CDS function 'resourceGroupForTenant' from index.cds and its action handler. - Detect multi-tenancy via standard CAP Java cds.multiTenancy.sidecar.url property and DeploymentService presence instead of custom flag. - Update RecommendationClientResolver to drop tenantId parameter. - Update samples, tests, and javadoc accordingly. Addresses review comments from PR #49 (Issue 2). * feat(ai-core): restrict AICore entity APIs to current tenant - Add tenant ownership verification on ResourceGroupHandler for READ (by-key), UPDATE, and DELETE operations. Returns 404 if the resource group belongs to a different tenant. - Scope list queries (READ without key) to the current tenant's resource groups via the tenant label filter in multi-tenancy mode. - Add ensureResourceGroupAccessible() guard to DeploymentHandler and ConfigurationHandler, validating the addressed resource group belongs to the current tenant before forwarding to AI Core. - Provider/system users are exempt from tenant restrictions and can access all resource groups (useful for ops/debug scenarios). - Add isProviderUser() and currentTenantId() as public helpers on AbstractAICoreService for use by handler classes. Addresses review comments from PR #49 (Issue 3a). * chore(ai-core): rename config namespace to cds.ai.core - Rename all configuration properties from cds.requires.AICore.* to cds.ai.core.* to align with CAP Java property naming conventions. - Rename cds.requires.recommendations.contextRowLimit to cds.ai.recommendations.contextRowLimit. - Drop the cds.requires.AICore.multiTenancy flag entirely; multi- tenancy is now auto-detected from standard CAP Java properties. - Update README with new configuration namespace and examples. Addresses review comments from PR #49 (Issue 3b). * fix(ai-core): handle null tenant in resourceGroupForTenant When resourceGroupForTenant is called with a null tenantId (which happens when currentTenantId() returns null in single-tenant or non-tenant-scoped RequestContexts), fall back to the default resource group instead of passing null to the Caffeine cache (which throws NPE). This fixes integration test failures in the CI pipeline where the ApplicationServiceDelegation and Recommendation tests run without an explicit tenant in the RequestContext. * fix(ci): cleanup all run attempts and cds-itest resource groups The cleanup step previously only deleted resource groups matching the exact current run_id AND run_attempt. When a run failed and was re-run, the previous attempt's resource groups were never cleaned up, eventually hitting the AI Core resource group limit (50). Changes: - Match prefix 'itest-{run_id}-' (all attempts) instead of the exact 'itest-{run_id}-{run_attempt}' string. - Same for 'sonar-{run_id}-' prefix. - Also delete 'cds-itest-' prefixed resource groups which are created by the multi-tenancy integration tests via resourceGroupForTenant() and were never cleaned up by the pipeline. * fix(itest): align config namespace with cds.ai.core rename The source code (commit c30080b) renamed properties from cds.requires.AICore.* to cds.ai.core.*, but the integration test application.yaml files were not updated. This meant the CDS_AICORE_TEST_RESOURCE_GROUP env var set by CI was silently ignored and tests always ran against the literal default resource group. - spring/application.yaml: cds.requires.AICore -> cds.ai.core - mtx-local/application.yaml: remove obsolete cds.requires.AICore.multiTenancy (now auto-detected from cds.multi-tenancy.sidecar.url) * test(ai-core): add unit tests for tenant scoping and mock service Cover new code paths introduced by the tenant-scoping branch: - TenantScopingTest (7 tests): exercises every branch of AbstractCrudHandler.ensureResourceGroupAccessible() — provider bypass, single-tenancy bypass, null tenant, matching/non-matching labels, 404. - MockAICoreServiceImplTest (9 tests): both constructors, MT enabled/disabled, resourceGroupForTenant, cache isolation, clearTenantCache, getRetry, config property reads. - AICoreServiceImplDeploymentIdTest (+2 tests): resourceGroupForTenant(null) returns default even with MT enabled; single-tenancy always returns default. * chore(recommendations): add TODO for model-changed integration test Document the missing E2E coverage for RecommendationModelChangedHandler. The proper test requires an extensibility-enabled sidecar with extension JSON that adds prediction columns — not yet set up in mtx-local. The cache-invalidation logic itself is covered by the existing unit test FioriRecommendationHandlerTest.invalidateTenant_removesOnlyThatTenantsEntries. * update cleanup * fix(ci): scope resource group cleanup to own job only Each parallel CI job (Java 17, Java 21, SonarQube) was using broad prefixes in its cleanup step, deleting resource groups belonging to sibling jobs still in progress. This caused intermittent 403 Forbidden errors when the affected jobs tried to use their now-deleted resource groups. Narrow the cleanup prefixes so each job only deletes its own: - integration-tests: itest-{run_id}-{attempt}-j{version}* - scan-with-sonar: sonar-{run_id}-{attempt}* Both still clean up itest-rg-* (ResourceGroupTest leftovers). * test(ai-core): add unit tests for uncovered code paths - DeploymentHandler: test onCreate (with/without TTL) and onUpdate happy path (targetStatus and configurationId branches) - ResourceGroupHandler: test onUpdate with/without labels, buildTenantLabelSelector branches (tenantId filter, MT non-provider, MT null tenant, single tenancy), ensureOwnedByCurrentTenant branches (provider, single tenant, wrong tenant, matching tenant) - AICoreServiceConfiguration: test eventHandlers() MockAICoreServiceImpl branch (with and without multi-tenancy), test detectMultiTenancy via services() for sidecarUrl branch and no-MT fallback * fix(ci): include cds-itest- prefix in resource group cleanup The MultiTenancyTest creates per-tenant resource groups with names like cds-itest-mt-a-{timestamp} (from resourceGroupPrefix 'cds-' + tenant name 'itest-*'). These are unique per test run (timestamped) and safe to clean up from any job without cross-job interference. * refactor: extract inline cleanup script into shared JS file --------- Co-authored-by: Marvin L <marvin.lindner@sap.com>
This PR contains the full prototype implementation for review purposes.
Note: This PR is for code review only — do not merge it. Close it once the review is complete.