移除 thread_memory 对 memory_md/Markdown 解析的运行时依赖,仅保留 memory_json 读写路径。\n同步更新 SQLite/MySQL 存储实现与测试基线,并补充迁移文档的最终状态说明。
100 lines
3.5 KiB
Python
100 lines
3.5 KiB
Python
import json
|
|
|
|
from deerflow.agents.memory.thread_storage import SqliteThreadMemoryStorage
|
|
|
|
|
|
def _payload():
|
|
return {
|
|
"ownerId": None,
|
|
"user": {
|
|
"workContext": {"summary": "Frontend engineer", "updatedAt": "2026-05-08T00:00:00Z"},
|
|
"personalContext": {"summary": "Prefers Chinese", "updatedAt": "2026-05-08T00:00:00Z"},
|
|
"topOfMind": {"summary": "Thread memory migration", "updatedAt": "2026-05-08T00:00:00Z"},
|
|
},
|
|
"history": {
|
|
"recentMonths": {"summary": "Worked on memory features", "updatedAt": "2026-05-08T00:00:00Z"},
|
|
"earlierContext": {"summary": "", "updatedAt": ""},
|
|
"longTermBackground": {"summary": "Builds web products", "updatedAt": "2026-05-08T00:00:00Z"},
|
|
},
|
|
"facts": [],
|
|
}
|
|
|
|
|
|
def test_sqlite_thread_memory_compare_and_swap(tmp_path):
|
|
storage = SqliteThreadMemoryStorage(str(tmp_path / "thread-memory.db"))
|
|
thread_id = "thread-1"
|
|
|
|
assert storage.save(thread_id, _payload(), expected_version=0) is True
|
|
loaded = storage.load(thread_id)
|
|
assert loaded is not None
|
|
assert loaded["memoryVersion"] == 0
|
|
|
|
# wrong expected version should fail
|
|
assert storage.save(thread_id, _payload(), expected_version=9) is False
|
|
# correct version should pass and increment
|
|
assert storage.save(thread_id, _payload(), expected_version=0) is True
|
|
loaded2 = storage.load(thread_id)
|
|
assert loaded2 is not None
|
|
assert loaded2["memoryVersion"] == 1
|
|
|
|
|
|
def test_sqlite_thread_memory_saves_json_payload(tmp_path):
|
|
db_path = tmp_path / "thread-memory.db"
|
|
storage = SqliteThreadMemoryStorage(str(db_path))
|
|
thread_id = "thread-md"
|
|
|
|
assert storage.save(thread_id, _payload(), expected_version=0) is True
|
|
|
|
with storage._lock:
|
|
row = storage._conn.execute("SELECT memory_json FROM thread_memory WHERE thread_id = ?", (thread_id,)).fetchone()
|
|
assert row is not None
|
|
assert isinstance(row[0], str)
|
|
parsed = json.loads(row[0])
|
|
assert parsed["user"]["workContext"]["summary"] == "Frontend engineer"
|
|
|
|
|
|
def test_sqlite_thread_memory_uses_owner_id_column_when_json_missing_owner(tmp_path):
|
|
db_path = tmp_path / "thread-memory.db"
|
|
storage = SqliteThreadMemoryStorage(str(db_path))
|
|
thread_id = "thread-load"
|
|
payload = _payload()
|
|
|
|
with storage._lock:
|
|
storage._conn.execute(
|
|
"""
|
|
INSERT INTO thread_memory (thread_id, owner_id, memory_json, memory_version, last_updated)
|
|
VALUES (?, ?, ?, 0, datetime('now'))
|
|
""",
|
|
(
|
|
thread_id,
|
|
"owner-1",
|
|
json.dumps(
|
|
{
|
|
"user": payload["user"],
|
|
"history": payload["history"],
|
|
"facts": [],
|
|
},
|
|
ensure_ascii=False,
|
|
),
|
|
),
|
|
)
|
|
storage._conn.commit()
|
|
|
|
loaded = storage.load(thread_id)
|
|
assert loaded is not None
|
|
assert loaded["ownerId"] == "owner-1"
|
|
assert loaded["user"]["workContext"]["summary"] == "Frontend engineer"
|
|
assert loaded["facts"] == []
|
|
|
|
|
|
def test_sqlite_thread_memory_backfill_is_noop_after_migration(tmp_path):
|
|
db_path = tmp_path / "thread-memory.db"
|
|
storage = SqliteThreadMemoryStorage(str(db_path))
|
|
|
|
assert storage.count_legacy_rows() == 0
|
|
stats = storage.backfill_legacy_rows()
|
|
assert stats["scanned"] == 0
|
|
assert stats["updated"] == 0
|
|
assert stats["failed"] == 0
|
|
assert storage.count_legacy_rows() == 0
|