From 63420631fe0bb6cda7b3b26e366de5413affb0cb Mon Sep 17 00:00:00 2001 From: Harry Phan Date: Sun, 31 May 2026 17:58:47 +0700 Subject: [PATCH 1/5] Add Walrus Memory Python SDK Colab --- docs/docs.json | 1 + docs/python-sdk/api-reference.md | 4 +- docs/python-sdk/changelog.mdx | 12 +- docs/python-sdk/colab.md | 24 + docs/python-sdk/quick-start.md | 12 +- docs/python-sdk/usage.md | 6 +- docs/python-sdk/usage/with-memwal.md | 2 +- packages/python-sdk-memwal/.gitignore | 2 + packages/python-sdk-memwal/CHANGELOG.md | 6 + packages/python-sdk-memwal/README.md | 23 +- .../python-sdk-memwal/examples/.env.example | 4 +- packages/python-sdk-memwal/memwal/__init__.py | 4 +- packages/python-sdk-memwal/memwal/client.py | 12 +- .../python-sdk-memwal/memwal/middleware.py | 34 +- .../notebooks/walrus_memory_python_sdk.ipynb | 774 ++++++++++++++++++ .../python-sdk-memwal/tests/test_client.py | 26 +- .../tests/test_integration.py | 12 +- .../tests/test_middleware.py | 32 + .../python-sdk-memwal/tests/test_signing.py | 6 +- 19 files changed, 951 insertions(+), 45 deletions(-) create mode 100644 docs/python-sdk/colab.md create mode 100644 packages/python-sdk-memwal/notebooks/walrus_memory_python_sdk.ipynb diff --git a/docs/docs.json b/docs/docs.json index cda3e8f5..2b05f810 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -97,6 +97,7 @@ "group": "Python SDK", "pages": [ "python-sdk/quick-start", + "python-sdk/colab", { "group": "Usage", "root": "python-sdk/usage", diff --git a/docs/python-sdk/api-reference.md b/docs/python-sdk/api-reference.md index 7f2ef11a..88e31784 100644 --- a/docs/python-sdk/api-reference.md +++ b/docs/python-sdk/api-reference.md @@ -1,6 +1,6 @@ --- title: "API Reference" -description: "Full method signatures, result dataclasses, and utilities for the MemWal Python SDK." +description: "Full method signatures, result dataclasses, and utilities for the Walrus Memory Python SDK." --- See also: @@ -25,7 +25,7 @@ MemWal.create( | Argument | Type | Required | Default | Notes | | --- | --- | --- | --- | --- | | `key` | `str` | Yes | — | Ed25519 delegate private key in hex | -| `account_id` | `str` | Yes | — | MemWalAccount object ID on Sui | +| `account_id` | `str` | Yes | — | Walrus Memory account object ID on Sui | | `server_url` | `str` | No | `http://localhost:8000` | Explicit relayer URL — wins over `env` | | `namespace` | `str` | No | `"default"` | Default namespace for memory isolation | | `env` | `str` | No | — | Preset: `prod` / `dev` / `staging` / `local`. Unknown → `ValueError` | diff --git a/docs/python-sdk/changelog.mdx b/docs/python-sdk/changelog.mdx index f20e1a62..44e13891 100644 --- a/docs/python-sdk/changelog.mdx +++ b/docs/python-sdk/changelog.mdx @@ -1,12 +1,22 @@ --- title: "Changelog" -description: "Release history for the MemWal Python SDK." +description: "Release history for the Walrus Memory Python SDK." --- Track what's new, changed, and fixed in `memwal` (Python). For the latest version, see the [PyPI project page](https://pypi.org/project/memwal/). +## Unreleased + +### Added + +- Added a runnable [Walrus Memory Python SDK Colab](https://colab.research.google.com/drive/1SaKjkSp0DXnM_nktWSiEC-l9qGtVr6ph) covering installation, secure `staging` configuration, optional `prod`, `MemWalSync`, health/compatibility checks, delegate public-key/address derivation, `remember`, async job waiting, `recall`, bulk remember, `remember_bulk_and_wait`, optional `ask`, `analyze`, `analyze_and_wait`, `embed`, manual methods with scoring weights, `restore`, optional OpenAI/LangChain middleware, OpenAI-compatible provider settings such as `OPENAI_BASE_URL`, and troubleshooting. + +### Fixed + +- Fixed `MemWalSync` reuse inside notebooks so repeated calls do not reuse an HTTP transport from a closed event loop. + ## 0.1.3 ### Added diff --git a/docs/python-sdk/colab.md b/docs/python-sdk/colab.md new file mode 100644 index 00000000..cb193f42 --- /dev/null +++ b/docs/python-sdk/colab.md @@ -0,0 +1,24 @@ +--- +title: "Colab Notebook" +description: "Run the Walrus Memory Python SDK from Google Colab." +--- + +Use the runnable [Walrus Memory Python SDK Colab](https://colab.research.google.com/drive/1SaKjkSp0DXnM_nktWSiEC-l9qGtVr6ph) when you want a notebook-first walkthrough. + +The notebook covers: + +- installing `memwal` +- loading credentials through Colab Secrets or hidden prompts +- configuring the SDK without exposing private keys, defaulting to `staging` +- switching to `prod` when you have production credentials +- creating `MemWalSync` for notebook-friendly calls +- checking relayer `health`, compatibility, and delegate public key derivation +- storing memory with `remember` +- waiting for async remember jobs to persist on Walrus +- retrieving memory with `recall` +- using `remember_and_wait`, bulk remember, `remember_bulk_and_wait`, `ask`, `analyze`, `analyze_and_wait`, `embed`, manual search/register with scoring weights, and `restore` +- optionally wrapping OpenAI and LangChain clients with Walrus Memory middleware +- using `OPENAI_BASE_URL` for OpenAI-compatible providers such as OpenRouter +- basic troubleshooting for auth, namespaces, and async remember jobs + +The repo copy lives in [`packages/python-sdk-memwal/notebooks/walrus_memory_python_sdk.ipynb`](https://github.com/MystenLabs/MemWal/blob/main/packages/python-sdk-memwal/notebooks/walrus_memory_python_sdk.ipynb). diff --git a/docs/python-sdk/quick-start.md b/docs/python-sdk/quick-start.md index 48ca6c26..5af9b7bb 100644 --- a/docs/python-sdk/quick-start.md +++ b/docs/python-sdk/quick-start.md @@ -1,9 +1,9 @@ --- title: "Quick Start" -description: "Install the MemWal Python SDK and store your first memory in under a minute." +description: "Install the Walrus Memory Python SDK and store your first memory in under a minute." --- -The MemWal Python SDK (`memwal` on PyPI) gives your app persistent, encrypted memory — store, recall, and analyze context across sessions. It mirrors the TypeScript `MemWal` client: same relayer, same Ed25519 auth, same methods. +The Walrus Memory Python SDK (`memwal` on PyPI) gives your app persistent, encrypted memory — store, recall, and analyze context across sessions. It mirrors the TypeScript `MemWal` client: same relayer, same Ed25519 auth, same methods. | Entry point | Import | When to use | | --- | --- | --- | @@ -17,6 +17,10 @@ The MemWal Python SDK (`memwal` on PyPI) gives your app persistent, encrypted me pip install memwal ``` +## Try It In Colab + +Open the runnable [Walrus Memory Python SDK Colab](https://colab.research.google.com/drive/1SaKjkSp0DXnM_nktWSiEC-l9qGtVr6ph) for a notebook walkthrough covering installation, secure configuration, health checks, `remember`, async job waiting, `recall`, bulk remember, `remember_bulk_and_wait`, optional SDK utilities, OpenAI/LangChain middleware, OpenAI-compatible provider settings such as `OPENAI_BASE_URL`, and basic troubleshooting. It defaults to `staging` for test credentials and can switch to `prod` for production credentials. + Optional integrations: @@ -41,7 +45,7 @@ Requires Python 3.9+. Core dependencies are `httpx` and `PyNaCl` (Ed25519 signin Before wiring the SDK into your app: -- Generate a MemWal account ID and delegate private key for your client using the hosted endpoint: +- Generate a Walrus Memory account ID and delegate private key for your client using the hosted endpoint: - Production (mainnet): `https://memwal.ai` - Staging (testnet): `https://staging.memwal.ai` - Choose a relayer: @@ -53,7 +57,7 @@ Before wiring the SDK into your app: | Argument | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `key` | `str` | Yes | — | Ed25519 delegate private key in hex | -| `account_id` | `str` | Yes | — | MemWalAccount object ID on Sui | +| `account_id` | `str` | Yes | — | Walrus Memory account object ID on Sui | | `server_url` | `str` | No | `http://localhost:8000` | Explicit relayer URL — wins over `env` | | `namespace` | `str` | No | `"default"` | Default namespace for memory isolation | | `env` | `str` | No | — | Relayer preset: `prod` / `dev` / `staging` / `local` | diff --git a/docs/python-sdk/usage.md b/docs/python-sdk/usage.md index e92034d0..96ea50f4 100644 --- a/docs/python-sdk/usage.md +++ b/docs/python-sdk/usage.md @@ -1,6 +1,6 @@ --- title: "Usage" -description: "Async vs sync clients, namespace rules, manual methods, and AI middleware for the MemWal Python SDK." +description: "Async vs sync clients, namespace rules, manual methods, and AI middleware for the Walrus Memory Python SDK." --- The Python SDK exposes one relayer-backed client in two forms, plus middleware: @@ -25,7 +25,7 @@ Detailed pages: from memwal import MemWal, RecallParams memwal = MemWal.create(key="...", account_id="0x...", env="prod") -job = await memwal.remember("User prefers dark mode.") +done = await memwal.remember_and_wait("User prefers dark mode.") result = await memwal.recall(RecallParams(query="preferences")) await memwal.close() ``` @@ -36,7 +36,7 @@ await memwal.close() from memwal import MemWalSync, RecallParams client = MemWalSync.create(key="...", account_id="0x...", env="prod") -client.remember("User prefers dark mode.") +client.remember_and_wait("User prefers dark mode.") result = client.recall(RecallParams(query="preferences")) client.close() ``` diff --git a/docs/python-sdk/usage/with-memwal.md b/docs/python-sdk/usage/with-memwal.md index 62d25cac..03ebe302 100644 --- a/docs/python-sdk/usage/with-memwal.md +++ b/docs/python-sdk/usage/with-memwal.md @@ -73,7 +73,7 @@ response = await smart_client.chat.completions.create( **Before generation:** - Reads the last user message -- Runs `recall()` against MemWal +- Runs `recall()` against Walrus Memory - Filters by `min_relevance` (default `0.3`) - Injects matching memories as a system message before the last user message diff --git a/packages/python-sdk-memwal/.gitignore b/packages/python-sdk-memwal/.gitignore index 0e84e3db..8ecf8d7b 100644 --- a/packages/python-sdk-memwal/.gitignore +++ b/packages/python-sdk-memwal/.gitignore @@ -60,6 +60,8 @@ share/python-wheels/ # Jupyter .ipynb_checkpoints/ *.ipynb +!notebooks/ +!notebooks/walrus_memory_python_sdk.ipynb # Docs build site/ diff --git a/packages/python-sdk-memwal/CHANGELOG.md b/packages/python-sdk-memwal/CHANGELOG.md index b1e25b55..9ccab81d 100644 --- a/packages/python-sdk-memwal/CHANGELOG.md +++ b/packages/python-sdk-memwal/CHANGELOG.md @@ -1,5 +1,11 @@ # memwal +## Unreleased + +### Added + +- Added a runnable [Walrus Memory Python SDK Colab](https://colab.research.google.com/drive/1SaKjkSp0DXnM_nktWSiEC-l9qGtVr6ph) covering installation, secure `staging` configuration, optional `prod`, health checks, async `remember` job waiting, `recall`, bulk remember, manual methods with scoring weights, middleware examples, OpenAI-compatible provider settings such as `OPENAI_BASE_URL`, and troubleshooting. + ## 0.1.3 ### Added diff --git a/packages/python-sdk-memwal/README.md b/packages/python-sdk-memwal/README.md index bbcf16a8..12fdc508 100644 --- a/packages/python-sdk-memwal/README.md +++ b/packages/python-sdk-memwal/README.md @@ -18,6 +18,10 @@ pip install memwal[openai] # OpenAI SDK support pip install memwal[all] # Everything ``` +## Try It In Colab + +Open the runnable [Walrus Memory Python SDK Colab](https://colab.research.google.com/drive/1SaKjkSp0DXnM_nktWSiEC-l9qGtVr6ph) for a notebook walkthrough covering installation, secure `staging` configuration, optional `prod`, health checks, `remember`, async job waiting, `recall`, bulk remember, `remember_bulk_and_wait`, optional SDK utilities, OpenAI/LangChain middleware, OpenAI-compatible provider settings such as `OPENAI_BASE_URL`, and troubleshooting. + ## Quick Start Set your environment variables first: @@ -45,8 +49,8 @@ async def main(): server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"), ) - # Store a memory - result = await memwal.remember("I'm allergic to peanuts") + # Store a memory and wait until the background job is searchable + result = await memwal.remember_and_wait("I'm allergic to peanuts") print(result.blob_id) # Recall memories @@ -54,8 +58,8 @@ async def main(): for memory in matches.results: print(f"{memory.text} (relevance: {1 - memory.distance:.2f})") - # Analyze conversation for facts - analysis = await memwal.analyze("I love coffee and live in Tokyo") + # Analyze conversation for facts and wait until extracted facts are searchable + analysis = await memwal.analyze_and_wait("I love coffee and live in Tokyo") for fact in analysis.facts: print(fact.text) @@ -76,7 +80,7 @@ client = MemWalSync.create( server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"), ) -result = client.remember("I'm allergic to peanuts") +result = client.remember_and_wait("I'm allergic to peanuts") matches = client.recall(RecallParams(query="food allergies")) client.close() ``` @@ -91,7 +95,7 @@ async with MemWal.create( key=os.environ["MEMWAL_PRIVATE_KEY"], account_id=os.environ["MEMWAL_ACCOUNT_ID"], ) as memwal: - await memwal.remember("I prefer dark mode") + await memwal.remember_and_wait("I prefer dark mode") ``` ## Environment Presets @@ -176,7 +180,12 @@ Create a new async client. | Method | Description | |--------|-------------| -| `await remember(text, namespace?)` | Store a memory | +| `await remember(text, namespace?)` | Accept a background remember job and return `job_id` | +| `await wait_for_remember_job(job_id, ...)` | Poll one remember job until it is searchable | +| `await remember_and_wait(text, namespace?, ...)` | Store a memory and wait until it is searchable | +| `await remember_bulk(items)` | Accept several background remember jobs | +| `await wait_for_remember_jobs(job_ids, opts?)` | Poll several remember jobs together | +| `await remember_bulk_and_wait(items, opts?)` | Store several memories and wait for completion | | `await recall(RecallParams(query, limit?, namespace?, max_distance?))` | Search memories, optionally filtering by distance | | `await analyze(text, namespace?)` | Extract and store facts | | `await ask(question, limit?, namespace?)` | Ask a question answered using memories | diff --git a/packages/python-sdk-memwal/examples/.env.example b/packages/python-sdk-memwal/examples/.env.example index 1cef1e92..59e85aea 100644 --- a/packages/python-sdk-memwal/examples/.env.example +++ b/packages/python-sdk-memwal/examples/.env.example @@ -1,10 +1,10 @@ # Local server (default) or remote relayer MEMWAL_SERVER_URL=http://localhost:8000 # Ed25519 delegate private key (64-hex). Get from Walrus Memory dashboard. -MEMWAL_PRIVATE_KEY=21b423e72282dcc47805de48ef9130331b642667b7b2a5cd621767928205e360 +MEMWAL_PRIVATE_KEY= # Optional: paste the dashboard delegate public key so verification can catch mismatches. MEMWAL_DELEGATE_PUBLIC_KEY= # Walrus Memory account object ID on Sui (the wallet's account) -MEMWAL_ACCOUNT_ID=0x8a1121b8f95d79e68bd07efaf71689ce6fd832b369cdb1b2a943ec7beb822392 +MEMWAL_ACCOUNT_ID=0x-your-walrus-memory-account-id # Namespace for these test memories MEMWAL_NAMESPACE=python-sdk-example diff --git a/packages/python-sdk-memwal/memwal/__init__.py b/packages/python-sdk-memwal/memwal/__init__.py index bf63d5b5..f1017d79 100644 --- a/packages/python-sdk-memwal/memwal/__init__.py +++ b/packages/python-sdk-memwal/memwal/__init__.py @@ -13,13 +13,13 @@ ) # Async - result = await memwal.remember("I love coffee") + result = await memwal.remember_and_wait("I love coffee") matches = await memwal.recall(RecallParams(query="beverage preferences")) # Sync wrapper from memwal import MemWalSync client = MemWalSync.create(key="...", account_id="0x...") - result = client.remember("I love coffee") + result = client.remember_and_wait("I love coffee") """ from .client import ( diff --git a/packages/python-sdk-memwal/memwal/client.py b/packages/python-sdk-memwal/memwal/client.py index 3f1847cb..cbebaa3c 100644 --- a/packages/python-sdk-memwal/memwal/client.py +++ b/packages/python-sdk-memwal/memwal/client.py @@ -1141,6 +1141,12 @@ def _run(self, coro: Any) -> Any: except RuntimeError: loop = None + # Reset the httpx client before every asyncio.run() path so it is + # recreated inside the loop that will use it. This matters in + # notebooks/Jupyter where the sync wrapper runs coroutines in worker + # threads with short-lived event loops. + self._inner._client = None + if loop is not None and loop.is_running(): # Already inside an event loop (e.g. Jupyter). # Create a new loop in a thread. @@ -1149,12 +1155,6 @@ def _run(self, coro: Any) -> Any: with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: return pool.submit(asyncio.run, coro).result() else: - # Reset the httpx client before each asyncio.run() so it is - # recreated fresh inside the new event loop. Without this, - # reusing a MemWalSync instance across multiple calls raises - # "RuntimeError: Event loop is closed" because the client's - # transport is still bound to the previous (now-closed) loop. - self._inner._client = None return asyncio.run(coro) def remember( diff --git a/packages/python-sdk-memwal/memwal/middleware.py b/packages/python-sdk-memwal/memwal/middleware.py index c4d8b699..a39f1ddd 100644 --- a/packages/python-sdk-memwal/memwal/middleware.py +++ b/packages/python-sdk-memwal/memwal/middleware.py @@ -128,6 +128,22 @@ def _run() -> None: thread.start() +def _run_blocking(coro_factory: Callable[[], Any]) -> Any: + """Run a coroutine factory from sync code, including notebooks.""" + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = None + + if loop is not None and loop.is_running(): + import concurrent.futures + + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: + return pool.submit(lambda: asyncio.run(coro_factory())).result() + + return asyncio.run(coro_factory()) + + # ============================================================ # LangChain Integration # ============================================================ @@ -417,9 +433,12 @@ def _wrap_sync_openai( """Wrap a sync OpenAI client's chat.completions.create.""" original_create = client.chat.completions.create - def patched_create(*args: Any, **kwargs: Any) -> Any: - import asyncio + def _run_memwal(coro_factory: Callable[[], Any]) -> Any: + # Keep httpx clients bound to the short-lived loop that uses them. + memwal._client = None + return _run_blocking(coro_factory) + def patched_create(*args: Any, **kwargs: Any) -> Any: messages = kwargs.get("messages") or (args[0] if args else None) if messages is None: return original_create(*args, **kwargs) @@ -428,8 +447,8 @@ def patched_create(*args: Any, **kwargs: Any) -> Any: user_text = _find_last_user_message(messages) if user_text: try: - recall_result = asyncio.run( - memwal.recall(user_text, max_memories, namespace) + recall_result = _run_memwal( + lambda: memwal.recall(user_text, max_memories, namespace) ) relevant = [ m for m in recall_result.results @@ -450,13 +469,14 @@ def patched_create(*args: Any, **kwargs: Any) -> Any: # Fire-and-forget analyze if auto_save and user_text: - async def _analyze() -> None: + def _analyze() -> None: try: - await memwal.analyze(user_text, namespace) + _run_memwal(lambda: memwal.analyze(user_text, namespace)) except Exception as e: log(f"[Walrus Memory] Auto-save failed: {e}") - _fire_and_forget(_analyze()) + thread = threading.Thread(target=_analyze, daemon=True) + thread.start() return result diff --git a/packages/python-sdk-memwal/notebooks/walrus_memory_python_sdk.ipynb b/packages/python-sdk-memwal/notebooks/walrus_memory_python_sdk.ipynb new file mode 100644 index 00000000..5e648562 --- /dev/null +++ b/packages/python-sdk-memwal/notebooks/walrus_memory_python_sdk.ipynb @@ -0,0 +1,774 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Agent with memory using Walrus Memory\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook is a runnable end-user walkthrough for the Walrus Memory Python SDK.\n", + "\n", + "It uses the Python package/import name `memwal`, while the user-facing product name is Walrus Memory. The flow follows a small customer-support assistant that remembers a support conversation, waits for the memory job to persist, recalls the context, and then tours the SDK features that are useful in notebooks and application backends.\n", + "\n", + "You will run:\n", + "\n", + "- installation\n", + "- secure Colab Secrets or hidden prompts\n", + "- `MemWalSync.create(...)` for notebook-friendly calls\n", + "- relayer `health()` and `compatibility()`\n", + "- delegate public-key/address derivation without printing private keys\n", + "- `remember(...)` plus `wait_for_remember_job(...)`\n", + "- `recall(RecallParams(...))`\n", + "- `remember_and_wait(...)`\n", + "- bulk remember helpers\n", + "- optional `ask(...)`, `analyze(...)`, `analyze_and_wait(...)`, `embed(...)`, manual recall/register with scoring weights, and `restore(...)`\n", + "- optional OpenAI and LangChain middleware examples\n", + "- cleanup and troubleshooting\n", + "\n", + "The notebook defaults to `staging` for test credentials. You can switch to `prod` when you have production credentials.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Requirements\n", + "\n", + "Install the SDK from PyPI. The Colab runtime may show dependency resolver output; that is fine as long as `memwal` installs successfully.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%pip install -q -U memwal\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configure Secrets\n", + "\n", + "Create or open a Walrus Memory account and delegate key from the hosted dashboard for the environment you want to use.\n", + "\n", + "In Colab, the safest path is the key icon in the left sidebar. Add these secrets:\n", + "\n", + "- `MEMWAL_ACCOUNT_ID`\n", + "- `MEMWAL_PRIVATE_KEY`\n", + "- optional `WALRUS_MEMORY_ENV` with `staging` or `prod`\n", + "- optional `WALRUS_MEMORY_NAMESPACE`\n", + "- optional `RUN_OPTIONAL_SDK_METHODS`, set to `false` to skip slower optional Walrus Memory SDK cells\n", + "- optional `RUN_MIDDLEWARE_DEMO`, set to `true` only if you also want to run the OpenAI/LangChain wrapper cells\n", + "- optional `OPENAI_API_KEY`, `OPENAI_BASE_URL`, and `OPENAI_MODEL` for the middleware cells\n", + "\n", + "If a value is missing from Colab Secrets, the next cell asks through a hidden prompt. Do not paste real secrets into notebook source cells or commit them to Git.\n", + "\n", + "For OpenAI-compatible providers such as OpenRouter, set `OPENAI_BASE_URL` to the provider API base URL and set `OPENAI_MODEL` to that provider's model id.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "try:\n", + " from google.colab import userdata\n", + "except Exception:\n", + " userdata = None\n", + "\n", + "ALLOWED_ENVS = {\"staging\", \"prod\"}\n", + "DEFAULT_ENV = \"staging\"\n", + "\n", + "\n", + "def read_secret(name: str, prompt: str, required: bool = True) -> str:\n", + " value = os.environ.get(name, \"\")\n", + " if not value and userdata is not None:\n", + " try:\n", + " value = userdata.get(name) or \"\"\n", + " except Exception:\n", + " value = \"\"\n", + " if not value and required:\n", + " value = getpass.getpass(prompt)\n", + " if value:\n", + " os.environ[name] = value\n", + " return value\n", + "\n", + "\n", + "def read_setting(name: str, default: str) -> str:\n", + " value = os.environ.get(name, \"\")\n", + " if not value and userdata is not None:\n", + " try:\n", + " value = userdata.get(name) or \"\"\n", + " except Exception:\n", + " value = \"\"\n", + " value = (value or default).strip()\n", + " os.environ[name] = value\n", + " return value\n", + "\n", + "\n", + "def mask(value: str, keep: int = 8) -> str:\n", + " if not value:\n", + " return \"\"\n", + " if len(value) <= keep * 2:\n", + " return value[:2] + \"...\"\n", + " return f\"{value[:keep]}...{value[-keep:]}\"\n", + "\n", + "\n", + "def as_bool(value: str) -> bool:\n", + " return value.strip().lower() in {\"1\", \"true\", \"yes\", \"y\", \"on\"}\n", + "\n", + "\n", + "read_secret(\"MEMWAL_PRIVATE_KEY\", \"Paste Walrus Memory delegate private key (hidden): \")\n", + "read_secret(\"MEMWAL_ACCOUNT_ID\", \"Paste Walrus Memory account ID (hidden): \")\n", + "\n", + "WALRUS_MEMORY_ENV = read_setting(\"WALRUS_MEMORY_ENV\", DEFAULT_ENV).lower()\n", + "if WALRUS_MEMORY_ENV not in ALLOWED_ENVS:\n", + " raise ValueError(\"WALRUS_MEMORY_ENV must be 'staging' or 'prod' for this notebook.\")\n", + "os.environ[\"WALRUS_MEMORY_ENV\"] = WALRUS_MEMORY_ENV\n", + "\n", + "DEFAULT_NAMESPACE = f\"colab-{WALRUS_MEMORY_ENV}-customer-service-test\"\n", + "WALRUS_MEMORY_NAMESPACE = read_setting(\"WALRUS_MEMORY_NAMESPACE\", DEFAULT_NAMESPACE)\n", + "RUN_OPTIONAL_SDK_METHODS = as_bool(read_setting(\"RUN_OPTIONAL_SDK_METHODS\", \"true\"))\n", + "RUN_MIDDLEWARE_DEMO = as_bool(read_setting(\"RUN_MIDDLEWARE_DEMO\", \"false\"))\n", + "OPENAI_BASE_URL = read_setting(\"OPENAI_BASE_URL\", \"\")\n", + "OPENAI_MODEL = read_setting(\"OPENAI_MODEL\", \"gpt-4o-mini\")\n", + "\n", + "print(\"Walrus Memory configuration loaded\")\n", + "print(\"account_id:\", mask(os.environ.get(\"MEMWAL_ACCOUNT_ID\", \"\")))\n", + "print(\"env:\", WALRUS_MEMORY_ENV)\n", + "print(\"namespace:\", WALRUS_MEMORY_NAMESPACE)\n", + "print(\"optional SDK method cells:\", RUN_OPTIONAL_SDK_METHODS)\n", + "print(\"optional middleware cells:\", RUN_MIDDLEWARE_DEMO)\n", + "print(\"OpenAI-compatible base URL:\", OPENAI_BASE_URL or \"default OpenAI API\")\n", + "print(\"OpenAI model for optional middleware:\", OPENAI_MODEL)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create The Client\n", + "\n", + "`MemWalSync` is the notebook-friendly wrapper. It exposes the same API surface as the async `MemWal` client, but you do not need `await` in Colab cells.\n", + "\n", + "This cell also checks relayer health, validates SDK compatibility, derives the delegate public key, and derives the delegate Sui address locally. It masks public identifiers and never prints the private key.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from memwal import (\n", + " MemWalSync,\n", + " RecallParams,\n", + " RecallManualOptions,\n", + " RememberBulkItem,\n", + " RememberBulkOptions,\n", + " RememberManualOptions,\n", + " ScoringWeights,\n", + " delegate_key_to_sui_address,\n", + ")\n", + "\n", + "client_kwargs = {\n", + " \"key\": os.environ[\"MEMWAL_PRIVATE_KEY\"],\n", + " \"account_id\": os.environ[\"MEMWAL_ACCOUNT_ID\"],\n", + " \"namespace\": WALRUS_MEMORY_NAMESPACE,\n", + " \"env\": WALRUS_MEMORY_ENV,\n", + "}\n", + "\n", + "memory = MemWalSync.create(**client_kwargs)\n", + "\n", + "\n", + "def memwal_call(action):\n", + " # Keeps older memwal releases notebook-safe when multiple sync calls reuse the client.\n", + " inner = getattr(memory, \"_inner\", None)\n", + " if inner is not None:\n", + " inner._client = None\n", + " return action()\n", + "\n", + "\n", + "def show_error(name: str, exc: Exception) -> None:\n", + " print(f\"{name} skipped: {type(exc).__name__}: {str(exc)[:220]}\")\n", + "\n", + "\n", + "health = memwal_call(memory.health)\n", + "print(f\"relayer status={health.status} version={health.version}\")\n", + "\n", + "compatibility = memwal_call(memory.compatibility)\n", + "print(\"api version:\", compatibility.get(\"apiVersion\") or compatibility.get(\"api_version\") or \"unknown\")\n", + "print(\"min python sdk:\", (compatibility.get(\"minSupportedSdk\") or {}).get(\"python\", \"unknown\"))\n", + "\n", + "public_key_hex = memwal_call(memory.get_public_key_hex)\n", + "delegate_address = delegate_key_to_sui_address(os.environ[\"MEMWAL_PRIVATE_KEY\"])\n", + "print(\"delegate public key:\", mask(public_key_hex))\n", + "print(\"delegate Sui address:\", mask(delegate_address))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Build A Support Conversation\n", + "\n", + "The example memory is a realistic customer-support exchange. It contains an issue, a product, an email, an order number, and an appointment window. The namespace isolates this demo from your application data.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "conversation = [\n", + " {\n", + " \"role\": \"assistant\",\n", + " \"content\": \"Hi, I am the support chatbot. Thanks for being a member. What can I help you with?\",\n", + " },\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": \"I am seeing horizontal lines on my TV. Model: Sony 77 inch BRAVIA XR A80K OLED 4K UHD Smart TV.\",\n", + " },\n", + " {\n", + " \"role\": \"assistant\",\n", + " \"content\": \"I can help check warranty and repair options. Can you confirm the email on the account?\",\n", + " },\n", + " {\"role\": \"user\", \"content\": \"demo.customer@example.com\"},\n", + " {\n", + " \"role\": \"assistant\",\n", + " \"content\": \"Could you share the order number so I can look it up quickly?\",\n", + " },\n", + " {\"role\": \"user\", \"content\": \"Order number: 112217629\"},\n", + " {\n", + " \"role\": \"assistant\",\n", + " \"content\": \"The order is in warranty. A technician appointment is available tomorrow from 9 AM to noon.\",\n", + " },\n", + "]\n", + "\n", + "conversation_text = \"Customer service conversation:\\n\" + \"\\n\".join(\n", + " f\"{message['role']}: {message['content']}\" for message in conversation\n", + ")\n", + "\n", + "print(conversation_text)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Remember And Wait For Persistence\n", + "\n", + "`remember(...)` returns quickly with a job id. The relayer then encrypts, uploads to Walrus, and indexes in the background. Use `wait_for_remember_job(...)` when you need proof that the memory is persisted before continuing.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "accepted = memwal_call(lambda: memory.remember(conversation_text))\n", + "print(f\"remember accepted: job_id={accepted.job_id} status={accepted.status}\")\n", + "\n", + "stored = memwal_call(lambda: memory.wait_for_remember_job(accepted.job_id, timeout_ms=120_000))\n", + "print(\"remember job status=done\")\n", + "print(f\"stored: blob_id={stored.blob_id[:24]}... namespace={stored.namespace}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Recall The Support Context\n", + "\n", + "`recall(RecallParams(...))` performs semantic search inside your account and namespace. Lower distance means stronger similarity; relevance below is displayed as `1 - distance` for readability.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "question = \"What order number and service appointment did the customer already discuss?\"\n", + "relevant_memories = memwal_call(\n", + " lambda: memory.recall(\n", + " RecallParams(\n", + " query=question,\n", + " limit=5,\n", + " namespace=WALRUS_MEMORY_NAMESPACE,\n", + " max_distance=0.85,\n", + " )\n", + " )\n", + ")\n", + "\n", + "print(f\"recall returned {len(relevant_memories.results)} result(s)\")\n", + "for index, item in enumerate(relevant_memories.results, start=1):\n", + " relevance = 1 - item.distance\n", + " print(f\"\\nResult {index} relevance={relevance:.2f}\")\n", + " print(item.text[:900])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## One-Call Persistence With `remember_and_wait`\n", + "\n", + "Use `remember_and_wait(...)` when you do not need the accepted-job phase separately.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "preference_text = (\n", + " \"Customer service preference: The customer prefers SMS updates, morning appointments, \"\n", + " \"and warranty-first troubleshooting for TV repair requests.\"\n", + ")\n", + "\n", + "preference = memwal_call(\n", + " lambda: memory.remember_and_wait(preference_text, timeout_ms=120_000)\n", + ")\n", + "print(f\"preference stored: blob_id={preference.blob_id[:24]}... namespace={preference.namespace}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Bulk Remember\n", + "\n", + "Bulk APIs are useful when you import several small facts at once. The accepted result returns one job id per item; then you can poll all jobs together.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bulk_items = [\n", + " RememberBulkItem(text=\"Support fact: customer owns a Sony BRAVIA XR A80K TV.\"),\n", + " RememberBulkItem(text=\"Support fact: customer order number is 112217629.\"),\n", + " RememberBulkItem(text=\"Support fact: available appointment window is tomorrow from 9 AM to noon.\"),\n", + "]\n", + "\n", + "bulk = memwal_call(lambda: memory.remember_bulk(bulk_items))\n", + "print(f\"bulk accepted: total={bulk.total} status={bulk.status}\")\n", + "print(\"bulk job ids:\", \", \".join(job_id[:8] + \"...\" for job_id in bulk.job_ids))\n", + "\n", + "initial_status = memwal_call(lambda: memory.get_remember_bulk_status(bulk.job_ids))\n", + "print(\"initial statuses:\", [item.status for item in initial_status.results])\n", + "\n", + "bulk_done = memwal_call(\n", + " lambda: memory.wait_for_remember_jobs(\n", + " bulk.job_ids,\n", + " RememberBulkOptions(timeout_ms=120_000),\n", + " )\n", + ")\n", + "print(\n", + " f\"bulk settled: total={bulk_done.total} \"\n", + " f\"succeeded={bulk_done.succeeded} failed={bulk_done.failed} timed_out={bulk_done.timed_out}\"\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## One-Call Bulk Persistence\n", + "\n", + "`remember_bulk_and_wait(...)` is the convenience form when you want to submit a batch and wait for every item to settle in one call. The explicit accepted-job aliases `remember_async(...)` and `remember_bulk_async(...)` mirror the same accepted-job behavior used by `remember(...)` and `remember_bulk(...)` in this notebook.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "followup_bulk = [\n", + " RememberBulkItem(text=\"Support preference: customer wants SMS updates before technician arrival.\"),\n", + " RememberBulkItem(text=\"Support preference: customer prefers morning service appointments.\"),\n", + "]\n", + "\n", + "followup_done = memwal_call(\n", + " lambda: memory.remember_bulk_and_wait(\n", + " followup_bulk,\n", + " RememberBulkOptions(timeout_ms=120_000),\n", + " )\n", + ")\n", + "print(\n", + " f\"remember_bulk_and_wait settled: total={followup_done.total} \"\n", + " f\"succeeded={followup_done.succeeded} failed={followup_done.failed} timed_out={followup_done.timed_out}\"\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Ask From Memory\n", + "\n", + "`ask(...)` is the highest-level retrieval helper. It recalls relevant memory and returns an answer with the number of memories used. This is optional because some hosted environments may gate answer synthesis.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if RUN_OPTIONAL_SDK_METHODS:\n", + " try:\n", + " answer = memwal_call(lambda: memory.ask(\"When is the technician appointment?\", limit=5))\n", + " print(\"ask answer:\", answer.answer)\n", + " print(\"ask memories used:\", answer.memories_used)\n", + " except Exception as exc:\n", + " show_error(\"ask\", exc)\n", + "else:\n", + " print(\"ask skipped. Set RUN_OPTIONAL_SDK_METHODS=true to run optional SDK methods.\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Analyze Facts Without Waiting\n", + "\n", + "`analyze(...)` extracts memorable facts and returns accepted remember job IDs. Use it when you want to inspect or manage the jobs yourself.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if RUN_OPTIONAL_SDK_METHODS:\n", + " try:\n", + " accepted_analysis = memwal_call(\n", + " lambda: memory.analyze(\n", + " \"Customer prefers SMS updates, warranty-first troubleshooting, and morning appointments.\"\n", + " )\n", + " )\n", + " print(f\"analyze accepted facts={accepted_analysis.fact_count} status={accepted_analysis.status}\")\n", + " print(\"analyze job ids:\", \", \".join(job_id[:8] + \"...\" for job_id in accepted_analysis.job_ids))\n", + " for fact in accepted_analysis.facts[:3]:\n", + " print(\"-\", fact.text)\n", + " except Exception as exc:\n", + " show_error(\"analyze\", exc)\n", + "else:\n", + " print(\"analyze skipped.\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Analyze And Persist Facts\n", + "\n", + "`analyze_and_wait(...)` asks the relayer to extract memorable facts from text and waits for the resulting remember jobs to settle. It is useful when you want the SDK to decide which facts should become memory.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if RUN_OPTIONAL_SDK_METHODS:\n", + " try:\n", + " analyzed = memwal_call(\n", + " lambda: memory.analyze_and_wait(\n", + " \"Customer says they prefer SMS updates and morning support appointments.\",\n", + " opts=RememberBulkOptions(timeout_ms=120_000),\n", + " )\n", + " )\n", + " print(f\"analyze facts={len(analyzed.facts)} succeeded={analyzed.succeeded}\")\n", + " for fact in analyzed.facts[:3]:\n", + " print(\"-\", fact.text)\n", + " except Exception as exc:\n", + " show_error(\"analyze_and_wait\", exc)\n", + "else:\n", + " print(\"analyze_and_wait skipped.\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Embeddings And Manual Indexing\n", + "\n", + "`embed(...)`, `remember_manual(...)`, and `recall_manual(...)` expose the lower-level vector path. Most users should prefer `remember(...)` and `recall(...)`, but the manual methods are useful when you already have a Walrus blob ID and want to register or search by a supplied vector.\n", + "\n", + "This example also passes `ScoringWeights` to `recall_manual(...)`, which mirrors the same weighted-ranking option available in the TypeScript SDK.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "embedded_vector = None\n", + "\n", + "if RUN_OPTIONAL_SDK_METHODS:\n", + " try:\n", + " embedded = memwal_call(lambda: memory.embed(\"warranty repair appointment order number\"))\n", + " embedded_vector = embedded.vector\n", + " print(\"embedding dimensions:\", len(embedded_vector))\n", + " except Exception as exc:\n", + " show_error(\"embed\", exc)\n", + "\n", + " if embedded_vector:\n", + " try:\n", + " manual = memwal_call(\n", + " lambda: memory.remember_manual(\n", + " RememberManualOptions(\n", + " blob_id=stored.blob_id,\n", + " vector=embedded_vector,\n", + " namespace=WALRUS_MEMORY_NAMESPACE,\n", + " )\n", + " )\n", + " )\n", + " print(f\"manual register: blob_id={manual.blob_id[:24]}... namespace={manual.namespace}\")\n", + " except Exception as exc:\n", + " show_error(\"remember_manual\", exc)\n", + "\n", + " try:\n", + " manual_hits = memwal_call(\n", + " lambda: memory.recall_manual(\n", + " RecallManualOptions(\n", + " vector=embedded_vector,\n", + " limit=5,\n", + " namespace=WALRUS_MEMORY_NAMESPACE,\n", + " scoring_weights=ScoringWeights(\n", + " semantic=1.0,\n", + " recency=0.1,\n", + " recency_half_life_days=7,\n", + " importance=0.0,\n", + " ),\n", + " )\n", + " )\n", + " )\n", + " print(f\"manual recall hits: {manual_hits.total}\")\n", + " for hit in manual_hits.results[:3]:\n", + " print(f\"- blob_id={hit.blob_id[:24]}... distance={hit.distance:.3f}\")\n", + " except Exception as exc:\n", + " show_error(\"recall_manual\", exc)\n", + "else:\n", + " print(\"embed/manual indexing skipped.\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Restore Index Rows\n", + "\n", + "`restore(...)` asks the relayer to repair missing index rows from Walrus-backed memory for a namespace. It is safe to run with a small limit in a demo namespace.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if RUN_OPTIONAL_SDK_METHODS:\n", + " try:\n", + " restored = memwal_call(lambda: memory.restore(WALRUS_MEMORY_NAMESPACE, limit=10))\n", + " print(\n", + " f\"restore: restored={restored.restored} skipped={restored.skipped} \"\n", + " f\"total={restored.total} namespace={restored.namespace}\"\n", + " )\n", + " except Exception as exc:\n", + " show_error(\"restore\", exc)\n", + "else:\n", + " print(\"restore skipped.\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Optional OpenAI Middleware\n", + "\n", + "`with_memwal_openai(...)` wraps an OpenAI SDK client. Before each chat call, it recalls relevant Walrus Memory context and injects it into the message list. After the call, it can analyze and save the latest user message.\n", + "\n", + "This section is off by default because it installs an optional dependency, requires `OPENAI_API_KEY`, and sends the prompt plus recalled memory context to your configured model provider. For OpenAI-compatible providers such as OpenRouter, also set `OPENAI_BASE_URL` and the provider-specific `OPENAI_MODEL`. Set `RUN_MIDDLEWARE_DEMO=true` in Colab Secrets when you want to run it.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if RUN_MIDDLEWARE_DEMO:\n", + " import subprocess\n", + " import sys\n", + "\n", + " subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"-U\", \"memwal[openai]\", \"openai\"])\n", + "\n", + " from openai import AsyncOpenAI\n", + " from memwal import with_memwal_openai\n", + "\n", + " read_secret(\"OPENAI_API_KEY\", \"Paste OpenAI API key for middleware demo (hidden): \")\n", + "\n", + " openai_kwargs = {\"api_key\": os.environ[\"OPENAI_API_KEY\"]}\n", + " if OPENAI_BASE_URL:\n", + " openai_kwargs[\"base_url\"] = OPENAI_BASE_URL\n", + " openai_client = AsyncOpenAI(**openai_kwargs)\n", + " smart_openai = with_memwal_openai(\n", + " openai_client,\n", + " key=os.environ[\"MEMWAL_PRIVATE_KEY\"],\n", + " account_id=os.environ[\"MEMWAL_ACCOUNT_ID\"],\n", + " namespace=WALRUS_MEMORY_NAMESPACE,\n", + " env=WALRUS_MEMORY_ENV,\n", + " max_memories=5,\n", + " min_relevance=0.2,\n", + " )\n", + "\n", + " response = await smart_openai.chat.completions.create(\n", + " model=OPENAI_MODEL,\n", + " messages=[\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": \"Use my support memory: what TV issue and appointment window should the agent remember?\",\n", + " }\n", + " ],\n", + " max_tokens=160,\n", + " )\n", + " print(response.choices[0].message.content)\n", + "else:\n", + " print(\"OpenAI middleware demo skipped. Set RUN_MIDDLEWARE_DEMO=true to run it.\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Optional LangChain Middleware\n", + "\n", + "`with_memwal_langchain(...)` wraps a LangChain chat model with the same recall-before-call and save-after-call behavior. This is the Python SDK equivalent of adding memory to an existing agent or assistant stack without changing your chain logic.\n", + "\n", + "Use the same `OPENAI_BASE_URL` and `OPENAI_MODEL` settings as the OpenAI middleware cell when running through an OpenAI-compatible provider.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if RUN_MIDDLEWARE_DEMO:\n", + " import subprocess\n", + " import sys\n", + "\n", + " subprocess.check_call([\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"-U\",\n", + " \"memwal[langchain]\",\n", + " \"langchain-openai\",\n", + " ])\n", + "\n", + " from langchain_core.messages import HumanMessage\n", + " from langchain_openai import ChatOpenAI\n", + " from memwal import with_memwal_langchain\n", + "\n", + " read_secret(\"OPENAI_API_KEY\", \"Paste OpenAI API key for LangChain demo (hidden): \")\n", + "\n", + " llm_kwargs = {\"model\": OPENAI_MODEL, \"api_key\": os.environ[\"OPENAI_API_KEY\"]}\n", + " if OPENAI_BASE_URL:\n", + " llm_kwargs[\"base_url\"] = OPENAI_BASE_URL\n", + " llm = ChatOpenAI(**llm_kwargs)\n", + " smart_llm = with_memwal_langchain(\n", + " llm,\n", + " key=os.environ[\"MEMWAL_PRIVATE_KEY\"],\n", + " account_id=os.environ[\"MEMWAL_ACCOUNT_ID\"],\n", + " namespace=WALRUS_MEMORY_NAMESPACE,\n", + " env=WALRUS_MEMORY_ENV,\n", + " max_memories=5,\n", + " min_relevance=0.2,\n", + " )\n", + "\n", + " response = await smart_llm.ainvoke([\n", + " HumanMessage(content=\"Summarize the customer's support context in one sentence.\")\n", + " ])\n", + " print(response.content)\n", + "else:\n", + " print(\"LangChain middleware demo skipped. Set RUN_MIDDLEWARE_DEMO=true to run it.\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Basic Troubleshooting\n", + "\n", + "- `401` or auth rejected: confirm the delegate private key belongs to `MEMWAL_ACCOUNT_ID`, and that both were created for the selected `staging` or `prod` environment.\n", + "- Invalid environment: this notebook only accepts `staging` and `prod`.\n", + "- Empty recall: wait for the remember job to finish, query the same namespace, and broaden the query or raise `max_distance`.\n", + "- Remember timeout: rerun `wait_for_remember_job(job_id, timeout_ms=...)` with a larger timeout.\n", + "- `Event loop is closed`: upgrade `memwal` and rerun the client cell. This notebook also resets the underlying HTTP client before each sync call for older releases.\n", + "- Optional SDK method skipped: the relayer may not have that feature enabled, or the current credentials may not have access.\n", + "- Middleware skipped: set `RUN_MIDDLEWARE_DEMO=true` and provide `OPENAI_API_KEY` through Colab Secrets or a hidden prompt.\n", + "- Secret safety: never print, paste into source cells, commit, or publish `MEMWAL_PRIVATE_KEY`. Rotate a delegate key if it was pasted into chat, logs, or a shared notebook output.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Cleanup\n", + "\n", + "Close the underlying HTTP client when you are done.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "memwal_call(memory.close)\n" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/packages/python-sdk-memwal/tests/test_client.py b/packages/python-sdk-memwal/tests/test_client.py index 3d7da352..9cdbe118 100644 --- a/packages/python-sdk-memwal/tests/test_client.py +++ b/packages/python-sdk-memwal/tests/test_client.py @@ -16,7 +16,7 @@ import pytest import respx -from memwal.client import MemWal, MemWalCompatibilityError, MemWalError +from memwal.client import MemWal, MemWalCompatibilityError, MemWalError, MemWalSync from memwal.types import ( RecallManualOptions, RecallParams, @@ -108,6 +108,30 @@ def memwal_client() -> MemWal: ) +# ============================================================ +# sync wrapper tests +# ============================================================ + + +class _SyncRunInner: + def __init__(self) -> None: + self._client = object() + + +class TestMemWalSyncRun: + async def test_resets_http_client_when_called_inside_running_loop(self) -> None: + inner = _SyncRunInner() + sync = MemWalSync(inner) # type: ignore[arg-type] + + async def operation() -> str: + return "ok" + + result = sync._run(operation()) + + assert result == "ok" + assert inner._client is None + + # ============================================================ # remember() tests # ============================================================ diff --git a/packages/python-sdk-memwal/tests/test_integration.py b/packages/python-sdk-memwal/tests/test_integration.py index 497c5105..44d93fb4 100644 --- a/packages/python-sdk-memwal/tests/test_integration.py +++ b/packages/python-sdk-memwal/tests/test_integration.py @@ -2,7 +2,7 @@ """ Integration tests for the Walrus Memory Python SDK against a live server. -Targets MEMWAL_SERVER_URL (default: https://relayer.dev.memwal.ai). +Targets MEMWAL_SERVER_URL (default: https://relayer.staging.memwal.ai). No-auth tests (always run, no env vars needed): - /health endpoint @@ -28,10 +28,10 @@ # Run full suite with real credentials MEMWAL_PRIVATE_KEY= MEMWAL_ACCOUNT_ID=0x... python -m pytest tests/test_integration.py -v - # Run against dev server using env vars - export MEMWAL_PRIVATE_KEY="944aa24c09d8b6d6cc6a8fbedc6dc0942a46e49db7d36596e1b6af6061ec9261" - export MEMWAL_ACCOUNT_ID="0x70f9a6ff2df0ef6a9ecbfdc3f44c27c289ec3eb0cab5e10a5c07ca6165528565" - export MEMWAL_SERVER_URL="https://relayer.dev.memwal.ai" + # Run against staging using env vars + export MEMWAL_PRIVATE_KEY="" + export MEMWAL_ACCOUNT_ID="0x-your-walrus-memory-account-id" + export MEMWAL_SERVER_URL="https://relayer.staging.memwal.ai" python -m pytest tests/test_integration.py -v """ @@ -52,7 +52,7 @@ # ── Config ─────────────────────────────────────────────────────────────────── -SERVER_URL = os.environ.get("MEMWAL_SERVER_URL", "https://relayer.dev.memwal.ai") +SERVER_URL = os.environ.get("MEMWAL_SERVER_URL", "https://relayer.staging.memwal.ai") PRIVATE_KEY_HEX = os.environ.get("MEMWAL_PRIVATE_KEY", "") ACCOUNT_ID = os.environ.get("MEMWAL_ACCOUNT_ID", "") diff --git a/packages/python-sdk-memwal/tests/test_middleware.py b/packages/python-sdk-memwal/tests/test_middleware.py index 913677ad..6ff8bdaa 100644 --- a/packages/python-sdk-memwal/tests/test_middleware.py +++ b/packages/python-sdk-memwal/tests/test_middleware.py @@ -602,3 +602,35 @@ def test_sync_client_wraps_create(self) -> None: # The create method should now be a different callable (patched) assert client.chat.completions.create is not original_create + + @respx.mock + async def test_sync_client_injects_memory_inside_running_loop(self) -> None: + """Sync OpenAI client still injects memory from notebook-style event loops.""" + _mock_seal_session_prereqs() + client = self._make_sync_client() + captured: list = [] + + def _capture(*args, **kwargs): + msgs = kwargs.get("messages") or (args[0] if args else []) + captured.extend(msgs) + return MagicMock() + + client.chat.completions.create = _capture + + respx.post(_RECALL_URL).mock( + return_value=_mock_recall([ + {"blob_id": "b1", "text": "TV support appointment is 9 AM to noon", "distance": 0.05} + ]) + ) + + smart = with_memwal_openai( + client, key=_KEY_HEX, account_id=_ACCOUNT_ID, server_url=_SERVER, auto_save=False + ) + smart.chat.completions.create( + model="gpt-4o", + messages=[{"role": "user", "content": "When is my TV support appointment?"}], + ) + + system_msgs = [m for m in captured if isinstance(m, dict) and m.get("role") == "system"] + assert len(system_msgs) == 1 + assert "TV support appointment is 9 AM to noon" in system_msgs[0]["content"] diff --git a/packages/python-sdk-memwal/tests/test_signing.py b/packages/python-sdk-memwal/tests/test_signing.py index 26720988..1bd2eef1 100644 --- a/packages/python-sdk-memwal/tests/test_signing.py +++ b/packages/python-sdk-memwal/tests/test_signing.py @@ -206,9 +206,9 @@ def test_body_sha256_matches_json(self) -> None: class TestDelegateKeyUtils: """Tests for delegate_key_to_sui_address and delegate_key_to_public_key.""" - # Known test key used throughout this session - _KEY = "944aa24c09d8b6d6cc6a8fbedc6dc0942a46e49db7d36596e1b6af6061ec9261" - _EXPECTED_PUB = "d5b76c57ad1b78438ab9df984b65aa6e2692045a9f3ba642773edd46f3b987b9" + # Deterministic dummy key; never use a live delegate key in fixtures. + _KEY = "aa" * 32 + _EXPECTED_PUB = "e734ea6c2b6257de72355e472aa05a4c487e6b463c029ed306df2f01b5636b58" def test_public_key_matches_known(self): from memwal.utils import delegate_key_to_public_key From 6d49886236de3f8696c569868608a67a3cfc25fe Mon Sep 17 00:00:00 2001 From: Harry Phan Date: Sun, 31 May 2026 18:12:03 +0700 Subject: [PATCH 2/5] Expand Colab SDK method coverage --- docs/python-sdk/changelog.mdx | 2 +- docs/python-sdk/colab.md | 2 +- docs/python-sdk/quick-start.md | 2 +- packages/python-sdk-memwal/CHANGELOG.md | 2 +- packages/python-sdk-memwal/README.md | 2 +- .../notebooks/walrus_memory_python_sdk.ipynb | 38 +++++++++++++++++-- packages/python-sdk-memwal/pyproject.toml | 3 ++ .../tests/test_integration.py | 2 + 8 files changed, 44 insertions(+), 9 deletions(-) diff --git a/docs/python-sdk/changelog.mdx b/docs/python-sdk/changelog.mdx index 44e13891..51a29247 100644 --- a/docs/python-sdk/changelog.mdx +++ b/docs/python-sdk/changelog.mdx @@ -11,7 +11,7 @@ For the latest version, see the [PyPI project page](https://pypi.org/project/mem ### Added -- Added a runnable [Walrus Memory Python SDK Colab](https://colab.research.google.com/drive/1SaKjkSp0DXnM_nktWSiEC-l9qGtVr6ph) covering installation, secure `staging` configuration, optional `prod`, `MemWalSync`, health/compatibility checks, delegate public-key/address derivation, `remember`, async job waiting, `recall`, bulk remember, `remember_bulk_and_wait`, optional `ask`, `analyze`, `analyze_and_wait`, `embed`, manual methods with scoring weights, `restore`, optional OpenAI/LangChain middleware, OpenAI-compatible provider settings such as `OPENAI_BASE_URL`, and troubleshooting. +- Added a runnable [Walrus Memory Python SDK Colab](https://colab.research.google.com/drive/1SaKjkSp0DXnM_nktWSiEC-l9qGtVr6ph) covering installation, secure `staging` configuration, optional `prod`, `MemWalSync`, health/compatibility checks, delegate public-key/address derivation, `remember`, `remember_async`, async job waiting, `recall`, bulk remember, `remember_bulk_async`, `remember_bulk_and_wait`, optional `ask`, `analyze`, `analyze_and_wait`, `embed`, manual methods with scoring weights, `restore`, optional OpenAI/LangChain middleware, OpenAI-compatible provider settings such as `OPENAI_BASE_URL`, and troubleshooting. ### Fixed diff --git a/docs/python-sdk/colab.md b/docs/python-sdk/colab.md index cb193f42..0cc1bd98 100644 --- a/docs/python-sdk/colab.md +++ b/docs/python-sdk/colab.md @@ -16,7 +16,7 @@ The notebook covers: - storing memory with `remember` - waiting for async remember jobs to persist on Walrus - retrieving memory with `recall` -- using `remember_and_wait`, bulk remember, `remember_bulk_and_wait`, `ask`, `analyze`, `analyze_and_wait`, `embed`, manual search/register with scoring weights, and `restore` +- using `remember_async`, `remember_and_wait`, bulk remember, `remember_bulk_async`, `remember_bulk_and_wait`, `ask`, `analyze`, `analyze_and_wait`, `embed`, manual search/register with scoring weights, and `restore` - optionally wrapping OpenAI and LangChain clients with Walrus Memory middleware - using `OPENAI_BASE_URL` for OpenAI-compatible providers such as OpenRouter - basic troubleshooting for auth, namespaces, and async remember jobs diff --git a/docs/python-sdk/quick-start.md b/docs/python-sdk/quick-start.md index 5af9b7bb..59f04e2e 100644 --- a/docs/python-sdk/quick-start.md +++ b/docs/python-sdk/quick-start.md @@ -19,7 +19,7 @@ pip install memwal ## Try It In Colab -Open the runnable [Walrus Memory Python SDK Colab](https://colab.research.google.com/drive/1SaKjkSp0DXnM_nktWSiEC-l9qGtVr6ph) for a notebook walkthrough covering installation, secure configuration, health checks, `remember`, async job waiting, `recall`, bulk remember, `remember_bulk_and_wait`, optional SDK utilities, OpenAI/LangChain middleware, OpenAI-compatible provider settings such as `OPENAI_BASE_URL`, and basic troubleshooting. It defaults to `staging` for test credentials and can switch to `prod` for production credentials. +Open the runnable [Walrus Memory Python SDK Colab](https://colab.research.google.com/drive/1SaKjkSp0DXnM_nktWSiEC-l9qGtVr6ph) for a notebook walkthrough covering installation, secure configuration, health checks, `remember`, `remember_async`, async job waiting, `recall`, bulk remember, `remember_bulk_async`, `remember_bulk_and_wait`, optional SDK utilities, OpenAI/LangChain middleware, OpenAI-compatible provider settings such as `OPENAI_BASE_URL`, and basic troubleshooting. It defaults to `staging` for test credentials and can switch to `prod` for production credentials. Optional integrations: diff --git a/packages/python-sdk-memwal/CHANGELOG.md b/packages/python-sdk-memwal/CHANGELOG.md index 9ccab81d..2666a65e 100644 --- a/packages/python-sdk-memwal/CHANGELOG.md +++ b/packages/python-sdk-memwal/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- Added a runnable [Walrus Memory Python SDK Colab](https://colab.research.google.com/drive/1SaKjkSp0DXnM_nktWSiEC-l9qGtVr6ph) covering installation, secure `staging` configuration, optional `prod`, health checks, async `remember` job waiting, `recall`, bulk remember, manual methods with scoring weights, middleware examples, OpenAI-compatible provider settings such as `OPENAI_BASE_URL`, and troubleshooting. +- Added a runnable [Walrus Memory Python SDK Colab](https://colab.research.google.com/drive/1SaKjkSp0DXnM_nktWSiEC-l9qGtVr6ph) covering installation, secure `staging` configuration, optional `prod`, health checks, async `remember` and `remember_async` job waiting, `recall`, bulk remember, `remember_bulk_async`, manual methods with scoring weights, middleware examples, OpenAI-compatible provider settings such as `OPENAI_BASE_URL`, and troubleshooting. ## 0.1.3 diff --git a/packages/python-sdk-memwal/README.md b/packages/python-sdk-memwal/README.md index 12fdc508..f74f29f9 100644 --- a/packages/python-sdk-memwal/README.md +++ b/packages/python-sdk-memwal/README.md @@ -20,7 +20,7 @@ pip install memwal[all] # Everything ## Try It In Colab -Open the runnable [Walrus Memory Python SDK Colab](https://colab.research.google.com/drive/1SaKjkSp0DXnM_nktWSiEC-l9qGtVr6ph) for a notebook walkthrough covering installation, secure `staging` configuration, optional `prod`, health checks, `remember`, async job waiting, `recall`, bulk remember, `remember_bulk_and_wait`, optional SDK utilities, OpenAI/LangChain middleware, OpenAI-compatible provider settings such as `OPENAI_BASE_URL`, and troubleshooting. +Open the runnable [Walrus Memory Python SDK Colab](https://colab.research.google.com/drive/1SaKjkSp0DXnM_nktWSiEC-l9qGtVr6ph) for a notebook walkthrough covering installation, secure `staging` configuration, optional `prod`, health checks, `remember`, `remember_async`, async job waiting, `recall`, bulk remember, `remember_bulk_async`, `remember_bulk_and_wait`, optional SDK utilities, OpenAI/LangChain middleware, OpenAI-compatible provider settings such as `OPENAI_BASE_URL`, and troubleshooting. ## Quick Start diff --git a/packages/python-sdk-memwal/notebooks/walrus_memory_python_sdk.ipynb b/packages/python-sdk-memwal/notebooks/walrus_memory_python_sdk.ipynb index 5e648562..fee92f5b 100644 --- a/packages/python-sdk-memwal/notebooks/walrus_memory_python_sdk.ipynb +++ b/packages/python-sdk-memwal/notebooks/walrus_memory_python_sdk.ipynb @@ -25,7 +25,7 @@ "- `remember(...)` plus `wait_for_remember_job(...)`\n", "- `recall(RecallParams(...))`\n", "- `remember_and_wait(...)`\n", - "- bulk remember helpers\n", + "- bulk remember helpers, including explicit async aliases\n", "- optional `ask(...)`, `analyze(...)`, `analyze_and_wait(...)`, `embed(...)`, manual recall/register with scoring weights, and `restore(...)`\n", "- optional OpenAI and LangChain middleware examples\n", "- cleanup and troubleshooting\n", @@ -271,7 +271,7 @@ "source": [ "## Remember And Wait For Persistence\n", "\n", - "`remember(...)` returns quickly with a job id. The relayer then encrypts, uploads to Walrus, and indexes in the background. Use `wait_for_remember_job(...)` when you need proof that the memory is persisted before continuing.\n" + "`remember(...)` returns quickly with a job id. The relayer then encrypts, uploads to Walrus, and indexes in the background. Use `wait_for_remember_job(...)` when you need proof that the memory is persisted before continuing. The cell also calls `remember_async(...)`, the explicit accepted-job alias for the same pattern.\n" ] }, { @@ -285,7 +285,17 @@ "\n", "stored = memwal_call(lambda: memory.wait_for_remember_job(accepted.job_id, timeout_ms=120_000))\n", "print(\"remember job status=done\")\n", - "print(f\"stored: blob_id={stored.blob_id[:24]}... namespace={stored.namespace}\")\n" + "print(f\"stored: blob_id={stored.blob_id[:24]}... namespace={stored.namespace}\")\n", + "\n", + "alias_accepted = memwal_call(\n", + " lambda: memory.remember_async(\"Support fact: customer prefers warranty-first troubleshooting.\")\n", + ")\n", + "print(f\"remember_async alias accepted: job_id={alias_accepted.job_id[:8]}... status={alias_accepted.status}\")\n", + "\n", + "alias_stored = memwal_call(\n", + " lambda: memory.wait_for_remember_job(alias_accepted.job_id, timeout_ms=120_000)\n", + ")\n", + "print(f\"remember_async alias stored: blob_id={alias_stored.blob_id[:24]}... namespace={alias_stored.namespace}\")\n" ] }, { @@ -354,7 +364,7 @@ "source": [ "## Bulk Remember\n", "\n", - "Bulk APIs are useful when you import several small facts at once. The accepted result returns one job id per item; then you can poll all jobs together.\n" + "Bulk APIs are useful when you import several small facts at once. The accepted result returns one job id per item; then you can poll all jobs together. The cell also calls `remember_bulk_async(...)`, the explicit accepted-job alias.\n" ] }, { @@ -385,6 +395,26 @@ "print(\n", " f\"bulk settled: total={bulk_done.total} \"\n", " f\"succeeded={bulk_done.succeeded} failed={bulk_done.failed} timed_out={bulk_done.timed_out}\"\n", + ")\n", + "\n", + "alias_bulk = memwal_call(\n", + " lambda: memory.remember_bulk_async([\n", + " RememberBulkItem(text=\"Support alias fact: customer wants warranty-first troubleshooting.\"),\n", + " RememberBulkItem(text=\"Support alias fact: customer wants morning repair windows.\"),\n", + " ])\n", + ")\n", + "print(f\"remember_bulk_async alias accepted: total={alias_bulk.total} status={alias_bulk.status}\")\n", + "print(\"remember_bulk_async alias job ids:\", \", \".join(job_id[:8] + \"...\" for job_id in alias_bulk.job_ids))\n", + "\n", + "alias_bulk_done = memwal_call(\n", + " lambda: memory.wait_for_remember_jobs(\n", + " alias_bulk.job_ids,\n", + " RememberBulkOptions(timeout_ms=120_000),\n", + " )\n", + ")\n", + "print(\n", + " f\"remember_bulk_async alias settled: total={alias_bulk_done.total} \"\n", + " f\"succeeded={alias_bulk_done.succeeded} failed={alias_bulk_done.failed} timed_out={alias_bulk_done.timed_out}\"\n", ")\n" ] }, diff --git a/packages/python-sdk-memwal/pyproject.toml b/packages/python-sdk-memwal/pyproject.toml index df665846..549d0ff6 100644 --- a/packages/python-sdk-memwal/pyproject.toml +++ b/packages/python-sdk-memwal/pyproject.toml @@ -54,6 +54,9 @@ packages = ["memwal"] [tool.pytest.ini_options] asyncio_mode = "auto" testpaths = ["tests"] +markers = [ + "integration: live relayer integration tests", +] [tool.ruff] target-version = "py39" diff --git a/packages/python-sdk-memwal/tests/test_integration.py b/packages/python-sdk-memwal/tests/test_integration.py index 44d93fb4..ffa43471 100644 --- a/packages/python-sdk-memwal/tests/test_integration.py +++ b/packages/python-sdk-memwal/tests/test_integration.py @@ -50,6 +50,8 @@ from memwal.client import MemWal, MemWalCompatibilityError, MemWalError, MemWalSync from memwal.utils import build_signature_message, bytes_to_hex +pytestmark = pytest.mark.integration + # ── Config ─────────────────────────────────────────────────────────────────── SERVER_URL = os.environ.get("MEMWAL_SERVER_URL", "https://relayer.staging.memwal.ai") From 40e70139a616ac077ba9bff0b309bb6b63ffa36b Mon Sep 17 00:00:00 2001 From: Harry Phan Date: Sun, 31 May 2026 18:37:57 +0700 Subject: [PATCH 3/5] Restrict public SDK env docs to staging and prod --- docs/python-sdk/api-reference.md | 2 +- docs/python-sdk/quick-start.md | 6 ++---- docs/python-sdk/usage/with-memwal.md | 2 +- packages/python-sdk-memwal/README.md | 4 +--- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/python-sdk/api-reference.md b/docs/python-sdk/api-reference.md index 88e31784..8fcf5558 100644 --- a/docs/python-sdk/api-reference.md +++ b/docs/python-sdk/api-reference.md @@ -28,7 +28,7 @@ MemWal.create( | `account_id` | `str` | Yes | — | Walrus Memory account object ID on Sui | | `server_url` | `str` | No | `http://localhost:8000` | Explicit relayer URL — wins over `env` | | `namespace` | `str` | No | `"default"` | Default namespace for memory isolation | -| `env` | `str` | No | — | Preset: `prod` / `dev` / `staging` / `local`. Unknown → `ValueError` | +| `env` | `str` | No | — | Hosted preset: `staging` for testing or `prod` for production. Unknown → `ValueError` | You may also build a `MemWalConfig` and call `MemWal(config)` directly; `env` resolution happens in `MemWalConfig.__post_init__`. diff --git a/docs/python-sdk/quick-start.md b/docs/python-sdk/quick-start.md index 59f04e2e..cf77d3ee 100644 --- a/docs/python-sdk/quick-start.md +++ b/docs/python-sdk/quick-start.md @@ -60,18 +60,16 @@ Before wiring the SDK into your app: | `account_id` | `str` | Yes | — | Walrus Memory account object ID on Sui | | `server_url` | `str` | No | `http://localhost:8000` | Explicit relayer URL — wins over `env` | | `namespace` | `str` | No | `"default"` | Default namespace for memory isolation | -| `env` | `str` | No | — | Relayer preset: `prod` / `dev` / `staging` / `local` | +| `env` | `str` | No | — | Hosted relayer preset: `staging` for testing or `prod` for production | ### Environment presets -Instead of hardcoding a URL, pass `env`. Same shorthand as the TypeScript SDK and MCP package. +Instead of hardcoding a URL, pass `env`. The public docs and Colab example use `staging` for testing and `prod` for production credentials. | `env` | Relayer URL | | --- | --- | | `prod` | `https://relayer.memwal.ai` | -| `dev` | `https://relayer.dev.memwal.ai` | | `staging` | `https://relayer.staging.memwal.ai` | -| `local` | `http://127.0.0.1:8000` | Precedence: an explicit non-default `server_url` > `env` > the default. An unknown preset raises `ValueError`. diff --git a/docs/python-sdk/usage/with-memwal.md b/docs/python-sdk/usage/with-memwal.md index 03ebe302..04b5727c 100644 --- a/docs/python-sdk/usage/with-memwal.md +++ b/docs/python-sdk/usage/with-memwal.md @@ -89,7 +89,7 @@ Both wrappers accept the same keyword arguments: | Option | Default | Description | | --- | --- | --- | | `server_url` | `http://localhost:8000` | Explicit relayer URL (wins over `env`) | -| `env` | — | Relayer preset: `prod` / `dev` / `staging` / `local` | +| `env` | — | Hosted relayer preset: `staging` for testing or `prod` for production | | `namespace` | `"default"` | Memory namespace | | `max_memories` | `5` | Max memories injected per request | | `auto_save` | `True` | Auto-save new facts from the conversation | diff --git a/packages/python-sdk-memwal/README.md b/packages/python-sdk-memwal/README.md index f74f29f9..1ee146ea 100644 --- a/packages/python-sdk-memwal/README.md +++ b/packages/python-sdk-memwal/README.md @@ -109,16 +109,14 @@ from memwal import MemWal memwal = MemWal.create( key=os.environ["MEMWAL_PRIVATE_KEY"], account_id=os.environ["MEMWAL_ACCOUNT_ID"], - env="prod", # prod | dev | staging | local + env="staging", # staging for testing, prod for production ) ``` | `env` | Relayer URL | |-------|-------------| | `prod` | `https://relayer.memwal.ai` | -| `dev` | `https://relayer.dev.memwal.ai` | | `staging` | `https://relayer.staging.memwal.ai` | -| `local` | `http://127.0.0.1:8000` | Precedence: an explicit non-default **`server_url` wins over `env`**, which wins over the default. An unknown preset raises `ValueError`. `env` is also accepted From 333d3279f59c2a033225bc99238b7586474333fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20Ph=E1=BA=A1m=20Minh=20H=C3=B9ng?= <46132442+hungtranphamminh@users.noreply.github.com> Date: Tue, 2 Jun 2026 17:07:14 +0700 Subject: [PATCH 4/5] chore(changeset): drop ticket ID from changeset filenames (#218) Rename to descriptive, ticket-free names per repo convention. Pure rename; frontmatter unchanged, both packages bump as before. --- .../{walm-55-occurred-at-openclaw.md => occurred-at-openclaw.md} | 0 .changeset/{walm-55-occurred-at-sdk.md => occurred-at-sdk.md} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename .changeset/{walm-55-occurred-at-openclaw.md => occurred-at-openclaw.md} (100%) rename .changeset/{walm-55-occurred-at-sdk.md => occurred-at-sdk.md} (100%) diff --git a/.changeset/walm-55-occurred-at-openclaw.md b/.changeset/occurred-at-openclaw.md similarity index 100% rename from .changeset/walm-55-occurred-at-openclaw.md rename to .changeset/occurred-at-openclaw.md diff --git a/.changeset/walm-55-occurred-at-sdk.md b/.changeset/occurred-at-sdk.md similarity index 100% rename from .changeset/walm-55-occurred-at-sdk.md rename to .changeset/occurred-at-sdk.md From 8758a3867278f85a4be394a6565b74c6d0703f2f Mon Sep 17 00:00:00 2001 From: hungtranphamminh Date: Tue, 2 Jun 2026 17:18:26 +0700 Subject: [PATCH 5/5] RELEASING: Releasing 2 package(s) Releases: @mysten-incubation/oc-memwal@0.0.4 @mysten-incubation/memwal@0.0.7 [skip ci] --- .changeset/occurred-at-openclaw.md | 14 -------------- .changeset/occurred-at-sdk.md | 11 ----------- packages/openclaw-memory-memwal/CHANGELOG.md | 18 ++++++++++++++++++ packages/openclaw-memory-memwal/package.json | 2 +- packages/sdk/CHANGELOG.md | 12 ++++++++++++ packages/sdk/package.json | 2 +- 6 files changed, 32 insertions(+), 27 deletions(-) delete mode 100644 .changeset/occurred-at-openclaw.md delete mode 100644 .changeset/occurred-at-sdk.md diff --git a/.changeset/occurred-at-openclaw.md b/.changeset/occurred-at-openclaw.md deleted file mode 100644 index e98519b1..00000000 --- a/.changeset/occurred-at-openclaw.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@mysten-incubation/oc-memwal": patch ---- - -Wire temporal anchoring through the agent-side memory tools. - -### Added - -- `memory_store` tool now accepts an optional `occurredAt` argument (RFC-3339 / ISO-8601 string) so agents can anchor recounted past events to the date they actually occurred. Description tells the LLM to omit it when unknown rather than guess. - -### Changed - -- Auto-capture hook (`agent_end`) now passes `new Date()` as `occurredAt` to `analyze()`. Every captured conversation now gets temporal anchoring automatically — the server extractor resolves in-turn relative references ("yesterday", "last Friday") into absolute dates inside the stored fact text. Facts captured by this version now carry resolved dates. -- SDK dependency bumped from published `^0.0.2` to `workspace:*` to consume the new `AnalyzeOptions` signature. diff --git a/.changeset/occurred-at-sdk.md b/.changeset/occurred-at-sdk.md deleted file mode 100644 index 4c034cbf..00000000 --- a/.changeset/occurred-at-sdk.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@mysten-incubation/memwal": patch ---- - -Add optional `occurredAt` to `analyze()` and `analyzeAndWait()` for temporal anchoring of extracted facts. - -- New `AnalyzeOptions` overload: `analyze(text, { namespace, occurredAt })` accepts `Date` or RFC-3339 string. The legacy `analyze(text, namespace?)` signature still works unchanged. -- When `occurredAt` is supplied, the server resolves in-turn relative references ("last Friday", "yesterday") into absolute dates inside the extracted fact text before embedding and encryption. -- Wire format is RFC-3339 UTC with millisecond precision (e.g. `"2023-05-25T17:50:00.000Z"`). -- Invalid `Date` instances (constructed from malformed input) now throw a diagnostic `TypeError` from the SDK rather than an opaque `RangeError` from `toISOString()`. -- Field is omitted from the request body when not supplied — existing callers see byte-identical wire payloads. diff --git a/packages/openclaw-memory-memwal/CHANGELOG.md b/packages/openclaw-memory-memwal/CHANGELOG.md index bd0904d7..ec076fd6 100644 --- a/packages/openclaw-memory-memwal/CHANGELOG.md +++ b/packages/openclaw-memory-memwal/CHANGELOG.md @@ -1,5 +1,23 @@ # @mysten-incubation/oc-memwal +## 0.0.4 + +### Patch Changes + +- [#218](https://github.com/MystenLabs/MemWal/pull/218) [`333d327`](https://github.com/MystenLabs/MemWal/commit/333d3279f59c2a033225bc99238b7586474333fb) Thanks [@hungtranphamminh](https://github.com/hungtranphamminh)! - Wire temporal anchoring through the agent-side memory tools. + + ### Added + + - `memory_store` tool now accepts an optional `occurredAt` argument (RFC-3339 / ISO-8601 string) so agents can anchor recounted past events to the date they actually occurred. Description tells the LLM to omit it when unknown rather than guess. + + ### Changed + + - Auto-capture hook (`agent_end`) now passes `new Date()` as `occurredAt` to `analyze()`. Every captured conversation now gets temporal anchoring automatically — the server extractor resolves in-turn relative references ("yesterday", "last Friday") into absolute dates inside the stored fact text. Facts captured by this version now carry resolved dates. + - SDK dependency bumped from published `^0.0.2` to `workspace:*` to consume the new `AnalyzeOptions` signature. + +- Updated dependencies [[`333d327`](https://github.com/MystenLabs/MemWal/commit/333d3279f59c2a033225bc99238b7586474333fb)]: + - @mysten-incubation/memwal@0.0.7 + ## 0.0.3 ### Patch Changes diff --git a/packages/openclaw-memory-memwal/package.json b/packages/openclaw-memory-memwal/package.json index 59afae72..3d88dae1 100644 --- a/packages/openclaw-memory-memwal/package.json +++ b/packages/openclaw-memory-memwal/package.json @@ -1,6 +1,6 @@ { "name": "@mysten-incubation/oc-memwal", - "version": "0.0.3", + "version": "0.0.4", "type": "module", "description": "NemoClaw/OpenClaw memory plugin — encrypted, decentralized long-term memory via Walrus Memory", "license": "Apache-2.0", diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index b79ee866..0f082a2d 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -1,5 +1,17 @@ # @mysten-incubation/memwal +## 0.0.7 + +### Patch Changes + +- [#218](https://github.com/MystenLabs/MemWal/pull/218) [`333d327`](https://github.com/MystenLabs/MemWal/commit/333d3279f59c2a033225bc99238b7586474333fb) Thanks [@hungtranphamminh](https://github.com/hungtranphamminh)! - Add optional `occurredAt` to `analyze()` and `analyzeAndWait()` for temporal anchoring of extracted facts. + + - New `AnalyzeOptions` overload: `analyze(text, { namespace, occurredAt })` accepts `Date` or RFC-3339 string. The legacy `analyze(text, namespace?)` signature still works unchanged. + - When `occurredAt` is supplied, the server resolves in-turn relative references ("last Friday", "yesterday") into absolute dates inside the extracted fact text before embedding and encryption. + - Wire format is RFC-3339 UTC with millisecond precision (e.g. `"2023-05-25T17:50:00.000Z"`). + - Invalid `Date` instances (constructed from malformed input) now throw a diagnostic `TypeError` from the SDK rather than an opaque `RangeError` from `toISOString()`. + - Field is omitted from the request body when not supplied — existing callers see byte-identical wire payloads. + ## 0.0.6 ### Added diff --git a/packages/sdk/package.json b/packages/sdk/package.json index fb008c98..beb441c9 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@mysten-incubation/memwal", - "version": "0.0.6", + "version": "0.0.7", "description": "Walrus Memory — Privacy-first AI memory SDK with Ed25519 delegate key auth", "type": "module", "main": "./dist/index.js",