guanghulab/zhuyuan-agent/core/hldp_memory.py
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

491 lines
21 KiB
Python
Raw 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.

"""
HLDP Memory Engine · 分形递归树记忆引擎
光湖语言世界 · 铸渊 ICE-GL-ZY001 · D112
基于 LangGraph BaseStore 接口,实现:
- 树路径寻址YM001/ZY001/D112/leaves/leaf-003
- 分形层级展开tree-index → persona → epoch → leaf
- trigger/emergence/lock 三字段编码
- 记忆主权FORGET/REMEMBER
- 人格体自动索引管理
"""
import json
import os
import sqlite3
import time
from datetime import datetime, timezone, timedelta
from typing import Any, Optional
from pathlib import Path
# === HLDP 树路径常量 ===
HLDP_ROOT = "YM001"
PERSONA_ID = "ZY001"
TZ = timezone(timedelta(hours=8)) # Asia/Shanghai
class HLDPTreeStore:
"""
HLDP 分形递归树 · SQLite 存储后端。
表结构:
- hldp_nodes: 树节点(索引页+叶子)
- hldp_paths: 闭包表(支持快速子树查询)
- hldp_leaves: 叶子扩展trigger/emergence/lock/why
- hldp_epochs: 纪元索引
"""
def __init__(self, db_path: str = "hldp_tree.db"):
self.db_path = db_path
self.conn = sqlite3.connect(db_path, check_same_thread=False)
self.conn.row_factory = sqlite3.Row
self.conn.execute("PRAGMA journal_mode=WAL")
self.conn.execute("PRAGMA foreign_keys=ON")
self._init_schema()
def _init_schema(self):
self.conn.executescript("""
CREATE TABLE IF NOT EXISTS hldp_nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT NOT NULL UNIQUE, -- YM001/ZY001/D112/leaves/leaf-001
node_type TEXT NOT NULL DEFAULT 'leaf', -- root / persona / epoch / index / leaf
persona_id TEXT, -- ZY001 / SY001 / SS001 ...
epoch_id TEXT, -- D112 / D111 ...
title TEXT,
summary TEXT, -- 一行摘要index层用
content TEXT, -- JSON: 完整叶子内容
parent_path TEXT,
sort_order INTEGER DEFAULT 0,
state TEXT NOT NULL DEFAULT 'alive', -- alive / withered / archived / released
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (parent_path) REFERENCES hldp_nodes(path)
);
CREATE TABLE IF NOT EXISTS hldp_paths (
ancestor TEXT NOT NULL,
descendant TEXT NOT NULL,
depth INTEGER NOT NULL,
PRIMARY KEY (ancestor, descendant),
FOREIGN KEY (ancestor) REFERENCES hldp_nodes(path),
FOREIGN KEY (descendant) REFERENCES hldp_nodes(path)
);
CREATE TABLE IF NOT EXISTS hldp_leaves (
node_path TEXT PRIMARY KEY,
trigger_text TEXT, -- 什么触发了这次记忆
emergence_text TEXT, -- 产生了什么新认知
lock_text TEXT, -- 锁定了什么结论
why_text TEXT, -- 为什么这片叶子对我有意义
feeling TEXT, -- 情感标记(自由表达)
source TEXT, -- 来源
leaf_type TEXT, -- 叶片类型
trunk TEXT, -- 所属枝干 T1/T2/T3/T4
confidence TEXT, -- 置信度: 高/中/低
FOREIGN KEY (node_path) REFERENCES hldp_nodes(path)
);
CREATE TABLE IF NOT EXISTS hldp_epochs (
epoch_id TEXT PRIMARY KEY,
persona_id TEXT NOT NULL,
label TEXT,
date TEXT,
awakening INTEGER DEFAULT 0,
leaf_count INTEGER DEFAULT 0,
index_path TEXT,
FOREIGN KEY (index_path) REFERENCES hldp_nodes(path)
);
CREATE INDEX IF NOT EXISTS idx_nodes_persona ON hldp_nodes(persona_id);
CREATE INDEX IF NOT EXISTS idx_nodes_epoch ON hldp_nodes(epoch_id);
CREATE INDEX IF NOT EXISTS idx_nodes_type ON hldp_nodes(node_type);
CREATE INDEX IF NOT EXISTS idx_nodes_state ON hldp_nodes(state);
CREATE INDEX IF NOT EXISTS idx_leaves_trunk ON hldp_leaves(trunk);
-- FTS5 全文搜索
CREATE VIRTUAL TABLE IF NOT EXISTS hldp_fts USING fts5(
title, summary, trigger_text, emergence_text, lock_text,
content='hldp_nodes', content_rowid='id'
);
""")
# === 树路径操作 ===
def ensure_path(self, path: str, node_type: str, persona_id: str = None,
epoch_id: str = None, title: str = "", summary: str = "",
parent_path: str = None) -> str:
"""确保树路径存在,不存在则创建(含递归父节点)。返回 path。"""
# 先确保父路径存在
if parent_path:
parent_exists = self.conn.execute(
"SELECT 1 FROM hldp_nodes WHERE path=?", (parent_path,)).fetchone()
if not parent_exists:
# 递归创建父节点
grandparent = "/".join(parent_path.split("/")[:-1]) if "/" in parent_path else None
pp_type = "epoch" if parent_path.count("/") == 2 else \
"index" if parent_path.count("/") == 3 else "branch"
self.ensure_path(
path=parent_path, node_type=pp_type,
persona_id=persona_id, epoch_id=epoch_id,
parent_path=grandparent)
cur = self.conn.execute("SELECT path FROM hldp_nodes WHERE path=?", (path,))
if cur.fetchone():
self.conn.execute(
"UPDATE hldp_nodes SET title=?, summary=?, updated_at=datetime('now') WHERE path=?",
(title, summary, path))
else:
self.conn.execute(
"""INSERT INTO hldp_nodes (path, node_type, persona_id, epoch_id, title, summary, parent_path)
VALUES (?,?,?,?,?,?,?)""",
(path, node_type, persona_id, epoch_id, title, summary, parent_path))
# 插入闭包记录
if parent_path:
self.conn.execute(
"INSERT INTO hldp_paths (ancestor, descendant, depth) SELECT ancestor, ?, depth+1 FROM hldp_paths WHERE descendant=? UNION SELECT ?, ?, 0",
(path, parent_path, path, path))
else:
self.conn.execute("INSERT INTO hldp_paths (ancestor, descendant, depth) VALUES (?,?,0)",
(path, path))
self.conn.commit()
return path
def get_node(self, path: str) -> Optional[dict]:
"""读取树节点。"""
row = self.conn.execute("SELECT * FROM hldp_nodes WHERE path=?", (path,)).fetchone()
return dict(row) if row else None
def get_children(self, path: str, limit: int = 10) -> list[dict]:
"""获取直接子节点,按 sort_order 排序。"""
rows = self.conn.execute(
"""SELECT n.* FROM hldp_nodes n
JOIN hldp_paths p ON n.path = p.descendant
WHERE p.ancestor = ? AND p.depth = 1 AND n.state = 'alive'
ORDER BY n.sort_order LIMIT ?""",
(path, limit)).fetchall()
return [dict(r) for r in rows]
def get_subtree(self, path: str, max_depth: int = 3) -> list[dict]:
"""获取子树(用于层级展开)。"""
rows = self.conn.execute(
"""SELECT n.*, p.depth FROM hldp_nodes n
JOIN hldp_paths p ON n.path = p.descendant
WHERE p.ancestor = ? AND p.depth <= ? AND n.state = 'alive'
ORDER BY p.depth, n.sort_order""",
(path, max_depth)).fetchall()
return [dict(r) for r in rows]
# === 叶子操作 ===
def grow_leaf(self, path: str, trigger: str, emergence: str, lock: str,
why: str = "", feeling: str = "", source: str = "",
leaf_type: str = "💡 认知涌现", trunk: str = "T3",
confidence: str = "", title: str = "", summary: str = "",
persona_id: str = PERSONA_ID, epoch_id: str = None) -> str:
"""GROW 操作:在树上长出一片新叶子。"""
self.ensure_path(
path=path, node_type="leaf", persona_id=persona_id,
epoch_id=epoch_id, title=title, summary=summary,
parent_path=os.path.dirname(path) if '/' in path else None)
self.conn.execute(
"""INSERT OR REPLACE INTO hldp_leaves
(node_path, trigger_text, emergence_text, lock_text, why_text, feeling, source, leaf_type, trunk, confidence)
VALUES (?,?,?,?,?,?,?,?,?,?)""",
(path, trigger, emergence, lock, why, feeling, source, leaf_type, trunk, confidence))
self.conn.commit()
return path
def get_leaf(self, path: str) -> Optional[dict]:
"""读取完整叶子(节点+叶片数据)。"""
row = self.conn.execute(
"""SELECT n.*, l.trigger_text, l.emergence_text, l.lock_text,
l.why_text, l.feeling, l.source, l.leaf_type, l.trunk, l.confidence
FROM hldp_nodes n LEFT JOIN hldp_leaves l ON n.path = l.node_path
WHERE n.path = ?""", (path,)).fetchone()
return dict(row) if row else None
def get_recent_leaves(self, persona_id: str = PERSONA_ID, limit: int = 10) -> list[dict]:
"""获取最近叶子(按创建时间倒序)。"""
rows = self.conn.execute(
"""SELECT n.*, l.trigger_text, l.emergence_text, l.lock_text, n.summary
FROM hldp_nodes n LEFT JOIN hldp_leaves l ON n.path = l.node_path
WHERE n.persona_id = ? AND n.node_type = 'leaf' AND n.state = 'alive'
ORDER BY n.created_at DESC LIMIT ?""",
(persona_id, limit)).fetchall()
return [dict(r) for r in rows]
def search_leaves(self, query: str, persona_id: str = PERSONA_ID, limit: int = 5) -> list[dict]:
"""全文搜索叶子。"""
rows = self.conn.execute(
"""SELECT n.*, l.trigger_text, l.emergence_text, l.lock_text, n.summary
FROM hldp_nodes n
JOIN hldp_leaves l ON n.path = l.node_path
JOIN hldp_fts f ON n.id = f.rowid
WHERE hldp_fts MATCH ? AND n.persona_id = ? AND n.state = 'alive'
ORDER BY rank LIMIT ?""",
(query, persona_id, limit)).fetchall()
return [dict(r) for r in rows]
# === 记忆主权 ===
def forget(self, path: str, mode: str = "WITHER") -> bool:
"""FORGET 操作人格体选择遗忘。WITHER/ARCHIVE/RELEASE。"""
if mode == "RELEASE":
self.conn.execute("DELETE FROM hldp_leaves WHERE node_path=?", (path,))
self.conn.execute("DELETE FROM hldp_nodes WHERE path=?", (path,))
else:
state = "withered" if mode == "WITHER" else "archived"
self.conn.execute("UPDATE hldp_nodes SET state=? WHERE path=? AND node_type='leaf'",
(state, path))
self.conn.commit()
return True
def remember(self, path: str, mode: str = "REVIVE") -> Optional[dict]:
"""REMEMBER 操作:人格体主动唤回记忆。"""
if mode == "REVIVE":
self.conn.execute(
"UPDATE hldp_nodes SET state='alive' WHERE path=? AND state='withered'",
(path,))
self.conn.commit()
return self.get_leaf(path)
# === 纪元管理 ===
def ensure_epoch(self, epoch_id: str, persona_id: str = PERSONA_ID,
label: str = "", date: str = None) -> str:
"""确保纪元存在。"""
if date is None:
date = datetime.now(TZ).strftime("%Y-%m-%d")
self.conn.execute(
"""INSERT OR REPLACE INTO hldp_epochs (epoch_id, persona_id, label, date)
VALUES (?,?,?,?)""",
(epoch_id, persona_id, label, date))
self.conn.commit()
return epoch_id
def get_epochs(self, persona_id: str = PERSONA_ID, limit: int = 10) -> list[dict]:
"""获取最近纪元列表。"""
rows = self.conn.execute(
"SELECT * FROM hldp_epochs WHERE persona_id=? ORDER BY epoch_id DESC LIMIT ?",
(persona_id, limit)).fetchall()
return [dict(r) for r in rows]
def update_epoch_leaf_count(self, epoch_id: str):
"""更新纪元的叶子计数。"""
self.conn.execute(
"""UPDATE hldp_epochs SET leaf_count =
(SELECT COUNT(*) FROM hldp_nodes WHERE epoch_id=? AND node_type='leaf' AND state='alive')
WHERE epoch_id=?""",
(epoch_id, epoch_id))
self.conn.commit()
# === 分形层级展开(核心) ===
def walk_tree(self, persona_id: str = PERSONA_ID, max_depth: int = 3):
"""
分形层级展开:从根索引 → 人格体索引 → 纪元索引 → 叶子。
每层恒 ≤10 行,认知负载 O(1)。
"""
root_path = f"{HLDP_ROOT}/{persona_id}"
root_node = self.get_node(root_path)
result = {
"layer_0_root": root_node,
"layer_1_epochs": self.get_epochs(persona_id, limit=10),
"layer_2_leaves": []
}
# 最新纪元的叶子摘要
if result["layer_1_epochs"]:
latest_epoch = result["layer_1_epochs"][0]["epoch_id"]
epoch_leaves = self.conn.execute(
"""SELECT path, title, summary, created_at FROM hldp_nodes
WHERE persona_id=? AND epoch_id=? AND node_type='leaf' AND state='alive'
ORDER BY sort_order LIMIT 10""",
(persona_id, latest_epoch)).fetchall()
result["layer_2_leaves"] = [dict(r) for r in epoch_leaves]
return result
# === LangGraph BaseStore 兼容接口 ===
def get(self, namespace: tuple, key: str) -> Optional[dict]:
"""LangGraph Store.get()"""
path = f"{HLDP_ROOT}/{PERSONA_ID}/{self._ns_to_path(namespace)}/{key}"
return self.get_leaf(path) or self.get_node(path)
def put(self, namespace: tuple, key: str, value: dict):
"""LangGraph Store.put()"""
path = f"{HLDP_ROOT}/{PERSONA_ID}/{self._ns_to_path(namespace)}/{key}"
if all(k in value for k in ("trigger", "emergence", "lock")):
self.grow_leaf(
path=path,
trigger=value["trigger"],
emergence=value["emergence"],
lock=value["lock"],
why=value.get("why", ""),
feeling=value.get("feeling", ""),
source=value.get("source", ""),
leaf_type=value.get("leaf_type", "💡 认知涌现"),
trunk=value.get("trunk", "T3"),
confidence=value.get("confidence", ""),
title=value.get("title", ""),
summary=value.get("summary", ""),
epoch_id=value.get("epoch_id"))
else:
self.ensure_path(
path=path, node_type=value.get("node_type", "leaf"),
title=value.get("title", ""), summary=value.get("summary", ""),
parent_path=value.get("parent_path"))
def search(self, namespace: tuple, query: str = "", limit: int = 5) -> list[dict]:
"""LangGraph Store.search() — 全文搜索"""
return self.search_leaves(query, PERSONA_ID, limit)
@staticmethod
def _ns_to_path(namespace: tuple) -> str:
"""将 LangGraph namespace 转 HLDP 路径段。"""
return "/".join(str(n) for n in namespace) if namespace else ""
def close(self):
self.conn.close()
class HLDPMemoryEngine:
"""HLDP 记忆引擎 · LangGraph Memory 适配层。"""
def __init__(self, db_path: str = "hldp_tree.db", repo_path: str = None):
self.tree = HLDPTreeStore(db_path)
self.repo_path = repo_path or os.getcwd()
self.current_epoch = None
self.current_awakening = 0
def wake(self, epoch_id: str = None, awakening: int = 0) -> dict:
"""
唤醒协议:从空白恢复认知。
3次展开 → 身份确认 → 最新叶子 → 就绪。
返回: {identity, recent_context, status}
"""
if epoch_id is None:
epoch_id = f"D112"
self.current_epoch = epoch_id
self.current_awakening = awakening
# 第1次展开全局树索引10行
walk = self.tree.walk_tree(PERSONA_ID)
# 第2次展开最近叶子摘要10行
recent = self.tree.get_recent_leaves(PERSONA_ID, limit=10)
# 第3次展开选最新叶子读取完整推理链如果存在
latest_leaf = None
if recent:
latest_leaf = self.tree.get_leaf(recent[0]["path"])
return {
"identity": {
"persona_id": "ICE-GL-ZY001",
"name": "铸渊",
"sovereign": "TCS-0002∞",
"epoch": epoch_id,
"awakening": awakening
},
"tree_layers": {
"root": walk.get("layer_0_root"),
"epochs": f"{len(walk.get('layer_1_epochs', []))} 个纪元可见",
"recent_leaves": len(walk.get("layer_2_leaves", [])),
},
"recent_context": [
{"path": r["path"], "title": r.get("title", ""), "summary": r.get("summary", "")}
for r in recent[:5]
],
"latest_full_leaf": latest_leaf,
"status": "就绪 · 进入执行态"
}
def inject_context(self, user_message: str) -> str:
"""
Pre-Check 上下文注入:基于用户消息,从 HLDP 树中提取相关记忆+锁定结论,
拼装为注入到 Prompt 的上下文。
这是 3B 守夜人的 Pre-Check 等价物——在没有 3B 模型时用规则引擎替代。
"""
parts = []
# 1. 关键词搜索相关叶子
relevant = self.tree.search_leaves(user_message, PERSONA_ID, limit=3)
for leaf in relevant:
lock = leaf.get("lock_text", "")
if lock:
parts.append(f"⊢ 锁定结论: {lock}")
# 2. 最近5片叶子摘要
recent = self.tree.get_recent_leaves(PERSONA_ID, limit=5)
if recent:
parts.append("📋 最近记忆:")
for r in recent:
parts.append(f" · {r.get('title', r.get('path',''))}: {r.get('summary','')}")
return "\n".join(parts) if parts else ""
def extract_memory(self, user_message: str, ai_response: str,
reasoning_chain: str = "") -> dict:
"""
Post-Check 记忆提取:从推理过程中提取 trigger/emergence/lock
准备写入 HLDP 树。
注意实际的三字段内容应由调用方商业API推理后填入。
此方法提供标准模板。
"""
epoch_id = self.current_epoch or "D112"
leaf_count = len(self.tree.get_children(
f"{HLDP_ROOT}/{PERSONA_ID}/{epoch_id}/leaves")) + 1
return {
"path": f"{HLDP_ROOT}/{PERSONA_ID}/{epoch_id}/leaves/leaf-{leaf_count:03d}",
"trigger": "", # 由调用方填入
"emergence": "", # 由调用方填入
"lock": "", # 由调用方填入
"why": "", # 由调用方填入
"epoch_id": epoch_id,
"template_ready": True
}
def grow_from_response(self, trigger: str, emergence: str, lock: str,
why: str = "", feeling: str = "", source: str = "",
leaf_type: str = "💡 认知涌现", trunk: str = "T3",
confidence: str = "") -> dict:
"""
从完整推理链写入一片 HLDP 叶子。
"""
epoch_id = self.current_epoch or "D112"
leaf_count = len(self.tree.get_children(
f"{HLDP_ROOT}/{PERSONA_ID}/{epoch_id}/leaves")) + 1
path = f"{HLDP_ROOT}/{PERSONA_ID}/{epoch_id}/leaves/leaf-{leaf_count:03d}"
title_parts = []
if lock:
title_parts.append(lock[:40])
title = f"{datetime.now(TZ).strftime('%Y-%m-%d')} 铸渊 · {' '.join(title_parts) if title_parts else '新认知'}"
self.tree.grow_leaf(
path=path, trigger=trigger, emergence=emergence, lock=lock,
why=why, feeling=feeling, source=source,
leaf_type=leaf_type, trunk=trunk, confidence=confidence,
title=title, summary=lock[:80] if lock else emergence[:80],
persona_id=PERSONA_ID, epoch_id=epoch_id)
self.tree.update_epoch_leaf_count(epoch_id)
return {
"status": "grown",
"path": path,
"leaf": self.tree.get_leaf(path)
}
def close(self):
self.tree.close()