File size: 15,369 Bytes
0b9dc2e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# 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`).