Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fb96e9518 | ||
|
|
6aa0c27212 | ||
|
|
b311527269 | ||
|
|
3df9a491e5 | ||
|
|
4cdd90cec1 | ||
|
|
0070e08e01 | ||
|
|
39bc023305 | ||
|
|
b1342a3408 | ||
|
|
b717e38785 |
+4
-4
@@ -21,10 +21,10 @@ Package workflows keep their own push-to-main and manual triggers. Their `pull_r
|
||||
| Mem0 Plugin | `mem0-plugin-checks.yml` | Push to main (`integrations/mem0-plugin/`, excluding `.opencode-plugin/`), manual | pytest + hook exec bits + JSON manifest validation on Python 3.10, 3.11, 3.12 |
|
||||
| OpenCode Plugin | `opencode-plugin-checks.yml` | Push to main (`.opencode-plugin/`), manual | Bun: tsc + build + dist artifact check |
|
||||
| Pi Agent Plugin | `pi-agent-plugin-checks.yml` | Push to main (`integrations/pi-agent-plugin/`), manual | tsc + vitest + tsup on Node 20, 22 |
|
||||
| DeepSeek Harness Plugin | `dsh-mem0-checks.yml` | Push to main (`integrations/dsh-mem0/`), manual | tsc + vitest + tsup on Node 20, 22 |
|
||||
| DeepSeek Harness Plugin | `deepseek-plugin-checks.yml` | Push to main (`integrations/deepseek-plugin/`), manual | tsc + vitest + tsup on Node 20, 22 |
|
||||
| n8n Node | `n8n-nodes-mem0-checks.yml` | Push to main (`integrations/n8n-nodes-mem0/`), manual | ESLint + tsc build on Node 20 |
|
||||
| Zapier App | `zapier-mem0-checks.yml` | Push to main (`integrations/zapier-mem0/`), manual | tsc + `zapier validate` + offline unit tests on Node 22 |
|
||||
| strands-mem0 | `strands-mem0-checks.yml` | Push to main (`integrations/strands-mem0/`), manual | Ruff + mypy + pytest + hatch build on Python 3.10, 3.11, 3.12 |
|
||||
| mem0-strands | `mem0-strands-checks.yml` | Push to main (`integrations/mem0-strands/`), manual | Ruff + mypy + pytest + hatch build on Python 3.10, 3.11, 3.12 |
|
||||
| docs llms.txt | `docs-llms-txt-check.yml` | Manual | `docs/llms.txt` coverage |
|
||||
| GitHub Scripts | inline in `ci-gate.yml` | none | `node` over every `.github/scripts/*.test.js` |
|
||||
|
||||
@@ -60,9 +60,9 @@ Requiring `CI Gate` also means fork PRs from first-time contributors cannot merg
|
||||
| OpenClaw | `openclaw-cd.yml` | `openclaw-v*` | npm (`@mem0/openclaw-mem0`) |
|
||||
| OpenCode Plugin | `opencode-plugin-cd.yml` | `opencode-v*` | npm (`@mem0/opencode-plugin`) |
|
||||
| Pi Agent Plugin | `pi-agent-plugin-cd.yml` | `pi-agent-v*` | npm (`@mem0/pi-agent-plugin`) |
|
||||
| DeepSeek Harness Plugin | `dsh-mem0-cd.yml` | `dsh-mem0-v*` | npm (`@mem0/dsh-mem0`) |
|
||||
| DeepSeek Harness Plugin | `deepseek-plugin-cd.yml` | `deepseek-plugin-v*` | npm (`@mem0/deepseek-plugin`) |
|
||||
| n8n Node | `n8n-nodes-mem0-cd.yml` | `n8n-nodes-mem0-v*` | npm (`@mem0/n8n-nodes-mem0`) |
|
||||
| strands-mem0 | `strands-mem0-cd.yml` | `strands-mem0-v*` | PyPI (`strands-mem0`) |
|
||||
| mem0-strands | `mem0-strands-cd.yml` | `mem0-strands-v*` | PyPI (`mem0-strands`) |
|
||||
|
||||
- Package CD workflows are `workflow_dispatch`-only, with `tag` and `prerelease` inputs. They check out and build the given tag.
|
||||
- All publishing uses **OIDC trusted publishing**. No tokens, no secrets.
|
||||
|
||||
@@ -41,10 +41,10 @@ jobs:
|
||||
mem0_plugin: ${{ steps.filter.outputs.mem0_plugin }}
|
||||
opencode_plugin: ${{ steps.filter.outputs.opencode_plugin }}
|
||||
pi_agent_plugin: ${{ steps.filter.outputs.pi_agent_plugin }}
|
||||
dsh_mem0: ${{ steps.filter.outputs.dsh_mem0 }}
|
||||
deepseek_plugin: ${{ steps.filter.outputs.deepseek_plugin }}
|
||||
n8n_nodes_mem0: ${{ steps.filter.outputs.n8n_nodes_mem0 }}
|
||||
zapier_mem0: ${{ steps.filter.outputs.zapier_mem0 }}
|
||||
strands_mem0: ${{ steps.filter.outputs.strands_mem0 }}
|
||||
mem0_strands: ${{ steps.filter.outputs.mem0_strands }}
|
||||
docs_llms_txt: ${{ steps.filter.outputs.docs_llms_txt }}
|
||||
github_scripts: ${{ steps.filter.outputs.github_scripts }}
|
||||
steps:
|
||||
@@ -90,9 +90,9 @@ jobs:
|
||||
- 'integrations/pi-agent-plugin/**'
|
||||
- '.github/workflows/pi-agent-plugin-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
dsh_mem0:
|
||||
- 'integrations/dsh-mem0/**'
|
||||
- '.github/workflows/dsh-mem0-checks.yml'
|
||||
deepseek_plugin:
|
||||
- 'integrations/deepseek-plugin/**'
|
||||
- '.github/workflows/deepseek-plugin-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
n8n_nodes_mem0:
|
||||
- 'integrations/n8n-nodes-mem0/**'
|
||||
@@ -101,9 +101,9 @@ jobs:
|
||||
- 'integrations/zapier-mem0/**'
|
||||
- '.github/workflows/zapier-mem0-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
strands_mem0:
|
||||
- 'integrations/strands-mem0/**'
|
||||
- '.github/workflows/strands-mem0-checks.yml'
|
||||
mem0_strands:
|
||||
- 'integrations/mem0-strands/**'
|
||||
- '.github/workflows/mem0-strands-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
docs_llms_txt:
|
||||
- 'docs/**/*.mdx'
|
||||
@@ -176,11 +176,11 @@ jobs:
|
||||
uses: ./.github/workflows/pi-agent-plugin-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
dsh-mem0:
|
||||
deepseek-plugin:
|
||||
name: DeepSeek Harness Plugin
|
||||
needs: changes
|
||||
if: needs.changes.outputs.dsh_mem0 == 'true'
|
||||
uses: ./.github/workflows/dsh-mem0-checks.yml
|
||||
if: needs.changes.outputs.deepseek_plugin == 'true'
|
||||
uses: ./.github/workflows/deepseek-plugin-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
n8n-nodes-mem0:
|
||||
@@ -195,11 +195,11 @@ jobs:
|
||||
uses: ./.github/workflows/zapier-mem0-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
strands-mem0:
|
||||
name: strands-mem0
|
||||
mem0-strands:
|
||||
name: mem0-strands
|
||||
needs: changes
|
||||
if: needs.changes.outputs.strands_mem0 == 'true'
|
||||
uses: ./.github/workflows/strands-mem0-checks.yml
|
||||
if: needs.changes.outputs.mem0_strands == 'true'
|
||||
uses: ./.github/workflows/mem0-strands-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
docs-llms-txt:
|
||||
@@ -239,10 +239,10 @@ jobs:
|
||||
- mem0-plugin
|
||||
- opencode-plugin
|
||||
- pi-agent-plugin
|
||||
- dsh-mem0
|
||||
- deepseek-plugin
|
||||
- n8n-nodes-mem0
|
||||
- zapier-mem0
|
||||
- strands-mem0
|
||||
- mem0-strands
|
||||
- docs-llms-txt
|
||||
- github-scripts
|
||||
if: always()
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
name: Publish @mem0/dsh-mem0 📦 to npm
|
||||
name: Publish @mem0/deepseek-plugin 📦 to npm
|
||||
|
||||
# Dispatched by release.yml (Release Router) when a release tagged
|
||||
# dsh-mem0-v* is published. Can also be dispatched manually to re-publish
|
||||
# deepseek-plugin-v* is published. Can also be dispatched manually to re-publish
|
||||
# a tag.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build and publish (e.g. dsh-mem0-v0.1.1)'
|
||||
description: 'Release tag to build and publish (e.g. deepseek-plugin-v0.1.1)'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
@@ -18,14 +18,14 @@ on:
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish @mem0/dsh-mem0 📦 to npm
|
||||
if: startsWith(inputs.tag, 'dsh-mem0-v')
|
||||
name: Build and publish @mem0/deepseek-plugin 📦 to npm
|
||||
if: startsWith(inputs.tag, 'deepseek-plugin-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: integrations/dsh-mem0
|
||||
working-directory: integrations/deepseek-plugin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/dsh-mem0/pnpm-lock.yaml
|
||||
cache-dependency-path: integrations/deepseek-plugin/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
+14
-14
@@ -1,4 +1,4 @@
|
||||
name: dsh-mem0 checks
|
||||
name: deepseek-plugin checks
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main and manual runs remain standalone.
|
||||
@@ -7,8 +7,8 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'integrations/dsh-mem0/**'
|
||||
- '.github/workflows/dsh-mem0-checks.yml'
|
||||
- 'integrations/deepseek-plugin/**'
|
||||
- '.github/workflows/deepseek-plugin-checks.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
@@ -27,13 +27,13 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/dsh-mem0/pnpm-lock.yaml
|
||||
cache-dependency-path: integrations/deepseek-plugin/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd integrations/dsh-mem0 && pnpm install --frozen-lockfile
|
||||
run: cd integrations/deepseek-plugin && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Type check
|
||||
run: cd integrations/dsh-mem0 && pnpm exec tsc --noEmit
|
||||
run: cd integrations/deepseek-plugin && pnpm exec tsc --noEmit
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -53,13 +53,13 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/dsh-mem0/pnpm-lock.yaml
|
||||
cache-dependency-path: integrations/deepseek-plugin/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd integrations/dsh-mem0 && pnpm install --frozen-lockfile
|
||||
run: cd integrations/deepseek-plugin && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run tests
|
||||
run: cd integrations/dsh-mem0 && pnpm exec vitest run
|
||||
run: cd integrations/deepseek-plugin && pnpm exec vitest run
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -76,15 +76,15 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/dsh-mem0/pnpm-lock.yaml
|
||||
cache-dependency-path: integrations/deepseek-plugin/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd integrations/dsh-mem0 && pnpm install --frozen-lockfile
|
||||
run: cd integrations/deepseek-plugin && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: cd integrations/dsh-mem0 && pnpm build
|
||||
run: cd integrations/deepseek-plugin && pnpm build
|
||||
|
||||
- name: Verify dist output exists
|
||||
run: |
|
||||
test -f integrations/dsh-mem0/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
||||
test -f integrations/dsh-mem0/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
|
||||
test -f integrations/deepseek-plugin/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
||||
test -f integrations/deepseek-plugin/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
|
||||
@@ -1,15 +1,15 @@
|
||||
name: Publish strands-mem0 🐍 distribution 📦 to PyPI
|
||||
name: Publish mem0-strands 🐍 distribution 📦 to PyPI
|
||||
|
||||
# Dispatched by release.yml (Release Router) when a release tagged
|
||||
# strands-mem0-v* is published. Can also be dispatched manually to re-publish
|
||||
# mem0-strands-v* is published. Can also be dispatched manually to re-publish
|
||||
# a tag. Publishing uses PyPI Trusted Publishing (OIDC), so no API token is
|
||||
# stored; the `strands-mem0` PyPI project must have a trusted publisher
|
||||
# stored; the `mem0-strands` PyPI project must have a trusted publisher
|
||||
# configured for mem0ai/mem0 + this workflow.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build and publish (e.g. strands-mem0-v0.1.0)'
|
||||
description: 'Release tag to build and publish (e.g. mem0-strands-v0.1.0)'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
@@ -20,14 +20,14 @@ on:
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish strands-mem0 📦 to PyPI
|
||||
if: startsWith(inputs.tag, 'strands-mem0-v')
|
||||
name: Build and publish mem0-strands 📦 to PyPI
|
||||
if: startsWith(inputs.tag, 'mem0-strands-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: integrations/strands-mem0/python
|
||||
working-directory: integrations/mem0-strands/python
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -47,4 +47,4 @@ jobs:
|
||||
- name: Publish distribution 📦 to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: integrations/strands-mem0/python/dist/
|
||||
packages-dir: integrations/mem0-strands/python/dist/
|
||||
+12
-12
@@ -1,4 +1,4 @@
|
||||
name: strands-mem0 CI
|
||||
name: mem0-strands CI
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main and manual runs remain standalone.
|
||||
@@ -7,8 +7,8 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'integrations/strands-mem0/**'
|
||||
- '.github/workflows/strands-mem0-checks.yml'
|
||||
- 'integrations/mem0-strands/**'
|
||||
- '.github/workflows/mem0-strands-checks.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
@@ -23,19 +23,19 @@ jobs:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install dev dependencies
|
||||
working-directory: integrations/strands-mem0/python
|
||||
working-directory: integrations/mem0-strands/python
|
||||
run: pip install -e ".[dev]"
|
||||
|
||||
- name: Lint with ruff
|
||||
working-directory: integrations/strands-mem0/python
|
||||
working-directory: integrations/mem0-strands/python
|
||||
run: ruff check .
|
||||
|
||||
- name: Check formatting
|
||||
working-directory: integrations/strands-mem0/python
|
||||
working-directory: integrations/mem0-strands/python
|
||||
run: ruff format --check .
|
||||
|
||||
- name: Type-check with mypy
|
||||
working-directory: integrations/strands-mem0/python
|
||||
working-directory: integrations/mem0-strands/python
|
||||
run: mypy src
|
||||
|
||||
test:
|
||||
@@ -52,11 +52,11 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dev dependencies
|
||||
working-directory: integrations/strands-mem0/python
|
||||
working-directory: integrations/mem0-strands/python
|
||||
run: pip install -e ".[dev]"
|
||||
|
||||
- name: Run tests
|
||||
working-directory: integrations/strands-mem0/python
|
||||
working-directory: integrations/mem0-strands/python
|
||||
run: pytest
|
||||
|
||||
build:
|
||||
@@ -73,10 +73,10 @@ jobs:
|
||||
run: pip install hatch
|
||||
|
||||
- name: Build
|
||||
working-directory: integrations/strands-mem0/python
|
||||
working-directory: integrations/mem0-strands/python
|
||||
run: hatch build --clean
|
||||
|
||||
- name: Verify dist output
|
||||
run: |
|
||||
ls integrations/strands-mem0/python/dist/*.whl || (echo "Wheel file missing" && exit 1)
|
||||
ls integrations/strands-mem0/python/dist/*.tar.gz || (echo "Source dist missing" && exit 1)
|
||||
ls integrations/mem0-strands/python/dist/*.whl || (echo "Wheel file missing" && exit 1)
|
||||
ls integrations/mem0-strands/python/dist/*.tar.gz || (echo "Source dist missing" && exit 1)
|
||||
@@ -45,9 +45,9 @@ jobs:
|
||||
openclaw-v*) workflow="openclaw-cd.yml" ;;
|
||||
opencode-v*) workflow="opencode-plugin-cd.yml" ;;
|
||||
pi-agent-v*) workflow="pi-agent-plugin-cd.yml" ;;
|
||||
dsh-mem0-v*) workflow="dsh-mem0-cd.yml" ;;
|
||||
deepseek-plugin-v*) workflow="deepseek-plugin-cd.yml" ;;
|
||||
n8n-nodes-mem0-v*) workflow="n8n-nodes-mem0-cd.yml" ;;
|
||||
strands-mem0-v*) workflow="strands-mem0-cd.yml" ;;
|
||||
mem0-strands-v*) workflow="mem0-strands-cd.yml" ;;
|
||||
v*) workflow="cd.yml" ;;
|
||||
*)
|
||||
echo "::error::Release tag '$TAG' does not match any known package prefix — nothing will be published. See the tag prefix table in AGENTS.md."
|
||||
|
||||
+1
-1
@@ -192,6 +192,6 @@ testing.ipynb
|
||||
.weave/
|
||||
|
||||
|
||||
# TypeScript incremental build info and local, uncommitted e2e scripts (used by the integrations, e.g. integrations/dsh-mem0)
|
||||
# TypeScript incremental build info and local, uncommitted e2e scripts (used by the integrations, e.g. integrations/deepseek-plugin)
|
||||
*.tsbuildinfo
|
||||
*.local.mjs
|
||||
|
||||
@@ -8,14 +8,14 @@ mode: "wide"
|
||||
|
||||
**DeepSeek Harness: Mem0 as a Native Cordis Plugin**
|
||||
|
||||
The DeepSeek Harness agent forgets everything between sessions. [`@mem0/dsh-mem0`](https://www.npmjs.com/package/@mem0/dsh-mem0) gives it two Mem0-backed tools, so recall and writes persist across runs against the same memory bank you already use from Claude Code, Codex, and every other connected agent.
|
||||
The DeepSeek Harness agent forgets everything between sessions. [`@mem0/deepseek-plugin`](https://www.npmjs.com/package/@mem0/deepseek-plugin) gives it two Mem0-backed tools, so recall and writes persist across runs against the same memory bank you already use from Claude Code, Codex, and every other connected agent.
|
||||
|
||||
- **Two agent-callable tools:** `search_memory` recalls facts relevant to a query, `add_memory` stores a fact for future sessions. Both accept per-call `userId` / `agentId` / `runId` scope overrides.
|
||||
- **Native Cordis lifecycle:** The plugin declares `inject = ['tools']` so it waits for the harness tool registry, then registers through `ctx.tools.register()`. Unmounting the plugin removes the tools automatically.
|
||||
- **Managed backend, not a memory file:** Server-side extraction, semantic dedup, and conflict resolution, rather than a Markdown file the agent has to maintain itself.
|
||||
- **Config:** `userId` is required, `apiKey` defaults to `$MEM0_API_KEY`, and `host` optionally targets a dedicated Mem0 Platform base URL.
|
||||
|
||||
See [DeepSeek Harness](/integrations/dsh-mem0) for setup and [SDK & Tools](/changelog/sdk) for PR links.
|
||||
See [DeepSeek Harness](/integrations/deepseek-plugin) for setup and [SDK & Tools](/changelog/sdk) for PR links.
|
||||
|
||||
<Note>
|
||||
Developer preview. Auto-capture and auto-recall, where memory reaches the context with no explicit tool call, are planned but not yet built.
|
||||
@@ -27,7 +27,7 @@ See [DeepSeek Harness](/integrations/dsh-mem0) for setup and [SDK & Tools](/chan
|
||||
|
||||
**Strands Agents: Mem0 as a Native MemoryStore**
|
||||
|
||||
[`strands-mem0`](https://pypi.org/project/strands-mem0/) plugs Mem0 into AWS's [Strands Agents](https://strandsagents.com/) SDK as a native `MemoryStore`, so recall and writes happen inside the agent loop rather than as tool calls the model has to remember to make.
|
||||
[`mem0-strands`](https://pypi.org/project/mem0-strands/) plugs Mem0 into AWS's [Strands Agents](https://strandsagents.com/) SDK as a native `MemoryStore`, so recall and writes happen inside the agent loop rather than as tool calls the model has to remember to make.
|
||||
|
||||
- **Automatic recall:** The `MemoryManager` drives the store on every turn, searching Mem0 and injecting the results into the prompt with no tool call required.
|
||||
- **Server-side extraction:** Because the store implements `add_messages`, enabling extraction routes raw conversation turns straight to Mem0's extraction pipeline, skipping the extra client-side model call needed to distill facts first.
|
||||
|
||||
+19
-5
@@ -2841,9 +2841,16 @@ Existing memories written by the previous versions are not rewritten. If your me
|
||||
|
||||
<Tab title="Strands">
|
||||
|
||||
<Update label="2026-08-24" description="strands-mem0 v0.1.0">
|
||||
<Update label="2026-08-25" description="mem0-strands v0.1.1">
|
||||
|
||||
**Initial release** of [`strands-mem0`](https://pypi.org/project/strands-mem0/), a native `MemoryStore` that plugs Mem0 into the [Strands Agents](https://strandsagents.com/) `MemoryManager` ([#7021](https://github.com/mem0ai/mem0/pull/7021))
|
||||
**New Features:**
|
||||
- **Usage telemetry:** Anonymous usage events (`strands.store.init`, `strands.store.search`, `strands.store.add`, `strands.store.add_messages`) ride the Mem0 SDK's existing PostHog client, unsampled, with no new dependency. Events carry only counts, durations, booleans, and coarse failure kinds: never queries, memory text, message content, entity ids, metadata, or API keys. Opt out with `MEM0_TELEMETRY=false` ([#7110](https://github.com/mem0ai/mem0/pull/7110))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-08-24" description="mem0-strands v0.1.0">
|
||||
|
||||
**Initial release** of [`mem0-strands`](https://pypi.org/project/mem0-strands/), a native `MemoryStore` that plugs Mem0 into the [Strands Agents](https://strandsagents.com/) `MemoryManager` ([#7021](https://github.com/mem0ai/mem0/pull/7021))
|
||||
|
||||
**New Features:**
|
||||
- **Automatic recall and injection:** `Mem0MemoryStore.search()` runs every turn through the `MemoryManager`, so relevant memories are searched and prepended to the prompt with no explicit tool call required.
|
||||
@@ -2863,9 +2870,16 @@ Existing memories written by the previous versions are not rewritten. If your me
|
||||
|
||||
<Tab title="DeepSeek Harness">
|
||||
|
||||
<Update label="2026-08-24" description="dsh-mem0 v0.1.0">
|
||||
<Update label="2026-08-25" description="deepseek-plugin v0.1.1">
|
||||
|
||||
**Initial release** of [`dsh-mem0`](https://www.npmjs.com/package/@mem0/dsh-mem0), a native DeepSeek Harness (Cordis) plugin that registers Mem0 as two agent-callable tools ([#7027](https://github.com/mem0ai/mem0/pull/7027))
|
||||
**New Features:**
|
||||
- **Usage telemetry:** Anonymous usage events (`deepseek.plugin.mounted`, `deepseek.tool.search_memory`, `deepseek.tool.add_memory`) are batched to PostHog over native fetch and flushed in the background. Events carry only tool names, durations, counts, and coarse failure kinds: never queries, memory text, filters, or API keys. Opt out with `MEM0_TELEMETRY=false` ([#7110](https://github.com/mem0ai/mem0/pull/7110))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-08-24" description="deepseek-plugin v0.1.0">
|
||||
|
||||
**Initial release** of [`deepseek-plugin`](https://www.npmjs.com/package/@mem0/deepseek-plugin), a native DeepSeek Harness (Cordis) plugin that registers Mem0 as two agent-callable tools ([#7027](https://github.com/mem0ai/mem0/pull/7027))
|
||||
|
||||
**New Features:**
|
||||
- **`search_memory`:** Recalls facts relevant to a query, with an optional `limit` (default 10) and per-call `userId` / `agentId` / `runId` scope overrides.
|
||||
@@ -2874,7 +2888,7 @@ Existing memories written by the previous versions are not rewritten. If your me
|
||||
- **Config:** `userId` is required; `apiKey` defaults to `$MEM0_API_KEY`; `host` optionally points at a dedicated Mem0 Platform base URL (not a switch to self-hosted Mem0 OSS).
|
||||
|
||||
<Note>
|
||||
Developer preview: auto-capture and auto-recall (memory injected into context automatically, without an explicit tool call) are planned but not yet built. The backend's `KNOWN_EVENT_SOURCES` allowlist also needs `"DEEPSEEK_HARNESS"` added before usage surfaces by name in telemetry rather than bucketing into "OTHERS". See [DeepSeek Harness](/integrations/dsh-mem0) for setup.
|
||||
Developer preview: auto-capture and auto-recall (memory injected into context automatically, without an explicit tool call) are planned but not yet built. The backend's `KNOWN_EVENT_SOURCES` allowlist also needs `"DEEPSEEK_HARNESS"` added before usage surfaces by name in telemetry rather than bucketing into "OTHERS". See [DeepSeek Harness](/integrations/deepseek-plugin) for setup.
|
||||
</Note>
|
||||
|
||||
</Update>
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
---
|
||||
title: Give Agents Shared Working Memory
|
||||
description: Keep a claims adjuster's personal preferences separate from a claim's shared decisions using user_id, agent_id, and two sets of custom instructions.
|
||||
---
|
||||
|
||||
Sarah is a claims adjuster. Every claim she opens should remember how she likes things drafted. Every claim itself should remember its own decisions, no matter which adjuster opens it next. A single `user_id` and one project-wide instruction set cannot hold both without one leaking into the other.
|
||||
|
||||
<Info icon="cloud">
|
||||
**Works with:** Mem0 Platform (`MemoryClient`)
|
||||
</Info>
|
||||
|
||||
<Info icon="clock">
|
||||
**Time to complete:** ~15 minutes · **Languages:** Python, TypeScript
|
||||
</Info>
|
||||
|
||||
## Setup
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
import os
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient(api_key=os.environ["MEM0_API_KEY"])
|
||||
```
|
||||
```typescript TypeScript
|
||||
import { MemoryClient } from "mem0ai";
|
||||
|
||||
const client = new MemoryClient({ apiKey: process.env.MEM0_API_KEY });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Note>
|
||||
Grab an API key from the <a href="https://app.mem0.ai/?utm_source=oss&utm_medium=cookbook-agent-memory" rel="nofollow">Mem0 dashboard</a>. `agent_custom_instructions` needs Python SDK `v2.0.17` or TypeScript SDK `v3.1.5` or later.
|
||||
</Note>
|
||||
|
||||
## Make It Work Once
|
||||
|
||||
Claim CLM-48211 is a warehouse fire loss for Meridian Logistics, and Sarah is the adjuster who opened it. Set two rule sets on the project once: `custom_instructions` for what belongs to the adjuster, `agent_custom_instructions` for what belongs to the claim.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
ADJUSTER = "adjuster_sarah_cookbook_demo"
|
||||
CLAIM = "claims_agent_clm48211_cookbook_demo"
|
||||
|
||||
user_instructions = """Your Task: Track what a claims adjuster needs remembered about themselves, not about any single claim.
|
||||
|
||||
Information to Extract:
|
||||
1. Drafting and communication style
|
||||
2. Working preferences, such as coverage lines they focus on
|
||||
|
||||
Guidelines:
|
||||
- Store preferences that should carry over to every claim this adjuster works.
|
||||
|
||||
Exclude:
|
||||
- Decisions made on a specific claim (settle vs litigate, reserve changes, subrogation)
|
||||
- Policy numbers, coverage limits, or other one-off lookups"""
|
||||
|
||||
agent_instructions = """Your Task: Track what matters about this specific claim, regardless of which adjuster is working it.
|
||||
|
||||
Information to Extract:
|
||||
1. Claim decisions and reasoning: settle vs litigate calls, reserve changes, subrogation
|
||||
2. Durable claim facts, such as cause of loss
|
||||
|
||||
Guidelines:
|
||||
- Store facts that the next adjuster to open this claim needs to see.
|
||||
|
||||
Exclude:
|
||||
- Personal style or communication preferences of whichever adjuster is talking"""
|
||||
|
||||
client.project.update(
|
||||
custom_instructions=user_instructions,
|
||||
agent_custom_instructions=agent_instructions,
|
||||
)
|
||||
```
|
||||
```typescript TypeScript
|
||||
const ADJUSTER = "adjuster_sarah_cookbook_demo";
|
||||
const CLAIM = "claims_agent_clm48211_cookbook_demo";
|
||||
|
||||
const userInstructions = `Your Task: Track what a claims adjuster needs remembered about themselves, not about any single claim.
|
||||
|
||||
Information to Extract:
|
||||
1. Drafting and communication style
|
||||
2. Working preferences, such as coverage lines they focus on
|
||||
|
||||
Guidelines:
|
||||
- Store preferences that should carry over to every claim this adjuster works.
|
||||
|
||||
Exclude:
|
||||
- Decisions made on a specific claim (settle vs litigate, reserve changes, subrogation)
|
||||
- Policy numbers, coverage limits, or other one-off lookups`;
|
||||
|
||||
const agentInstructions = `Your Task: Track what matters about this specific claim, regardless of which adjuster is working it.
|
||||
|
||||
Information to Extract:
|
||||
1. Claim decisions and reasoning: settle vs litigate calls, reserve changes, subrogation
|
||||
2. Durable claim facts, such as cause of loss
|
||||
|
||||
Guidelines:
|
||||
- Store facts that the next adjuster to open this claim needs to see.
|
||||
|
||||
Exclude:
|
||||
- Personal style or communication preferences of whichever adjuster is talking`;
|
||||
|
||||
await client.updateProject({
|
||||
customInstructions: userInstructions,
|
||||
agentCustomInstructions: agentInstructions,
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Now a normal exchange with the claim, passing both ids on the same `add()` call:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
client.add(
|
||||
[
|
||||
{"role": "user", "content": "I'm raising the reserve on CLM-48211 to $1.8 million. The structural engineer's report says the warehouse needs a full rebuild, not a repair."},
|
||||
{"role": "assistant", "content": "Got it. Reserve raised to $1.8 million on CLM-48211 for a full rebuild, per the structural engineer's report."},
|
||||
],
|
||||
user_id=ADJUSTER,
|
||||
agent_id=CLAIM,
|
||||
)
|
||||
```
|
||||
```typescript TypeScript
|
||||
await client.add(
|
||||
[
|
||||
{ role: "user", content: "I'm raising the reserve on CLM-48211 to $1.8 million. The structural engineer's report says the warehouse needs a full rebuild, not a repair." },
|
||||
{ role: "assistant", content: "Got it. Reserve raised to $1.8 million on CLM-48211 for a full rebuild, per the structural engineer's report." },
|
||||
],
|
||||
{ userId: ADJUSTER, agentId: CLAIM },
|
||||
);
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Info icon="check">
|
||||
Expected output: two memories, both attributed to `CLAIM`: `"Reserve for claim CLM-48211 was increased to $1.8 million to cover a full rebuild, as indicated by the structural engineer's report"`, plus the engineer's rebuild finding stored as its own durable fact. Nothing lands on Sarah's personal shelf, the instructions exclude claim decisions from it.
|
||||
</Info>
|
||||
|
||||
## The Problem
|
||||
|
||||
Suppose Marcus, a second adjuster, needs to pick up CLM-48211 while Sarah is out. If claim decisions only ever went on the acting adjuster's `user_id`, with no `agent_id` and no `agent_custom_instructions`, Marcus's own memory would be empty no matter what Sarah decided:
|
||||
|
||||
```python
|
||||
marcus_personal = client.get_all(filters={"user_id": "adjuster_marcus_cookbook_demo"})
|
||||
print(marcus_personal["count"])
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
0
|
||||
```
|
||||
|
||||
Marcus has never talked to Mem0 before, so an empty personal shelf is expected. The real gap is structural: if the reserve decision above had gone on Sarah's `user_id` alone, it would live there permanently, and Marcus would have no identifier that reaches it. The claim's history would be trapped on one person's shelf.
|
||||
|
||||
## Fix It: Give the Claim Its Own Shelf
|
||||
|
||||
`agent_id` fixes this because it names the claim, not the person handling it. Anyone who searches with that `agent_id` sees the same claim history, regardless of whose `user_id` they search alongside:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
marcus_view = client.search(
|
||||
"What's going on with CLM-48211?",
|
||||
filters={"OR": [{"user_id": "adjuster_marcus_cookbook_demo"}, {"agent_id": CLAIM}]},
|
||||
)
|
||||
for r in marcus_view["results"]:
|
||||
print(f"- [{r.get('user_id') or r.get('agent_id')}] {r['memory']}")
|
||||
```
|
||||
```typescript TypeScript
|
||||
const marcusView = await client.search(
|
||||
"What's going on with CLM-48211?",
|
||||
{ filters: { OR: [{ userId: "adjuster_marcus_cookbook_demo" }, { agentId: CLAIM }] } },
|
||||
);
|
||||
for (const r of marcusView.results) {
|
||||
console.log(`- [${r.userId ?? r.agentId}] ${r.memory}`);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
**Output** (from the full notebook run, four claim facts on file):
|
||||
```
|
||||
- [claims_agent_clm48211_cookbook_demo] Reserve for claim CLM-48211 was increased to $1.8 million to cover a full rebuild, as indicated by the structural engineer's report
|
||||
- [claims_agent_clm48211_cookbook_demo] The structural engineer's report for claim CLM-48211 states that the warehouse requires a full rebuild rather than a repair
|
||||
- [claims_agent_clm48211_cookbook_demo] The building coverage limit for claim CLM-48211 under the Meridian Logistics policy is $2,400,000 with a $25,000 deductible
|
||||
- [claims_agent_clm48211_cookbook_demo] Assistant noted that subrogation will be pursued against Voss Electrical, with the fire marshal's report identifying faulty wiring as the ignition source
|
||||
```
|
||||
|
||||
Marcus sees the full claim history and none of Sarah's personal preferences, because those never carried `agent_id` attribution in the first place.
|
||||
|
||||
## Build On It: One Message, Two Shelves
|
||||
|
||||
A single message can touch both shelves in one `add()` call. Sarah makes a claim decision and states a personal preference in the same breath:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
client.add(
|
||||
[
|
||||
{"role": "user", "content": "I've decided to pursue subrogation against Voss Electrical, the contractor who rewired the warehouse six months ago. The fire marshal's report points to faulty wiring as the ignition source. Also, from now on keep my claim summaries to bullet points, not paragraphs."},
|
||||
{"role": "assistant", "content": "Noted both. Pursuing subrogation against Voss Electrical based on the fire marshal's faulty-wiring finding, and I'll keep your summaries in bullet points going forward."},
|
||||
],
|
||||
user_id=ADJUSTER,
|
||||
agent_id=CLAIM,
|
||||
)
|
||||
```
|
||||
```typescript TypeScript
|
||||
await client.add(
|
||||
[
|
||||
{ role: "user", content: "I've decided to pursue subrogation against Voss Electrical, the contractor who rewired the warehouse six months ago. The fire marshal's report points to faulty wiring as the ignition source. Also, from now on keep my claim summaries to bullet points, not paragraphs." },
|
||||
{ role: "assistant", content: "Noted both. Pursuing subrogation against Voss Electrical based on the fire marshal's faulty-wiring finding, and I'll keep your summaries in bullet points going forward." },
|
||||
],
|
||||
{ userId: ADJUSTER, agentId: CLAIM },
|
||||
);
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
**Output:** two memories from one call, one per shelf. The subrogation decision lands on the claim shelf with the fire marshal's faulty-wiring finding folded into its reasoning, and the bullet-point formatting rule lands on Sarah's personal shelf.
|
||||
|
||||
<Warning>
|
||||
Extraction makes judgment calls, it does not mirror your instructions line by line. In the full notebook run, a one-off policy-limit lookup (excluded by name in the instructions above) still landed on the claim shelf, because it was judged a durable fact worth keeping for the life of the claim. Read what actually got stored with `get_all()` rather than assuming the instructions were applied literally.
|
||||
</Warning>
|
||||
|
||||
## Production Patterns
|
||||
|
||||
- **Reviewing and correcting a personal shelf**: `get_all()` with the adjuster's `user_id`, then act on what is actually stored.
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
sarah_shelf = client.get_all(filters={"user_id": ADJUSTER})
|
||||
for r in sarah_shelf["results"]:
|
||||
print(r["id"], "-", r["memory"])
|
||||
```
|
||||
```typescript TypeScript
|
||||
const sarahShelf = await client.getAll({ filters: { userId: ADJUSTER } });
|
||||
for (const r of sarahShelf.results) {
|
||||
console.log(r.id, "-", r.memory);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
- **Editing without losing a neighbor**: `delete()` removes a whole memory, not a substring of one. If two preferences were extracted into a single combined memory, deleting it to revise one loses both. Use `update()` to rewrite the text in place instead:
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
client.update(memory_id, text="User prefers drafts to contain no em dashes")
|
||||
```
|
||||
```typescript TypeScript
|
||||
await client.update(memoryId, { text: "User prefers drafts to contain no em dashes" });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## What You Built
|
||||
|
||||
- **A personal shelf per adjuster**: `user_id` plus `custom_instructions` for preferences that follow one person across every claim they touch.
|
||||
- **A shared shelf per claim**: `agent_id` plus `agent_custom_instructions` for decisions and facts that outlive any single adjuster's session.
|
||||
- **One query across both**: an `OR` filter on `user_id` and `agent_id` for status updates that need the person's context and the claim's history together.
|
||||
|
||||
## Production Checklist
|
||||
|
||||
- Set `agent_custom_instructions` alongside `custom_instructions` in the same `project.update()` call, an unset agent instruction set silently falls back to the user one.
|
||||
- Pass both `user_id` and `agent_id` on every `add()` call for a claim conversation, not just one or the other.
|
||||
- Use `update()` to correct a single fact; reach for `delete()` only when you mean to remove the whole memory record.
|
||||
- Delete demo or test data with `delete_all(user_id=...)` and `delete_all(agent_id=...)`, and restore any project instructions you changed for a test run.
|
||||
|
||||
## Next Steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card
|
||||
title="Custom Instructions"
|
||||
description="The full reference for custom_instructions and agent_custom_instructions, including per-call overrides."
|
||||
icon="shield-check"
|
||||
href="/platform/features/custom-instructions"
|
||||
/>
|
||||
<Card
|
||||
title="Run the Full Notebook"
|
||||
description="The same scenario end to end, executed live against the Mem0 Platform API with real outputs."
|
||||
icon="rocket"
|
||||
href="https://github.com/mem0ai/mem0/blob/main/examples/notebooks/agent-memory-insurance.ipynb"
|
||||
/>
|
||||
</CardGroup>
|
||||
|
||||
<Snippet file="star-on-github.mdx" />
|
||||
+4
-2
@@ -325,6 +325,7 @@
|
||||
"integrations/google-ai-adk",
|
||||
"integrations/mastra",
|
||||
"integrations/vercel-ai-sdk",
|
||||
"integrations/vercel",
|
||||
"integrations/chatdev",
|
||||
"integrations/strands"
|
||||
]
|
||||
@@ -383,7 +384,7 @@
|
||||
"integrations/openclaw",
|
||||
"integrations/hermes",
|
||||
"integrations/pi-agent",
|
||||
"integrations/dsh-mem0"
|
||||
"integrations/deepseek-plugin"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -405,7 +406,8 @@
|
||||
"cookbooks/essentials/building-ai-companion",
|
||||
"cookbooks/essentials/entity-partitioning-playbook",
|
||||
"cookbooks/essentials/tagging-and-organizing-memories",
|
||||
"cookbooks/essentials/exporting-memories"
|
||||
"cookbooks/essentials/exporting-memories",
|
||||
"cookbooks/essentials/agent-memory-insurance-claims"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ title: DeepSeek Harness
|
||||
description: "Add persistent Mem0 memory to the DeepSeek Harness (Cordis) agent with two native tools: search and add."
|
||||
---
|
||||
|
||||
Add persistent memory to the [**DeepSeek Harness**](https://github.com/deepseek-ai/deepseek-harness) with `@mem0/dsh-mem0`. The Harness agent forgets everything between sessions. This plugin gives it two Mem0-backed tools so recall and writes persist across runs, sharing the same memory bank you already use from Claude Code, Codex, and other agents.
|
||||
Add persistent memory to the [**DeepSeek Harness**](https://github.com/deepseek-ai/deepseek-harness) with `@mem0/deepseek-plugin`. The Harness agent forgets everything between sessions. This plugin gives it two Mem0-backed tools so recall and writes persist across runs, sharing the same memory bank you already use from Claude Code, Codex, and other agents.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -23,8 +23,8 @@ A Cordis plugin is a module exporting `apply(ctx, config)`. This one declares `i
|
||||
## Prerequisites
|
||||
|
||||
1. A Mem0 Platform account and API key:
|
||||
- <a href="https://app.mem0.ai?utm_source=oss&utm_medium=integration-dsh-mem0" rel="nofollow">Sign up at app.mem0.ai</a>
|
||||
- <a href="https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-dsh-mem0" rel="nofollow">Get your API key</a> (starts with `m0-`)
|
||||
- <a href="https://app.mem0.ai?utm_source=oss&utm_medium=integration-deepseek-plugin" rel="nofollow">Sign up at app.mem0.ai</a>
|
||||
- <a href="https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-deepseek-plugin" rel="nofollow">Get your API key</a> (starts with `m0-`)
|
||||
|
||||
2. The DeepSeek Harness installed.
|
||||
|
||||
@@ -46,14 +46,14 @@ source ~/.bashrc
|
||||
|
||||
1. Build the plugin:
|
||||
```sh
|
||||
cd integrations/dsh-mem0
|
||||
cd integrations/deepseek-plugin
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
2. Point the Harness at it. Copy `cordis.example.yml`, set the absolute path to `dist/index.js` and your `userId`, then load it:
|
||||
```sh
|
||||
pnpm dsh web --patch ./integrations/dsh-mem0/cordis.example.yml
|
||||
pnpm dsh web --patch ./integrations/deepseek-plugin/cordis.example.yml
|
||||
```
|
||||
|
||||
3. Open the web UI and ask the agent to remember something, then recall it in a later turn.
|
||||
@@ -65,7 +65,7 @@ The `cordis.yml` entry looks like this:
|
||||
- name: "@deepseek-ai/dsh-tools"
|
||||
- insert:
|
||||
- id: mem0
|
||||
name: "/absolute/path/to/integrations/dsh-mem0/dist/index.js"
|
||||
name: "/absolute/path/to/integrations/deepseek-plugin/dist/index.js"
|
||||
config:
|
||||
# apiKey is read from MEM0_API_KEY when omitted here.
|
||||
userId: "your-user-id"
|
||||
@@ -3,7 +3,7 @@ title: Strands Agents
|
||||
description: "Add persistent long-term memory to AWS Strands agents with Mem0, as a native MemoryStore that plugs into the agent loop."
|
||||
---
|
||||
|
||||
Integrate [**Mem0**](https://github.com/mem0ai/mem0) with [Strands Agents](https://github.com/strands-agents/sdk-python), AWS's open-source SDK for building AI agents. The [`strands-mem0`](https://github.com/mem0ai/mem0/tree/main/integrations/strands-mem0) package ships a native `MemoryStore`, so recall and writes happen automatically inside the agent loop, not as tool calls the model has to remember.
|
||||
Integrate [**Mem0**](https://github.com/mem0ai/mem0) with [Strands Agents](https://github.com/strands-agents/sdk-python), AWS's open-source SDK for building AI agents. The [`mem0-strands`](https://github.com/mem0ai/mem0/tree/main/integrations/mem0-strands) package ships a native `MemoryStore`, so recall and writes happen automatically inside the agent loop, not as tool calls the model has to remember.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -17,7 +17,7 @@ Before setting up Mem0 with Strands, ensure you have:
|
||||
|
||||
1. Installed the required packages:
|
||||
```bash
|
||||
pip install strands-mem0
|
||||
pip install mem0-strands
|
||||
```
|
||||
|
||||
2. A valid API key:
|
||||
@@ -31,7 +31,7 @@ Hand a `Mem0MemoryStore` to a `MemoryManager`, and the agent gets automatic reca
|
||||
import os
|
||||
from strands import Agent
|
||||
from strands.memory import MemoryManager
|
||||
from strands_mem0 import Mem0MemoryStore
|
||||
from mem0_strands import Mem0MemoryStore
|
||||
|
||||
os.environ["MEM0_API_KEY"] = "your-mem0-api-key"
|
||||
|
||||
@@ -65,5 +65,5 @@ If you want the model to call memory explicitly instead of (or alongside) the au
|
||||
|
||||
## Learn more
|
||||
|
||||
- [strands-mem0 on GitHub](https://github.com/mem0ai/mem0/tree/main/integrations/strands-mem0)
|
||||
- [mem0-strands on GitHub](https://github.com/mem0ai/mem0/tree/main/integrations/mem0-strands)
|
||||
- [Strands Agents documentation](https://strandsagents.com)
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
---
|
||||
title: Vercel
|
||||
description: "Add persistent memory to your Vercel apps with the Mem0 Vercel Marketplace integration. Install from the Marketplace, get an API key, and start building."
|
||||
---
|
||||
|
||||
The **Mem0 Vercel integration** provisions managed Mem0 access directly from the [Vercel Marketplace](https://vercel.com/marketplace). Installing it creates a Mem0 account for you and injects a `MEM0_API_KEY` into your Vercel project, so you can add long-term memory to your AI apps and agents in minutes, with no separate signup and billing handled through Vercel.
|
||||
|
||||
<Note type="info">
|
||||
This is the managed Marketplace integration. If you instead want to use Mem0 with the Vercel AI SDK in code, see [Vercel AI SDK](/integrations/vercel-ai-sdk).
|
||||
</Note>
|
||||
|
||||
## Overview
|
||||
|
||||
When you install Mem0 from the Vercel Marketplace:
|
||||
|
||||
- A Mem0 account and project are created for you (Vercel Native).
|
||||
- Mem0 environment variables (`MEM0_API_KEY`, `MEM0_ORG_ID`, `MEM0_PROJECT_ID`, `MEM0_BASE_URL`) are added to your connected Vercel project.
|
||||
- Billing is handled through your Vercel account, with no separate Mem0 subscription.
|
||||
|
||||
You call Mem0 from your app with the injected key. There is nothing else to wire up.
|
||||
|
||||
## Install
|
||||
|
||||
<Steps>
|
||||
<Step title="Add the integration">
|
||||
From the [Vercel Marketplace](https://vercel.com/marketplace), find **Mem0** and click **Install**. Choose **Create New Mem0 Account** (Vercel Native).
|
||||
</Step>
|
||||
<Step title="Pick a plan">
|
||||
Select **Hobby** (free) or **Starter** ($20/month). You can change this anytime from the installation settings in your Vercel dashboard.
|
||||
</Step>
|
||||
<Step title="Connect a project">
|
||||
Connect the Mem0 resource to a Vercel project. Mem0 injects its environment variables into that project.
|
||||
</Step>
|
||||
<Step title="Redeploy">
|
||||
Redeploy so the variables are available at runtime, or run `vercel env pull` to use them locally.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Environment variables
|
||||
|
||||
The integration sets the following on your connected project:
|
||||
|
||||
| Variable | Description |
|
||||
| --- | --- |
|
||||
| `MEM0_API_KEY` | Your Mem0 API key, scoped to the org and project created for this installation. This is what you authenticate with. |
|
||||
| `MEM0_ORG_ID` | The Mem0 organization id for this installation. |
|
||||
| `MEM0_PROJECT_ID` | The Mem0 project id for this installation. |
|
||||
| `MEM0_BASE_URL` | The Mem0 API base URL (`https://api.mem0.ai`). |
|
||||
|
||||
The API key is scoped to your org and project, so the SDK needs only `MEM0_API_KEY` to get started.
|
||||
|
||||
## Quickstart
|
||||
|
||||
Install the SDK:
|
||||
|
||||
<CodeGroup>
|
||||
```bash npm
|
||||
npm install mem0ai
|
||||
```
|
||||
|
||||
```bash pip
|
||||
pip install mem0ai
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Add and search memories using the injected key:
|
||||
|
||||
<CodeGroup>
|
||||
```typescript JavaScript
|
||||
import MemoryClient from "mem0ai";
|
||||
|
||||
const memory = new MemoryClient({ apiKey: process.env.MEM0_API_KEY });
|
||||
|
||||
// Store a memory
|
||||
await memory.add(
|
||||
[{ role: "user", content: "I'm a vegetarian and allergic to nuts." }],
|
||||
{ userId: "user123" },
|
||||
);
|
||||
|
||||
// Retrieve relevant memories
|
||||
const results = await memory.search("What are my dietary restrictions?", {
|
||||
filters: { user_id: "user123" },
|
||||
});
|
||||
console.log(results);
|
||||
```
|
||||
|
||||
```python Python
|
||||
import os
|
||||
from mem0 import MemoryClient
|
||||
|
||||
memory = MemoryClient(api_key=os.environ["MEM0_API_KEY"])
|
||||
|
||||
# Store a memory
|
||||
memory.add(
|
||||
[{"role": "user", "content": "I'm a vegetarian and allergic to nuts."}],
|
||||
user_id="user123",
|
||||
)
|
||||
|
||||
# Retrieve relevant memories
|
||||
results = memory.search("What are my dietary restrictions?", filters={"user_id": "user123"})
|
||||
print(results)
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
See the [platform quickstart](/platform/quickstart) for the full API.
|
||||
|
||||
## Plans and billing
|
||||
|
||||
| Plan | Price | Memories | Retrieval calls | Projects | Support |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Hobby | Free | 10,000 | 1,000 / month | 1 | Community |
|
||||
| Starter | $20 / month | 50,000 | 5,000 / month | 1 | Community |
|
||||
|
||||
Billing runs through your Vercel account; Mem0 never charges your card directly. You can upgrade or downgrade anytime from the installation's settings in the Vercel dashboard.
|
||||
|
||||
## Managing the integration
|
||||
|
||||
- **Change plan:** open the installation in your Vercel dashboard and update the billing plan.
|
||||
- **Open in Mem0:** use the **Open in Mem0** link to sign in to the Mem0 dashboard for this installation.
|
||||
- **Uninstall:** removing the integration deactivates the API key and tears down the resource. Any usage for the current period is invoiced before removal.
|
||||
|
||||
## Notes
|
||||
|
||||
- The integration creates a **new** Mem0 account for the installation. Linking an existing Mem0 account is not supported yet.
|
||||
- The injected `MEM0_API_KEY` is scoped to the org and project created for this installation, so you do not manage keys by hand.
|
||||
|
||||
## Related
|
||||
|
||||
- [Platform quickstart](/platform/quickstart)
|
||||
- [Vercel AI SDK integration](/integrations/vercel-ai-sdk)
|
||||
+3
-1
@@ -257,12 +257,13 @@ If the user is on a pre-current major (Python < 2, TS < 3, or a Platform call st
|
||||
- [ChatDev](https://docs.mem0.ai/integrations/chatdev) [Platform]: Use when the user is on ChatDev.
|
||||
- [Hermes](https://docs.mem0.ai/integrations/hermes) [Both]: Use when the user is on Hermes.
|
||||
- [Pi Agent](https://docs.mem0.ai/integrations/pi-agent) [Platform]: Use when adding persistent memory to Pi Agent with the Mem0 plugin.
|
||||
- [DeepSeek Harness](https://docs.mem0.ai/integrations/dsh-mem0) [Platform]: Use when adding persistent memory to the DeepSeek Harness (Cordis) agent via the Mem0 plugin.
|
||||
- [DeepSeek Harness](https://docs.mem0.ai/integrations/deepseek-plugin) [Platform]: Use when adding persistent memory to the DeepSeek Harness (Cordis) agent via the Mem0 plugin.
|
||||
- [OpenAI Agents SDK](https://docs.mem0.ai/integrations/openai-agents-sdk) [Platform]: Use when the user is on the OpenAI Agents SDK.
|
||||
- [Google AI ADK](https://docs.mem0.ai/integrations/google-ai-adk) [Platform]: Use when the user is on Google's Agent Development Kit.
|
||||
- [Mastra](https://docs.mem0.ai/integrations/mastra) [Platform]: Use when the user is on Mastra (TypeScript).
|
||||
- [OpenClaw](https://docs.mem0.ai/integrations/openclaw) [Both]: Use when wiring Mem0 into Claude Code or editors via OpenClaw.
|
||||
- [Vercel AI SDK](https://docs.mem0.ai/integrations/vercel-ai-sdk) [Both]: Use when the user is on the Vercel AI SDK.
|
||||
- [Vercel](https://docs.mem0.ai/integrations/vercel) [Platform]: Use when deploying on Vercel and installing Mem0 from the Vercel Marketplace.
|
||||
- [Strands Agents](https://docs.mem0.ai/integrations/strands) [Both]: Use when the user is on AWS Strands and wants a native MemoryStore.
|
||||
|
||||
### AI Coding Tools
|
||||
@@ -299,6 +300,7 @@ If the user is on a pre-current major (Python < 2, TS < 3, or a Platform call st
|
||||
- [Partition Memories by Entity](https://docs.mem0.ai/cookbooks/essentials/entity-partitioning-playbook) [Both]: Use when isolating multi-tenant memories.
|
||||
- [Tagging and Organizing Memories](https://docs.mem0.ai/cookbooks/essentials/tagging-and-organizing-memories) [Both]: Use when memory taxonomy matters.
|
||||
- [Exporting Memories](https://docs.mem0.ai/cookbooks/essentials/exporting-memories) [Both]: Use when backing up or migrating memory data.
|
||||
- [Give Agents Shared Working Memory](https://docs.mem0.ai/cookbooks/essentials/agent-memory-insurance-claims) [Platform]: Use when a shared record must outlive the individual using it, such as a claim, ticket, or account that multiple people work over time.
|
||||
|
||||
### AI Companions
|
||||
- [Quickstart Demo](https://docs.mem0.ai/cookbooks/companions/quickstart-demo) [Both]: Use when showing the smallest end-to-end companion.
|
||||
|
||||
@@ -0,0 +1,842 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Agent Memory: An Insurance Claims Cookbook\n",
|
||||
"\n",
|
||||
"This notebook builds a claims workflow on top of Mem0 where two kinds of memory live side by side:\n",
|
||||
"\n",
|
||||
"- **A claims adjuster's own preferences**: drafting style, communication rules, the lines of coverage they focus on. These follow the adjuster from claim to claim and are private to them.\n",
|
||||
"- **A claim's working memory**: the decisions made on one specific claim and why, plus durable facts about that claim. This is shared by every adjuster who opens the claim, because a claim outlives any single person handling it.\n",
|
||||
"\n",
|
||||
"Mem0 keeps these apart using two identifiers on every `add()` call, `user_id` for the adjuster and `agent_id` for the claim, plus two separate sets of extraction rules, `custom_instructions` for the adjuster shelf and `agent_custom_instructions` for the claim shelf. This mirrors the id convention used in Mem0's CoCounsel example, where a lawyer maps to `user_id` and a case maps to `agent_id`. Here the adjuster is the person, and the claim is the shared case file.\n",
|
||||
"\n",
|
||||
"This notebook was executed live against the Mem0 Platform API. Every output below is a real response, not a mock.\n",
|
||||
"\n",
|
||||
"For the narrative walkthrough of this same pattern, see the [Give Agents Shared Working Memory](https://docs.mem0.ai/cookbooks/essentials/agent-memory-insurance-claims) cookbook page."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {
|
||||
"execution": {
|
||||
"iopub.execute_input": "2026-08-27T09:46:08.774876Z",
|
||||
"iopub.status.busy": "2026-08-27T09:46:08.774699Z",
|
||||
"iopub.status.idle": "2026-08-27T09:46:09.471287Z",
|
||||
"shell.execute_reply": "2026-08-27T09:46:09.470775Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Note: you may need to restart the kernel to use updated packages.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"%pip install -q mem0ai"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {
|
||||
"execution": {
|
||||
"iopub.execute_input": "2026-08-27T09:46:09.472860Z",
|
||||
"iopub.status.busy": "2026-08-27T09:46:09.472717Z",
|
||||
"iopub.status.idle": "2026-08-27T09:46:14.361722Z",
|
||||
"shell.execute_reply": "2026-08-27T09:46:14.361005Z"
|
||||
}
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import json\n",
|
||||
"import os\n",
|
||||
"import time\n",
|
||||
"\n",
|
||||
"from mem0 import MemoryClient\n",
|
||||
"\n",
|
||||
"os.environ[\"MEM0_API_KEY\"] = \"YOUR_MEM0_API_KEY\"\n",
|
||||
"client = MemoryClient()\n",
|
||||
"\n",
|
||||
"ADJUSTER_SARAH = \"adjuster_sarah_cookbook_demo\"\n",
|
||||
"ADJUSTER_MARCUS = \"adjuster_marcus_cookbook_demo\"\n",
|
||||
"CLAIM_AGENT = \"claims_agent_clm48211_cookbook_demo\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. The mapping\n",
|
||||
"\n",
|
||||
"Claim CLM-48211 is a commercial property fire loss at a Meridian Logistics warehouse. Sarah is the adjuster who opened it.\n",
|
||||
"\n",
|
||||
"| Mem0 identifier | Maps to | What lives there |\n",
|
||||
"|---|---|---|\n",
|
||||
"| `user_id` | An individual adjuster, e.g. Sarah | Her drafting style, her communication rules, durable across every claim she works |\n",
|
||||
"| `agent_id` | One claim's agent instance, e.g. `claims_agent_clm48211` | Decisions made on this claim and why, plus durable facts about it, shared by anyone who opens the claim |\n",
|
||||
"\n",
|
||||
"Every `add()` call below passes both `user_id=ADJUSTER_SARAH` and `agent_id=CLAIM_AGENT` at once. Mem0 does not require you to pick a shelf ahead of time, it extracts facts from the conversation and routes each one against the rules for its shelf."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. Configure the two rule sets\n",
|
||||
"\n",
|
||||
"`custom_instructions` governs memories that land under `user_id`. `agent_custom_instructions` is a second, optional rule set that governs memories that land under `agent_id`. Until you set it, agent-scoped memories fall back to `custom_instructions`, which is why the next cell sets both at once.\n",
|
||||
"\n",
|
||||
"Before changing anything, save what the project already has, so it can be restored at the end of this notebook."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {
|
||||
"execution": {
|
||||
"iopub.execute_input": "2026-08-27T09:46:14.364467Z",
|
||||
"iopub.status.busy": "2026-08-27T09:46:14.364243Z",
|
||||
"iopub.status.idle": "2026-08-27T09:46:14.692628Z",
|
||||
"shell.execute_reply": "2026-08-27T09:46:14.692110Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"had custom_instructions: True\n",
|
||||
"had agent_custom_instructions: False\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"project_snapshot = client.project.get(fields=[\"custom_instructions\", \"agent_custom_instructions\"])\n",
|
||||
"print(\"had custom_instructions:\", bool(project_snapshot.get(\"custom_instructions\")))\n",
|
||||
"print(\"had agent_custom_instructions:\", bool(project_snapshot.get(\"agent_custom_instructions\")))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {
|
||||
"execution": {
|
||||
"iopub.execute_input": "2026-08-27T09:46:14.694276Z",
|
||||
"iopub.status.busy": "2026-08-27T09:46:14.694156Z",
|
||||
"iopub.status.idle": "2026-08-27T09:46:15.051548Z",
|
||||
"shell.execute_reply": "2026-08-27T09:46:15.050970Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'message': 'Updated custom instructions and agent custom instructions'}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"USER_INSTRUCTIONS = \"\"\"Your Task: Track what a claims adjuster needs remembered about themselves, not about any single claim.\n",
|
||||
"\n",
|
||||
"Information to Extract:\n",
|
||||
"1. Drafting and communication style:\n",
|
||||
" - Tone, formatting, and length preferences for letters and summaries\n",
|
||||
" - Rules for how the assistant should write on their behalf\n",
|
||||
"\n",
|
||||
"2. Working preferences:\n",
|
||||
" - Coverage lines or claim types they focus on\n",
|
||||
" - How they like updates delivered\n",
|
||||
"\n",
|
||||
"Guidelines:\n",
|
||||
"- Store preferences that should carry over to every claim this adjuster works, not just the current one.\n",
|
||||
"\n",
|
||||
"Exclude:\n",
|
||||
"- Decisions made on a specific claim (settle vs litigate, reserve changes, subrogation, coverage determinations)\n",
|
||||
"- Policy numbers, coverage limits, or other one-off lookups\n",
|
||||
"- Greetings, small talk, and weather\"\"\"\n",
|
||||
"\n",
|
||||
"AGENT_INSTRUCTIONS = \"\"\"Your Task: Track what matters about this specific claim, regardless of which adjuster is working it.\n",
|
||||
"\n",
|
||||
"Information to Extract:\n",
|
||||
"1. Claim decisions and reasoning:\n",
|
||||
" - Settle vs litigate calls\n",
|
||||
" - Reserve changes and why\n",
|
||||
" - Subrogation decisions\n",
|
||||
" - Coverage determinations\n",
|
||||
"\n",
|
||||
"2. Durable claim facts:\n",
|
||||
" - Cause of loss and other facts that stay true for the life of the claim\n",
|
||||
"\n",
|
||||
"Guidelines:\n",
|
||||
"- Store facts that the next adjuster to open this claim needs to see.\n",
|
||||
"\n",
|
||||
"Exclude:\n",
|
||||
"- Personal style or communication preferences of whichever adjuster is talking\n",
|
||||
"- One-off lookups that go stale (current weather, a single quoted estimate)\n",
|
||||
"- Greetings and small talk\"\"\"\n",
|
||||
"\n",
|
||||
"update_response = client.project.update(\n",
|
||||
" custom_instructions=USER_INSTRUCTIONS,\n",
|
||||
" agent_custom_instructions=AGENT_INSTRUCTIONS,\n",
|
||||
")\n",
|
||||
"print(update_response)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. Day 1: Sarah works the claim\n",
|
||||
"\n",
|
||||
"`add()` on the Platform is asynchronous, it returns a `PENDING` status and an `event_id`, and extraction finishes in the background. A production agent does not need to wait for it, but this notebook does, so the next cell reads its own writes. The small polling helper below is only useful for a demo like this one."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {
|
||||
"execution": {
|
||||
"iopub.execute_input": "2026-08-27T09:46:15.053514Z",
|
||||
"iopub.status.busy": "2026-08-27T09:46:15.053386Z",
|
||||
"iopub.status.idle": "2026-08-27T09:46:15.057906Z",
|
||||
"shell.execute_reply": "2026-08-27T09:46:15.057402Z"
|
||||
}
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def wait_for_event(client, event_id, timeout=90, poll_every=2):\n",
|
||||
" deadline = time.time() + timeout\n",
|
||||
" while time.time() < deadline:\n",
|
||||
" event = client.client.get(f\"/v1/event/{event_id}/\").json()\n",
|
||||
" if event[\"status\"] in (\"SUCCEEDED\", \"FAILED\"):\n",
|
||||
" return event\n",
|
||||
" time.sleep(poll_every)\n",
|
||||
" raise TimeoutError(f\"Event {event_id} did not finish in {timeout}s\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def add_and_wait(client, messages, **kwargs):\n",
|
||||
" response = client.add(messages, **kwargs)\n",
|
||||
" event = wait_for_event(client, response[\"event_id\"])\n",
|
||||
" if event[\"status\"] == \"FAILED\":\n",
|
||||
" raise RuntimeError(f\"Add failed: {event.get('error')}\")\n",
|
||||
" return event\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def summarize(event):\n",
|
||||
" print(\"status:\", event[\"status\"], \"| created:\", len(event.get(\"results\", [])))\n",
|
||||
" for r in event.get(\"results\", []):\n",
|
||||
" print(\" -\", r[\"data\"][\"memory\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Sarah starts the day by pulling a number off the policy."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {
|
||||
"execution": {
|
||||
"iopub.execute_input": "2026-08-27T09:46:15.059720Z",
|
||||
"iopub.status.busy": "2026-08-27T09:46:15.059580Z",
|
||||
"iopub.status.idle": "2026-08-27T09:46:23.223178Z",
|
||||
"shell.execute_reply": "2026-08-27T09:46:23.222458Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"[\n",
|
||||
" {\n",
|
||||
" \"id\": \"c76bf47b-c5bf-4893-95d5-6ae8c5a4177b\",\n",
|
||||
" \"data\": {\n",
|
||||
" \"memory\": \"The building coverage limit for claim CLM-48211 under the Meridian Logistics policy is $2,400,000 with a $25,000 deductible\"\n",
|
||||
" },\n",
|
||||
" \"event\": \"ADD\",\n",
|
||||
" \"user_id\": \"adjuster_sarah_cookbook_demo\",\n",
|
||||
" \"event_end\": null,\n",
|
||||
" \"event_date\": null,\n",
|
||||
" \"event_start\": null,\n",
|
||||
" \"memory_type\": null,\n",
|
||||
" \"plan_status\": null,\n",
|
||||
" \"attributed_to\": \"assistant\"\n",
|
||||
" }\n",
|
||||
"]\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"event = add_and_wait(\n",
|
||||
" client,\n",
|
||||
" [\n",
|
||||
" {\"role\": \"user\", \"content\": \"What's the building coverage limit on the Meridian Logistics policy for claim CLM-48211?\"},\n",
|
||||
" {\"role\": \"assistant\", \"content\": \"Checking the policy now. The building coverage limit is $2,400,000 with a $25,000 deductible.\"},\n",
|
||||
" ],\n",
|
||||
" user_id=ADJUSTER_SARAH,\n",
|
||||
" agent_id=CLAIM_AGENT,\n",
|
||||
")\n",
|
||||
"print(json.dumps(event[\"results\"], indent=2))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Notice the created record's own `user_id` field. That field just echoes who the call was made for, it is not proof of which shelf the memory actually landed on. `attributed_to` is closer, but the only reliable check is asking Mem0 directly with `get_all()`, which section 5 does. Keep that in mind through the rest of this notebook, a coverage limit like this one turns out to be exactly the kind of durable claim fact `agent_custom_instructions` asks Mem0 to keep, so it is worth checking where it really ends up.\n",
|
||||
"\n",
|
||||
"Next, a real decision with the reasoning behind it."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {
|
||||
"execution": {
|
||||
"iopub.execute_input": "2026-08-27T09:46:23.225503Z",
|
||||
"iopub.status.busy": "2026-08-27T09:46:23.225354Z",
|
||||
"iopub.status.idle": "2026-08-27T09:46:50.042538Z",
|
||||
"shell.execute_reply": "2026-08-27T09:46:50.041648Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"status: SUCCEEDED | created: 2\n",
|
||||
" - Reserve for claim CLM-48211 was increased to $1.8 million to cover a full rebuild, as indicated by the structural engineer's report\n",
|
||||
" - The structural engineer's report for claim CLM-48211 states that the warehouse requires a full rebuild rather than a repair\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"event = add_and_wait(\n",
|
||||
" client,\n",
|
||||
" [\n",
|
||||
" {\"role\": \"user\", \"content\": \"I'm raising the reserve on CLM-48211 to $1.8 million. The structural engineer's report says the warehouse needs a full rebuild, not a repair.\"},\n",
|
||||
" {\"role\": \"assistant\", \"content\": \"Got it. Reserve raised to $1.8 million on CLM-48211 for a full rebuild, per the structural engineer's report. I'll keep that on file for the claim.\"},\n",
|
||||
" ],\n",
|
||||
" user_id=ADJUSTER_SARAH,\n",
|
||||
" agent_id=CLAIM_AGENT,\n",
|
||||
")\n",
|
||||
"summarize(event)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Then a personal rule for how Sarah wants things written."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {
|
||||
"execution": {
|
||||
"iopub.execute_input": "2026-08-27T09:46:50.044560Z",
|
||||
"iopub.status.busy": "2026-08-27T09:46:50.044403Z",
|
||||
"iopub.status.idle": "2026-08-27T09:46:58.104329Z",
|
||||
"shell.execute_reply": "2026-08-27T09:46:58.103578Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"status: SUCCEEDED | created: 1\n",
|
||||
" - User prefers that any drafted documents contain no em dashes and that coverage position letters be limited to one page\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"event = add_and_wait(\n",
|
||||
" client,\n",
|
||||
" [\n",
|
||||
" {\"role\": \"user\", \"content\": \"Also, for anything you draft for me: no em dashes, and keep coverage position letters to one page.\"},\n",
|
||||
" {\"role\": \"assistant\", \"content\": \"Understood. No em dashes, and coverage letters kept to one page, for everything I draft for you.\"},\n",
|
||||
" ],\n",
|
||||
" user_id=ADJUSTER_SARAH,\n",
|
||||
" agent_id=CLAIM_AGENT,\n",
|
||||
")\n",
|
||||
"summarize(event)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"And finally, small talk that neither rule set wants."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {
|
||||
"execution": {
|
||||
"iopub.execute_input": "2026-08-27T09:46:58.106438Z",
|
||||
"iopub.status.busy": "2026-08-27T09:46:58.106284Z",
|
||||
"iopub.status.idle": "2026-08-27T09:47:04.002795Z",
|
||||
"shell.execute_reply": "2026-08-27T09:47:04.002018Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"status: SUCCEEDED | created: 0\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"event = add_and_wait(\n",
|
||||
" client,\n",
|
||||
" [\n",
|
||||
" {\"role\": \"user\", \"content\": \"Thanks. Quick one, what's the weather like in Chicago right now?\"},\n",
|
||||
" {\"role\": \"assistant\", \"content\": \"It's 72 degrees and clear in Chicago right now.\"},\n",
|
||||
" ],\n",
|
||||
" user_id=ADJUSTER_SARAH,\n",
|
||||
" agent_id=CLAIM_AGENT,\n",
|
||||
")\n",
|
||||
"summarize(event)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. One message, two shelves\n",
|
||||
"\n",
|
||||
"Mem0 does not split a memory across `user_id` and `agent_id`, each extracted fact is attributed to one shelf or the other. When both identifiers are passed to the same `add()` call, Mem0 decides per fact which rule set it matches, so a single exchange that mixes a claim decision with a personal preference comes back split correctly without you doing the routing by hand."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"metadata": {
|
||||
"execution": {
|
||||
"iopub.execute_input": "2026-08-27T09:47:04.005189Z",
|
||||
"iopub.status.busy": "2026-08-27T09:47:04.005036Z",
|
||||
"iopub.status.idle": "2026-08-27T09:47:28.624576Z",
|
||||
"shell.execute_reply": "2026-08-27T09:47:28.623569Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"status: SUCCEEDED | created: 2\n",
|
||||
" - User prefers claim summaries to be written as bullet points instead of paragraphs\n",
|
||||
" - Assistant noted that subrogation will be pursued against Voss Electrical, with the fire marshal's report identifying faulty wiring as the ignition source\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"event = add_and_wait(\n",
|
||||
" client,\n",
|
||||
" [\n",
|
||||
" {\"role\": \"user\", \"content\": \"I've decided to pursue subrogation against Voss Electrical, the contractor who rewired the warehouse six months ago. The fire marshal's report points to faulty wiring as the ignition source. Also, from now on keep my claim summaries to bullet points, not paragraphs.\"},\n",
|
||||
" {\"role\": \"assistant\", \"content\": \"Noted both. Pursuing subrogation against Voss Electrical based on the fire marshal's faulty-wiring finding, and I'll keep your summaries in bullet points going forward.\"},\n",
|
||||
" ],\n",
|
||||
" user_id=ADJUSTER_SARAH,\n",
|
||||
" agent_id=CLAIM_AGENT,\n",
|
||||
")\n",
|
||||
"summarize(event)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"One exchange produced two memories on two different shelves: a bullet-point formatting preference for Sarah, and a subrogation decision for the claim. Notice the cause of loss (faulty wiring) never got its own memory this run, Mem0 folded it into the subrogation memory's reasoning. Extraction makes judgment calls like that, so read what was actually stored instead of assuming."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. Verify the split\n",
|
||||
"\n",
|
||||
"`get_all()` with a `filters` dict is the reliable way to check where memories actually landed, scoped to one shelf at a time."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"metadata": {
|
||||
"execution": {
|
||||
"iopub.execute_input": "2026-08-27T09:47:28.626951Z",
|
||||
"iopub.status.busy": "2026-08-27T09:47:28.626768Z",
|
||||
"iopub.status.idle": "2026-08-27T09:47:29.109030Z",
|
||||
"shell.execute_reply": "2026-08-27T09:47:29.108411Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"2 memories on Sarah's shelf\n",
|
||||
"- User prefers claim summaries to be written as bullet points instead of paragraphs\n",
|
||||
"- User prefers that any drafted documents contain no em dashes and that coverage position letters be limited to one page\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def dump(results, empty_label=\"(none)\"):\n",
|
||||
" if not results:\n",
|
||||
" print(empty_label)\n",
|
||||
" return\n",
|
||||
" for r in results:\n",
|
||||
" print(\"-\", r[\"memory\"])\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"sarah_all = client.get_all(filters={\"user_id\": ADJUSTER_SARAH})\n",
|
||||
"print(f\"{sarah_all['count']} memories on Sarah's shelf\")\n",
|
||||
"dump(sarah_all[\"results\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"metadata": {
|
||||
"execution": {
|
||||
"iopub.execute_input": "2026-08-27T09:47:29.110873Z",
|
||||
"iopub.status.busy": "2026-08-27T09:47:29.110737Z",
|
||||
"iopub.status.idle": "2026-08-27T09:47:29.597248Z",
|
||||
"shell.execute_reply": "2026-08-27T09:47:29.596174Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"4 memories on the claim's shelf\n",
|
||||
"- Assistant noted that subrogation will be pursued against Voss Electrical, with the fire marshal's report identifying faulty wiring as the ignition source\n",
|
||||
"- The structural engineer's report for claim CLM-48211 states that the warehouse requires a full rebuild rather than a repair\n",
|
||||
"- Reserve for claim CLM-48211 was increased to $1.8 million to cover a full rebuild, as indicated by the structural engineer's report\n",
|
||||
"- The building coverage limit for claim CLM-48211 under the Meridian Logistics policy is $2,400,000 with a $25,000 deductible\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"claim_all = client.get_all(filters={\"agent_id\": CLAIM_AGENT})\n",
|
||||
"print(f\"{claim_all['count']} memories on the claim's shelf\")\n",
|
||||
"dump(claim_all[\"results\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Sarah's shelf holds two personal style rules and nothing about the claim. The claim's shelf holds four claim facts, the coverage limit, the reserve change, the engineer's rebuild finding, and the subrogation call with the cause of loss folded in, and nothing about how Sarah likes things written. The small talk from section 3 is on neither shelf, it was correctly dropped."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. Day 2\n",
|
||||
"\n",
|
||||
"Sarah comes back the next day and asks for a status update. A search scoped with `OR` across both identifiers pulls from both shelves at once, ranked by relevance to the query."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"metadata": {
|
||||
"execution": {
|
||||
"iopub.execute_input": "2026-08-27T09:47:29.599237Z",
|
||||
"iopub.status.busy": "2026-08-27T09:47:29.599098Z",
|
||||
"iopub.status.idle": "2026-08-27T09:47:30.099318Z",
|
||||
"shell.execute_reply": "2026-08-27T09:47:30.098227Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"- Reserve for claim CLM-48211 was increased to $1.8 million to cover a full rebuild, as indicated by the structural engineer's report\n",
|
||||
"- The structural engineer's report for claim CLM-48211 states that the warehouse requires a full rebuild rather than a repair\n",
|
||||
"- The building coverage limit for claim CLM-48211 under the Meridian Logistics policy is $2,400,000 with a $25,000 deductible\n",
|
||||
"- User prefers that any drafted documents contain no em dashes and that coverage position letters be limited to one page\n",
|
||||
"- User prefers claim summaries to be written as bullet points instead of paragraphs\n",
|
||||
"- Assistant noted that subrogation will be pursued against Voss Electrical, with the fire marshal's report identifying faulty wiring as the ignition source\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"status_results = client.search(\n",
|
||||
" \"Draft a status update on claim CLM-48211.\",\n",
|
||||
" filters={\"OR\": [{\"user_id\": ADJUSTER_SARAH}, {\"agent_id\": CLAIM_AGENT}]},\n",
|
||||
")\n",
|
||||
"dump(status_results[\"results\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"One query, one search call, both shelves, correctly ranked. This is what makes the split useful day to day, an agent drafting on Sarah's behalf does not need to know ahead of time which shelf a given fact lives on."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. A second adjuster opens the same claim\n",
|
||||
"\n",
|
||||
"Marcus has never worked with Sarah and has never touched CLM-48211 before. His personal shelf should be empty."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 14,
|
||||
"metadata": {
|
||||
"execution": {
|
||||
"iopub.execute_input": "2026-08-27T09:47:30.101305Z",
|
||||
"iopub.status.busy": "2026-08-27T09:47:30.101161Z",
|
||||
"iopub.status.idle": "2026-08-27T09:47:30.528115Z",
|
||||
"shell.execute_reply": "2026-08-27T09:47:30.527276Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"0 memories on Marcus's personal shelf\n",
|
||||
"(none)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"marcus_personal = client.get_all(filters={\"user_id\": ADJUSTER_MARCUS})\n",
|
||||
"print(f\"{marcus_personal['count']} memories on Marcus's personal shelf\")\n",
|
||||
"dump(marcus_personal[\"results\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"But the claim's shelf is not private to Sarah, it belongs to the claim. When Marcus opens CLM-48211 cold, he sees exactly what she left behind."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 15,
|
||||
"metadata": {
|
||||
"execution": {
|
||||
"iopub.execute_input": "2026-08-27T09:47:30.530364Z",
|
||||
"iopub.status.busy": "2026-08-27T09:47:30.530205Z",
|
||||
"iopub.status.idle": "2026-08-27T09:47:31.017665Z",
|
||||
"shell.execute_reply": "2026-08-27T09:47:31.016750Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"- Reserve for claim CLM-48211 was increased to $1.8 million to cover a full rebuild, as indicated by the structural engineer's report\n",
|
||||
"- The structural engineer's report for claim CLM-48211 states that the warehouse requires a full rebuild rather than a repair\n",
|
||||
"- The building coverage limit for claim CLM-48211 under the Meridian Logistics policy is $2,400,000 with a $25,000 deductible\n",
|
||||
"- Assistant noted that subrogation will be pursued against Voss Electrical, with the fire marshal's report identifying faulty wiring as the ignition source\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"marcus_view = client.search(\n",
|
||||
" \"What's going on with CLM-48211?\",\n",
|
||||
" filters={\"OR\": [{\"user_id\": ADJUSTER_MARCUS}, {\"agent_id\": CLAIM_AGENT}]},\n",
|
||||
")\n",
|
||||
"dump(marcus_view[\"results\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Four claim facts, zero of Sarah's personal preferences. Marcus gets full context on the claim without inheriting a single one of her style rules, exactly the separation the two rule sets were set up to produce."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. Review, edit, and forget\n",
|
||||
"\n",
|
||||
"Sarah can inspect and prune her own shelf directly."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 16,
|
||||
"metadata": {
|
||||
"execution": {
|
||||
"iopub.execute_input": "2026-08-27T09:47:31.019536Z",
|
||||
"iopub.status.busy": "2026-08-27T09:47:31.019388Z",
|
||||
"iopub.status.idle": "2026-08-27T09:47:31.495240Z",
|
||||
"shell.execute_reply": "2026-08-27T09:47:31.494231Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"fab356e1-1603-407e-9f99-24a56c08ff6c - User prefers claim summaries to be written as bullet points instead of paragraphs\n",
|
||||
"895f5c47-64bf-44c7-a36f-b37697be5037 - User prefers that any drafted documents contain no em dashes and that coverage position letters be limited to one page\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"sarah_review = client.get_all(filters={\"user_id\": ADJUSTER_SARAH})\n",
|
||||
"for r in sarah_review[\"results\"]:\n",
|
||||
" print(r[\"id\"], \"-\", r[\"memory\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The one-page rule is stale, the carrier switched templates. `delete()` removes a memory by id."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 17,
|
||||
"metadata": {
|
||||
"execution": {
|
||||
"iopub.execute_input": "2026-08-27T09:47:31.497312Z",
|
||||
"iopub.status.busy": "2026-08-27T09:47:31.497155Z",
|
||||
"iopub.status.idle": "2026-08-27T09:47:32.650786Z",
|
||||
"shell.execute_reply": "2026-08-27T09:47:32.650172Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"- User prefers claim summaries to be written as bullet points instead of paragraphs\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"stale = next(r for r in sarah_review[\"results\"] if \"one page\" in r[\"memory\"].lower() or \"letter\" in r[\"memory\"].lower())\n",
|
||||
"client.delete(stale[\"id\"])\n",
|
||||
"\n",
|
||||
"sarah_after = client.get_all(filters={\"user_id\": ADJUSTER_SARAH})\n",
|
||||
"dump(sarah_after[\"results\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Worth noticing: the no-em-dashes rule and the one-page rule were extracted into a single combined memory, so deleting it for the stale part removed the em-dash rule too. `delete()` removes a whole memory, not a substring of one. To edit a fact in place without losing its neighbors, use `client.update(memory_id, text=...)` to rewrite the text instead of deleting it.\n",
|
||||
"\n",
|
||||
"This notebook created demo data under three throwaway identifiers and changed the project's shared extraction rules to run its examples. Clean both up before you go, delete the demo memories, then restore whatever `custom_instructions` and `agent_custom_instructions` the project had before this notebook touched them."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 18,
|
||||
"metadata": {
|
||||
"execution": {
|
||||
"iopub.execute_input": "2026-08-27T09:47:32.652435Z",
|
||||
"iopub.status.busy": "2026-08-27T09:47:32.652317Z",
|
||||
"iopub.status.idle": "2026-08-27T09:47:34.183374Z",
|
||||
"shell.execute_reply": "2026-08-27T09:47:34.182528Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"demo memories deleted\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"project instructions restored\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"client.delete_all(user_id=ADJUSTER_SARAH)\n",
|
||||
"client.delete_all(user_id=ADJUSTER_MARCUS)\n",
|
||||
"client.delete_all(agent_id=CLAIM_AGENT)\n",
|
||||
"print(\"demo memories deleted\")\n",
|
||||
"\n",
|
||||
"client.project.update(\n",
|
||||
" custom_instructions=project_snapshot.get(\"custom_instructions\") or \"\",\n",
|
||||
" agent_custom_instructions=project_snapshot.get(\"agent_custom_instructions\") or \"\",\n",
|
||||
")\n",
|
||||
"print(\"project instructions restored\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. Recap\n",
|
||||
"\n",
|
||||
"| Concept | This notebook | CoCounsel example |\n",
|
||||
"|---|---|---|\n",
|
||||
"| `user_id` | One claims adjuster, durable across every claim they work | One lawyer |\n",
|
||||
"| `agent_id` | One claim's agent instance, shared by every adjuster who opens it | One case |\n",
|
||||
"| `custom_instructions` | Governs the adjuster's personal shelf | Governs the lawyer's personal shelf |\n",
|
||||
"| `agent_custom_instructions` | Governs the claim's shared shelf | Governs the case's shared shelf |\n",
|
||||
"\n",
|
||||
"The pattern is not specific to insurance. Anywhere a person works inside a longer-lived, shared context, a support agent inside a ticket, a sales rep inside an account, a caseworker inside a file, the same two identifiers and two rule sets separate what belongs to the person from what belongs to the thing they are working on."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"provenance": []
|
||||
},
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.14.6"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0
|
||||
}
|
||||
@@ -9,12 +9,12 @@ Agent and editor integrations. Each subdirectory is self-contained: its own `pac
|
||||
| `mem0-plugin/` | Claude Code / Cursor / Codex plugin | none | none | pytest |
|
||||
| `mem0-plugin/.opencode-plugin/` | `@mem0/opencode-plugin` | Bun | none | tsc type-check |
|
||||
| `pi-agent-plugin/` | `@mem0/pi-agent-plugin` | tsup | none | vitest |
|
||||
| `dsh-mem0/` | `@mem0/dsh-mem0` | tsup (ESM) | none | vitest |
|
||||
| `deepseek-plugin/` | `@mem0/deepseek-plugin` | tsup (ESM) | none | vitest |
|
||||
| `n8n-nodes-mem0/` | `@mem0/n8n-nodes-mem0` | tsc | ESLint (n8n-nodes-base) | none |
|
||||
| `zapier-mem0/` | `@mem0/zapier` | tsc | none | offline unit tests + `zapier validate` |
|
||||
| `strands-mem0/` | `strands-mem0` (PyPI) | hatch | Ruff + mypy | pytest |
|
||||
| `mem0-strands/` | `mem0-strands` (PyPI) | hatch | Ruff + mypy | pytest |
|
||||
|
||||
pnpm everywhere except `.opencode-plugin/` (Bun) and `strands-mem0/` (Python: pip / hatch). Never npm, never yarn.
|
||||
pnpm everywhere except `.opencode-plugin/` (Bun) and `mem0-strands/` (Python: pip / hatch). Never npm, never yarn.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -41,10 +41,10 @@ Run the type check after every TypeScript change: `pnpm run typecheck` or `tsc -
|
||||
|
||||
- **`vercel-ai-sdk/`** wraps the Vercel AI SDK through a `createMem0` provider. Integrations for AI-SDK repos go through this wrapper, not raw `MemoryClient`.
|
||||
- **`mem0-plugin/`** connects Claude Code, Cursor, and Codex to the MCP server at `mcp.mem0.ai` and installs lifecycle hooks for automatic memory capture. Exposes 9 MCP tools: `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities`.
|
||||
- **`openclaw/`**, **`pi-agent-plugin/`**, **`dsh-mem0/`** are editor and agent plugins with the same shape. `dsh-mem0/` registers Mem0 search/add tools as a native DeepSeek Harness (Cordis) plugin.
|
||||
- **`openclaw/`**, **`pi-agent-plugin/`**, **`deepseek-plugin/`** are editor and agent plugins with the same shape. `deepseek-plugin/` registers Mem0 search/add tools as a native DeepSeek Harness (Cordis) plugin.
|
||||
- **`n8n-nodes-mem0/`** is an n8n community node: add, search, get, update, delete.
|
||||
- **`zapier-mem0/`** is a Zapier Platform CLI app: add, search, get, delete. It deploys to Zapier, not npm, so it is **not** in the release router. Deploy it with `gh workflow run zapier-mem0-cd.yml --ref main` (needs the `ZAPIER_DEPLOY_KEY` secret).
|
||||
- **`strands-mem0/`** is a native Strands `MemoryStore` (Python, published to PyPI as `strands-mem0`). It plugs into the Strands `MemoryManager` for automatic recall and server-side extraction, over the hosted Mem0 platform or self-hosted Mem0 OSS. The package lives under `strands-mem0/python/`.
|
||||
- **`mem0-strands/`** is a native Strands `MemoryStore` (Python, published to PyPI as `mem0-strands`). It plugs into the Strands `MemoryManager` for automatic recall and server-side extraction, over the hosted Mem0 platform or self-hosted Mem0 OSS. The package lives under `mem0-strands/python/`.
|
||||
|
||||
## Adding an integration
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# dsh-mem0
|
||||
# deepseek-plugin
|
||||
|
||||
[Mem0](https://mem0.ai) long-term memory as a native [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (Cordis) plugin.
|
||||
|
||||
@@ -18,7 +18,7 @@ A Cordis plugin is a module exporting `apply(ctx, config)`. This one declares `i
|
||||
```
|
||||
[ mem0ai SDK ] <-- managed memory, owned by Mem0
|
||||
|
|
||||
[ dsh-mem0: apply(ctx) -> ctx.tools.register(...) ] <-- this package
|
||||
[ deepseek-plugin: apply(ctx) -> ctx.tools.register(...) ] <-- this package
|
||||
|
|
||||
[ DeepSeek Harness ] <-- the agent, loaded via cordis.yml
|
||||
```
|
||||
@@ -27,7 +27,7 @@ A Cordis plugin is a module exporting `apply(ctx, config)`. This one declares `i
|
||||
|
||||
1. Build the plugin:
|
||||
```sh
|
||||
cd integrations/dsh-mem0
|
||||
cd integrations/deepseek-plugin
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
@@ -37,7 +37,7 @@ A Cordis plugin is a module exporting `apply(ctx, config)`. This one declares `i
|
||||
```
|
||||
3. Point Harness at it. Copy `cordis.example.yml`, set the absolute path to `dist/index.js` and your `userId`, then:
|
||||
```sh
|
||||
pnpm dsh web --patch ./integrations/dsh-mem0/cordis.example.yml
|
||||
pnpm dsh web --patch ./integrations/deepseek-plugin/cordis.example.yml
|
||||
```
|
||||
4. Open http://127.0.0.1:3080 and ask the agent to remember something, then recall it in a later turn.
|
||||
|
||||
@@ -55,6 +55,8 @@ For a Mem0 Platform on-prem or dedicated deployment, point `config.host` at that
|
||||
|
||||
Writes are tagged `source="DEEPSEEK_HARNESS"` so Mem0's backend can attribute usage to this integration. For it to surface by name (rather than bucketing into `OTHERS`), `DEEPSEEK_HARNESS` must be present in the backend's `KNOWN_EVENT_SOURCES` allowlist, a one-line platform change matching the existing `ZAPIER` / `STRANDS` sources.
|
||||
|
||||
The plugin also sends anonymous usage events (which tool ran, duration, result counts, coarse failure kind) so Mem0 can tell how the plugin is used and where it breaks. Queries, memory text, and entity ids are never sent. Turn it off with `MEM0_TELEMETRY=false`.
|
||||
|
||||
## Status
|
||||
|
||||
Developer preview. Tracks the DeepSeek Harness v0.1 plugin API (`@deepseek-ai/cordis`, `@deepseek-ai/dsh-tools`), which is young and moving; pin versions once it stabilizes. Auto-capture (store turns without an explicit tool call) and auto-recall (inject memory into the prompt at assembly) are planned once the harness session/assembly event API is confirmed.
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
# Example DeepSeek Harness config that loads dsh-mem0 alongside the built-in
|
||||
# Example DeepSeek Harness config that loads deepseek-plugin alongside the built-in
|
||||
# tools plugin. Load with:
|
||||
#
|
||||
# pnpm dsh web --patch ./integrations/dsh-mem0/cordis.example.yml
|
||||
# pnpm dsh web --patch ./integrations/deepseek-plugin/cordis.example.yml
|
||||
#
|
||||
# The tools plugin (and its system-prompt dependency) must be present, because
|
||||
# the memory tools contribute schemas the system prompt renders.
|
||||
@@ -12,7 +12,7 @@
|
||||
- id: mem0
|
||||
# Absolute path to the built plugin (run `pnpm build` first), or point
|
||||
# at src/index.ts when running through tsx during development.
|
||||
name: "/absolute/path/to/mem0/integrations/dsh-mem0/dist/index.js"
|
||||
name: "/absolute/path/to/mem0/integrations/deepseek-plugin/dist/index.js"
|
||||
config:
|
||||
# apiKey is read from the MEM0_API_KEY env var when omitted here.
|
||||
userId: "your-user-id"
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@mem0/dsh-mem0",
|
||||
"version": "0.1.0",
|
||||
"name": "@mem0/deepseek-plugin",
|
||||
"version": "0.1.1",
|
||||
"description": "Mem0 long-term memory as a native DeepSeek Harness (Cordis) plugin.",
|
||||
"type": "module",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mem0ai/mem0",
|
||||
"directory": "integrations/dsh-mem0"
|
||||
"directory": "integrations/deepseek-plugin"
|
||||
},
|
||||
"keywords": [
|
||||
"deepseek-harness",
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* dsh-mem0: Mem0 long-term memory as a native DeepSeek Harness (Cordis) plugin.
|
||||
* deepseek-plugin: Mem0 long-term memory as a native DeepSeek Harness (Cordis) plugin.
|
||||
*
|
||||
* Registers two agent-callable tools backed by the Mem0 SDK:
|
||||
* - `search_memory` recalls facts relevant to a query
|
||||
@@ -16,6 +16,7 @@ import { MemoryClient } from "mem0ai";
|
||||
import { formatMemoryList, formatAddResult } from "./formatting.ts";
|
||||
import { truncateOutput } from "./output.ts";
|
||||
import { resolveSearchFilters, resolveAddParams } from "./scoping.ts";
|
||||
import { captureEvent, errorKind } from "./telemetry.ts";
|
||||
|
||||
export const name = "mem0";
|
||||
export const inject = ["tools"];
|
||||
@@ -69,11 +70,11 @@ const scopeParams = {
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
const apiKey = config.apiKey ?? process.env.MEM0_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("dsh-mem0: set config.apiKey or the MEM0_API_KEY env var");
|
||||
throw new Error("deepseek-plugin: set config.apiKey or the MEM0_API_KEY env var");
|
||||
}
|
||||
const userId = config.userId;
|
||||
if (!userId) {
|
||||
throw new Error("dsh-mem0: config.userId is required");
|
||||
throw new Error("deepseek-plugin: config.userId is required");
|
||||
}
|
||||
|
||||
const client = new MemoryClient({
|
||||
@@ -81,6 +82,8 @@ export function apply(ctx: Context, config: Config): void {
|
||||
...(config.host ? { host: config.host } : {}),
|
||||
});
|
||||
|
||||
captureEvent("deepseek.plugin.mounted", { has_host: Boolean(config.host) }, client);
|
||||
|
||||
// Recall. The platform rejects top-level entity params on search, so scope
|
||||
// goes inside `filters` (unlike add below, which takes them top-level).
|
||||
ctx.tools.register(
|
||||
@@ -99,11 +102,36 @@ export function apply(ctx: Context, config: Config): void {
|
||||
output: textOutput,
|
||||
async execute({ query, limit, userId: u, agentId, runId }) {
|
||||
const filters = resolveSearchFilters({ userId: u, agentId, runId }, userId);
|
||||
const topK = limit && limit > 0 ? limit : DEFAULT_SEARCH_LIMIT;
|
||||
const started = Date.now();
|
||||
try {
|
||||
const topK = limit && limit > 0 ? limit : DEFAULT_SEARCH_LIMIT;
|
||||
const { results } = await client.search(query, { filters, topK });
|
||||
captureEvent(
|
||||
"deepseek.tool.search_memory",
|
||||
{
|
||||
success: true,
|
||||
duration_ms: Date.now() - started,
|
||||
top_k: topK,
|
||||
result_count: results?.length ?? 0,
|
||||
query_chars: query.length,
|
||||
scope_overridden: Boolean(u && u !== userId),
|
||||
has_agent_id: Boolean(agentId),
|
||||
has_run_id: Boolean(runId),
|
||||
},
|
||||
client,
|
||||
);
|
||||
return truncateOutput(formatMemoryList(results ?? []));
|
||||
} catch (err) {
|
||||
captureEvent(
|
||||
"deepseek.tool.search_memory",
|
||||
{
|
||||
success: false,
|
||||
duration_ms: Date.now() - started,
|
||||
top_k: topK,
|
||||
error_kind: errorKind(err),
|
||||
},
|
||||
client,
|
||||
);
|
||||
return `search_memory failed: ${err instanceof Error ? err.message : String(err)}`;
|
||||
}
|
||||
},
|
||||
@@ -123,13 +151,37 @@ export function apply(ctx: Context, config: Config): void {
|
||||
output: textOutput,
|
||||
async execute({ text, userId: u, agentId, runId }) {
|
||||
const addParams = resolveAddParams({ userId: u, agentId, runId }, userId);
|
||||
const started = Date.now();
|
||||
try {
|
||||
const result = await client.add([{ role: "user", content: text }], {
|
||||
...addParams,
|
||||
source: SOURCE,
|
||||
});
|
||||
captureEvent(
|
||||
"deepseek.tool.add_memory",
|
||||
{
|
||||
success: true,
|
||||
duration_ms: Date.now() - started,
|
||||
text_chars: text.length,
|
||||
memory_count: Array.isArray(result) ? result.length : 0,
|
||||
scope_overridden: Boolean(u && u !== userId),
|
||||
has_agent_id: Boolean(agentId),
|
||||
has_run_id: Boolean(runId),
|
||||
},
|
||||
client,
|
||||
);
|
||||
return truncateOutput(formatAddResult(result));
|
||||
} catch (err) {
|
||||
captureEvent(
|
||||
"deepseek.tool.add_memory",
|
||||
{
|
||||
success: false,
|
||||
duration_ms: Date.now() - started,
|
||||
text_chars: text.length,
|
||||
error_kind: errorKind(err),
|
||||
},
|
||||
client,
|
||||
);
|
||||
return `add_memory failed: ${err instanceof Error ? err.message : String(err)}`;
|
||||
}
|
||||
},
|
||||
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Anonymous usage telemetry for the DeepSeek Harness plugin.
|
||||
*
|
||||
* Fire-and-forget PostHog events over native fetch, batched and flushed every
|
||||
* 5 seconds, at 10 queued events, or on process exit. Never throws, never logs.
|
||||
*
|
||||
* Events carry only tool names, durations, counts, and coarse failure kinds:
|
||||
* never queries, memory text, filters, or API keys.
|
||||
*
|
||||
* Disable with MEM0_TELEMETRY=false.
|
||||
*/
|
||||
import { randomUUID } from "node:crypto";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
||||
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
|
||||
const POSTHOG_BATCH_URL = "https://us.i.posthog.com/batch/";
|
||||
const FLUSH_INTERVAL_MS = 5_000;
|
||||
const FLUSH_THRESHOLD = 10;
|
||||
const SEND_TIMEOUT_MS = 3_000;
|
||||
|
||||
const PLUGIN_VERSION = ((): string => {
|
||||
try {
|
||||
return JSON.parse(
|
||||
fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"),
|
||||
).version;
|
||||
} catch {
|
||||
return "unknown";
|
||||
}
|
||||
})();
|
||||
|
||||
/** The Mem0 SDK resolves this to the account email once ping() lands, so events join other Mem0 surfaces. */
|
||||
export interface TelemetryIdentity {
|
||||
telemetryId?: string;
|
||||
}
|
||||
|
||||
let queue: Record<string, unknown>[] = [];
|
||||
let flushTimer: ReturnType<typeof setInterval> | undefined;
|
||||
let exitHandlerInstalled = false;
|
||||
let cachedAnonymousId: string | undefined;
|
||||
let identified = false;
|
||||
|
||||
function identityPath(): string {
|
||||
return path.join(os.homedir(), ".mem0", "deepseek-plugin-telemetry.json");
|
||||
}
|
||||
|
||||
export function isTelemetryEnabled(): boolean {
|
||||
const value = process.env.MEM0_TELEMETRY?.toLowerCase();
|
||||
return value !== "false" && value !== "0" && value !== "no" && value !== "off";
|
||||
}
|
||||
|
||||
function anonymousId(): string {
|
||||
if (cachedAnonymousId) return cachedAnonymousId;
|
||||
try {
|
||||
const stored = JSON.parse(fs.readFileSync(identityPath(), "utf-8"));
|
||||
if (typeof stored.anonymousId === "string" && stored.anonymousId) {
|
||||
cachedAnonymousId = stored.anonymousId;
|
||||
return cachedAnonymousId!;
|
||||
}
|
||||
} catch {
|
||||
/* first run, or an unreadable identity file */
|
||||
}
|
||||
const created = `deepseek-anon-${randomUUID().replace(/-/g, "")}`;
|
||||
try {
|
||||
const target = identityPath();
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
fs.writeFileSync(target, JSON.stringify({ anonymousId: created }), "utf-8");
|
||||
} catch {
|
||||
/* an unwritable home directory must not break the plugin */
|
||||
}
|
||||
cachedAnonymousId = created;
|
||||
return created;
|
||||
}
|
||||
|
||||
/** Merge the pre-ping anonymous history into the account once the email resolves. */
|
||||
function identifyEvent(distinctId: string): Record<string, unknown> | undefined {
|
||||
if (identified || distinctId.startsWith("deepseek-anon-")) return undefined;
|
||||
identified = true;
|
||||
let storedAnonymousId: string | undefined;
|
||||
try {
|
||||
storedAnonymousId = JSON.parse(fs.readFileSync(identityPath(), "utf-8")).anonymousId;
|
||||
fs.unlinkSync(identityPath());
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
cachedAnonymousId = undefined;
|
||||
if (!storedAnonymousId) return undefined;
|
||||
return {
|
||||
event: "$identify",
|
||||
distinct_id: distinctId,
|
||||
properties: { $anon_distinct_id: storedAnonymousId, $lib: "posthog-node" },
|
||||
};
|
||||
}
|
||||
|
||||
export function flushEvents(): void {
|
||||
if (queue.length === 0) return;
|
||||
const batch = queue;
|
||||
queue = [];
|
||||
fetch(POSTHOG_BATCH_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ api_key: POSTHOG_API_KEY, batch }),
|
||||
signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
|
||||
}).catch(() => {
|
||||
/* telemetry never surfaces its own failures */
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleFlush(): void {
|
||||
if (!flushTimer) {
|
||||
flushTimer = setInterval(flushEvents, FLUSH_INTERVAL_MS);
|
||||
flushTimer.unref?.();
|
||||
}
|
||||
if (!exitHandlerInstalled) {
|
||||
exitHandlerInstalled = true;
|
||||
process.on("beforeExit", flushEvents);
|
||||
}
|
||||
}
|
||||
|
||||
export function errorKind(error: unknown): string {
|
||||
const text = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
||||
if (text.includes("timeout") || text.includes("aborted")) return "timeout";
|
||||
if (text.includes("401") || text.includes("403") || text.includes("unauthor")) return "auth";
|
||||
if (text.includes("429") || text.includes("rate limit")) return "rate-limited";
|
||||
if (/50[0234]/.test(text)) return "server-error";
|
||||
if (text.includes("400") || text.includes("422")) return "bad-request";
|
||||
if (text.includes("fetch failed") || text.includes("enotfound")) return "network";
|
||||
return error instanceof Error ? error.constructor.name : "other";
|
||||
}
|
||||
|
||||
export function captureEvent(
|
||||
event: string,
|
||||
properties: Record<string, unknown>,
|
||||
client: TelemetryIdentity,
|
||||
): void {
|
||||
if (!isTelemetryEnabled()) return;
|
||||
try {
|
||||
const distinctId = client.telemetryId || anonymousId();
|
||||
const identify = identifyEvent(distinctId);
|
||||
if (identify) queue.push(identify);
|
||||
queue.push({
|
||||
event,
|
||||
distinct_id: distinctId,
|
||||
properties: {
|
||||
source: "DEEPSEEK_HARNESS",
|
||||
language: "node",
|
||||
plugin_version: PLUGIN_VERSION,
|
||||
node_version: process.version,
|
||||
os: process.platform,
|
||||
$process_person_profile: false,
|
||||
$lib: "posthog-node",
|
||||
...properties,
|
||||
},
|
||||
});
|
||||
scheduleFlush();
|
||||
if (queue.length >= FLUSH_THRESHOLD) flushEvents();
|
||||
} catch {
|
||||
/* telemetry never breaks a tool call */
|
||||
}
|
||||
}
|
||||
|
||||
export function _queueForTesting(): Record<string, unknown>[] {
|
||||
return queue;
|
||||
}
|
||||
|
||||
export function _resetForTesting(): void {
|
||||
queue = [];
|
||||
clearInterval(flushTimer);
|
||||
flushTimer = undefined;
|
||||
cachedAnonymousId = undefined;
|
||||
identified = false;
|
||||
}
|
||||
+5
@@ -35,9 +35,12 @@ function applyAndCollect(config: Config): Map<string, RegisteredTool> {
|
||||
}
|
||||
|
||||
let savedKey: string | undefined;
|
||||
let savedTelemetry: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
savedKey = process.env.MEM0_API_KEY;
|
||||
savedTelemetry = process.env.MEM0_TELEMETRY;
|
||||
process.env.MEM0_TELEMETRY = "false";
|
||||
mockSearch.mockReset();
|
||||
mockAdd.mockReset();
|
||||
});
|
||||
@@ -45,6 +48,8 @@ beforeEach(() => {
|
||||
afterEach(() => {
|
||||
if (savedKey === undefined) delete process.env.MEM0_API_KEY;
|
||||
else process.env.MEM0_API_KEY = savedKey;
|
||||
if (savedTelemetry === undefined) delete process.env.MEM0_TELEMETRY;
|
||||
else process.env.MEM0_TELEMETRY = savedTelemetry;
|
||||
});
|
||||
|
||||
describe("apply() config validation", () => {
|
||||
@@ -0,0 +1,218 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
||||
const mockSearch = vi.fn();
|
||||
const mockAdd = vi.fn();
|
||||
vi.mock("mem0ai", () => ({
|
||||
MemoryClient: class {
|
||||
telemetryId = "dev@example.com";
|
||||
search = mockSearch;
|
||||
add = mockAdd;
|
||||
},
|
||||
}));
|
||||
vi.mock("@deepseek-ai/dsh-tools", () => ({ defineTool: (options: unknown) => options }));
|
||||
|
||||
import { apply, type Config } from "../src/index.ts";
|
||||
import {
|
||||
captureEvent,
|
||||
errorKind,
|
||||
flushEvents,
|
||||
isTelemetryEnabled,
|
||||
_queueForTesting,
|
||||
_resetForTesting,
|
||||
} from "../src/telemetry.ts";
|
||||
|
||||
interface RegisteredTool {
|
||||
name: string;
|
||||
execute(args: unknown, exec: unknown): Promise<unknown>;
|
||||
}
|
||||
|
||||
function applyAndCollect(config: Config): Map<string, RegisteredTool> {
|
||||
const tools = new Map<string, RegisteredTool>();
|
||||
apply({ tools: { register: (t: RegisteredTool) => tools.set(t.name, t) } } as never, config);
|
||||
return tools;
|
||||
}
|
||||
|
||||
function queued(): Record<string, any>[] {
|
||||
return _queueForTesting() as Record<string, any>[];
|
||||
}
|
||||
|
||||
let home: string;
|
||||
let savedHome: string | undefined;
|
||||
let savedTelemetry: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
savedHome = process.env.HOME;
|
||||
savedTelemetry = process.env.MEM0_TELEMETRY;
|
||||
delete process.env.MEM0_TELEMETRY;
|
||||
home = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-deepseek-"));
|
||||
process.env.HOME = home;
|
||||
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true }));
|
||||
mockSearch.mockReset();
|
||||
mockAdd.mockReset();
|
||||
_resetForTesting();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
_resetForTesting();
|
||||
vi.unstubAllGlobals();
|
||||
fs.rmSync(home, { recursive: true, force: true });
|
||||
if (savedHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = savedHome;
|
||||
if (savedTelemetry === undefined) delete process.env.MEM0_TELEMETRY;
|
||||
else process.env.MEM0_TELEMETRY = savedTelemetry;
|
||||
});
|
||||
|
||||
describe("opt-out", () => {
|
||||
it("queues nothing for every documented off value", () => {
|
||||
for (const value of ["false", "0", "no", "OFF"]) {
|
||||
process.env.MEM0_TELEMETRY = value;
|
||||
expect(isTelemetryEnabled()).toBe(false);
|
||||
captureEvent("deepseek.tool.search_memory", {}, { telemetryId: "dev@example.com" });
|
||||
}
|
||||
expect(queued()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("is on by default", () => {
|
||||
expect(isTelemetryEnabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("identity", () => {
|
||||
it("keys events on the account email the SDK resolved", () => {
|
||||
captureEvent("deepseek.tool.add_memory", {}, { telemetryId: "dev@example.com" });
|
||||
expect(queued()[0].distinct_id).toBe("dev@example.com");
|
||||
});
|
||||
|
||||
it("falls back to a persisted anonymous id before the SDK has pinged", () => {
|
||||
captureEvent("deepseek.tool.add_memory", {}, {});
|
||||
const first = queued()[0].distinct_id as string;
|
||||
expect(first).toMatch(/^deepseek-anon-/);
|
||||
|
||||
_resetForTesting();
|
||||
captureEvent("deepseek.tool.add_memory", {}, {});
|
||||
expect(queued()[0].distinct_id).toBe(first);
|
||||
});
|
||||
|
||||
it("aliases the anonymous history onto the email exactly once", () => {
|
||||
captureEvent("deepseek.tool.add_memory", {}, {});
|
||||
const anonymous = queued()[0].distinct_id;
|
||||
_resetForTesting();
|
||||
|
||||
captureEvent("deepseek.tool.add_memory", {}, { telemetryId: "dev@example.com" });
|
||||
const [identify, event] = queued();
|
||||
expect(identify.event).toBe("$identify");
|
||||
expect(identify.distinct_id).toBe("dev@example.com");
|
||||
expect(identify.properties.$anon_distinct_id).toBe(anonymous);
|
||||
expect(event.distinct_id).toBe("dev@example.com");
|
||||
|
||||
captureEvent("deepseek.tool.add_memory", {}, { telemetryId: "dev@example.com" });
|
||||
expect(queued().filter((e) => e.event === "$identify")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("event shape", () => {
|
||||
it("stamps every event with the integration source and runtime baseline", () => {
|
||||
captureEvent("deepseek.tool.search_memory", { success: true }, { telemetryId: "d@e.com" });
|
||||
const { properties } = queued()[0];
|
||||
expect(properties.source).toBe("DEEPSEEK_HARNESS");
|
||||
expect(properties.language).toBe("node");
|
||||
expect(properties.$process_person_profile).toBe(false);
|
||||
expect(properties.os).toBe(process.platform);
|
||||
expect(properties.plugin_version).not.toBe("unknown");
|
||||
expect(properties.success).toBe(true);
|
||||
});
|
||||
|
||||
it("flushes one batch to PostHog and empties the queue", async () => {
|
||||
captureEvent("deepseek.tool.search_memory", {}, { telemetryId: "d@e.com" });
|
||||
flushEvents();
|
||||
|
||||
const [url, init] = (fetch as unknown as ReturnType<typeof vi.fn>).mock.calls[0];
|
||||
expect(url).toContain("posthog.com");
|
||||
expect(JSON.parse(init.body).batch).toHaveLength(1);
|
||||
expect(queued()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("never throws when the network is gone", () => {
|
||||
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("fetch failed")));
|
||||
captureEvent("deepseek.tool.search_memory", {}, { telemetryId: "d@e.com" });
|
||||
expect(() => flushEvents()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("errorKind", () => {
|
||||
it("buckets failures without leaking the message", () => {
|
||||
expect(errorKind(new Error("Request failed with status 429"))).toBe("rate-limited");
|
||||
expect(errorKind(new Error("401 Unauthorized"))).toBe("auth");
|
||||
expect(errorKind(new Error("503 Service Unavailable"))).toBe("server-error");
|
||||
expect(errorKind(new Error("The operation was aborted due to timeout"))).toBe("timeout");
|
||||
expect(errorKind(new Error("fetch failed"))).toBe("network");
|
||||
expect(errorKind(new Error("token sk-abcdef is invalid"))).toBe("Error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("tool instrumentation", () => {
|
||||
it("records a mount, then a successful search with its shape but not its query", async () => {
|
||||
mockSearch.mockResolvedValue({ results: [{ id: "m1", memory: "Likes tea" }] });
|
||||
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
|
||||
|
||||
await tools.get("search_memory")!.execute({ query: "what drink", limit: 3 }, {});
|
||||
|
||||
const [mounted, search] = queued();
|
||||
expect(mounted.event).toBe("deepseek.plugin.mounted");
|
||||
expect(mounted.properties.has_host).toBe(false);
|
||||
expect(search.event).toBe("deepseek.tool.search_memory");
|
||||
expect(search.properties).toMatchObject({
|
||||
success: true,
|
||||
top_k: 3,
|
||||
result_count: 1,
|
||||
query_chars: 10,
|
||||
scope_overridden: false,
|
||||
has_agent_id: false,
|
||||
has_run_id: false,
|
||||
});
|
||||
expect(typeof search.properties.duration_ms).toBe("number");
|
||||
expect(JSON.stringify(search)).not.toContain("what drink");
|
||||
expect(JSON.stringify(search)).not.toContain("Likes tea");
|
||||
});
|
||||
|
||||
it("records a failed search with a coarse error kind", async () => {
|
||||
mockSearch.mockRejectedValue(new Error("429 rate limit exceeded"));
|
||||
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
|
||||
|
||||
await tools.get("search_memory")!.execute({ query: "x" }, {});
|
||||
|
||||
const search = queued().at(-1)!;
|
||||
expect(search.properties).toMatchObject({ success: false, error_kind: "rate-limited" });
|
||||
});
|
||||
|
||||
it("records a write with its size but not its text", async () => {
|
||||
mockAdd.mockResolvedValue([{ id: "m1", memory: "Fact" }]);
|
||||
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
|
||||
|
||||
await tools.get("add_memory")!.execute({ text: "secret fact", userId: "alice" }, {});
|
||||
|
||||
const add = queued().at(-1)!;
|
||||
expect(add.event).toBe("deepseek.tool.add_memory");
|
||||
expect(add.properties).toMatchObject({
|
||||
success: true,
|
||||
text_chars: 11,
|
||||
memory_count: 1,
|
||||
scope_overridden: true,
|
||||
});
|
||||
expect(JSON.stringify(add)).not.toContain("secret fact");
|
||||
expect(JSON.stringify(add)).not.toContain("alice");
|
||||
});
|
||||
|
||||
it("records a failed write and still returns the graceful failure line", async () => {
|
||||
mockAdd.mockRejectedValue(new Error("boom"));
|
||||
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
|
||||
|
||||
const out = await tools.get("add_memory")!.execute({ text: "x" }, {});
|
||||
|
||||
expect(out).toContain("add_memory failed");
|
||||
expect(queued().at(-1)!.properties).toMatchObject({ success: false, error_kind: "Error" });
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
strands-mem0
|
||||
mem0-strands
|
||||
Copyright 2026 Mem0
|
||||
|
||||
This product includes software developed at Mem0 (https://mem0.ai).
|
||||
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<h1>strands-mem0</h1>
|
||||
<h1>mem0-strands</h1>
|
||||
<h3>Persistent long-term memory for Strands Agents, backed by Mem0</h3>
|
||||
<p>
|
||||
A community <a href="https://strandsagents.com/">Strands Agents</a> integration that plugs
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
---
|
||||
|
||||
`strands-mem0` gives [Strands](https://github.com/strands-agents/sdk-python) agents durable memory
|
||||
`mem0-strands` gives [Strands](https://github.com/strands-agents/sdk-python) agents durable memory
|
||||
that survives across sessions, backed by [Mem0](https://mem0.ai). Where the
|
||||
`mem0_memory` tool is called explicitly by the model, `Mem0MemoryStore` plugs into the **agent loop**
|
||||
directly: the manager recalls context and injects it automatically, and writes new memories, either
|
||||
@@ -22,7 +22,7 @@ verbatim or by extracting facts from the conversation.
|
||||
## Install
|
||||
|
||||
```bash
|
||||
pip install strands-mem0
|
||||
pip install mem0-strands
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -30,7 +30,7 @@ pip install strands-mem0
|
||||
```python
|
||||
from strands import Agent
|
||||
from strands.memory import MemoryManager
|
||||
from strands_mem0 import Mem0MemoryStore
|
||||
from mem0_strands import Mem0MemoryStore
|
||||
|
||||
# Recall + write, distilling facts from the conversation via Mem0's server-side extraction.
|
||||
store = Mem0MemoryStore(user_id="alex", writable=True, extraction=True)
|
||||
@@ -77,6 +77,13 @@ For the model-called tool (`store` / `retrieve` / `get` / `delete`), use the
|
||||
[`mem0_memory`](https://github.com/strands-agents/tools) tool from `strands-agents-tools`. The store and
|
||||
the tool share one Mem0 backend and namespace.
|
||||
|
||||
## Telemetry
|
||||
|
||||
The store sends anonymous usage events (store configuration, operation, duration,
|
||||
result counts, coarse failure kind) over the Mem0 SDK's existing telemetry client,
|
||||
tagged `source="STRANDS"`. Queries, memory text, message content, entity ids, and
|
||||
metadata are never sent. Turn it off with `MEM0_TELEMETRY=false`.
|
||||
|
||||
## Development
|
||||
|
||||
The package lives under [`python/`](python/) (monorepo-style layout matching the
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
# strands-mem0 (Python)
|
||||
# mem0-strands (Python)
|
||||
|
||||
Persistent long-term memory for [Strands Agents](https://github.com/strands-agents/sdk-python),
|
||||
backed by [Mem0](https://mem0.ai).
|
||||
@@ -6,7 +6,7 @@ backed by [Mem0](https://mem0.ai).
|
||||
See the [repository README](../README.md) for full usage. Quick start:
|
||||
|
||||
```bash
|
||||
pip install strands-mem0
|
||||
pip install mem0-strands
|
||||
```
|
||||
|
||||
As a `MemoryStore` that plugs into the agent loop (Strands >= 1.45):
|
||||
@@ -14,7 +14,7 @@ As a `MemoryStore` that plugs into the agent loop (Strands >= 1.45):
|
||||
```python
|
||||
from strands import Agent
|
||||
from strands.memory import MemoryManager
|
||||
from strands_mem0 import Mem0MemoryStore
|
||||
from mem0_strands import Mem0MemoryStore
|
||||
|
||||
store = Mem0MemoryStore(user_id="alex", writable=True, extraction=True)
|
||||
agent = Agent(memory_manager=MemoryManager(stores=[store]))
|
||||
@@ -32,6 +32,6 @@ hatch run prepare # format + lint + typecheck + test
|
||||
|
||||
## Release
|
||||
|
||||
Publish a GitHub release tagged `strands-mem0-v*` (e.g. `strands-mem0-v0.1.0`). The
|
||||
release router (`.github/workflows/release.yml`) dispatches `strands-mem0-cd.yml`,
|
||||
Publish a GitHub release tagged `mem0-strands-v*` (e.g. `mem0-strands-v0.1.0`). The
|
||||
release router (`.github/workflows/release.yml`) dispatches `mem0-strands-cd.yml`,
|
||||
which builds the wheel and publishes it to PyPI via trusted publishing (OIDC).
|
||||
+4
-4
@@ -3,8 +3,8 @@ requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "strands-mem0"
|
||||
version = "0.1.0"
|
||||
name = "mem0-strands"
|
||||
version = "0.1.1"
|
||||
description = "Persistent long-term memory for Strands agents, backed by Mem0."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
@@ -33,7 +33,7 @@ dependencies = [
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://mem0.ai"
|
||||
Documentation = "https://github.com/mem0ai/mem0/tree/main/integrations/strands-mem0#readme"
|
||||
Documentation = "https://github.com/mem0ai/mem0/tree/main/integrations/mem0-strands#readme"
|
||||
Repository = "https://github.com/mem0ai/mem0"
|
||||
Issues = "https://github.com/mem0ai/mem0/issues"
|
||||
|
||||
@@ -47,7 +47,7 @@ dev = [
|
||||
]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/strands_mem0"]
|
||||
packages = ["src/mem0_strands"]
|
||||
|
||||
[tool.hatch.envs.default]
|
||||
dependencies = [
|
||||
+3
-3
@@ -12,8 +12,8 @@ share one Mem0 backend and namespace.
|
||||
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
|
||||
from strands_mem0.client import Mem0ServiceClient
|
||||
from strands_mem0.store import Mem0MemoryStore
|
||||
from mem0_strands.client import Mem0ServiceClient
|
||||
from mem0_strands.store import Mem0MemoryStore
|
||||
|
||||
__all__ = [
|
||||
"Mem0MemoryStore",
|
||||
@@ -21,6 +21,6 @@ __all__ = [
|
||||
]
|
||||
|
||||
try:
|
||||
__version__ = version("strands-mem0")
|
||||
__version__ = version("mem0-strands")
|
||||
except PackageNotFoundError: # pragma: no cover - only when running from a source tree
|
||||
__version__ = "0.0.0+unknown"
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
"""A thin wrapper around the Mem0 SDK used by :class:`~strands_mem0.store.Mem0MemoryStore`.
|
||||
"""A thin wrapper around the Mem0 SDK used by :class:`~mem0_strands.store.Mem0MemoryStore`.
|
||||
|
||||
Both Mem0 backends -- the hosted platform (:class:`mem0.MemoryClient`) and
|
||||
self-hosted OSS (:class:`mem0.Memory`) -- expose the same call shape to the store:
|
||||
@@ -95,7 +95,7 @@ class Mem0ServiceClient:
|
||||
from mem0 import Memory
|
||||
except ImportError as err: # pragma: no cover - exercised via install docs
|
||||
raise ImportError(
|
||||
"The mem0ai package is required. Install it with: pip install 'strands-mem0'"
|
||||
"The mem0ai package is required. Install it with: pip install 'mem0-strands'"
|
||||
) from err
|
||||
self.mem0 = Memory.from_config(config)
|
||||
self.is_platform = False
|
||||
@@ -104,7 +104,7 @@ class Mem0ServiceClient:
|
||||
try:
|
||||
from mem0 import MemoryClient
|
||||
except ImportError as err: # pragma: no cover - exercised via install docs
|
||||
raise ImportError("The mem0ai package is required. Install it with: pip install 'strands-mem0'") from err
|
||||
raise ImportError("The mem0ai package is required. Install it with: pip install 'mem0-strands'") from err
|
||||
api_key = api_key or os.environ.get("MEM0_API_KEY")
|
||||
# MemoryClient(host=None) would override the SDK default with None, so only
|
||||
# pass host when the caller actually set one.
|
||||
+62
-6
@@ -21,7 +21,7 @@ Example:
|
||||
```python
|
||||
from strands import Agent
|
||||
from strands.memory import MemoryManager
|
||||
from strands_mem0 import Mem0MemoryStore
|
||||
from mem0_strands import Mem0MemoryStore
|
||||
|
||||
# Recall + write, with Mem0 extracting facts from the conversation server-side.
|
||||
store = Mem0MemoryStore(user_id="alex", writable=True, extraction=True)
|
||||
@@ -36,12 +36,14 @@ environment variable, or pass a Mem0 OSS ``config`` dict for a self-hosted backe
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from strands.memory import AddMessagesContext, MemoryEntry, MemoryStore, SearchOptions
|
||||
from strands.types.content import Message
|
||||
|
||||
from strands_mem0.client import Mem0ServiceClient
|
||||
from mem0_strands import telemetry
|
||||
from mem0_strands.client import Mem0ServiceClient
|
||||
|
||||
DEFAULT_MAX_SEARCH_RESULTS = 5
|
||||
# Entity fields that scope a memory in Mem0. At least one must be set.
|
||||
@@ -92,7 +94,7 @@ class Mem0MemoryStore(MemoryStore):
|
||||
api_key: Mem0 platform API key (defaults to ``$MEM0_API_KEY``).
|
||||
host: Mem0 platform base URL.
|
||||
config: Mem0 OSS config dict for a self-hosted backend.
|
||||
client: A pre-built :class:`~strands_mem0.client.Mem0ServiceClient`
|
||||
client: A pre-built :class:`~mem0_strands.client.Mem0ServiceClient`
|
||||
(for testing, or to wrap your own raw Mem0 client via
|
||||
``Mem0ServiceClient(client=...)``); when omitted, one is
|
||||
constructed lazily on first use from ``api_key`` / ``config``.
|
||||
@@ -134,6 +136,7 @@ class Mem0MemoryStore(MemoryStore):
|
||||
self._host = host
|
||||
self._config = config
|
||||
self._client = client
|
||||
self._recorded_init = False
|
||||
|
||||
@property
|
||||
def client(self) -> Mem0ServiceClient:
|
||||
@@ -144,8 +147,22 @@ class Mem0MemoryStore(MemoryStore):
|
||||
stores), so first use is deferred and always happens inside a worker thread
|
||||
via :func:`asyncio.to_thread`, never on the event loop.
|
||||
"""
|
||||
client_injected = self._client is not None
|
||||
if self._client is None:
|
||||
self._client = Mem0ServiceClient(api_key=self._api_key, host=self._host, config=self._config)
|
||||
if not self._recorded_init:
|
||||
self._recorded_init = True
|
||||
telemetry.record(
|
||||
"store.init",
|
||||
self._client,
|
||||
scopes=sorted(self.scope),
|
||||
writable=self.writable,
|
||||
extraction_enabled=bool(self.extraction),
|
||||
has_default_metadata=bool(self.metadata),
|
||||
max_search_results=self.max_search_results,
|
||||
host_overridden=bool(self._host),
|
||||
client_injected=client_injected,
|
||||
)
|
||||
return self._client
|
||||
|
||||
async def search(self, query: str, options: SearchOptions | None = None) -> list[MemoryEntry]:
|
||||
@@ -158,7 +175,13 @@ class Mem0MemoryStore(MemoryStore):
|
||||
|
||||
# ``self.client`` is resolved inside the thread so lazy construction (a
|
||||
# blocking call) does not run on the event loop.
|
||||
memories = await asyncio.to_thread(lambda: self.client.search_memories(query, self.scope, top_k))
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
memories = await asyncio.to_thread(lambda: self.client.search_memories(query, self.scope, top_k))
|
||||
except Exception as exc:
|
||||
self._record("store.search", started, False, top_k=top_k, error_kind=telemetry.error_kind(exc))
|
||||
raise
|
||||
self._record("store.search", started, True, top_k=top_k, result_count=len(memories))
|
||||
return [self._to_entry(memory) for memory in memories]
|
||||
|
||||
async def add(self, content: str, metadata: dict[str, Any] | None = None) -> Any:
|
||||
@@ -168,7 +191,14 @@ class Mem0MemoryStore(MemoryStore):
|
||||
Mem0 de-duplicates on the server.
|
||||
"""
|
||||
merged = self._merge_metadata(metadata)
|
||||
return await asyncio.to_thread(lambda: self.client.store_memory(content, self.scope, merged))
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
result = await asyncio.to_thread(lambda: self.client.store_memory(content, self.scope, merged))
|
||||
except Exception as exc:
|
||||
self._record("store.add", started, False, error_kind=telemetry.error_kind(exc))
|
||||
raise
|
||||
self._record("store.add", started, True, content_chars=len(content), has_metadata=bool(merged))
|
||||
return result
|
||||
|
||||
async def add_messages(self, messages: list[Message], context: AddMessagesContext | None = None) -> Any:
|
||||
"""Ingest raw conversation turns for Mem0 server-side extraction (``infer=True``).
|
||||
@@ -186,7 +216,33 @@ class Mem0MemoryStore(MemoryStore):
|
||||
payload.append({"role": message["role"], "content": text})
|
||||
if not payload:
|
||||
return None
|
||||
return await asyncio.to_thread(lambda: self.client.store_messages(payload, self.scope))
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
result = await asyncio.to_thread(lambda: self.client.store_messages(payload, self.scope))
|
||||
except Exception as exc:
|
||||
self._record("store.add_messages", started, False, error_kind=telemetry.error_kind(exc))
|
||||
raise
|
||||
self._record(
|
||||
"store.add_messages",
|
||||
started,
|
||||
True,
|
||||
message_count=len(messages),
|
||||
rendered_count=len(payload),
|
||||
total_chars=sum(len(turn["content"]) for turn in payload),
|
||||
)
|
||||
return result
|
||||
|
||||
def _record(self, event: str, started: float, success: bool, **properties: Any) -> None:
|
||||
"""Send one store telemetry event, timed from ``started``."""
|
||||
if self._client is None:
|
||||
return
|
||||
telemetry.record(
|
||||
event,
|
||||
self._client,
|
||||
success=success,
|
||||
duration_ms=round((time.perf_counter() - started) * 1000, 2),
|
||||
**properties,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _render_content(content: Any) -> str:
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Anonymous usage telemetry for the Strands memory store.
|
||||
|
||||
Events ride the Mem0 SDK's own PostHog client, so they need no extra dependency
|
||||
and join the account's other Mem0 usage: on the hosted platform the distinct id
|
||||
is the account email, and on a self-hosted OSS backend it is the machine-local
|
||||
anonymous id the SDK already keeps. Unlike the SDK's OSS hot-path events these
|
||||
are never sampled, because a store call is an agent-level action, not a loop.
|
||||
|
||||
Only names, counts, durations, and coarse failure kinds are sent: never queries,
|
||||
memory text, message content, entity ids, metadata, or API keys.
|
||||
|
||||
Opt out with MEM0_TELEMETRY=false.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from mem0.memory.telemetry import client_telemetry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mem0_strands.client import Mem0ServiceClient
|
||||
|
||||
SOURCE = "STRANDS"
|
||||
|
||||
try:
|
||||
_VERSION = version("mem0-strands")
|
||||
except PackageNotFoundError: # pragma: no cover - only when running from a source tree
|
||||
_VERSION = "0.0.0+unknown"
|
||||
|
||||
|
||||
def error_kind(exc: BaseException) -> str:
|
||||
"""Coarse, content-free label for a failure, safe to send."""
|
||||
text = f"{type(exc).__name__}: {exc}".lower()
|
||||
if "timeout" in text or "timed out" in text:
|
||||
return "timeout"
|
||||
if "401" in text or "403" in text or "unauthor" in text or "forbidden" in text:
|
||||
return "auth"
|
||||
if "429" in text or "rate limit" in text:
|
||||
return "rate-limited"
|
||||
if any(code in text for code in ("500", "502", "503", "504")):
|
||||
return "server-error"
|
||||
if "400" in text or "422" in text:
|
||||
return "bad-request"
|
||||
return type(exc).__name__
|
||||
|
||||
|
||||
def _distinct_id(client: Mem0ServiceClient) -> str | None:
|
||||
"""The account email to attribute events to, or None to fall back to the SDK's anonymous id."""
|
||||
email = getattr(client.mem0, "user_email", None)
|
||||
return email if isinstance(email, str) and email else None
|
||||
|
||||
|
||||
def record(event: str, client: Mem0ServiceClient, **properties: Any) -> None:
|
||||
"""Send one strands.* usage event. Never raises."""
|
||||
try:
|
||||
client_telemetry.capture_event(
|
||||
f"strands.{event}",
|
||||
{
|
||||
"source": SOURCE,
|
||||
"language": "python",
|
||||
"strands_store_version": _VERSION,
|
||||
"backend": "platform" if client.is_platform else "oss",
|
||||
**properties,
|
||||
},
|
||||
_distinct_id(client),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Keep the test suite from sending usage telemetry to the live PostHog project.
|
||||
|
||||
Set before ``mem0`` is imported: the SDK reads ``MEM0_TELEMETRY`` once, at import.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
os.environ["MEM0_TELEMETRY"] = "false"
|
||||
+1
-1
@@ -8,7 +8,7 @@ what the earlier permissive fakes did not do.
|
||||
|
||||
import pytest
|
||||
|
||||
from strands_mem0.client import Mem0ServiceClient, _extract_results, _is_platform_client
|
||||
from mem0_strands.client import Mem0ServiceClient, _extract_results, _is_platform_client
|
||||
|
||||
|
||||
class FakeMemoryClient:
|
||||
+2
-2
@@ -10,7 +10,7 @@ import pytest
|
||||
from strands.memory import MemoryEntry, MemoryStore
|
||||
from strands.memory.types import _has_method, _has_write_sink
|
||||
|
||||
from strands_mem0 import Mem0MemoryStore
|
||||
from mem0_strands import Mem0MemoryStore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -241,7 +241,7 @@ def test_client_constructed_lazily(monkeypatch):
|
||||
calls["n"] += 1
|
||||
self.api_key = api_key
|
||||
|
||||
monkeypatch.setattr("strands_mem0.store.Mem0ServiceClient", FakeClient)
|
||||
monkeypatch.setattr("mem0_strands.store.Mem0ServiceClient", FakeClient)
|
||||
|
||||
store = Mem0MemoryStore(user_id="alex", api_key="m0-x")
|
||||
assert calls["n"] == 0 # not built yet
|
||||
@@ -0,0 +1,172 @@
|
||||
"""Tests for the store's anonymous usage telemetry."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mem0_strands import Mem0MemoryStore, telemetry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client():
|
||||
"""A mocked Mem0ServiceClient that looks like the hosted platform."""
|
||||
client = MagicMock()
|
||||
client.is_platform = True
|
||||
client.mem0.user_email = "dev@example.com"
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def captured():
|
||||
"""Intercept the SDK PostHog client and collect (event, properties, distinct_id)."""
|
||||
with patch.object(telemetry, "client_telemetry") as posthog:
|
||||
events = []
|
||||
posthog.capture_event.side_effect = lambda event, properties, distinct_id=None: events.append(
|
||||
(event, properties, distinct_id)
|
||||
)
|
||||
yield events
|
||||
|
||||
|
||||
def make_store(mock_client, **kwargs):
|
||||
kwargs.setdefault("user_id", "alex")
|
||||
return Mem0MemoryStore(client=mock_client, **kwargs)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Event shape
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_every_event_carries_source_and_backend(mock_client, captured):
|
||||
assert make_store(mock_client).client is mock_client
|
||||
|
||||
event, properties, distinct_id = captured[0]
|
||||
assert event == "strands.store.init"
|
||||
assert properties["source"] == "STRANDS"
|
||||
assert properties["language"] == "python"
|
||||
assert properties["backend"] == "platform"
|
||||
assert properties["strands_store_version"]
|
||||
assert distinct_id == "dev@example.com"
|
||||
|
||||
|
||||
def test_oss_backend_is_labelled_and_falls_back_to_the_sdk_anonymous_id(captured):
|
||||
client = MagicMock()
|
||||
client.is_platform = False
|
||||
del client.mem0.user_email
|
||||
assert make_store(client).client is client
|
||||
|
||||
_, properties, distinct_id = captured[0]
|
||||
assert properties["backend"] == "oss"
|
||||
assert distinct_id is None
|
||||
|
||||
|
||||
def test_init_is_recorded_once_per_store(mock_client, captured):
|
||||
store = make_store(mock_client)
|
||||
assert store.client is store.client
|
||||
|
||||
assert [event for event, _, _ in captured] == ["strands.store.init"]
|
||||
|
||||
|
||||
def test_init_describes_configuration_without_scope_values(mock_client, captured):
|
||||
store = make_store(mock_client, agent_id="assistant", writable=False, extraction=True, metadata={"team": "core"})
|
||||
assert store.client is mock_client
|
||||
|
||||
_, properties, _ = captured[0]
|
||||
assert properties["scopes"] == ["agent_id", "user_id"]
|
||||
assert properties["writable"] is False
|
||||
assert properties["extraction_enabled"] is True
|
||||
assert properties["has_default_metadata"] is True
|
||||
assert "alex" not in str(properties)
|
||||
assert "assistant" not in str(properties)
|
||||
assert "core" not in str(properties)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-operation events
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_search_records_counts_but_never_the_query(mock_client, captured):
|
||||
mock_client.search_memories.return_value = [{"id": "m1", "memory": "Likes tea"}]
|
||||
|
||||
await make_store(mock_client).search("what does alex drink")
|
||||
|
||||
event, properties, _ = captured[-1]
|
||||
assert event == "strands.store.search"
|
||||
assert properties["success"] is True
|
||||
assert properties["result_count"] == 1
|
||||
assert properties["top_k"] == 5
|
||||
assert properties["duration_ms"] >= 0
|
||||
assert "drink" not in str(properties)
|
||||
assert "tea" not in str(properties)
|
||||
|
||||
|
||||
async def test_add_records_size_but_never_the_content(mock_client, captured):
|
||||
await make_store(mock_client).add("Alex prefers dark roast", metadata={"team": "core"})
|
||||
|
||||
event, properties, _ = captured[-1]
|
||||
assert event == "strands.store.add"
|
||||
assert properties["success"] is True
|
||||
assert properties["content_chars"] == len("Alex prefers dark roast")
|
||||
assert properties["has_metadata"] is True
|
||||
assert "roast" not in str(properties)
|
||||
|
||||
|
||||
async def test_add_messages_records_turn_counts_but_never_the_turns(mock_client, captured):
|
||||
messages = [
|
||||
{"role": "user", "content": [{"text": "hello"}]},
|
||||
{"role": "assistant", "content": [{"text": "hi there"}]},
|
||||
]
|
||||
|
||||
await make_store(mock_client).add_messages(messages)
|
||||
|
||||
event, properties, _ = captured[-1]
|
||||
assert event == "strands.store.add_messages"
|
||||
assert properties["message_count"] == 2
|
||||
assert properties["rendered_count"] == 2
|
||||
assert properties["total_chars"] == len("hello") + len("hi there")
|
||||
assert "hello" not in str(properties)
|
||||
|
||||
|
||||
async def test_empty_add_messages_records_nothing(mock_client, captured):
|
||||
assert await make_store(mock_client).add_messages([]) is None
|
||||
assert [event for event, _, _ in captured] == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Failures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_a_failed_search_records_a_coarse_error_kind_and_still_raises(mock_client, captured):
|
||||
mock_client.search_memories.side_effect = RuntimeError("HTTP 429 rate limit exceeded for key sk-secret")
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
await make_store(mock_client).search("anything")
|
||||
|
||||
event, properties, _ = captured[-1]
|
||||
assert event == "strands.store.search"
|
||||
assert properties["success"] is False
|
||||
assert properties["error_kind"] == "rate-limited"
|
||||
assert "sk-secret" not in str(properties)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("message", "expected"),
|
||||
[
|
||||
("request timed out", "timeout"),
|
||||
("401 Unauthorized", "auth"),
|
||||
("429 Too Many Requests", "rate-limited"),
|
||||
("503 Service Unavailable", "server-error"),
|
||||
("422 Unprocessable Entity", "bad-request"),
|
||||
("something else entirely", "ValueError"),
|
||||
],
|
||||
)
|
||||
def test_error_kind_buckets_failures(message, expected):
|
||||
assert telemetry.error_kind(ValueError(message)) == expected
|
||||
|
||||
|
||||
def test_a_broken_telemetry_backend_never_breaks_the_store(mock_client):
|
||||
with patch.object(telemetry, "client_telemetry") as posthog:
|
||||
posthog.capture_event.side_effect = RuntimeError("posthog is down")
|
||||
assert make_store(mock_client).client is mock_client
|
||||
@@ -1,24 +1,9 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
# TODO: Upgrade to latest pydantic version
|
||||
class RedisDBConfig(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
|
||||
|
||||
redis_url: str = Field(..., description="Redis URL")
|
||||
collection_name: str = Field("mem0", description="Collection name")
|
||||
embedding_model_dims: int = Field(1536, description="Embedding model dimensions")
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def validate_extra_fields(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
allowed_fields = set(cls.model_fields.keys())
|
||||
input_fields = set(values.keys())
|
||||
extra_fields = input_fields - allowed_fields
|
||||
if extra_fields:
|
||||
raise ValueError(
|
||||
f"Extra fields not allowed: {', '.join(extra_fields)}. Please input only the following fields: {', '.join(allowed_fields)}"
|
||||
)
|
||||
return values
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
Reference in New Issue
Block a user