# mem0 middleware example

One runnable demo (`oss_demo.py`) showing the
[mem0](https://github.com/mem0ai/mem0) middleware plugged into an
`agentscope.agent.Agent`. Drives two consecutive agent sessions for
the same `user_id` so mem0's cross-session memory effect is visible,
and prints each middleware contribution (retrieval / tool call /
write-back) inline so you can see when each path fires.

The demo defaults to the **OSS backend** (open-source mem0,
self-hosted via local Qdrant) with mem0 driven by AgentScope's own
DashScope chat + embedding model — no separate OpenAI key needed
by mem0. To run it against the hosted **mem0
Platform** instead, swap the `Mem0Middleware(...)` construction for
the alternative shown inline (look for the
``# For the hosted mem0 Platform, swap …`` comment in `oss_demo.py`)
— the rest of the demo is identical.

## Install

```bash
# mem0 is an optional AgentScope dependency — pull it via the extra:
pip install "agentscope[mem0]"      # resolves to mem0ai>=2.0.0,<3.0.0
# (equivalent to `pip install agentscope mem0ai>=2.0.0,<3.0.0`)

export DASHSCOPE_API_KEY=sk-...     # OSS path
# Platform path (only if you switch):
# export MEM0_API_KEY=m0-...
# export OPENAI_API_KEY=sk-...      # only needed if your agent's chat model is OpenAI
```

## Import path

`Mem0Middleware` is exported from the middleware package:

```python
from agentscope.middleware import Mem0Middleware
from agentscope.tool import Toolkit
```

## Three construction paths

```python
# 1. Models — build a local OSS AsyncMemory wired to your AgentScope
#    chat + embedding model. mem0 defaults for everything else.
Mem0Middleware(
    user_id="alice",
    chat_model=my_chat_model,
    embedding_model=my_embedding_model,
    mode="both",
)

# 2. Models + custom mem0_config — same as (1), but start from your
#    customized MemoryConfig (custom vector store, history DB,
#    reranker, ...). `chat_model` / `embedding_model` always WIN:
#    if mem0_config already specifies an .llm or .embedder, it gets
#    OVERWRITTEN by the AgentScope adapter built from your model.
#    Every other field of mem0_config (vector_store, history_db_path,
#    reranker, etc.) is preserved as-is.
Mem0Middleware(
    user_id="alice",
    chat_model=my_chat_model,
    embedding_model=my_embedding_model,
    mem0_config=MemoryConfig(
        vector_store=VectorStoreConfig(
            provider="qdrant",
            config={"host": "my-qdrant", "port": 6333},
        ),
        history_db_path="/data/mem0_history.db",
    ),
    mode="both",
)

# 3. Client — bring your own pre-built mem0 client. Accepts EITHER
#    backend: `mem0.AsyncMemory` (open-source / self-hosted) or
#    `mem0.AsyncMemoryClient` (hosted Platform). Use this when you
#    want full control over the mem0 setup — custom subclass, a
#    pre-warmed client shared across many agents, exotic config
#    that doesn't fit the `build_mem0_config` helper, etc.
#
# OSS backend (you assemble the AsyncMemory yourself):
Mem0Middleware(
    user_id="alice",
    client=AsyncMemory(),  # or AsyncMemory.from_config({...})
    mode="both",
)

# Hosted Platform backend:
Mem0Middleware(
    user_id="alice",
    client=AsyncMemoryClient(api_key="m0-..."),
    mode="both",
)
```

Precedence and validation matrix:

| `client` | `mem0_config` | `chat_model` | `embedding_model` | Behavior |
|:-:|:-:|:-:|:-:|---|
| ✓ | — | — | — | Use `client` as-is. |
| ✓ | any | any | any | Use `client`; the other three are ignored, and a `WARNING` log lists which kwargs got dropped. |
| — | ✓ | — | — | Wrap `mem0_config` in an `AsyncMemory`, no overrides. |
| — | ✓ | ✓ | — | Wrap + override `.llm` with the AgentScope adapter; keep `.embedder` from `mem0_config`. |
| — | ✓ | — | ✓ | Wrap + override `.embedder` only; keep `.llm` from `mem0_config`. |
| — | ✓ | ✓ | ✓ | Wrap + override both `.llm` and `.embedder` (other fields of `mem0_config` preserved). |
| — | — | ✓ | ✓ | Build a fresh `MemoryConfig` (mem0 defaults for vector store / history DB) with the AgentScope adapters wired in. |
| — | — | ✓ | — | ❌ `ValueError` — `chat_model` and `embedding_model` must be passed together when `mem0_config` is omitted. |
| — | — | — | ✓ | ❌ Same. |
| — | — | — | — | ❌ `ValueError` — need one of: `client`, `mem0_config`, or both `chat_model` + `embedding_model`. |

Why the "client wins" and "config override" paths exist:

- **`client` wins** lets one `Mem0Middleware(...)` call
  shape work for both library callers (who pass AgentScope models)
  and production setups (who supply a pre-built `client`). The
  `WARNING` log makes any mismatch visible without crashing.
- **Config override of `mem0_config.llm` / `.embedder`** lets you
  keep one canonical `MemoryConfig` template (custom vector store,
  history DB, reranker, …) and swap just the LLM / embedder per
  call site by passing `chat_model` / `embedding_model`.

## How the middleware controls memory

The `mode` parameter selects one of three patterns. They differ by
**what the LLM sees** and **what fires automatically**:

### `static_control`
The middleware does the work, the agent is unaware. Mirroring
AgentScope 1.x's `ReActAgent._retrieve_from_long_term_memory`:

1. **`on_reply` (pre)** queries mem0 with the latest user message
   and pre-fetches the results.
2. **At `ReplyStartEvent`** — which fires right after the agent has
   ingested the new user input into `state.context` and before the
   reasoning loop starts — the middleware appends an
   `AssistantMsg(name="memory", ...)` to `state.context`. This puts
   the memory note IMMEDIATELY after the user's new message, matching
   v1's placement (it ran right after `self.memory.add(msg)`).
3. **`on_reply` (post)** writes the new `(user, assistant)` exchange
   back to mem0.

The injected memory message **persists** in the agent's context
across turns. Long sessions accumulate one per turn that retrieved
anything; if that becomes a token concern, post-process with
`compress_context` or write your own middleware to pop them.

### `agent_control`
The middleware lists two tools — `search_memory(keywords, limit)` and
`add_memory(thinking, content)` — and otherwise stays out of the way.
Pass them into the agent's toolkit explicitly when constructing the
agent:

```python
mw = Mem0Middleware(..., mode="agent_control")
agent = Agent(
    ...,
    toolkit=Toolkit(tools=await mw.list_tools()),
    middlewares=[mw],
)
```

The system prompt gets a short nudge telling the agent that memory
tools exist; the actual per-tool usage guidance comes through the
standard tool schema. No automatic retrieval or write-back.

### `both` (default)
Both patterns are active simultaneously: memories are auto-retrieved
and appended to the agent's context as an assistant note, AND the
tools (with their system-prompt hint) are exposed for explicit
on-demand search / save. This matches AgentScope 1.x's
`ReActAgent.long_term_memory_mode` default.

## Sharing one middleware across agents

The local OSS mem0 backend uses on-disk Qdrant by default, and Qdrant
takes an **exclusive lock** on the storage folder
(``/tmp/qdrant`` by default). Two ``Mem0Middleware`` instances each
built from ``chat_model`` + ``embedding_model`` would each construct
their own ``AsyncMemory`` → second one crashes on the lock:

```
RuntimeError: Storage folder /tmp/qdrant is already accessed by
another instance of Qdrant client.
```

Fix: build **one** ``Mem0Middleware`` instance and pass it to every
agent that should share the same memory namespace:

```python
mw = Mem0Middleware(
    user_id="alice",
    chat_model=chat_model,
    embedding_model=embedding_model,
    mode="both",
)
agent_a = Agent(
    ...,
    toolkit=Toolkit(tools=await mw.list_tools()),
    middlewares=[mw],
)
agent_b = Agent(
    ...,
    toolkit=Toolkit(tools=await mw.list_tools()),
    middlewares=[mw],
)
```

This is what the demo does. The memory tools receive the live
`AgentState` at call time, and the middleware resolves the active
agent by `state.session_id`, so sharing one middleware across agents
is safe.

If you genuinely need a separate Qdrant store per agent, pass a
``mem0_config`` with a distinct ``vector_store.config.path`` or
``collection_name`` for each one.

### Recommended: run Qdrant in Docker (especially on Windows)

The local on-disk Qdrant works for single-process demos but is
brittle in real deployments — and **outright painful on Windows**,
where the filesystem-lock semantics differ from Unix and the
exclusive-lock failure mode is harder to recover from. For anything
beyond a single-process Linux/macOS sandbox, run Qdrant as a service:

```bash
docker run -p 6333:6333 -p 6334:6334 \
    -v $(pwd)/qdrant_storage:/qdrant/storage \
    qdrant/qdrant
```

Then point mem0 at it instead of the on-disk path:

```python
from mem0.configs.base import MemoryConfig
from mem0.vector_stores.configs import VectorStoreConfig

mem0_cfg = MemoryConfig(
    vector_store=VectorStoreConfig(
        provider="qdrant",
        config={
            "collection_name": "mem0",
            "host": "localhost",     # the Docker container
            "port": 6333,
            "embedding_model_dims": 1536,
        },
    ),
)
Mem0Middleware(
    user_id="alice",
    chat_model=chat_model,
    embedding_model=embedding_model,
    mem0_config=mem0_cfg,
)
```

Benefits over on-disk:

- No file-lock contention — multiple Python processes can connect.
- Survives across runs without manual file cleanup.
- Same shape works for remote Qdrant (Qdrant Cloud, your own
  Kubernetes deployment) — just change ``host`` / ``port`` /
  ``api_key``.

## Memory scoping (`user_id` × `agent_id`)

mem0 tags every stored memory with the `user_id` and `agent_id`
filter values passed at `add` time, and searches by AND-matching those
tags. The middleware exposes the agent dimension via the
`scope_search_by_agent` flag (default `True`):

| `scope_search_by_agent` | What `add` tags the memory with | What `search` filters by | Effect |
| --- | --- | --- | --- |
| `True` (default) | `user_id` + `agent_id` | `user_id` + `agent_id` | Strict per-agent silos. Agent A's memories invisible to agent B for the same user. |
| `False` | `user_id` + `agent_id` (unchanged) | `user_id` only | Read-broad, write-narrow. All agents for the same user share a memory pool, but each memory still records which agent wrote it (visible in mem0 metadata). |

`agent_id` defaults to `agent.name`. Override via `agent_id="..."` or
`agent_id=lambda agent: ...` on the middleware constructor.

When to relax `scope_search_by_agent`:

- One user has multiple specialized agents (research / coding /
  scheduling) that should benefit from each other's discoveries about
  the user.
- An agent's `name` might change across deployments but you want the
  memory to persist across name changes.

### A note on agent-centric extraction (currently unreachable)

mem0 v2's extraction prompt
([`ADDITIVE_EXTRACTION_PROMPT`](https://github.com/mem0ai/mem0/blob/main/mem0/configs/prompts.py))
has a conditional suffix that switches framing from user-centric
("User stated X") to **agent-centric** ("Agent was informed of X" /
"Agent recommended Y"). It's gated on
`is_agent_scoped = bool(filters.agent_id) and not filters.user_id` —
i.e. only when `agent_id` is provided *without* `user_id`. The
middleware always passes `user_id` (it's a required constructor arg),
so this agent-centric suffix is unreachable through `Mem0Middleware`
today. In practice that's fine — agent persona / configuration is
usually expressed via system prompt rather than long-term memory.

## Service-mode integration (`agentscope.app`)

The demos above use the **library mode** — you construct `Agent`
yourself and pass `Mem0Middleware` into its `middlewares=[...]`. For
production deployments via `agentscope.app` (the FastAPI service
layer), the `user_id` already flows through the framework from the
`X-User-ID` HTTP header. Hook in through the
[`extra_agent_middlewares`](../../../../src/agentscope/app/_types.py)
factory:

```python
from agentscope.app import create_app
from agentscope.middleware import Mem0Middleware
from agentscope.middleware._longterm_memory._mem0._agentscope_adapter \
    import build_mem0_config
from mem0 import AsyncMemory

# Build the mem0 client ONCE at module scope — local OSS Qdrant
# takes an exclusive lock on its storage folder; per-request
# construction would deadlock under concurrent traffic.
chat_model = ...     # shared AgentScope ChatModelBase
emb_model  = ...     # shared AgentScope EmbeddingModelBase
mem0_client = AsyncMemory(
    config=build_mem0_config(
        chat_model=chat_model,
        embedding_model=emb_model,
    ),
)


async def long_term_memory_factory(
    user_id: str,         # ← from the authenticated X-User-ID header
    agent_id: str,
    session_id: str,
) -> list:
    return [
        Mem0Middleware(
            user_id=user_id,
            client=mem0_client,    # shared across all requests
            mode="both",
        ),
    ]


app = create_app(
    ...,
    extra_agent_middlewares=long_term_memory_factory,
)
```

Key points:

- The factory is `async (user_id, agent_id, session_id) ->
  list[MiddlewareBase]`, called **once per agent assembly**
  (i.e. per chat turn / scheduled trigger). It returns fresh
  `Mem0Middleware` instances each time, but they share a single
  underlying mem0 client.
- `user_id` is the authenticated caller, injected by `agentscope.app`
  via `get_current_user_id` (currently from `X-User-ID` header; will
  become JWT-based when auth lands upstream). You forward it straight
  to `Mem0Middleware(user_id=user_id, ...)` — no resolver callable
  needed.
- For hosted mem0 Platform, swap the `AsyncMemory(config=...)`
  construction for `AsyncMemoryClient(api_key=...)` — same factory
  shape, no Qdrant lock concern.

## Notes on the AgentScope-as-mem0-backend path

When you pass `chat_model` + `embedding_model`, the middleware
internally:

1. Registers `AgentScopeLLM` / `AgentScopeEmbedding` in mem0's factory
   dicts under provider name `"agentscope"`.
2. Substitutes `LlmConfig` / `EmbedderConfig` with subclasses whose
   validator allows `"agentscope"` (mem0's stock validator hardcodes a
   whitelist that doesn't include us). Other providers continue to be
   rejected with mem0's original error.
3. Builds an `AsyncMemory` whose `.llm` and `.embedding_model` route
   through the AgentScope adapters.
4. Bridges mem0's sync API onto AgentScope's async models via a
   persistent background event loop, so async clients (e.g. Ollama's
   `AsyncClient`) keep their connection pool across calls.

Your embedding model's `dimensions` must match the vector store's
expected dim — mem0's default Qdrant expects 1536, which matches
DashScope's `text-embedding-v2` at `dimensions=1536` (the value used
in `oss_demo.py`).
