From 115cb67e0974130998ed356edfc0edafe167d3f3 Mon Sep 17 00:00:00 2001 From: Mikiko Bazeley Date: Mon, 31 Aug 2026 13:44:16 -0700 Subject: [PATCH 1/2] Add Memori + MongoDB agent memory notebook with Grove gateway support - Add notebook demonstrating agent memory with Memori, MongoDB Atlas, and Voyage AI vector search - Support custom API gateways (e.g. Grove/Azure APIM) via ANTHROPIC_BASE_URL env var, with transparent fallback to default Anthropic endpoint for public users - Work around Memori v3.3.6 Rust core bug that causes auto-recall to silently fail on MongoDB by setting use_rust_core=False - Add python-dotenv for local .env loading - Add .env, .venv, and .fastembed_cache to .gitignore Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 3 + .../memory/memori-mongodb-agent-memory.ipynb | 870 ++++++++++++++++++ 2 files changed, 873 insertions(+) create mode 100644 notebooks/memory/memori-mongodb-agent-memory.ipynb diff --git a/.gitignore b/.gitignore index d2921784..d462c5bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ .ipynb_checkpoints **/.DS_Store +.env +.venv +.fastembed_cache apps/video-intelligence/frontend/public/videos/*.mp4 diff --git a/notebooks/memory/memori-mongodb-agent-memory.ipynb b/notebooks/memory/memori-mongodb-agent-memory.ipynb new file mode 100644 index 00000000..5f63f7ec --- /dev/null +++ b/notebooks/memory/memori-mongodb-agent-memory.ipynb @@ -0,0 +1,870 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "baae23b5", + "metadata": {}, + "source": "# Agent Memory on MongoDB Atlas with Memori\n\n[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mongodb-developer/GenAI-Showcase/blob/main/notebooks/agents/memori-mongodb-agent-memory.ipynb)\n\nThis notebook builds an agent that remembers across sessions, using\n[Memori](https://github.com/MemoriLabs/Memori) as the memory layer and MongoDB Atlas as the store.\n\nIt is organized around a distinction that determines most of the architecture:\n\n| | **State** | **Memory** |\n|---|---|---|\n| Lifecycle | Run-bounded | Indefinite; decays via TTL or deletion |\n| Scope | One execution | Cross-session |\n| Source | Generated by execution | Extracted or curated over time |\n| Recovery role | Enables resume from checkpoint | Informs new runs; does not make a failed one recoverable |\n\nMemori writes both, into different collections. A conversation turn lands in\n`memori_conversation_message` as state. Some of what that turn contained is later\npromoted into `memori_entity_fact` as memory. Watching that promotion happen — and\nseeing that it is *not* instantaneous — is the core of this notebook.\n\n**What gets built**\n\n1. A control agent with no memory, to establish the baseline\n2. The same agent with Memori registered, answering the question the control could not\n3. Direct inspection of what MongoDB actually stores\n4. A second retrieval path over the same documents using Voyage AI embeddings and Atlas Vector Search\n5. Retention and scoping controls — TTL and per-entity isolation\n\nEvery cell is idempotent. Re-running the notebook top to bottom is safe.\n\n---\n\n## Setup\n\n### Get a MongoDB Atlas cluster\n\nThe free **M0** tier is enough. Atlas Vector Search (section 10) is not available on a local\n`mongod`, so a cluster is required rather than optional.\n\n1. Create a cluster at [mongodb.com/atlas](https://www.mongodb.com/atlas)\n2. **Network Access** → add your IP. Running in Colab? Add `0.0.0.0/0` — Colab's egress\n address is not predictable, and this is the single most common cause of a hanging\n connection cell.\n3. **Database Access** → create a user. Prefer an alphanumeric password: a password\n containing `@`, `/`, `:`, or `#` must be percent-encoded or the connection string will not\n parse, and the resulting error names a host that does not exist.\n4. **Connect → Drivers → Python** → copy the `mongodb+srv://...` string and substitute the\n real password.\n\nDo not append `?appName=` or a database name to the URI. The notebook sets both in code.\n\n### Collect API keys\n\n| Variable | Required | Where |\n|---|---|---|\n| `MONGODB_URI` | yes | Atlas → Connect → Drivers |\n| `ANTHROPIC_API_KEY` | yes | [console.anthropic.com](https://console.anthropic.com) |\n| `ANTHROPIC_BASE_URL` | no | Set only when using a proxy/gateway (e.g. Azure APIM). Omit for the default Anthropic endpoint. |\n| `VOYAGE_API_KEY` | yes | [dash.voyageai.com](https://dash.voyageai.com) |\n| `MEMORI_API_KEY` | no | `python -m memori sign-up you@example.com` — raises the augmentation quota from 100/month to 5,000 |\n\n### Choose how to run\n\n**Google Colab** (nothing to install). Click the badge above. Either paste values into the\nconfig block in section 2, or store them as Colab secrets: key icon in the left sidebar →\n**Add new secret** → name it exactly as in the table → toggle **Notebook access on**. That\ntoggle is per-notebook and easy to miss; without it the secret is invisible and the notebook\nfalls through to prompting you.\n\n**Locally**, use a virtual environment. This is not just hygiene: `memori` (v3) and the legacy\n`memorisdk` (v1/v2) both import as `memori`, so a stale install in system Python produces v1\nbehavior behind v3 calls and confusing errors. Recent Debian, Ubuntu, and Homebrew Pythons are\nPEP 668 \"externally managed\" and will refuse a system-wide `pip install` anyway.\n\n```bash\npython3 -m venv .venv\nsource .venv/bin/activate # Windows: .venv\\Scripts\\activate\npip install -r requirements.txt # or run the %pip cell in section 1\npython -m memori setup # pre-download the embedding model\n\npip install ipykernel\npython -m ipykernel install --user --name memori-demo --display-name \"Memori demo\"\n```\n\nThen select **Memori demo** from the kernel menu. Launching Jupyter without selecting that\nkernel is the usual reason a successful install is followed by\n`ModuleNotFoundError: No module named 'memori'`. Confirm with `import sys; sys.executable` —\nit should point inside `.venv`.\n\n### Credential resolution order\n\nSection 2 resolves each value in this order, so any of these approaches works and they can be\nmixed:\n\n1. Values pasted into the config block\n2. Environment variables\n3. Colab secrets\n4. Interactive `getpass` prompt (masked; nothing is written to disk)\n\nFor a live presentation the prompt is the safest option — no credential ends up in the\nnotebook file or in cell output.\n\n### Troubleshooting\n\n| Symptom | Cause |\n|---|---|\n| Connection cell hangs | Atlas IP allowlist — add `0.0.0.0/0` for Colab |\n| `ModuleNotFoundError: memori` | Jupyter is running a different kernel than the venv |\n| `QuotaExceededError` | Augmentation quota; check with `python -m memori quota` |\n| No facts after section 6 | Quota, or blocked egress to the augmentation service |\n| Vector search returns nothing | Index still building, or section 10's embedding cell has not run |\n| TTL index never expires | The indexed field is a string, not a BSON date — section 11 checks this |\n\nConnections are tagged with an `appName` so MongoDB DevRel can attribute hands-on usage from\nthis notebook. It has no effect on behavior; change or remove it freely when adapting this." + }, + { + "cell_type": "markdown", + "id": "036c7c8d", + "metadata": {}, + "source": [ + "## 1. Install\n", + "\n", + "Requires **Python 3.10+**. Colab satisfies this by default.\n", + "\n", + "`memori` v3 is a different PyPI package from the older `memorisdk`; if `memorisdk` is present\n", + "in the same environment, Memori emits a legacy warning. Versions are pinned here because the\n", + "v1 → v3 API break is large enough that an unpinned install against an older tutorial produces\n", + "confusing failures. `pymongo>=4.10` is required for the search-index helpers used in section 10." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e2122d71", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install --quiet --upgrade \\\n", + " \"memori==3.3.6\" \\\n", + " \"pymongo>=4.10,<5\" \\\n", + " \"anthropic>=0.40\" \\\n", + " \"voyageai>=0.3\" \\\n", + " \"python-dotenv\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d14af67", + "metadata": {}, + "outputs": [], + "source": [ + "from dotenv import load_dotenv\n", + "\n", + "load_dotenv() # loads .env from the notebook's directory into os.environ" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53e738dd", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import textwrap\n", + "import time\n", + "from datetime import datetime\n", + "\n", + "import anthropic\n", + "import voyageai\n", + "from memori import Memori\n", + "from pymongo import MongoClient\n", + "from pymongo.operations import SearchIndexModel\n", + "\n", + "# appName per DevRel spec: devrel-MEDIUM-PRIMARY-SECONDARY-OPTIONAL\n", + "# The GitHub value and the webinar/content value must differ so the two are\n", + "# tracked separately. Swap the constant below when presenting live.\n", + "APP_NAME = \"devrel-notebook-vectorsearch-memori\" # GitHub / GenAI-Showcase\n", + "# APP_NAME = \"devrel-webinar-vectorsearch-memori\" # webinar delivery\n", + "\n", + "DB_NAME = \"memori_webinar\"\n", + "MODEL = \"claude-sonnet-4-6\" # any Claude model; Memori is model-agnostic\n", + "VOYAGE_MODEL = \"voyage-3-large\"\n", + "VOYAGE_DIMS = 1024\n", + "VECTOR_INDEX = \"voyage_fact_index\"\n", + "\n", + "# Scope every write in this notebook to one user + one process.\n", + "ENTITY_ID = \"webinar-user-001\"\n", + "PROCESS_ID = \"memory-demo\"" + ] + }, + { + "cell_type": "markdown", + "id": "4f1db80d", + "metadata": {}, + "source": [ + "## 2. Preflight\n", + "\n", + "Two failure modes are worth catching before an audience is watching.\n", + "\n", + "**Augmentation quota.** `MEMORI_API_KEY` is optional, but without it augmentation is capped at\n", + "100 calls/month and metered by IP — on shared conference or office wifi that allowance may\n", + "already be partly consumed by someone else. With a key it is 5,000/month. Either way the\n", + "failure arrives as `QuotaExceededError` partway through a run, not at startup, so check first:\n", + "`python -m memori quota`.\n", + "\n", + "**An unreachable cluster.** Atlas connection strings fail on IP allowlist far more often\n", + "than on credentials, and the error arrives late. If you are running in Colab, the allowlist\n", + "must permit `0.0.0.0/0` or Colab's egress range.\n", + "\n", + "Credentials resolve in order: values pasted into the block below, then environment variables,\n", + "then Colab secrets, then an interactive `getpass` prompt. The notebook runs unchanged locally\n", + "or in Colab, and running it with nothing configured at all still works — it will just ask." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05831276", + "metadata": {}, + "outputs": [], + "source": [ + "# ─────────────────────────────────────────────────────────────────────────\n", + "# OPTION A — paste your values here.\n", + "# OPTION B — leave every line blank and the notebook will read environment\n", + "# variables, then Colab secrets, then prompt you interactively.\n", + "#\n", + "# Anything you paste here is saved in the .ipynb file, including if you\n", + "# commit it. Clear this block before sharing the notebook or pushing it.\n", + "# ─────────────────────────────────────────────────────────────────────────\n", + "\n", + "MONGODB_URI = \"\" # mongodb+srv://user:pass@cluster.mongodb.net\n", + "ANTHROPIC_API_KEY = \"\" # sk-ant-...\n", + "ANTHROPIC_BASE_URL = \"\" # optional — only for proxy/gateway (e.g. Azure APIM)\n", + "VOYAGE_API_KEY = \"\" # pa-...\n", + "MEMORI_API_KEY = \"\" # optional — raises augmentation quota to 5,000/month\n", + "\n", + "# ─────────────────────────────────────────────────────────────────────────\n", + "\n", + "_inline = {\n", + " \"MONGODB_URI\": MONGODB_URI,\n", + " \"ANTHROPIC_API_KEY\": ANTHROPIC_API_KEY,\n", + " \"ANTHROPIC_BASE_URL\": ANTHROPIC_BASE_URL,\n", + " \"VOYAGE_API_KEY\": VOYAGE_API_KEY,\n", + " \"MEMORI_API_KEY\": MEMORI_API_KEY,\n", + "}\n", + "\n", + "_pasted = [k for k, v in _inline.items() if v.strip()]\n", + "for key in _pasted:\n", + " os.environ[key] = _inline[key].strip()\n", + "\n", + "if _pasted:\n", + " print(f\"Using pasted values for: {', '.join(_pasted)}\")\n", + " print(\"REMINDER: clear this cell before committing or screen-sharing.\")\n", + "else:\n", + " print(\"No values pasted — falling back to env vars, Colab secrets, then prompt.\")" + ] + }, + { + "cell_type": "markdown", + "id": "29686670", + "metadata": {}, + "source": [ + "Anything pasted above lands in the `.ipynb` file itself. For a public repo the safest\n", + "combination is to leave the block empty and let the prompt handle it, which stores nothing.\n", + "If you do paste values while iterating, `nbstripout` or a pre-commit hook is worth having so\n", + "a filled-in cell cannot reach a commit by accident." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32990474", + "metadata": {}, + "outputs": [], + "source": [ + "def get_secret(name, required=True):\n", + " # Env var, then Colab secrets, then interactive prompt.\n", + " if os.environ.get(name):\n", + " return os.environ[name]\n", + " try:\n", + " from google.colab import userdata\n", + "\n", + " value = userdata.get(name)\n", + " if value:\n", + " os.environ[name] = value\n", + " return value\n", + " except Exception:\n", + " pass\n", + " if required:\n", + " import getpass\n", + "\n", + " value = getpass.getpass(f\"{name}: \").strip()\n", + " os.environ[name] = value\n", + " return value\n", + " return None\n", + "\n", + "\n", + "for key in [\"ANTHROPIC_API_KEY\", \"VOYAGE_API_KEY\", \"MONGODB_URI\"]:\n", + " get_secret(key)\n", + "\n", + "if not get_secret(\"MEMORI_API_KEY\", required=False):\n", + " print(\n", + " \"\\nNOTE: MEMORI_API_KEY not set — augmentation capped at 100/month, metered by IP.\"\n", + " )\n", + " print(\" Get a key: python -m memori sign-up you@example.com\")\n", + "\n", + "# ── Anthropic client kwargs ──────────────────────────────────────────────\n", + "# When ANTHROPIC_BASE_URL is set, the SDK targets that URL instead of\n", + "# api.anthropic.com. Azure APIM gateways (e.g. Grove) expect an \"api-key\"\n", + "# header rather than the SDK's default \"x-api-key\", so we inject it via\n", + "# default_headers. When the env var is unset, _client_kwargs is empty and\n", + "# every anthropic.Anthropic() call behaves exactly as before.\n", + "_base_url = os.environ.get(\"ANTHROPIC_BASE_URL\")\n", + "_client_kwargs = {}\n", + "if _base_url:\n", + " _client_kwargs[\"base_url\"] = _base_url\n", + " _client_kwargs[\"default_headers\"] = {\"api-key\": os.environ[\"ANTHROPIC_API_KEY\"]}\n", + " print(f\"\\nUsing custom base URL: {_base_url}\")\n", + "\n", + "# appname is passed via the driver API, not the connection string. The spec calls for\n", + "# this: a URI pasted from the Atlas UI carries its own appName, and the driver argument\n", + "# overrides it. Memori receives a database handle from this client, so every write\n", + "# Memori makes inherits the attribution.\n", + "mongo_client = MongoClient(os.environ[\"MONGODB_URI\"], appname=APP_NAME)\n", + "mongo_client.admin.command(\"ping\")\n", + "\n", + "is_atlas = \"mongodb.net\" in os.environ[\"MONGODB_URI\"]\n", + "print(\"\\nCredentials : ok\")\n", + "print(\"Atlas ping : ok\")\n", + "print(f\"appName : {APP_NAME}\")\n", + "print(f\"Atlas cluster : {is_atlas} (section 10 requires this to be True)\")" + ] + }, + { + "cell_type": "markdown", + "id": "04db65ef", + "metadata": {}, + "source": [ + "Memori generates embeddings locally and downloads the embedding model on first use. That\n", + "download is slow enough to be noticeable and there is no reason to discover it mid-demo.\n", + "`python -m memori setup` pre-fetches it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7230ddb9", + "metadata": {}, + "outputs": [], + "source": [ + "!python -m memori quota || echo \"(quota check needs MEMORI_API_KEY)\"\n", + "!python -m memori setup" + ] + }, + { + "cell_type": "markdown", + "id": "cc762e35", + "metadata": {}, + "source": [ + "## 3. Connect Memori to Atlas\n", + "\n", + "Memori's MongoDB adapter takes `conn` as a **callable that returns the database** — not a\n", + "database object and not a connection string. Memori invokes it whenever it needs a handle,\n", + "so PyMongo keeps managing its own pool.\n", + "\n", + "The documented order is `register()` → `attribution()` → `build()`. Attribution must be set\n", + "before any LLM call, since it determines the entity/process/session scope every write is\n", + "tagged with. `build()` applies schema migrations and is safe to run repeatedly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d548c689", + "metadata": {}, + "outputs": [], + "source": [ + "def get_db():\n", + " return mongo_client[DB_NAME]\n", + "\n", + "\n", + "memory_client = anthropic.Anthropic(**_client_kwargs)\n", + "\n", + "# use_rust_core=False works around a Memori v3.3.6 bug where the Rust adapter\n", + "# creates a duplicate entity when resolving ObjectId strings during recall,\n", + "# causing auto-injection to silently find no facts.\n", + "mem = Memori(conn=get_db, use_rust_core=False).llm.register(memory_client)\n", + "mem.attribution(entity_id=ENTITY_ID, process_id=PROCESS_ID)\n", + "mem.config.storage.build()\n", + "\n", + "db = get_db()\n", + "print(f\"Collections in '{DB_NAME}':\")\n", + "for name in sorted(db.list_collection_names()):\n", + " print(f\" {name:38s} {db[name].count_documents({}):>6d} docs\")" + ] + }, + { + "cell_type": "markdown", + "id": "dd6e0666", + "metadata": {}, + "source": [ + "Two of those collections carry the state/memory split:\n", + "\n", + "- **`memori_conversation_message`** — the turns themselves. Run-bounded. This is state.\n", + "- **`memori_entity_fact`** — durable extracted facts, scoped to an entity and surviving\n", + " across sessions. This is memory.\n", + "\n", + "The rest are supporting structure: entities, sessions, processes, and a knowledge graph of\n", + "subjects, predicates, and objects." + ] + }, + { + "cell_type": "markdown", + "id": "9f6c53c5", + "metadata": {}, + "source": [ + "## 4. Control: an agent with no memory\n", + "\n", + "Establishing the baseline first matters. Without it, a memory demo is just an assertion that\n", + "context was injected, and the audience has to take it on faith.\n", + "\n", + "This client is deliberately **not** registered with Memori." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc4cbfc2", + "metadata": {}, + "outputs": [], + "source": [ + "control_client = anthropic.Anthropic(**_client_kwargs)\n", + "\n", + "QUESTION = \"What do you know about my dietary restrictions and where I live?\"\n", + "\n", + "\n", + "def ask(client, prompt, model=MODEL):\n", + " resp = client.messages.create(\n", + " model=model,\n", + " max_tokens=300,\n", + " messages=[{\"role\": \"user\", \"content\": prompt}],\n", + " )\n", + " return resp.content[0].text\n", + "\n", + "\n", + "print(textwrap.fill(ask(control_client, QUESTION), 88))" + ] + }, + { + "cell_type": "markdown", + "id": "fc2a63cf", + "metadata": {}, + "source": [ + "The model has no basis to answer. Nothing is wrong — it simply has no prior turns." + ] + }, + { + "cell_type": "markdown", + "id": "63de0f27", + "metadata": {}, + "source": [ + "## 5. Register Memori and write the first session\n", + "\n", + "`memory_client` was registered in section 3 via `mem.llm.register()`, which detects the\n", + "provider automatically. The per-provider helpers (`mem.anthropic.register()`,\n", + "`mem.openai.register()`) still exist but emit a `DeprecationWarning` in v3 — published\n", + "tutorials written against v1 and v2 use those older forms.\n", + "\n", + "Anthropic requires `max_tokens` on every call. Memori captures the top-level `system`\n", + "parameter as well as the `messages` array." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "995b1b97", + "metadata": {}, + "outputs": [], + "source": [ + "session_one = [\n", + " \"I'm vegan, and I've been that way for about six years.\",\n", + " \"I moved to Lisbon last spring — still adjusting to the hills.\",\n", + " \"I'm allergic to walnuts, which makes a lot of vegan recipes annoying.\",\n", + "]\n", + "\n", + "for turn in session_one:\n", + " reply = ask(memory_client, turn)\n", + " print(f\"User : {turn}\")\n", + " print(f\"Assistant : {textwrap.fill(reply, 88, subsequent_indent=' ' * 12)}\\n\")" + ] + }, + { + "cell_type": "markdown", + "id": "6b427c97", + "metadata": {}, + "source": [ + "## 6. Wait for promotion — the async gate\n", + "\n", + "Memori captures the conversation immediately, then extracts facts from it **asynchronously**\n", + "on a background thread. `messages.create()` returns before extraction finishes. Asking the\n", + "recall question in the next cell is therefore a race — and losing that race on stage is the\n", + "classic way this demo fails.\n", + "\n", + "`mem.augmentation.wait()` is the documented answer for short-lived scripts, which is exactly\n", + "what a notebook is. It blocks on pending extraction futures, drains the database writer\n", + "queue, then waits out the final batch. It returns `False` on timeout rather than raising.\n", + "\n", + "Architecturally this is the moment worth naming: promotion from state to memory is a\n", + "*separate, later, asynchronous write*. Any system built on this has a window in which a turn\n", + "has happened but is not yet recallable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "abb8b245", + "metadata": {}, + "outputs": [], + "source": [ + "facts = db[\"memori_entity_fact\"]\n", + "\n", + "before = facts.count_documents({})\n", + "t0 = time.time()\n", + "\n", + "drained = mem.augmentation.wait(timeout=120)\n", + "\n", + "elapsed = time.time() - t0\n", + "after = facts.count_documents({})\n", + "\n", + "print(f\"augmentation.wait() -> {drained} ({elapsed:.1f}s)\")\n", + "print(f\"facts: {before} -> {after}\")\n", + "\n", + "if not drained:\n", + " print(\n", + " \"\\nWARNING: timed out. Check quota (python -m memori quota) and network egress.\"\n", + " )\n", + "elif after == before:\n", + " print(\n", + " \"\\nWARNING: queue drained but no new facts. Re-run section 5 with more specific statements.\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "3a183597", + "metadata": {}, + "source": [ + "## 7. The same question, with memory\n", + "\n", + "Same prompt as the control in section 4. Same model.\n", + "\n", + "The rigorous version of this test builds a **completely new Anthropic client and a new Memori\n", + "instance**, carrying no in-process context from the earlier turns. Only `entity_id` and\n", + "`process_id` connect the two. If the answer comes back correct, it came out of MongoDB and\n", + "nowhere else — which forecloses the obvious objection that the model simply still had the\n", + "messages in its context window." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "211858bb", + "metadata": {}, + "outputs": [], + "source": [ + "fresh_client = anthropic.Anthropic(**_client_kwargs)\n", + "fresh_mem = Memori(conn=get_db, use_rust_core=False).llm.register(fresh_client)\n", + "fresh_mem.attribution(entity_id=ENTITY_ID, process_id=PROCESS_ID)\n", + "\n", + "print(textwrap.fill(ask(fresh_client, QUESTION), 88))" + ] + }, + { + "cell_type": "markdown", + "id": "86ba3383", + "metadata": {}, + "source": [ + "Memori retrieved the relevant facts and injected them into the prompt. The agent code did\n", + "not change — no retrieval call, no prompt assembly, no context management in application\n", + "code. That work moved into the memory layer." + ] + }, + { + "cell_type": "markdown", + "id": "0de0eb93", + "metadata": {}, + "source": [ + "## 8. Inspect recall directly\n", + "\n", + "Recall runs in two modes. **Automatic** is the default: on every LLM call Memori intercepts\n", + "the outbound request, runs semantic search for the current entity, and injects the top\n", + "matches into the system prompt. That is what produced the answer in section 7.\n", + "\n", + "**Manual** recall via `mem.recall()` is the explicit read path — for debugging, custom\n", + "prompts, or rendering memory in a UI. Each fact carries `content`, `similarity`,\n", + "`rank_score`, and `date_created`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1716c1ad", + "metadata": {}, + "outputs": [], + "source": [ + "def show(f):\n", + " # Facts carry id, content, similarity, rank_score, date_created.\n", + " # Cloud mode can return mappings instead, so normalize defensively.\n", + " if hasattr(f, \"content\"):\n", + " return f\"{f.similarity:.4f} {f.content}\"\n", + " if isinstance(f, dict):\n", + " return f\"{f.get('similarity', 0):.4f} {f.get('content', f)}\"\n", + " return str(f)\n", + "\n", + "\n", + "for query in [\n", + " \"what does this person eat?\",\n", + " \"where do they live?\",\n", + " \"any allergies?\",\n", + "]:\n", + " print(f\"\\nQ: {query}\")\n", + " for fact in mem.recall(query, limit=3):\n", + " print(f\" {show(fact)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79851333", + "metadata": {}, + "outputs": [], + "source": [ + "# recall_relevance_threshold (default 0.1) sets the minimum similarity to include.\n", + "# recall_embeddings_limit (default 1000) caps how many embeddings are compared.\n", + "mem.config.recall_relevance_threshold = 0.05\n", + "mem.config.recall_embeddings_limit = 500\n", + "\n", + "print(\"Broad recall on a vague query:\")\n", + "for fact in mem.recall(\"tell me about this user\", limit=10):\n", + " print(f\" {show(fact)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "d71a04ac", + "metadata": {}, + "source": [ + "## 9. What MongoDB actually stores\n", + "\n", + "The value of a document store here is that memory is inspectable without a separate tool.\n", + "The fact schema carries `content`, a `content_embedding` computed by Memori, and reinforcement\n", + "counters (`num_times`, `date_last_time`) that let repeated facts strengthen over time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "757c67d7", + "metadata": {}, + "outputs": [], + "source": [ + "sample = facts.find_one({}, {\"content_embedding\": 0})\n", + "print(\"memori_entity_fact (embedding omitted for readability):\\n\")\n", + "for k, v in (sample or {}).items():\n", + " print(f\" {k:20s} {v}\")\n", + "\n", + "print(\n", + " f\"\\nState — memori_conversation_message : {db['memori_conversation_message'].count_documents({}):>4d} docs\"\n", + ")\n", + "print(f\"Memory — memori_entity_fact : {facts.count_documents({}):>4d} docs\")" + ] + }, + { + "cell_type": "markdown", + "id": "57f93e48", + "metadata": {}, + "source": [ + "The asymmetry is the point. Many turns of state compress into a handful of durable facts.\n", + "That compression ratio is what keeps long-running agents affordable — the alternative is\n", + "replaying the full transcript into every prompt." + ] + }, + { + "cell_type": "markdown", + "id": "0d328904", + "metadata": {}, + "source": [ + "## 10. A second retrieval path: Voyage AI + Atlas Vector Search\n", + "\n", + "Memori generates its own embeddings locally, using `all-MiniLM-L6-v2` at 384 dimensions\n", + "through a native backend. That backend is internal — TEI is offered only as a fallback for\n", + "deployments without the native extension — so Voyage cannot be substituted *inside* Memori's\n", + "recall path.\n", + "\n", + "What *is* possible, because the facts live in your cluster rather than behind a vendor API,\n", + "is building an independent retrieval path over the same documents. Here that means Voyage AI\n", + "embeddings in a separate field, indexed by Atlas Vector Search.\n", + "\n", + "This is the practical argument for bring-your-own-database. The memory framework's recall and\n", + "the data layer's retrieval are separable, and the database determines the ceiling on what the\n", + "second path can do.\n", + "\n", + "**Requires an Atlas cluster.** Vector search indexes are not available on a local `mongod`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32236811", + "metadata": {}, + "outputs": [], + "source": [ + "vo = voyageai.Client()\n", + "\n", + "pending = list(facts.find({\"voyage_embedding\": {\"$exists\": False}}, {\"content\": 1}))\n", + "print(f\"Facts needing a Voyage embedding: {len(pending)}\")\n", + "\n", + "if pending:\n", + " texts = [d[\"content\"] for d in pending]\n", + " vectors = vo.embed(\n", + " texts, model=VOYAGE_MODEL, input_type=\"document\", output_dimension=VOYAGE_DIMS\n", + " ).embeddings\n", + " for doc, vec in zip(pending, vectors):\n", + " facts.update_one({\"_id\": doc[\"_id\"]}, {\"$set\": {\"voyage_embedding\": vec}})\n", + " print(f\"Embedded {len(pending)} fact(s) with {VOYAGE_MODEL}\")\n", + "else:\n", + " print(\"All facts already embedded.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "06947760", + "metadata": {}, + "outputs": [], + "source": [ + "existing = {ix[\"name\"] for ix in facts.list_search_indexes()}\n", + "\n", + "if VECTOR_INDEX not in existing:\n", + " facts.create_search_index(\n", + " SearchIndexModel(\n", + " name=VECTOR_INDEX,\n", + " type=\"vectorSearch\",\n", + " definition={\n", + " \"fields\": [\n", + " {\n", + " \"type\": \"vector\",\n", + " \"path\": \"voyage_embedding\",\n", + " \"numDimensions\": VOYAGE_DIMS,\n", + " \"similarity\": \"cosine\",\n", + " },\n", + " {\"type\": \"filter\", \"path\": \"entity_id\"},\n", + " ]\n", + " },\n", + " )\n", + " )\n", + " print(f\"Created '{VECTOR_INDEX}'. Building...\")\n", + "else:\n", + " print(f\"'{VECTOR_INDEX}' already exists.\")\n", + "\n", + "# Index builds are asynchronous. Poll until queryable.\n", + "for _ in range(60):\n", + " status = next(\n", + " (i for i in facts.list_search_indexes() if i[\"name\"] == VECTOR_INDEX), None\n", + " )\n", + " if status and status.get(\"queryable\"):\n", + " print(\"Index queryable.\")\n", + " break\n", + " time.sleep(5)\n", + "else:\n", + " print(\"Index still building — the query below may return nothing yet.\")" + ] + }, + { + "cell_type": "markdown", + "id": "1e412eb9", + "metadata": {}, + "source": [ + "Atlas Vector Search indexes are updated asynchronously from the collection. A fact written a\n", + "moment ago may not be queryable immediately. This is the same class of window as the\n", + "augmentation delay in section 6, one layer down — worth designing for rather than assuming away." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23a041ff", + "metadata": {}, + "outputs": [], + "source": [ + "query = \"food this person cannot eat\"\n", + "qvec = vo.embed(\n", + " [query], model=VOYAGE_MODEL, input_type=\"query\", output_dimension=VOYAGE_DIMS\n", + ").embeddings[0]\n", + "\n", + "results = list(\n", + " facts.aggregate(\n", + " [\n", + " {\n", + " \"$vectorSearch\": {\n", + " \"index\": VECTOR_INDEX,\n", + " \"path\": \"voyage_embedding\",\n", + " \"queryVector\": qvec,\n", + " \"numCandidates\": 50,\n", + " \"limit\": 3,\n", + " }\n", + " },\n", + " {\"$project\": {\"content\": 1, \"score\": {\"$meta\": \"vectorSearchScore\"}}},\n", + " ]\n", + " )\n", + ")\n", + "\n", + "print(f\"Query: {query}\\n\")\n", + "for r in results:\n", + " print(f\" {r['score']:.4f} {r['content']}\")" + ] + }, + { + "cell_type": "markdown", + "id": "5544debc", + "metadata": {}, + "source": [ + "Note that \"food this person cannot eat\" shares no keywords with \"allergic to walnuts\" or\n", + "\"I'm vegan.\" A substring or regex filter over `content` returns nothing here. That vocabulary\n", + "gap between how a question is phrased and how a fact was recorded is the reason this path\n", + "needs embeddings at all." + ] + }, + { + "cell_type": "markdown", + "id": "2dcbb85b", + "metadata": {}, + "source": [ + "## 11. Retention and scoping\n", + "\n", + "Memory that only grows is a compliance problem. Two controls matter most.\n", + "\n", + "**TTL** expires facts automatically after a retention window. **Entity scoping** keeps one\n", + "user's memory out of another's recall — enforced at query time via the `entity_id` filter\n", + "declared in the vector index above.\n", + "\n", + "One trap worth knowing: MongoDB only expires documents whose indexed field is a **BSON date**.\n", + "Point a TTL index at a string timestamp and the index builds without complaint and then never\n", + "deletes anything. Silent, and invisible until an audit asks why five-year-old memory is still\n", + "queryable. Published Memori snippets index a `timestamp` field on a `memory_entries`\n", + "collection — both are v1 names that do not exist in the v3 schema — so verify against your own\n", + "documents rather than copying." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9cb5daac", + "metadata": {}, + "outputs": [], + "source": [ + "TTL_FIELD = \"date_created\"\n", + "sample_doc = facts.find_one({TTL_FIELD: {\"$exists\": True}}, {TTL_FIELD: 1})\n", + "\n", + "if not sample_doc:\n", + " print(\n", + " f\"No fact carries '{TTL_FIELD}'. Inspect a document and pick the right field:\"\n", + " )\n", + " print(\" \", sorted((facts.find_one({}, {\"content_embedding\": 0}) or {}).keys()))\n", + "elif not isinstance(sample_doc[TTL_FIELD], datetime):\n", + " print(f\"'{TTL_FIELD}' is {type(sample_doc[TTL_FIELD]).__name__}, not datetime.\")\n", + " print(\"A TTL index here would build successfully and never expire anything.\")\n", + " print(\"Store a BSON date alongside it, or expire on a schedule instead.\")\n", + "else:\n", + " facts.create_index(TTL_FIELD, expireAfterSeconds=90 * 24 * 60 * 60, name=\"fact_ttl\")\n", + " print(f\"TTL index created on '{TTL_FIELD}' (90 days).\")\n", + " print(\"Indexes:\", [i[\"name\"] for i in facts.list_indexes()])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d764a0d7", + "metadata": {}, + "outputs": [], + "source": [ + "# Entity isolation: a second user's recall must not surface the first user's facts.\n", + "other = Memori(conn=get_db, use_rust_core=False)\n", + "other.attribution(entity_id=\"webinar-user-002\", process_id=PROCESS_ID)\n", + "\n", + "print(\"user-001 recall :\", len(mem.recall(\"dietary restrictions\", limit=5)), \"fact(s)\")\n", + "print(\n", + " \"user-002 recall :\", len(other.recall(\"dietary restrictions\", limit=5)), \"fact(s)\"\n", + ")\n", + "other.close()" + ] + }, + { + "cell_type": "markdown", + "id": "5eb7a245", + "metadata": {}, + "source": [ + "`delete_entity_memories()` handles deletion requests — a right-to-erasure path that operates\n", + "on memory without touching the conversation state that may be under separate audit retention.\n", + "Those are different policies over the same cluster, which is the reason to keep the store\n", + "unified but the policies distinct." + ] + }, + { + "cell_type": "markdown", + "id": "bbdbf8bd", + "metadata": {}, + "source": [ + "## 12. Teardown\n", + "\n", + "Uncomment to reset between rehearsals." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "900b48a7", + "metadata": {}, + "outputs": [], + "source": [ + "# mongo_client.drop_database(DB_NAME)\n", + "# print(f\"Dropped '{DB_NAME}'\")\n", + "\n", + "mem.close()\n", + "fresh_mem.close()\n", + "print(\"Connections closed.\")" + ] + }, + { + "cell_type": "markdown", + "id": "e9573cf3", + "metadata": {}, + "source": [ + "## Where this leaves you\n", + "\n", + "The agent code never changed. What changed is that a memory layer sits between the client and\n", + "the model, and its writes land in collections you can query, index, expire, and audit.\n", + "\n", + "Three things worth carrying forward:\n", + "\n", + "- **Promotion from state to memory is asynchronous.** There is a window where a turn has\n", + " occurred and is not yet recallable. Section 6 makes it visible instead of hiding it.\n", + "- **Recall and retrieval are separable.** Memori's internal recall and the Voyage path in\n", + " section 10 read the same documents through different indexes.\n", + "- **One store, several policies.** Conversation state and durable memory can share a cluster\n", + " while carrying different retention and deletion rules.\n", + "\n", + "**References**\n", + "\n", + "- [Memori BYODB documentation](https://memorilabs.ai/docs/memori-byodb)\n", + "- [Memori on GitHub](https://github.com/MemoriLabs/Memori)\n", + "- [Atlas Vector Search](https://www.mongodb.com/docs/atlas/atlas-vector-search/)\n", + "- [Voyage AI embeddings](https://docs.voyageai.com/docs/embeddings)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {} + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 13c3c8fc0ea3f2d43f93baa74268b7ce4ede1c9f Mon Sep 17 00:00:00 2001 From: Mikiko Bazeley Date: Mon, 31 Aug 2026 15:10:28 -0700 Subject: [PATCH 2/2] Add DEMO_MODE for 5-7 min live presentation - DEMO_MODE flag skips sections 10-11 (Voyage vector search, TTL, entity isolation) - Trim session to 2 turns (vegan + walnut allergy) to save an LLM round-trip - Lower max_tokens to 150 for shorter replies during demo - Narrow recall question to match 2-turn data (dietary restrictions only) - Fix misleading WARNING when facts already exist from a prior run Co-Authored-By: Claude Opus 4.6 (1M context) --- .../memory/memori-mongodb-agent-memory.ipynb | 257 ++++++++++-------- 1 file changed, 141 insertions(+), 116 deletions(-) diff --git a/notebooks/memory/memori-mongodb-agent-memory.ipynb b/notebooks/memory/memori-mongodb-agent-memory.ipynb index 5f63f7ec..7cd3cede 100644 --- a/notebooks/memory/memori-mongodb-agent-memory.ipynb +++ b/notebooks/memory/memori-mongodb-agent-memory.ipynb @@ -66,6 +66,12 @@ "from pymongo import MongoClient\n", "from pymongo.operations import SearchIndexModel\n", "\n", + "# ── Demo mode ────────────────────────────────────────────────────────────\n", + "# Set to True for a 5-7 minute live presentation (skips sections 10-11).\n", + "# Set to False to run the full notebook including Voyage AI vector search,\n", + "# TTL indexes, and entity isolation.\n", + "DEMO_MODE = True\n", + "\n", "# appName per DevRel spec: devrel-MEDIUM-PRIMARY-SECONDARY-OPTIONAL\n", "# The GitHub value and the webinar/content value must differ so the two are\n", "# tracked separately. Swap the constant below when presenting live.\n", @@ -326,13 +332,13 @@ "source": [ "control_client = anthropic.Anthropic(**_client_kwargs)\n", "\n", - "QUESTION = \"What do you know about my dietary restrictions and where I live?\"\n", + "QUESTION = \"What do you know about my dietary restrictions?\"\n", "\n", "\n", "def ask(client, prompt, model=MODEL):\n", " resp = client.messages.create(\n", " model=model,\n", - " max_tokens=300,\n", + " max_tokens=150,\n", " messages=[{\"role\": \"user\", \"content\": prompt}],\n", " )\n", " return resp.content[0].text\n", @@ -353,17 +359,7 @@ "cell_type": "markdown", "id": "63de0f27", "metadata": {}, - "source": [ - "## 5. Register Memori and write the first session\n", - "\n", - "`memory_client` was registered in section 3 via `mem.llm.register()`, which detects the\n", - "provider automatically. The per-provider helpers (`mem.anthropic.register()`,\n", - "`mem.openai.register()`) still exist but emit a `DeprecationWarning` in v3 — published\n", - "tutorials written against v1 and v2 use those older forms.\n", - "\n", - "Anthropic requires `max_tokens` on every call. Memori captures the top-level `system`\n", - "parameter as well as the `messages` array." - ] + "source": "## 5. Register Memori and write the first session\n\n`memory_client` was registered in section 3 via `mem.llm.register()`, which detects the\nprovider automatically. The per-provider helpers (`mem.anthropic.register()`,\n`mem.openai.register()`) still exist but emit a `DeprecationWarning` in v3 — published\ntutorials written against v1 and v2 use those older forms.\n\nAnthropic requires `max_tokens` on every call. Memori captures the top-level `system`\nparameter as well as the `messages` array.\n\nTwo turns are enough to demonstrate recall. Add more (e.g. `\"I moved to Lisbon last spring\"`)\nif you want a richer fact set — each turn is one additional LLM round-trip (~3-5 seconds)." }, { "cell_type": "code", @@ -374,7 +370,6 @@ "source": [ "session_one = [\n", " \"I'm vegan, and I've been that way for about six years.\",\n", - " \"I moved to Lisbon last spring — still adjusting to the hills.\",\n", " \"I'm allergic to walnuts, which makes a lot of vegan recipes annoying.\",\n", "]\n", "\n", @@ -429,10 +424,12 @@ " print(\n", " \"\\nWARNING: timed out. Check quota (python -m memori quota) and network egress.\"\n", " )\n", - "elif after == before:\n", + "elif after == before and before == 0:\n", " print(\n", " \"\\nWARNING: queue drained but no new facts. Re-run section 5 with more specific statements.\"\n", - " )" + " )\n", + "elif after == before:\n", + " print(\"\\nFacts already present from a prior run — no new extraction needed.\")" ] }, { @@ -510,7 +507,6 @@ "\n", "for query in [\n", " \"what does this person eat?\",\n", - " \"where do they live?\",\n", " \"any allergies?\",\n", "]:\n", " print(f\"\\nQ: {query}\")\n", @@ -525,14 +521,17 @@ "metadata": {}, "outputs": [], "source": [ - "# recall_relevance_threshold (default 0.1) sets the minimum similarity to include.\n", - "# recall_embeddings_limit (default 1000) caps how many embeddings are compared.\n", - "mem.config.recall_relevance_threshold = 0.05\n", - "mem.config.recall_embeddings_limit = 500\n", + "if not DEMO_MODE:\n", + " # recall_relevance_threshold (default 0.1) sets the minimum similarity to include.\n", + " # recall_embeddings_limit (default 1000) caps how many embeddings are compared.\n", + " mem.config.recall_relevance_threshold = 0.05\n", + " mem.config.recall_embeddings_limit = 500\n", "\n", - "print(\"Broad recall on a vague query:\")\n", - "for fact in mem.recall(\"tell me about this user\", limit=10):\n", - " print(f\" {show(fact)}\")" + " print(\"Broad recall on a vague query:\")\n", + " for fact in mem.recall(\"tell me about this user\", limit=10):\n", + " print(f\" {show(fact)}\")\n", + "else:\n", + " print(\"Skipped (DEMO_MODE). Set DEMO_MODE = False to run.\")" ] }, { @@ -605,21 +604,27 @@ "metadata": {}, "outputs": [], "source": [ - "vo = voyageai.Client()\n", - "\n", - "pending = list(facts.find({\"voyage_embedding\": {\"$exists\": False}}, {\"content\": 1}))\n", - "print(f\"Facts needing a Voyage embedding: {len(pending)}\")\n", - "\n", - "if pending:\n", - " texts = [d[\"content\"] for d in pending]\n", - " vectors = vo.embed(\n", - " texts, model=VOYAGE_MODEL, input_type=\"document\", output_dimension=VOYAGE_DIMS\n", - " ).embeddings\n", - " for doc, vec in zip(pending, vectors):\n", - " facts.update_one({\"_id\": doc[\"_id\"]}, {\"$set\": {\"voyage_embedding\": vec}})\n", - " print(f\"Embedded {len(pending)} fact(s) with {VOYAGE_MODEL}\")\n", + "if not DEMO_MODE:\n", + " vo = voyageai.Client()\n", + "\n", + " pending = list(facts.find({\"voyage_embedding\": {\"$exists\": False}}, {\"content\": 1}))\n", + " print(f\"Facts needing a Voyage embedding: {len(pending)}\")\n", + "\n", + " if pending:\n", + " texts = [d[\"content\"] for d in pending]\n", + " vectors = vo.embed(\n", + " texts,\n", + " model=VOYAGE_MODEL,\n", + " input_type=\"document\",\n", + " output_dimension=VOYAGE_DIMS,\n", + " ).embeddings\n", + " for doc, vec in zip(pending, vectors):\n", + " facts.update_one({\"_id\": doc[\"_id\"]}, {\"$set\": {\"voyage_embedding\": vec}})\n", + " print(f\"Embedded {len(pending)} fact(s) with {VOYAGE_MODEL}\")\n", + " else:\n", + " print(\"All facts already embedded.\")\n", "else:\n", - " print(\"All facts already embedded.\")" + " print(\"Skipped (DEMO_MODE). Set DEMO_MODE = False to run.\")" ] }, { @@ -629,41 +634,44 @@ "metadata": {}, "outputs": [], "source": [ - "existing = {ix[\"name\"] for ix in facts.list_search_indexes()}\n", - "\n", - "if VECTOR_INDEX not in existing:\n", - " facts.create_search_index(\n", - " SearchIndexModel(\n", - " name=VECTOR_INDEX,\n", - " type=\"vectorSearch\",\n", - " definition={\n", - " \"fields\": [\n", - " {\n", - " \"type\": \"vector\",\n", - " \"path\": \"voyage_embedding\",\n", - " \"numDimensions\": VOYAGE_DIMS,\n", - " \"similarity\": \"cosine\",\n", - " },\n", - " {\"type\": \"filter\", \"path\": \"entity_id\"},\n", - " ]\n", - " },\n", + "if not DEMO_MODE:\n", + " existing = {ix[\"name\"] for ix in facts.list_search_indexes()}\n", + "\n", + " if VECTOR_INDEX not in existing:\n", + " facts.create_search_index(\n", + " SearchIndexModel(\n", + " name=VECTOR_INDEX,\n", + " type=\"vectorSearch\",\n", + " definition={\n", + " \"fields\": [\n", + " {\n", + " \"type\": \"vector\",\n", + " \"path\": \"voyage_embedding\",\n", + " \"numDimensions\": VOYAGE_DIMS,\n", + " \"similarity\": \"cosine\",\n", + " },\n", + " {\"type\": \"filter\", \"path\": \"entity_id\"},\n", + " ]\n", + " },\n", + " )\n", " )\n", - " )\n", - " print(f\"Created '{VECTOR_INDEX}'. Building...\")\n", - "else:\n", - " print(f\"'{VECTOR_INDEX}' already exists.\")\n", - "\n", - "# Index builds are asynchronous. Poll until queryable.\n", - "for _ in range(60):\n", - " status = next(\n", - " (i for i in facts.list_search_indexes() if i[\"name\"] == VECTOR_INDEX), None\n", - " )\n", - " if status and status.get(\"queryable\"):\n", - " print(\"Index queryable.\")\n", - " break\n", - " time.sleep(5)\n", + " print(f\"Created '{VECTOR_INDEX}'. Building...\")\n", + " else:\n", + " print(f\"'{VECTOR_INDEX}' already exists.\")\n", + "\n", + " # Index builds are asynchronous. Poll until queryable.\n", + " for _ in range(60):\n", + " status = next(\n", + " (i for i in facts.list_search_indexes() if i[\"name\"] == VECTOR_INDEX), None\n", + " )\n", + " if status and status.get(\"queryable\"):\n", + " print(\"Index queryable.\")\n", + " break\n", + " time.sleep(5)\n", + " else:\n", + " print(\"Index still building — the query below may return nothing yet.\")\n", "else:\n", - " print(\"Index still building — the query below may return nothing yet.\")" + " print(\"Skipped (DEMO_MODE). Set DEMO_MODE = False to run.\")" ] }, { @@ -683,31 +691,34 @@ "metadata": {}, "outputs": [], "source": [ - "query = \"food this person cannot eat\"\n", - "qvec = vo.embed(\n", - " [query], model=VOYAGE_MODEL, input_type=\"query\", output_dimension=VOYAGE_DIMS\n", - ").embeddings[0]\n", - "\n", - "results = list(\n", - " facts.aggregate(\n", - " [\n", - " {\n", - " \"$vectorSearch\": {\n", - " \"index\": VECTOR_INDEX,\n", - " \"path\": \"voyage_embedding\",\n", - " \"queryVector\": qvec,\n", - " \"numCandidates\": 50,\n", - " \"limit\": 3,\n", - " }\n", - " },\n", - " {\"$project\": {\"content\": 1, \"score\": {\"$meta\": \"vectorSearchScore\"}}},\n", - " ]\n", + "if not DEMO_MODE:\n", + " query = \"food this person cannot eat\"\n", + " qvec = vo.embed(\n", + " [query], model=VOYAGE_MODEL, input_type=\"query\", output_dimension=VOYAGE_DIMS\n", + " ).embeddings[0]\n", + "\n", + " results = list(\n", + " facts.aggregate(\n", + " [\n", + " {\n", + " \"$vectorSearch\": {\n", + " \"index\": VECTOR_INDEX,\n", + " \"path\": \"voyage_embedding\",\n", + " \"queryVector\": qvec,\n", + " \"numCandidates\": 50,\n", + " \"limit\": 3,\n", + " }\n", + " },\n", + " {\"$project\": {\"content\": 1, \"score\": {\"$meta\": \"vectorSearchScore\"}}},\n", + " ]\n", + " )\n", " )\n", - ")\n", "\n", - "print(f\"Query: {query}\\n\")\n", - "for r in results:\n", - " print(f\" {r['score']:.4f} {r['content']}\")" + " print(f\"Query: {query}\\n\")\n", + " for r in results:\n", + " print(f\" {r['score']:.4f} {r['content']}\")\n", + "else:\n", + " print(\"Skipped (DEMO_MODE). Set DEMO_MODE = False to run.\")" ] }, { @@ -749,22 +760,27 @@ "metadata": {}, "outputs": [], "source": [ - "TTL_FIELD = \"date_created\"\n", - "sample_doc = facts.find_one({TTL_FIELD: {\"$exists\": True}}, {TTL_FIELD: 1})\n", + "if not DEMO_MODE:\n", + " TTL_FIELD = \"date_created\"\n", + " sample_doc = facts.find_one({TTL_FIELD: {\"$exists\": True}}, {TTL_FIELD: 1})\n", "\n", - "if not sample_doc:\n", - " print(\n", - " f\"No fact carries '{TTL_FIELD}'. Inspect a document and pick the right field:\"\n", - " )\n", - " print(\" \", sorted((facts.find_one({}, {\"content_embedding\": 0}) or {}).keys()))\n", - "elif not isinstance(sample_doc[TTL_FIELD], datetime):\n", - " print(f\"'{TTL_FIELD}' is {type(sample_doc[TTL_FIELD]).__name__}, not datetime.\")\n", - " print(\"A TTL index here would build successfully and never expire anything.\")\n", - " print(\"Store a BSON date alongside it, or expire on a schedule instead.\")\n", + " if not sample_doc:\n", + " print(\n", + " f\"No fact carries '{TTL_FIELD}'. Inspect a document and pick the right field:\"\n", + " )\n", + " print(\" \", sorted((facts.find_one({}, {\"content_embedding\": 0}) or {}).keys()))\n", + " elif not isinstance(sample_doc[TTL_FIELD], datetime):\n", + " print(f\"'{TTL_FIELD}' is {type(sample_doc[TTL_FIELD]).__name__}, not datetime.\")\n", + " print(\"A TTL index here would build successfully and never expire anything.\")\n", + " print(\"Store a BSON date alongside it, or expire on a schedule instead.\")\n", + " else:\n", + " facts.create_index(\n", + " TTL_FIELD, expireAfterSeconds=90 * 24 * 60 * 60, name=\"fact_ttl\"\n", + " )\n", + " print(f\"TTL index created on '{TTL_FIELD}' (90 days).\")\n", + " print(\"Indexes:\", [i[\"name\"] for i in facts.list_indexes()])\n", "else:\n", - " facts.create_index(TTL_FIELD, expireAfterSeconds=90 * 24 * 60 * 60, name=\"fact_ttl\")\n", - " print(f\"TTL index created on '{TTL_FIELD}' (90 days).\")\n", - " print(\"Indexes:\", [i[\"name\"] for i in facts.list_indexes()])" + " print(\"Skipped (DEMO_MODE). Set DEMO_MODE = False to run.\")" ] }, { @@ -774,15 +790,24 @@ "metadata": {}, "outputs": [], "source": [ - "# Entity isolation: a second user's recall must not surface the first user's facts.\n", - "other = Memori(conn=get_db, use_rust_core=False)\n", - "other.attribution(entity_id=\"webinar-user-002\", process_id=PROCESS_ID)\n", + "if not DEMO_MODE:\n", + " # Entity isolation: a second user's recall must not surface the first user's facts.\n", + " other = Memori(conn=get_db, use_rust_core=False)\n", + " other.attribution(entity_id=\"webinar-user-002\", process_id=PROCESS_ID)\n", "\n", - "print(\"user-001 recall :\", len(mem.recall(\"dietary restrictions\", limit=5)), \"fact(s)\")\n", - "print(\n", - " \"user-002 recall :\", len(other.recall(\"dietary restrictions\", limit=5)), \"fact(s)\"\n", - ")\n", - "other.close()" + " print(\n", + " \"user-001 recall :\",\n", + " len(mem.recall(\"dietary restrictions\", limit=5)),\n", + " \"fact(s)\",\n", + " )\n", + " print(\n", + " \"user-002 recall :\",\n", + " len(other.recall(\"dietary restrictions\", limit=5)),\n", + " \"fact(s)\",\n", + " )\n", + " other.close()\n", + "else:\n", + " print(\"Skipped (DEMO_MODE). Set DEMO_MODE = False to run.\")" ] }, {