Spaces:
Paused
Paused
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`).
|