zhiyi/scripts/migrate_hermes_memory.py

175 lines
5.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
批量迁移 Hermes 记忆到织忆。
- 读取 ~/.hermes/memory_db/memory.fts.db
- 直接写入 Redis + JSONL绕过 commit 的索引重建)
- 最后一次性全量重建索引
"""
import sqlite3, json, os, time, uuid
from pathlib import Path
import sys
# Add zhiyi src to path
ZHIYI_ROOT = Path("/home/muc/projects/zhiyi")
sys.path.insert(0, str(ZHIYI_ROOT / "src"))
MEMORY_DB = Path.home() / ".hermes/memory_db/memory.fts.db"
ZHIYI_DATA = ZHIYI_ROOT / "data"
EPISODES_DIR = Path.home() / ".memory-fabric/zhiyi/episodes"
DISTILLED_DIR = Path.home() / ".memory-fabric/zhiyi/distilled"
# Redis config
REDIS_HOST = "localhost"
REDIS_PORT = 6379
SKIP_PATTERNS = [
"Context Compaction", "--- END OF", "## Active Task", "## Relevant Memory",
"Review the conversation above", "[System note:", "造成这种情况的原因",
]
def is_valid(content: str) -> bool:
if not content or len(content.strip()) < 10:
return False
for pat in SKIP_PATTERNS:
if pat in content:
return False
return True
def get_tag_category(tag: str) -> str:
"""Map Hermes tag to ZhiYi category."""
if tag in ("偏好", "配置", "环境", "认知突破"):
return "distilled"
return "distilled"
def load_hermes_memories():
"""Read all memories from Hermes FTS db."""
conn = sqlite3.connect(str(MEMORY_DB))
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute("SELECT content, tag, at FROM memory ORDER BY at DESC")
rows = cur.fetchall()
conn.close()
return [dict(r) for r in rows]
def write_jsonl(path: Path, doc: dict):
"""Append one doc to JSONL file."""
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "a", encoding="utf-8") as f:
f.write(json.dumps(doc, ensure_ascii=False) + "\n")
def write_redis(episode_id: str, doc: dict):
"""Write to Redis hash."""
import redis
r = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, decode_responses=True)
key = f"commit:{episode_id}"
r.hset(key, mapping={
"id": episode_id,
"content": doc["content"],
"category": doc.get("category", "distilled"),
"tags": doc.get("tags", ""),
"created_at": doc.get("created_at", ""),
})
r.expire(key, 86400 * 30) # 30 day TTL
def publish_event(episode_id: str, category: str):
"""Publish event to Redis Streams."""
import redis
r = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, decode_responses=True)
r.xadd("zhiyi:events", {
"type": "commit",
"episode_id": episode_id,
"category": category,
})
def migrate():
rows = load_hermes_memories()
print(f"Loaded {len(rows)} entries from Hermes memory")
# Filter valid entries
valid = [r for r in rows if is_valid(r["content"])]
print(f"Valid entries: {len(valid)} (skipped {len(rows) - len(valid)})")
# Get existing episode IDs to avoid duplicates (check JSONL)
existing_ids = set()
for f in DISTILLED_DIR.glob("*.jsonl"):
for line in open(f, encoding="utf-8"):
try:
d = json.loads(line)
if "episode_id" in d:
existing_ids.add(d["episode_id"])
except:
pass
print(f"Existing distilled entries: {len(existing_ids)}")
imported = skipped_dup = 0
for i, row in enumerate(valid):
content = row["content"].strip()
tag = row["tag"] or "general"
created_at = row["at"]
# Generate stable ID from content hash
import hashlib
h = hashlib.sha256(content.encode()).hexdigest()[:16]
episode_id = f"hermes-mig-{h}"
if episode_id in existing_ids:
skipped_dup += 1
continue
category = get_tag_category(tag)
doc = {
"episode_id": episode_id,
"content": content,
"category": category,
"tags": tag,
"created_at": created_at,
}
# Write to JSONL
if category == "distilled":
jsonl_path = DISTILLED_DIR / f"hermes-migrated-{created_at[:7]}.jsonl"
else:
jsonl_path = EPISODES_DIR / f"hermes-migrated-{created_at[:7]}.jsonl"
write_jsonl(jsonl_path, doc)
# Write to Redis
try:
write_redis(episode_id, doc)
publish_event(episode_id, category)
except Exception as e:
print(f" Redis error: {e}")
imported += 1
if (i + 1) % 50 == 0:
print(f" {i+1}/{len(valid)} processed, imported={imported}")
print(f"\nMigration done: {imported} imported, {skipped_dup} duplicate skipped")
# Save migration metadata
meta = {
"migrated_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
"total_loaded": len(rows),
"valid": len(valid),
"imported": imported,
"skipped_dup": skipped_dup,
"source": "hermes-memory-db",
}
print(f"Metadata: {json.dumps(meta, ensure_ascii=False)}")
# Now trigger one final index rebuild
print("\nTriggering index rebuild...")
import requests
try:
r = requests.post("http://localhost:7821/api/v1/rebuild-index", timeout=60)
print(f"Rebuild: {r.status_code} {r.text[:200]}")
except Exception as e:
print(f"Rebuild trigger failed: {e}")
print("NOTE: You may need to restart ZhiYi to see migrated memories in recall")
print("\nMigration complete!")
return meta
if __name__ == "__main__":
migrate()