Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 27 additions & 14 deletions notebooks/multitenant_schema_walkthrough.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
"source": [
"# From Prompt to Persistence: Multi-Tenant Memory Schema\n",
"\n",
"Companion notebook for **From Prompt to Persistence: Designing Multi-Tenant Agent Memory Schemas for SaaS**. Every SQL query from the article runs here against a real Oracle AI Database instance, in the order the article presents them: schema DDL, RLS policy, session context, sample data, canonical retrieval predicate, supersession, right-to-forget cascade, and the unified retrieval query.\n",
"Companion notebook for the two-part *From Prompt to Persistence* series:\n",
"\n",
"- Part 1 — [Designing Multi-Tenant Agent Memory Schemas for SaaS](https://blogs.oracle.com/developers/from-prompt-to-persistence-part-1-designing-multi-tenant-agent-memory-schemas-for-saas) designs the durable layer: eight typed memory tables that all carry the same scope columns, with tenant isolation enforced by row-level security rather than by convention.\n",
"- Part 2 — [Putting the Multi-Tenant Agent Memory Schema to Work](https://blogs.oracle.com/developers/from-prompt-to-persistence-part-2-putting-the-multi-tenant-agent-memory-schema-to-work) puts that schema to work: the retrieval query the agent loop reads from, plus versioned supersession and a provable right-to-forget cascade.\n",
"\n",
"Every SQL statement from both articles runs here against a real Oracle AI Database instance, in the order the articles present them: schema DDL, RLS policy, session context, sample data, the canonical retrieval predicate, supersession, the right-to-forget cascade, and the unified retrieval query.\n",
"\n",
"## Prerequisites\n",
"\n",
Expand All @@ -17,7 +22,7 @@
"- ONNX embedding model loaded (`ALL_MINILM_L12_V2`, 384-dim); see the project README for the loader script\n",
"- A `.env` file with `DB_USER`, `DB_PASSWORD`, `DB_DSN`\n",
"\n",
"The article's DDL declares vectors as `VECTOR(1024, FLOAT32)` as an illustrative dimension. This notebook uses `VECTOR(384, FLOAT32)` to match `ALL_MINILM_L12_V2`. The shape of the SQL is identical; only the dimension number changes."
"Part 1's DDL declares vectors as `VECTOR(1024, FLOAT32)` as an illustrative dimension. This notebook uses `VECTOR(384, FLOAT32)` to match `ALL_MINILM_L12_V2`. The shape of the SQL is identical; only the dimension number changes."
]
},
{
Expand Down Expand Up @@ -541,7 +546,7 @@
"source": [
"### 2h. `knowledge_base_document` + `knowledge_base_chunk` (Semantic)\n",
"\n",
"Note that `knowledge_base_document` stores a `source_uri` **pointer** to the original document (e.g. an object-storage path) plus metadata: title, collection, source type, and provenance. It does **not** store the document's content. The searchable text lives only in `knowledge_base_chunk.content`, chunked and embedded; the original bytes (the PDF, the HTML) stay in object storage, referenced by `source_uri`. The database holds the derived, retrievable form and a pointer home, not the raw file.\n"
"Note that `knowledge_base_document` stores a `source_uri` **pointer** to the original document (e.g. an object-storage path) plus metadata: title, collection, source type, and provenance. It does **not** store the document's content. The searchable text lives only in `knowledge_base_chunk.content`, chunked and embedded; the original bytes (the PDF, the HTML) stay in object storage, referenced by `source_uri`. The database holds the derived, retrievable form and a pointer home; the raw file never lands in a table."
]
},
{
Expand Down Expand Up @@ -674,7 +679,7 @@
"source": [
"## 3. Tenant isolation via row-level security\n",
"\n",
"The function returns the predicate `tenant_id = SYS_CONTEXT('memory_ctx','tenant_id')` and the loop attaches it to all nine tenant-scoped data tables (the seven `*_MEMORY` tables plus the two `knowledge_base_*` tables) for SELECT, INSERT, UPDATE, and DELETE. `update_check => TRUE` is what makes the INSERT half real: a careless INSERT with the wrong `tenant_id` is blocked at the statement layer, not just filtered on read. A developer who forgets to filter by tenant can't write a leaking query because the database refuses to run one."
"The function returns the predicate `tenant_id = SYS_CONTEXT('memory_ctx','tenant_id')` and the loop attaches it to all nine tenant-scoped data tables (the seven `*_MEMORY` tables plus the two `knowledge_base_*` tables) for SELECT, INSERT, UPDATE, and DELETE. `update_check => TRUE` is what makes the INSERT half real: a careless INSERT with the wrong `tenant_id` is blocked at the statement layer instead of merely filtered out on read. A developer who forgets to filter by tenant can't write a leaking query because the database refuses to run one."
]
},
{
Expand Down Expand Up @@ -1102,7 +1107,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"Inserted entity ent_96422feecdf8 (version 1).\n"
"Inserted entity ent_691e4ca03e04 (version 1).\n"
]
}
],
Expand Down Expand Up @@ -1158,8 +1163,8 @@
"output_type": "stream",
"text": [
"Supersession chain:\n",
" v1 id=ent_96422feecdf8 valid_until=2026-05-29T16:53:39.232652 superseded_by=ent_8474242e5a0c\n",
" v2 id=ent_8474242e5a0c valid_until=<active> superseded_by=None\n"
" v1 id=ent_691e4ca03e04 valid_until=2026-07-08T12:24:15.459233 superseded_by=ent_9b2f2462724f\n",
" v2 id=ent_9b2f2462724f valid_until=<active> superseded_by=None\n"
]
}
],
Expand Down Expand Up @@ -1232,7 +1237,7 @@
"output_type": "stream",
"text": [
"Active row(s) under default retrieval predicate:\n",
" v2 id=ent_8474242e5a0c Production database is in eu-west-1, replicas in eu-central-1. Migrated 2026-04.\n"
" v2 id=ent_9b2f2462724f Production database is in eu-west-1, replicas in eu-central-1. Migrated 2026-04.\n"
]
}
],
Expand Down Expand Up @@ -1263,13 +1268,13 @@
"\n",
"Three retrieval styles coexist in one statement, matching how the Retrieve step describes each type:\n",
"\n",
"- **Enumerated** (guideline, persona, toolbox): loaded in full with no score; a policy or tool that applies must apply.\n",
"- **Hybrid** (entity, summarization): a vector candidate pool and an Oracle Text `CONTAINS` pool, `FULL OUTER JOIN`ed so each row carries **both** a `vec_score` and a `lex_score`. We show the two signals side by side rather than fusing them into one number; fusion and reranking (`DBMS_VECTOR.RERANK`) are a separate, downstream topic.\n",
"- **Vector-only** (workflow, knowledge base): similarity match on the embedding.\n",
"- Enumerated types (guideline, persona, toolbox) load in full with no score; a policy or tool that applies must apply.\n",
"- Hybrid types (entity, summarization) build a vector candidate pool and an Oracle Text `CONTAINS` pool, `FULL OUTER JOIN`ed so each row carries **both** a `vec_score` and a `lex_score`. We show the two signals side by side rather than fusing them into one number; fusion and reranking (`DBMS_VECTOR.RERANK`) are a separate, downstream topic.\n",
"- Vector-only types (workflow, knowledge base) do a similarity match on the embedding.\n",
"\n",
"The `relevance` column maps the vector score to a tier the application can read directly: `high` ≥ 0.7, `standard` ≥ 0.5, `low` < 0.5. The `type` column tags every row so the application can route each one to the right part of the prompt (static prefix vs. ranked context).\n",
"\n",
"Note the knowledge-base branch returns the matched **chunk** plus its document's `title`/`collection` for citation and a `document_id`/`chunk_index` handle, never the full document body. That keeps the prompt lean and leaves the chunk *addressable*: if the agent decides it needs more, it can fetch the parent document or neighboring chunks on demand rather than paying for them up front.\n"
"Note the knowledge-base branch returns the matched **chunk** plus its document's `title`/`collection` for citation and a `document_id`/`chunk_index` handle, never the full document body. That keeps the prompt lean and leaves the chunk *addressable*: if the agent decides it needs more, it can fetch the parent document or neighboring chunks on demand rather than paying for them up front."
]
},
{
Expand Down Expand Up @@ -1748,10 +1753,18 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 24,
"id": "df23d90b3fa5",
"metadata": {},
"outputs": [],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Cleanup complete.\n"
]
}
],
"source": [
"# Drop policies (best-effort).\n",
"cur.execute(\"\"\"\n",
Expand Down