491 lines
21 KiB
Python
491 lines
21 KiB
Python
"""
|
||
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()
|