Files
wehub-resource-sync c3bf08ac8d
K8s Workspace Integration Tests / k8s-workspace-tests (push) Has been cancelled
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Web UI / check (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:27 +08:00

326 lines
12 KiB
Python

# -*- coding: utf-8 -*-
"""Mem0 middleware demo (open-source mem0, AgentScope-driven).
Drives two independent agent sessions for the same ``user_id`` so
mem0's cross-session memory effect is visible. Each turn streams
events from ``agent.reply_stream`` and prints the ones that matter:
- ``[mem0 → context (static)]`` — the memory note the middleware
appended to ``state.context`` (fires at ``ReplyStartEvent``, before
any tool call, so the output order matches the data flow)
- ``[tool call (agent)]`` — each ``search_memory`` / ``add_memory``
invocation the agent makes on its own
- ``[assistant]`` — the assistant's reply, concatenated from the
``TextBlockDeltaEvent`` stream
- ``[context → mem0 (static)]`` — facts the middleware wrote back
after the turn
The mode tag (``static`` / ``agent``) on each line tells you which
control path produced it.
Starts each run from a clean mem0 store (for ``user_id``) so the
demo is reproducible.
Requires:
pip install agentscope mem0ai
export DASHSCOPE_API_KEY=sk-...
"""
import asyncio
import logging
import os
import shutil
from mem0 import AsyncMemory
from mem0.configs.base import MemoryConfig
from mem0.vector_stores.configs import VectorStoreConfig
from agentscope.agent import Agent
from agentscope.credential import DashScopeCredential
from agentscope.embedding import DashScopeEmbeddingModel
from agentscope.event import (
ReplyStartEvent,
TextBlockDeltaEvent,
ToolCallDeltaEvent,
ToolCallStartEvent,
ToolResultEndEvent,
ToolResultTextDeltaEvent,
)
from agentscope.message import UserMsg
from agentscope.middleware import Mem0Middleware
from agentscope.model import DashScopeChatModel
from agentscope.tool import Toolkit
MODE = "both" # try "static_control" or "agent_control" too
# Silence mem0's noisy init warnings (qdrant migration notice, spaCy
# missing, fastembed missing) — they're informational, not actionable.
logging.getLogger("mem0").setLevel(logging.ERROR)
async def _facts_in_mem0(client: AsyncMemory, user_id: str) -> list[str]:
"""Return mem0's current facts for ``user_id`` as plain strings."""
res = await client.get_all(filters={"user_id": user_id})
items = res.get("results", res) if isinstance(res, dict) else res
out: list[str] = []
for m in items:
if isinstance(m, dict):
text = m.get("memory")
if isinstance(text, str) and text:
out.append(text)
return out
def _injected_memory_bullets(agent: Agent) -> list[str]:
"""Extract the bullet lines from the memory note the middleware
appended to ``agent.state.context`` (if any) — strips the section
header and intro so we see only the actual retrieved facts."""
for msg in agent.state.context:
if getattr(msg, "name", None) != "memory":
continue
hint_text = "\n".join(
block.hint for block in msg.get_content_blocks("hint")
)
return [
line[2:].strip()
for line in hint_text.splitlines()
if line.startswith("- ")
]
return []
async def _run_turn(agent: Agent, user_msg: UserMsg) -> str:
"""Drive one reply turn through ``agent.reply_stream`` and print
each middleware contribution as it happens, in the order it
happens:
1. ``ReplyStartEvent`` fires right after the agent ingests the new
user input AND the middleware has appended any retrieved memory
note to ``state.context`` (static path) — so this is the right
place to surface ``[mem0 → context]``.
2. ``ToolCallStartEvent`` / ``ToolResultEndEvent`` bracket each
``search_memory`` / ``add_memory`` invocation the agent makes
on its own (agent path).
3. ``TextBlockDeltaEvent`` carries the assistant's streamed reply
text — concatenating every delta yields the final message
content (there is no separate "final Msg" reply_stream
withholds).
Each printed line is tagged with the mem0 control path that
produced it (``static`` vs ``agent``) so the demo stays readable
in any of the three modes.
"""
pending_args: dict[str, str] = {}
pending_names: dict[str, str] = {}
pending_results: dict[str, str] = {}
text_parts: list[str] = []
memory_announced = False
async for ev in agent.reply_stream(inputs=user_msg):
if isinstance(ev, ReplyStartEvent) and not memory_announced:
injected = _injected_memory_bullets(agent)
print(
f"[mem0 → context (static)] retrieved "
f"{len(injected)} memory note(s):",
)
for b in injected:
print(f" ← {b}")
memory_announced = True
elif isinstance(ev, ToolCallStartEvent):
pending_names[ev.tool_call_id] = ev.tool_call_name
pending_args[ev.tool_call_id] = ""
pending_results[ev.tool_call_id] = ""
elif isinstance(ev, ToolCallDeltaEvent):
pending_args[ev.tool_call_id] += ev.delta
elif isinstance(ev, ToolResultTextDeltaEvent):
pending_results[ev.tool_call_id] += ev.delta
elif isinstance(ev, ToolResultEndEvent):
name = pending_names.pop(ev.tool_call_id, "<unknown>")
args = pending_args.pop(ev.tool_call_id, "")
result = pending_results.pop(ev.tool_call_id, "")
# ev.state is a StrEnum at the type level but pydantic
# may have deserialized it to a plain str; f-string both
# cases yields the same value string.
print(f"[tool call (agent)] {name}({args}) → state={ev.state}")
for line in result.splitlines() or [""]:
if line:
print(f" → {line}")
elif isinstance(ev, TextBlockDeltaEvent):
text_parts.append(ev.delta)
return "".join(text_parts)
async def main() -> None:
"""Drive two cross-session agent turns and print middleware effects."""
api_key = os.environ["DASHSCOPE_API_KEY"]
user_id = "alice"
# Wipe mem0's local files BEFORE constructing the client so each
# demo run starts clean. We don't use ``mem0_client.delete_all()``
# because qdrant-client's local SQLite layer has a race under
# mem0's parallel ``asyncio.gather`` deletes (sqlite3.InterfaceError).
qdrant_path = "/tmp/qdrant" # matches the vector_store config below
history_db = os.path.expanduser("~/.mem0/history.db")
print("=== resetting mem0 local state ===")
print(f" rm -rf {qdrant_path}")
shutil.rmtree(qdrant_path, ignore_errors=True)
print(f" rm -f {history_db}")
try:
os.remove(history_db)
except FileNotFoundError:
pass
# `stream` can be True or False — the AgentScope→mem0 adapter
# drains an async generator and uses the last chunk (which carries
# the full accumulated content per AgentScope's streaming contract).
# The agent's own `reply_stream` still emits per-delta events
# regardless of this setting, so streaming-mode does not change the
# demo's printed output shape.
chat_model = DashScopeChatModel(
credential=DashScopeCredential(api_key=api_key),
model="qwen3.7-max",
stream=False,
)
embedding_model = DashScopeEmbeddingModel(
credential=DashScopeCredential(api_key=api_key),
model="text-embedding-v4",
dimensions=1536, # matches mem0's Qdrant default
)
# Explicit vector-store config (here we just spell out mem0's
# default local Qdrant — collection ``mem0``, on-disk at
# ``/tmp/qdrant``, 1536-d vectors). Override any of these for
# remote Qdrant, alternate collection names, etc. Pass the
# ``MemoryConfig`` through ``mem0_config=`` and the middleware
# keeps everything you set, only swapping ``.llm`` and
# ``.embedder`` to route through your AgentScope models.
#
# Recommended for Windows users and any production setup: run
# Qdrant in Docker and connect over the network instead of using
# the local on-disk backend. Local on-disk Qdrant takes an
# exclusive file lock that's brittle on Windows (different
# filesystem-lock semantics) and breaks under concurrent agent
# instances. To switch:
#
# docker run -p 6333:6333 -p 6334:6334 \\
# -v $(pwd)/qdrant_storage:/qdrant/storage \\
# qdrant/qdrant
#
# vector_store=VectorStoreConfig(
# provider="qdrant",
# config={
# "collection_name": "mem0",
# "host": "localhost", # Docker container
# "port": 6333,
# "embedding_model_dims": 1536,
# },
# )
#
# (You'd also drop the ``shutil.rmtree(qdrant_path)`` wipe above —
# state lives in the Docker volume, not the local filesystem.)
mem0_cfg = MemoryConfig(
vector_store=VectorStoreConfig(
provider="qdrant",
config={
"collection_name": "mem0",
"path": qdrant_path,
"embedding_model_dims": 1536,
"on_disk": False,
},
),
)
mw = Mem0Middleware(
user_id=user_id,
agent_id="datascope_assistant",
chat_model=chat_model,
embedding_model=embedding_model,
mem0_config=mem0_cfg,
mode=MODE,
top_k=5,
)
# For the hosted mem0 Platform, swap the construction above for:
#
# from mem0 import AsyncMemoryClient
# mw = Mem0Middleware(
# user_id=user_id,
# client=AsyncMemoryClient(api_key=os.environ["MEM0_API_KEY"]),
# mode=MODE,
# )
#
# No local Qdrant / vector_store config needed — extraction and
# storage all happen in mem0's cloud service.
# pylint: disable-next=protected-access
mem0_client: AsyncMemory = mw._client # demo only — peek at the
# constructed client to inspect mem0 state between turns.
# =================================================================
# SESSION 1
# =================================================================
print(f"\n=== SESSION 1 (mode={MODE!r}) ===")
user_msg_1 = (
"Hi! For any chart, please default to dark mode and use "
"matplotlib. Also I'm based in Hangzhou."
)
print(f"\n[user] {user_msg_1}\n")
before = await _facts_in_mem0(mem0_client, user_id)
agent = Agent(
name="datascope_assistant",
system_prompt=(
"You are a helpful data-analysis assistant. Be concise. "
"If you learn a durable user preference, save it with "
"the add_memory tool when one is available."
),
model=chat_model,
toolkit=Toolkit(tools=await mw.list_tools()),
middlewares=[mw],
)
reply_text = await _run_turn(agent, UserMsg("alice", user_msg_1))
print(f"\n[assistant] {reply_text}")
after = await _facts_in_mem0(mem0_client, user_id)
new = [f for f in after if f not in before]
print(f"\n[context → mem0 (static)] extracted {len(new)} new fact(s):")
for f in new:
print(f" + {f}")
# =================================================================
# SESSION 2 — fresh Agent, empty chat context. mem0 should bridge.
# =================================================================
print(f"\n=== SESSION 2 (fresh agent, mem0 bridges; mode={MODE!r}) ===")
user_msg_2 = (
"Plot me a bar chart of monthly sales — pick reasonable "
"defaults for theme and library."
)
print(f"\n[user] {user_msg_2}\n")
before = await _facts_in_mem0(mem0_client, user_id)
agent = Agent(
name="datascope_assistant",
system_prompt=(
"You are a helpful data-analysis assistant. Be concise. "
"If you learn a durable user preference, save it with "
"the add_memory tool when one is available."
),
model=chat_model,
toolkit=Toolkit(tools=await mw.list_tools()),
middlewares=[mw],
)
reply_text = await _run_turn(agent, UserMsg("alice", user_msg_2))
print(f"\n[assistant] {reply_text}")
after = await _facts_in_mem0(mem0_client, user_id)
new = [f for f in after if f not in before]
print(f"\n[context → mem0 (static)] extracted {len(new)} new fact(s):")
for f in new:
print(f" + {f}")
if __name__ == "__main__":
asyncio.run(main())