diff --git a/.deploy/mta.yaml b/.deploy/mta.yaml index ddad64655..d173ec062 100644 --- a/.deploy/mta.yaml +++ b/.deploy/mta.yaml @@ -10,7 +10,7 @@ ID: tutorials-ims # Bump this on each release you deploy — it's the version shown by `cf mtas` # and in the mtar filename (tutorials-ims_.mtar). Deploy is manual: # `cd .deploy && mbt build && cf deploy mta_archives/tutorials-ims_.mtar -e ../deploy/.mtaext -f`. -version: 1.40.1 +version: 1.40.2 # Top-level parameters (overridable per-env via deploy/.mtaext). parameters: diff --git a/CLAUDE.md b/CLAUDE.md index 672b79242..168cd58d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,6 +74,7 @@ Deep dives (do not duplicate here — read the doc when relevant): - [docs/developers/operations/testing-endpoints.md](docs/developers/operations/testing-endpoints.md) — **canonical UI + API endpoint reference** (services, custom endpoints, auth scopes) - [docs/developers/operations/mta-deployment.md](docs/developers/operations/mta-deployment.md) — deploy runbook - [docs/developers/operations/rebuild-content-workflow.md](docs/developers/operations/rebuild-content-workflow.md) — three rebuild modes, auto-classify +- [docs/developers/operations/hana-aicore-embeddings.md](docs/developers/operations/hana-aicore-embeddings.md) — connect HANA to AI Core for DB-native `VECTOR_EMBEDDING` (PSE + `sapgenaihub` remote source); built-in NLP vs AI Core tiers; auto-embedding mechanisms (#2810) - [docs/developers/operations/qa-channel-bootstrap.md](docs/developers/operations/qa-channel-bootstrap.md) — QA author-preview channel - [docs/developers/operations/scheduler-troubleshooting.md](docs/developers/operations/scheduler-troubleshooting.md) — outbox-wedge recovery runbook - [docs/developers/reference/tutorials-ims-gotchas.md](docs/developers/reference/tutorials-ims-gotchas.md) — everything else (build pipeline quirks, publish flags, AI features, env vars, migration) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 59c3db376..97f06e4b1 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -182,6 +182,7 @@ export default defineConfig({ { text: 'Gameboard PROD data troubleshooting', link: '/developers/operations/gameboard-prod-data-troubleshooting' }, { text: 'GitHub App setup', link: '/developers/operations/github-app-setup' }, { text: 'GitHub dispatch PAT rotation', link: '/developers/operations/github-dispatch-pat-rotation' }, + { text: 'HANA AI Core embeddings', link: '/developers/operations/hana-aicore-embeddings' }, { text: 'HDI deploy checklist', link: '/developers/operations/hdi-deploy-checklist' }, { text: 'IAS setup', link: '/developers/operations/ias-setup' }, { text: 'Identity dedup reconciliation', link: '/developers/operations/identity-dedup-reconciliation' }, diff --git a/docs/developers/operations/hana-aicore-embeddings.md b/docs/developers/operations/hana-aicore-embeddings.md new file mode 100644 index 000000000..1fadbc61a --- /dev/null +++ b/docs/developers/operations/hana-aicore-embeddings.md @@ -0,0 +1,254 @@ +# HANA-Native AI Core Embeddings — Setup Runbook + +> **What this is.** How to connect the existing SAP AI Core instance to a +> `tutorials-ims` HANA container so embeddings can be generated **by SQL +> inside the database** (`VECTOR_EMBEDDING`), instead of only at the +> Node/CAP layer. Tracks issue +> [#2810](https://github.com/sap-tutorials/tutorials-ims/issues/2810). +> Design rationale: [`docs/superpowers/specs/2026-10-19-hana-aicore-embedding-design.md`](../../superpowers/specs/2026-10-19-hana-aicore-embedding-design.md). +> +> **Status:** DEV + PROD live and verified (2026-10-19). This is the +> *connection + proof* unit only — no +> application feature has been moved to DB-side embeddings yet. + +## Two tiers + +| Tier | What | AI Core needed? | Privilege needed? | +| --- | --- | --- | --- | +| **A — built-in NLP** | HANA's own embedding models (`SAP_GXY.20250407`, `SAP_NEB.20240715`) | No | None | +| **B — AI Core remote source** | AI Core gen-AI hub models (`text-embedding-3-small`, …) via `sapgenaihub` adapter | Yes | admin creates PSE + remote source (one-time); container inherits `EXECUTE` via `::external_privileges_role` | + +Tier A works out of the box and is the fastest sanity check. Tier B is +issue #2810's actual target. + +--- + +## Tier A — built-in NLP embedding (no setup) + +Run as the container runtime user (any `tutorials-hana` service key / +`cds bind`). Confirms `VECTOR_EMBEDDING` works from the DB layer: + +```sql +SELECT VECTOR_EMBEDDING('Hello world!', 'DOCUMENT', 'SAP_GXY.20250407') FROM DUMMY; +``` + +Expected: a 768-dimension `REAL_VECTOR`. List available built-in models +with `SELECT * FROM NLP_VECTOR_EMBEDDING_MODELS;`. + +> **Verified on DEV 2026-10-19:** returned a 768-dim vector. + +--- + +## Tier B — AI Core remote source + +### Prerequisites + +1. **Dedicated x509 service key** on the AI Core instance. On DEV this is + `hana-ai-core` on `tutorials-aicore` (a separate key from the CAP app + binding — different security requirements). Create with: + ```bash + cf create-service-key tutorials-aicore hana-ai-core \ + -c '{"credential-type":"x509","validity":365,"validity-type":"DAYS"}' + cf service-key tutorials-aicore hana-ai-core # read values + ``` + You need from the key JSON: `serviceurls.AI_API_URL`, `certurl`, + `clientid`, `certificate`, `key`. + +2. **AI Core deployment ID** of an orchestration deployment. List RUNNING + deployments (x509 mTLS against the token endpoint, then `/v2/lm/deployments`): + ``` + POST {certurl}/oauth/token grant_type=client_credentials&client_id={clientid} (client cert + key as mTLS) + GET {AI_API_URL}/v2/lm/deployments?status=RUNNING Authorization: Bearer AI-Resource-Group: default + ``` + Pick an `orchestration`-scenario deployment ID. + + > **DEV worked example:** deployment `da6d9c5e3fb50c3d` + > (`joule-chat-orchestration`). Other RUNNING on DEV: + > `d2f9fa53543e5c4e` (defaultOrchestrationConfig), + > `dfad0fa7cd47ab38` (text-embedding-3-small). + +### Step B1 — one-time admin setup: PSE + remote source + +> **This is an instance-level admin action, not a container artifact.** A +> PSE and a remote source are database-level objects created **once** by a +> privileged user (DBADMIN or equivalent holding `TRUST ADMIN`). They are +> shared — one AI Core remote source can serve every container/app on the +> instance. Do **not** try to grant `TRUST ADMIN`/PSE privileges into an +> HDI container; the container only needs `EXECUTE` on the finished remote +> source (Step B2). +> +> **Why not the container:** we initially tried to grant the PSE privilege +> into the `tutorials-hana` container (via `_grants.hdbgrants`, then via +> `default_access_role.hdbrole`). That is the wrong shape — it would give +> every app binding user a powerful PKI system privilege permanently for a +> one-time task, and the container `#OO` can't grant these privileges +> anyway (`#OO` is not allowed to grant `CERTIFICATE ADMIN`). See the +> "Failed approaches" box below. + +Run as a **DEV HANA admin**. Paste the **private key then the client +certificate** from the `hana-ai-core` service key, unescaped (`\n` → real +line breaks) — the key's `key` value (RSA private key PEM block) directly +followed by its `certificate` value (certificate PEM block): + +```sql +CREATE PSE TUTORIALS_AICORE_PSE; +ALTER PSE TUTORIALS_AICORE_PSE SET OWN CERTIFICATE ' +<<< hana-ai-core service-key "key" value here (RSA private key PEM block) >>> +<<< hana-ai-core service-key "certificate" value here (certificate PEM block) >>> +'; + +CREATE REMOTE SOURCE TUTORIALS_AICORE ADAPTER "sapgenaihub" CONFIGURATION + 'aiApiUrl=; + authUrl=; + resourceGroup=default; + deploymentId=; + clientId=' +WITH CREDENTIAL TYPE 'X509' PSE TUTORIALS_AICORE_PSE; +``` + +DEV worked values: `aiApiUrl=https://api.ai.prod.eu-central-1.aws.ml.hana.ondemand.com`, +`authUrl=https://tutorial-system.authentication.cert.eu10.hana.ondemand.com`, +`deploymentId=da6d9c5e3fb50c3d`, +`clientId=sb-78c0840a-3613-4a19-be4f-5cd52760090e!b676072|aicore!b540`. + +Privileges the admin needs: `TRUST ADMIN` (for `CREATE PSE`), +`CREATE REMOTE SOURCE`. `CERTIFICATE ADMIN` is only for *managed* PSEs +(JWKS-URL), not needed for `SET OWN CERTIFICATE`. + +> **Secrets:** cert + private key come only from the live service key — +> never commit them. Pull at setup via `cf service-key tutorials-aicore +> hana-ai-core`. Rotate `hana-ai-core` if the key JSON was exposed +> anywhere shared. + +
Failed approaches (do not repeat) + +1. `CREATE PSE` in `_grants.hdbgrants` under `tutorials-kg-grantor` (DEV + `20261019-105602`) → `invalid role name: CREATE PSE`. System + privileges can't be granted via that SQL-grantor path. +2. `"system_privileges": ["CREATE PSE"]` in `default_access_role.hdbrole` + (DEV `20261019-115443`) → `"CREATE PSE": invalid system privilege`. + `CREATE PSE` is the statement; the privilege is `TRUST ADMIN`. +3. `"system_privileges": ["TRUST ADMIN","CERTIFICATE ADMIN"]` in the role + (DEV `20261019-125352`) → `user "…#OO" is not allowed to grant … + "CERTIFICATE ADMIN"`. The container `#OO` cannot delegate PKI system + privileges — confirming PSE setup does not belong in the container. + +Root cause: PSE/remote-source setup is instance-level admin work, not a +per-container grant. `default_access_role.hdbrole` remains the right +mechanism for privileges the container user *should* own long-term — just +not for this. +
+ +### Step B2 — grant the container `EXECUTE` via `external_privileges_role` + +The **only** container-scoped grant needed. Grant it to the container's +broker-created **`::external_privileges_role`** — the role +purpose-built for "access to objects outside the HDI container". Every +binding user inherits it automatically and **it survives rebinds/ +redeploys** (the role persists for the life of the container; new binding +users are auto-assigned it by the broker). The admin owns the remote +source, so it can grant directly — no grantor service, no `.hdbgrants`, +no `#OO` intermediary. + +```sql +-- DEV +GRANT EXECUTE ON REMOTE SOURCE TUTORIALS_AICORE + TO "AC9753D6C4764F5ABE3B3CA4E88233C0::external_privileges_role"; +-- PROD (different container schema) +GRANT EXECUTE ON REMOTE SOURCE TUTORIALS_AICORE + TO "2FEC6DAB47984DB482E0B3BB80AFFEF7::external_privileges_role"; +``` + +> **Do NOT grant to a specific `_RT` user.** Each CF binding mints a new +> `_RT` user, so a per-user grant breaks on the next rebind (and granting +> to a throwaway binding user fails outright once that binding is gone — +> `invalid user name`). The role is the stable, documented target. +> +> **Do NOT use `default_access_role.hdbrole` for this.** When present, +> that artifact *revokes* the container's default run-time-schema +> permissions and replaces them — unwanted side effects — and `#OO` +> cannot grant `EXECUTE` on a remote source it doesn't own anyway. The SAP +> HDI docs name `external_privileges_role` as the external-object path. +> (Earlier failed attempts chasing the container-grant angle are recorded +> in the "Failed approaches" box under B1.) + +Verify (as admin or any app user): +```sql +SELECT PRIVILEGE, GRANTEE FROM GRANTED_PRIVILEGES + WHERE OBJECT_NAME = 'TUTORIALS_AICORE'; +-- expect: EXECUTE -> ::external_privileges_role +``` + +> **Verified on DEV + PROD (2026-10-19):** a container `_RT` user that only +> inherits `::external_privileges_role` (no direct grant) returned a +> 1536-dim `text-embedding-3-small` vector via `TUTORIALS_AICORE`. + +### Step B3 — prove it + +```sql +CALL GET_REMOTE_SOURCE_AI_MODELS('TUTORIALS_AICORE', ?); -- enumerate reachable embedding models +-- use a model+version from that list, e.g. text-embedding-3-small.1 +SELECT VECTOR_EMBEDDING('Hello world!', 'DOCUMENT', + 'text-embedding-3-small.1', TUTORIALS_AICORE) FROM DUMMY; +``` + +Expected: a non-null `REAL_VECTOR` (dimension depends on the model). +Recommended: a thin hybrid test asserting a non-null vector returns. + +--- + +## PROD status — DONE (2026-10-19) + +Both tiers are live and verified on PROD (container schema +`2FEC6DAB47984DB482E0B3BB80AFFEF7`): PSE + remote source created, EXECUTE +granted to `::external_privileges_role`, and a container `_RT` user +returned a 1536-dim embedding via `TUTORIALS_AICORE`. The checklist below +is retained for re-provisioning / a fresh environment. + +- [x] Dedicated x509 key on the PROD AI Core instance (`hana-ai-core`). +- [x] PROD orchestration deployment ID (`da6d9c5e3fb50c3d`). +- [x] PROD HANA admin ran B1 (CREATE PSE + remote source). +- [x] Granted `EXECUTE ON REMOTE SOURCE` to + `2FEC6DAB47984DB482E0B3BB80AFFEF7::external_privileges_role` (B2). +- [x] B3 proof (1536-dim vector as `_RT` user). + +--- + +## Appendix 1 — Automatic embedding generation (for the later rewrite) + +Once the remote source works, HANA can keep embeddings in sync with +source text with no application pipeline. Four mechanisms: + +| Method | Column type | Timing | +| --- | --- | --- | +| `CREATE_VECTOR_EMBEDDINGS` system procedure | NCLOB, NVARCHAR | On-demand / one-time backfill | +| Scheduler job (`CREATE SCHEDULER JOB` → the procedure) | NCLOB, NVARCHAR | Periodic background sync | +| `GENERATED ALWAYS AS VECTOR_EMBEDDING(...)` computed column | **NVARCHAR only** | Synchronous on insert/update | +| `BEFORE INSERT OR UPDATE` trigger | NCLOB, NVARCHAR | Synchronous on insert/update | + +> This platform's content is stored as **NCLOB** (gzip BLOB / long text), +> so `GENERATED ALWAYS AS` (NVARCHAR-only) does **not** apply — use the +> procedure (backfill), scheduler job (periodic), or trigger (synchronous) +> routes. Example backfill: +> ```sql +> CALL SYS.CREATE_VECTOR_EMBEDDINGS( +> SCHEMA_NAME => CURRENT_SCHEMA, TABLE_NAME => 'KNOWLEDGE_BASE', +> TEXT_COLUMN_NAME => 'TEXT', EMBEDDING_COLUMN_NAME => 'TEXT_EMBEDDING', +> TEXT_TYPE => 'DOCUMENT', MODEL_AND_VERSION => 'SAP_GXY.20250407'); +> ``` + +## Appendix 2 — Wider HANA AI function surface + +From the SAP HANA AI Development Guide — candidates for later moving +Node/CAP AI work into the DB layer (each worth its own evaluation unit): + +- `VECTOR_EMBEDDING`, `CREATE_VECTOR_EMBEDDINGS` — embeddings (this doc). +- `AI_TEXT_COMPLETION` — LLM text generation in SQL. +- `AI_CORE_REQUEST` — generic AI Core call from SQL. +- `CROSS_ENCODE` — reranking / cross-encoder scoring. +- `AI_TABULAR_PREDICTION` — tabular ML prediction (cf. RPT ValueList use). + +These overlap with features currently served via `@cap-js/ai` / +`@sap-ai-sdk/orchestration` (semantic search, text-gen, Joule, RPT +recommendations); consolidation is a future architecture decision, not +part of this unit. diff --git a/docs/superpowers/specs/2026-10-19-hana-aicore-embedding-design.md b/docs/superpowers/specs/2026-10-19-hana-aicore-embedding-design.md new file mode 100644 index 000000000..9e171cde4 --- /dev/null +++ b/docs/superpowers/specs/2026-10-19-hana-aicore-embedding-design.md @@ -0,0 +1,183 @@ +# HANA-Native AI Core Embeddings — DEV Setup & Proof (design) + +**Issue:** [#2810](https://github.com/sap-tutorials/tutorials-ims/issues/2810) +**Date:** 2026-10-19 +**Status:** design — no SQL run against HANA yet +**Scope of this unit:** basic setup only. Connect the existing AI Core +instance to the DEV HANA container and prove `VECTOR_EMBEDDING` works from +the DB layer. **No application rewrite.** Moving existing Node/CAP +embeddings into the DB is explicitly a later unit. + +## Intent & why + +Today every embedding this platform produces is computed at the Node/CAP +layer via `@cap-js/ai` / the AI SDK (`@sap-ai-sdk/orchestration`) calling +AI Core, then written to HANA. HANA Cloud now exposes `VECTOR_EMBEDDING` +(and a wider AI function family) that can call AI Core **from inside the +database**, removing a network round-trip and the application-side +pipeline for keeping embeddings in sync with their source text. + +Before committing to any rewrite, we want a de-risking proof: reuse the +**existing** AI Core service instance + a dedicated x509 service key, wire +it into the DEV HANA container as a remote source, and confirm an +embedding can be generated by SQL. Prove it on DEV; repeat on PROD only +after. + +## Goals + +- Reuse the existing `tutorials-aicore` instance via the dedicated + `hana-ai-core` (x509) service key — no new AI Core instance. +- Create the PSE + remote source **inside the `tutorials-hana` HDI + container** (shippable shape CAP can later consume), not as a throwaway + admin-level object. +- Extend the container user's privileges **declaratively** through the + existing `db/src/_grants.hdbgrants` mechanism — no manual one-off + DBADMIN grant. +- Produce a committed operations runbook so the PROD repeat is mechanical. +- Confirm two tiers of embedding work: HANA built-in NLP, and AI Core via + the remote source. + +## Non-goals / anti-goals + +- **Not** rewriting any existing embedding feature (freshness corpus, + semantic search, KG, RPT ValueList) to use DB-side embeddings. Later. +- **Not** touching PROD in this unit. +- **Anti-goal:** creating the remote source as a bare instance-level + admin object that CAP can't reach — that would "work" in a test and + still require a full redo to ship. +- **Anti-goal:** hand-running `GRANT` as DBADMIN outside the HDI deploy — + it drifts from the repo and is lost on next container rebuild. + +## Confirmed facts (verified this session, DEV / `tutorial-system/dev`) + +### AI Core — instance `tutorials-aicore`, key `hana-ai-core` +- `credential-type`: **x509** (client cert + RSA key; no clientsecret). +- `AI_API_URL`: `https://api.ai.prod.eu-central-1.aws.ml.hana.ondemand.com` +- `authUrl` (certurl): + `https://tutorial-system.authentication.cert.eu10.hana.ondemand.com` +- `clientId`: `sb-78c0840a-3613-4a19-be4f-5cd52760090e!b676072|aicore!b540` +- RUNNING deployments (via `/v2/lm/deployments`): `text-embedding-3-small` + (`dfad0fa7cd47ab38`), `defaultOrchestrationConfig` (`d2f9fa53543e5c4e`), + **`joule-chat-orchestration` (`da6d9c5e3fb50c3d`)**, `claude-4.6-sonnet`, + `sap-rpt-1-small`. +- **Chosen `deploymentId` for the remote source: `da6d9c5e3fb50c3d`** + (joule-chat-orchestration) — per Tom's decision. The `sapgenaihub` + adapter routes by model name over an orchestration deployment; + `GET_REMOTE_SOURCE_AI_MODELS` then lists reachable embedding models. + +### HANA — container `tutorials-hana` +- Instance `f1f99bc6-89c8-4f81-a089-1c3732c30b13.hna2.prod-eu10.hanacloud.ondemand.com:443` +- Schema / runtime user `AC9753D6C4764F5ABE3B3CA4E88233C0` / `..._RT`. +- **Privilege gap (verified by capability probe):** the only missing + privilege is **`CREATE PSE`** (probe: `CREATE PSE` → *insufficient + privilege*). **`CREATE REMOTE SOURCE` is ALREADY held** — the probe got + past authorization and failed only on a missing-PSE error, proving the + right exists. A standalone `CREATE CREDENTIAL` object is not needed for + the X509-PSE remote-source form. So the AI Core tier needs exactly one + grant: `CREATE PSE`. +- **Built-in NLP models visible now:** `SAP_GXY.20250407`, + `SAP_NEB.20240715` (from `NLP_VECTOR_EMBEDDING_MODELS`). The built-in + tier needs no AI Core setup. +- **Tier A PROVEN (2026-10-19):** + `VECTOR_EMBEDDING('...','DOCUMENT','SAP_GXY.20250407')` run as the `_RT` + user returned a **768-dim** `REAL_VECTOR`. The DB-layer embedding path + works today. + +## How it gets built (the decided parts) + +### Tier A — built-in NLP embedding (no AI Core, immediate) +Proves `VECTOR_EMBEDDING` end-to-end with zero new privileges: +```sql +SELECT VECTOR_EMBEDDING('Hello world!', 'DOCUMENT', 'SAP_GXY.20250407') FROM DUMMY; +``` +Run as the container `_RT` user via a `tutorials-hana` service key +(hana-cli / `@sap/hana-client`). Expected: a 768-dim `REAL_VECTOR`. + +### Tier B — AI Core remote source (the issue's actual target) + +**B1. Declarative privilege grant.** Extend `db/src/_grants.hdbgrants` +(the file that already grants `SPARQL QUERY/UPDATE` via the +`tutorials-kg-grantor` UPS) with the one missing system privilege, under +both `application_user` and `object_owner`: **`CREATE PSE`**. Applied by +`tutorials-db-deployer` on deploy. (`CREATE REMOTE SOURCE` is already +held — see confirmed facts.) + +> **Deploy-time risk (try-first, per decision):** the grant only succeeds +> if the `tutorials-kg-grantor` grantor identity holds `CREATE PSE` **WITH +> ADMIN OPTION**. SPARQL privileges were delegable; `CREATE PSE` may not +> be. **Failure signature at deploy:** the `tutorials-db-deployer` task +> fails with a grant/insufficient-privilege error naming `CREATE PSE`. +> **Fallback:** a one-time admin grant of `CREATE PSE` WITH ADMIN OPTION +> to the grantor user (or `CREATE PSE` directly to the `_RT` container +> user), then re-deploy. Runbook documents both. + +**B2. PSE + remote source SQL** (verbatim shape from the HANA Vector +Engine Guide "Connecting to SAP AI Core", mapped to our values): +```sql +CREATE PSE TUTORIALS_AICORE_PSE; +ALTER PSE TUTORIALS_AICORE_PSE SET OWN CERTIFICATE ' + + +'; + +CREATE REMOTE SOURCE TUTORIALS_AICORE ADAPTER "sapgenaihub" CONFIGURATION + 'aiApiUrl=https://api.ai.prod.eu-central-1.aws.ml.hana.ondemand.com; + authUrl=https://tutorial-system.authentication.cert.eu10.hana.ondemand.com; + resourceGroup=default; + deploymentId=da6d9c5e3fb50c3d; + clientId=sb-78c0840a-3613-4a19-be4f-5cd52760090e!b676072|aicore!b540' +WITH CREDENTIAL TYPE 'X509' PSE TUTORIALS_AICORE_PSE; +``` +Open question for the runbook: whether the PSE/remote source are best +created as container design-time artifacts (`.hdbpse` / grants) or as a +bootstrap SQL step run as the `_RT` user once privileges land. Default +call: bootstrap SQL first to prove it, then convert to design-time +artifacts as a follow-up so it survives container rebuilds. Flagged as my +call, revisit in the runbook. + +**B3. Proof queries:** +```sql +CALL GET_REMOTE_SOURCE_AI_MODELS('TUTORIALS_AICORE', ?); -- lists embedding models +SELECT VECTOR_EMBEDDING('Hello world!', 'DOCUMENT', + 'text-embedding-3-small.1', TUTORIALS_AICORE) FROM DUMMY; -- model from B3 list +``` + +### Secrets handling +The x509 cert+key live only in the `hana-ai-core` service key. For PSE +creation they are pasted into `ALTER PSE ... SET OWN CERTIFICATE`. They +must **not** be committed to the repo. The runbook instructs pulling them +at setup time via `cf service-key tutorials-aicore hana-ai-core`. +Consider rotating `hana-ai-core` after setup if the key JSON was exposed +in any shared transcript. + +## Deliverable: committed repo runbook + +A new operations runbook, to be linked from `CLAUDE.md`'s deep-dive list +and from the issue: +`docs/developers/operations/hana-aicore-embeddings.md`, covering: +1. Prerequisites (x509 key, deployment ID retrieval via `/v2/lm/deployments`). +2. Tier A built-in NLP proof. +3. Tier B privilege grant (declarative `_grants.hdbgrants` + grantor + delegation caveat + admin fallback). +4. Tier B PSE + remote source SQL with our DEV values as the worked + example and `` for PROD. +5. Proof queries + expected output. +6. PROD repeat checklist (same steps, PROD key/deployment/container). +7. **Reference appendix — Automatic embedding generation** (the four HANA + mechanisms: `CREATE_VECTOR_EMBEDDINGS` procedure, scheduler job, + `GENERATED ALWAYS AS` (NVARCHAR only), BEFORE INSERT/UPDATE trigger + (NCLOB)). Captured for the later rewrite; our content BLOBs are + NCLOB/gzip so the trigger/procedure routes apply, not computed column. +8. **Reference appendix — wider HANA AI surface** from the AI Development + Guide (`AI_CORE_REQUEST`, `AI_TEXT_COMPLETION`, `CROSS_ENCODE`, + `AI_TABULAR_PREDICTION`) as candidates for later consolidation of + text-gen / Joule / RPT work into the DB layer. + +## What's left to the builder / implementer + +- Decide design-time artifact vs bootstrap SQL for the PSE/remote source + (default: bootstrap first — see B2). +- Confirm grantor delegation (B1 caveat); pick declarative vs admin-fallback. +- Pick the exact embedding model string from `GET_REMOTE_SOURCE_AI_MODELS` + output (likely `text-embedding-3-small.1`). +- Whether to add a thin hybrid test asserting a non-null vector comes back. diff --git a/hugo-apps/src/app-space/AppSpace.vue b/hugo-apps/src/app-space/AppSpace.vue index 3e6225b18..63ae089e9 100644 --- a/hugo-apps/src/app-space/AppSpace.vue +++ b/hugo-apps/src/app-space/AppSpace.vue @@ -1325,6 +1325,25 @@ const emptyStateMessage = computed(() => { } } +/* ═══════════════════════════════════════════════════════════════════ + Themed heroes — text colour (#2839) + The Joule/Sapphire hero gradients are dark in both light AND dark mode, + but the base .hero only inherits --sapShell_TextColor, which resolves to + a DARK value in the Horizon light theme → near-illegible banner text on + the purple/blue gradient. Pin white here (and on the skeleton) so the + themed hero reads correctly regardless of the active Horizon palette. + ═══════════════════════════════════════════════════════════════════ */ + +.app-space[data-theme="joule"] .hero, +.app-space[data-theme="sapphire"] .hero { + color: #fff; +} + +.app-space[data-theme="joule"] .hero-title-skeleton, +.app-space[data-theme="sapphire"] .hero-title-skeleton { + background: rgba(255, 255, 255, 0.18); +} + /* ═══════════════════════════════════════════════════════════════════ Joule Theme — Light Mode ═══════════════════════════════════════════════════════════════════ */ diff --git a/hugo/content/api-docs/_index.md b/hugo/content/api-docs/_index.md index 51944ff76..82d19f49a 100644 --- a/hugo/content/api-docs/_index.md +++ b/hugo/content/api-docs/_index.md @@ -201,4 +201,4 @@ Curated pointers into the developer docs that a curious integrator might find us Bug or gap? Open an issue on the [tutorials-ims repo](https://github.com/sap-tutorials/tutorials-ims/issues). PRs welcome. -{{< scavenger-hunt >}} + diff --git a/hugo/data/whats_new.json b/hugo/data/whats_new.json index 19ea0cd40..0ed97a86c 100644 --- a/hugo/data/whats_new.json +++ b/hugo/data/whats_new.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-10-18T19:56:36.553Z", + "generatedAt": "2026-10-19T21:53:00.976Z", "repos": [ { "key": "tutorials-ims", @@ -8,6 +8,136 @@ } ], "entries": [ + { + "id": "tutorials-ims#2851", + "repo": "tutorials-ims", + "label": "Developer Portal", + "number": 2851, + "title": "Docs site builds green again", + "summary": "Restored the documentation site build by registering the new HANA AI Core embeddings runbook in the docs sidebar.", + "category": "Fix", + "mergedAt": "2026-10-19T20:42:24Z", + "week": "2026-W43", + "weekStart": "2026-10-19", + "url": "https://github.com/sap-tutorials/tutorials-ims/pull/2851" + }, + { + "id": "tutorials-ims#2849", + "repo": "tutorials-ims", + "label": "Developer Portal", + "number": 2849, + "title": "HANA to AI Core embedding setup, now live on both tiers", + "summary": "Finalized the setup guide for connecting SAP HANA Cloud to AI Core for vector embeddings, with a rebind-proof grant approach verified live on both development and production.", + "category": "Docs", + "mergedAt": "2026-10-19T20:03:15Z", + "week": "2026-W43", + "weekStart": "2026-10-19", + "url": "https://github.com/sap-tutorials/tutorials-ims/pull/2849" + }, + { + "id": "tutorials-ims#2848", + "repo": "tutorials-ims", + "label": "Developer Portal", + "number": 2848, + "title": "Unblocked database deploys for AI Core embeddings", + "summary": "Corrected the HANA to AI Core setup to use a one-time admin action instead of a container grant, unblocking database deployments that had been failing.", + "category": "Fix", + "mergedAt": "2026-10-19T17:29:43Z", + "week": "2026-W43", + "weekStart": "2026-10-19", + "url": "https://github.com/sap-tutorials/tutorials-ims/pull/2848" + }, + { + "id": "tutorials-ims#2847", + "repo": "tutorials-ims", + "label": "Developer Portal", + "number": 2847, + "title": "Corrected PSE privilege names in embeddings runbook", + "summary": "Fixed the privilege names in the HANA AI Core embeddings setup guide so the database security role deploys correctly.", + "category": "Docs", + "mergedAt": "2026-10-19T16:27:34Z", + "week": "2026-W43", + "weekStart": "2026-10-19", + "url": "https://github.com/sap-tutorials/tutorials-ims/pull/2847" + }, + { + "id": "tutorials-ims#2846", + "repo": "tutorials-ims", + "label": "Developer Portal", + "number": 2846, + "title": "Corrected the AI Core embeddings grant mechanism", + "summary": "Documented the correct mechanism for granting the HANA container the privilege it needs to reach AI Core for embeddings.", + "category": "Docs", + "mergedAt": "2026-10-19T15:50:12Z", + "week": "2026-W43", + "weekStart": "2026-10-19", + "url": "https://github.com/sap-tutorials/tutorials-ims/pull/2846" + }, + { + "id": "tutorials-ims#2844", + "repo": "tutorials-ims", + "label": "Developer Portal", + "number": 2844, + "title": "HANA-native AI Core embedding setup guide", + "summary": "Added a setup runbook for generating vector embeddings directly from the HANA database layer, proving the built-in natural-language path works today.", + "category": "Docs", + "mergedAt": "2026-10-19T14:50:30Z", + "week": "2026-W43", + "weekStart": "2026-10-19", + "url": "https://github.com/sap-tutorials/tutorials-ims/pull/2844" + }, + { + "id": "tutorials-ims#2842", + "repo": "tutorials-ims", + "label": "Developer Portal", + "number": 2842, + "title": "Tutorial option-block images stay indented", + "summary": "Fixed images inside tutorial OPTION blocks so they stay properly nested and indented under their step instead of drifting to the left margin.", + "category": "Fix", + "mergedAt": "2026-10-19T13:15:50Z", + "week": "2026-W43", + "weekStart": "2026-10-19", + "url": "https://github.com/sap-tutorials/tutorials-ims/pull/2842" + }, + { + "id": "tutorials-ims#2843", + "repo": "tutorials-ims", + "label": "Developer Portal", + "number": 2843, + "title": "Retired this year's Devtoberfest scavenger hunt", + "summary": "Removed the hidden Devtoberfest scavenger-hunt clues from the homepage and other pages now that the event has ended; the feature is dormant and ready to re-enable next year.", + "category": "Maintenance", + "mergedAt": "2026-10-19T13:12:44Z", + "week": "2026-W43", + "weekStart": "2026-10-19", + "url": "https://github.com/sap-tutorials/tutorials-ims/pull/2843" + }, + { + "id": "tutorials-ims#2840", + "repo": "tutorials-ims", + "label": "Developer Portal", + "number": 2840, + "title": "Readable App Space hero text in light mode", + "summary": "Fixed hard-to-read dark banner text on the themed App Space hero in light mode by pinning it to white across all theme variants.", + "category": "Fix", + "mergedAt": "2026-10-19T11:36:20Z", + "week": "2026-W43", + "weekStart": "2026-10-19", + "url": "https://github.com/sap-tutorials/tutorials-ims/pull/2840" + }, + { + "id": "tutorials-ims#2838", + "repo": "tutorials-ims", + "label": "Developer Portal", + "number": 2838, + "title": "Scheduler and sign-in reliability fixes", + "summary": "Fixed two reliability regressions from the identity-pin change: the background scheduler no longer stalls on every tick, and authenticated sign-ins no longer hit a token error.", + "category": "Fix", + "mergedAt": "2026-10-19T10:37:09Z", + "week": "2026-W43", + "weekStart": "2026-10-19", + "url": "https://github.com/sap-tutorials/tutorials-ims/pull/2838" + }, { "id": "tutorials-ims#2832", "repo": "tutorials-ims", diff --git a/hugo/layouts/_default/baseof.html b/hugo/layouts/_default/baseof.html index 4e4b11503..6e364407a 100644 --- a/hugo/layouts/_default/baseof.html +++ b/hugo/layouts/_default/baseof.html @@ -14,7 +14,8 @@ {{ partial "head.html" . }} {{ if not site.Params.qa }}{{ $joule := resources.Get "css/joule.css" }}{{ $joule.Publish }}{{ end }} {{/* #1908 Devtoberfest scavenger hunt — clue styles, only on the three hunt pages. */}} - {{ if or .IsHome (eq .RelPermalink "/ai/") (eq .RelPermalink "/api-docs/") }}{{ $sh := resources.Get "css/scavenger-hunt.css" | minify | fingerprint }}{{ end }} + {{/* #2841 Disabled: hidden clues removed. Re-enable next Devtoberfest by uncommenting. */}} + {{/* {{ if or .IsHome (eq .RelPermalink "/ai/") (eq .RelPermalink "/api-docs/") }}{{ $sh := resources.Get "css/scavenger-hunt.css" | minify | fingerprint }}{{ end }} */}} {{ partial "site-jsonld.html" . }} @@ -78,7 +79,8 @@ {{ $codeCopy := resources.Get "js/code-copy.ts" | js.Build (dict "minify" true) }} {{/* #1908 Devtoberfest scavenger hunt — self-gating body-level overlays (home + /ai/). */}} - {{ partial "scavenger-hunt.html" . }} + {{/* #2841 Disabled: hidden clues removed. Re-enable next Devtoberfest by uncommenting. */}} + {{/* {{ partial "scavenger-hunt.html" . }} */}} {{ if not site.Params.qa }} diff --git a/mta-mcp.yaml b/mta-mcp.yaml index cf363813b..3ac374ead 100644 --- a/mta-mcp.yaml +++ b/mta-mcp.yaml @@ -4,7 +4,7 @@ _schema-version: 3.3.0 # This file lives at repo root → path: gen/srv-mcp (root-relative, no ../). # before-all runs from ROOT (no cd ..) — mbt uses the descriptor's directory as cwd. ID: tutorials-mcp -version: 1.0.1 +version: 1.0.2 # Destinations are subaccount-level, so DEV and PROD (sharing one subaccount) # need DISTINCT destination names. `dest-main-srv` defaults to the PROD name; diff --git a/packages/core/resolve-db-user.js b/packages/core/resolve-db-user.js index 62dff642b..7d0fa3887 100644 --- a/packages/core/resolve-db-user.js +++ b/packages/core/resolve-db-user.js @@ -348,8 +348,12 @@ export async function resolveUser(user, db) { */ export async function pinResolvedUser(user, db) { if (!user || !user.id || user.id === 'anonymous') return null; - // Idempotent: if already pinned this request, don't re-resolve. - if (user.attr?.dbUserId) return { ID: user.attr.dbUserId, sapId: user.authInfo?.token?.userId ?? null }; + // Idempotent: if already pinned this request, don't re-resolve. Read the + // cached sapId from user.attr (always writable) first, falling back to the + // token for older pins. + if (user.attr?.dbUserId) { + return { ID: user.attr.dbUserId, sapId: user.attr.sapId ?? user.authInfo?.token?.userId ?? null }; + } let row; try { row = await resolveUser(user, db); @@ -362,9 +366,23 @@ export async function pinResolvedUser(user, db) { user.attr = user.attr || {}; user.attr.dbUserId = row.ID; if (row.sapId) { - user.authInfo = user.authInfo || {}; - user.authInfo.token = user.authInfo.token || {}; - user.authInfo.token.userId = row.sapId; + // Cache sapId on user.attr (always a plain writable object). + user.attr.sapId = row.sapId; + // Best-effort mirror onto authInfo.token.userId for any consumer that still + // reads it there. In PROD the real @sap/xssec XsuaaToken exposes `userId` + // as a GETTER-ONLY property, so a bare assignment throws + // `Cannot set property userId of # which has only a getter` + // (#2552). Only assign when the slot is actually writable. + try { + user.authInfo = user.authInfo || {}; + user.authInfo.token = user.authInfo.token || {}; + const desc = Object.getOwnPropertyDescriptor(user.authInfo.token, 'userId'); + const writable = !desc || desc.writable || typeof desc.set === 'function'; + if (writable) user.authInfo.token.userId = row.sapId; + } catch { + // Getter-only token (production XSUAA): the attr.sapId cache above is the + // source of truth; swallow and continue. + } } return row; } diff --git a/scripts/__tests__/options-hugo.test.ts b/scripts/__tests__/options-hugo.test.ts index 41c76977e..4ddedd1ea 100644 --- a/scripts/__tests__/options-hugo.test.ts +++ b/scripts/__tests__/options-hugo.test.ts @@ -285,15 +285,17 @@ ML2 }); }) -describe('convertOptionBlocks (hugo) — indented OPTION blocks inside list items (#2703)', () => { +describe('convertOptionBlocks (hugo) — indented OPTION blocks inside list items (#2703, #2820)', () => { // When an OPTION block lives inside a numbered substep it is authored - // indented 4 spaces. The shortcode is emitted FLUSH-LEFT (dedented): the - // markdown list closes and Goldmark re-opens it with
    after - // the block, preserving substep numbering, while the panel BODY is dedented - // to column 0 so os-panel.html's `{{ .Inner | markdownify }}` parses the - // code fences / images / notes as markdown instead of literal indented code. - - it('emits the shortcode flush-left and dedents the body for an indented OS group', () => { + // indented 4 spaces. Approach A (#2820): the shortcode WRAPPER lines keep that + // indentation so the block stays inside the surrounding
  1. (the list never + // closes — this is what keeps figures nested, see + // test/unit/option-figure-list-nesting.test.ts for the render-level proof), + // while the panel BODY is dedented to column 0 so os-panel.html's + // `{{ .Inner | markdownify }}` parses the code fences / images / notes as + // markdown instead of literal indented code. + + it('indents the shortcode wrappers to the substep and dedents the body for an indented OS group', () => { const input = [ '3. Start the installer.', '', @@ -319,12 +321,12 @@ describe('convertOptionBlocks (hugo) — indented OPTION blocks inside list item const out = convertOptionBlocks(input, 'hugo'); - // Shortcode wrappers are flush-left (NOT indented) so Goldmark treats the - // block as a sibling of the list; the following `4.` reopens numbering. - expect(out).toContain('\n{{< os-options >}}'); - expect(out).toContain('\n{{< /os-options >}}'); - expect(out).toContain('{{< os-panel os="Windows" >}}'); - expect(out).not.toContain(' {{< os-options >}}'); + // Shortcode wrappers carry the substep's 4-space indent so Goldmark keeps + // the block inside the
  2. (list stays open; #2820). The body stays at + // column 0. + expect(out).toContain('\n {{< os-options >}}'); + expect(out).toContain('\n {{< /os-options >}}'); + expect(out).toContain(' {{< os-panel os="Windows" >}}'); // The code fence inside the panel is dedented to column 0 so markdownify // parses it as a fenced block (not a 4-space indented-code block). @@ -333,8 +335,6 @@ describe('convertOptionBlocks (hugo) — indented OPTION blocks inside list item // The following substep "4." survives at column 0. expect(out).toContain('\n4. Next substep.'); - // No residual leading indent left on the first wrapper line. - expect(out).not.toMatch(/\n {4}\{\{< os-options/); }); it('dedents the body of an indented legacy (non-OS) option-tabs group', () => { @@ -359,7 +359,8 @@ describe('convertOptionBlocks (hugo) — indented OPTION blocks inside list item const out = convertOptionBlocks(input, 'hugo'); expect(out).toContain('{{% option-tabs tabs="JSON,XML" %}}'); - expect(out).not.toContain(' {{% option-tabs'); + // Approach A (#2820): wrappers carry the substep indent; body at column 0. + expect(out).toContain(' {{% option-tabs tabs="JSON,XML" %}}'); // Body dedented to column 0. expect(out).toContain('\njson body\n'); expect(out).toContain('\n3. Done.'); @@ -421,7 +422,7 @@ describe('convertOptionBlocks (hugo) — real QA substep patterns (#2703 follow- expect(out).not.toMatch(/\n {4}```\n/); }); - it('dedents substep content that trails the OPTION group within the same list item', () => { + it('leaves substep content that trails the OPTION group at its substep indent (stays in the list item)', () => { const input = [ '3. Start the installer.', '', @@ -454,11 +455,15 @@ describe('convertOptionBlocks (hugo) — real QA substep patterns (#2703 follow- const out = convertOptionBlocks(input, 'hugo'); - // Trailing substep-3 content must be dedented to column 0 so Goldmark keeps - // it as list-item prose (not an indented-code block) and substep 4 reopens. - expect(out).not.toMatch(/\n {4}Set the install directory/); - expect(out).not.toMatch(/\n {4}!\[Client install\]/); - expect(out).not.toMatch(/\n {4}> \[!TIP\]/); + // Approach A (#2820): the shortcode wrappers stay indented, so the list + // never closes. Trailing substep-3 content therefore KEEPS its 4-space + // substep indent and remains valid list-item continuation — dedenting it to + // column 0 (the old behaviour) would eject it from the
  3. , which is the + // bug. See test/unit/option-figure-list-nesting.test.ts for the render-level + // proof that the trailing figure nests inside
  4. /
      . + expect(out).toMatch(/\n {4}Set the install directory/); + expect(out).toMatch(/\n {4}!\[Client install\]/); + expect(out).toMatch(/\n {4}> \[!TIP\]/); expect(out).toContain('\n4. After installation'); }); }) diff --git a/scripts/parsers/options.ts b/scripts/parsers/options.ts index 8c45272cd..811d5e99e 100644 --- a/scripts/parsers/options.ts +++ b/scripts/parsers/options.ts @@ -94,6 +94,26 @@ function dedentBlock(text: string): string { return dedented.join('\n'); } +/** Re-indent only the shortcode WRAPPER lines of a built replacement block by + * `indent`, leaving every body line at its current (column-0) position. + * + * Approach A for #2820: the OPTION shortcode must stay inside the surrounding + * numbered
    1. , so its wrapper lines carry the substep's indentation — but the + * panel body must stay at column 0 so os-panel.html's `{{ .Inner | markdownify }}` + * parses it as markdown rather than a literal indented-code block (#2703). + * + * A "wrapper line" is a Hugo shortcode delimiter (`{{< … >}}` / `{{% … %}}`) or + * a VitePress `` / `