refactor: clean harmonyos branch to only contain HarmonyOS project in harmony/
This commit is contained in:
@ -1,12 +0,0 @@
|
||||
from .server import BackendServer, Packet, PacketType, PacketResponse
|
||||
from .client import BackendClient
|
||||
from .embedded_db import EmbeddedGraphDB
|
||||
|
||||
__all__ = [
|
||||
"BackendServer",
|
||||
"BackendClient",
|
||||
"EmbeddedGraphDB",
|
||||
"Packet",
|
||||
"PacketType",
|
||||
"PacketResponse"
|
||||
]
|
||||
@ -1,41 +0,0 @@
|
||||
import sqlite3
|
||||
import time
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
|
||||
class ActivityRecorder:
|
||||
"""记录 AI 对图数据库的操作到内存 SQLite"""
|
||||
|
||||
def __init__(self):
|
||||
self.conn = sqlite3.connect(":memory:", check_same_thread=False)
|
||||
self.conn.execute("CREATE TABLE activities (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp REAL, action TEXT, tool_name TEXT, entity TEXT, detail TEXT)")
|
||||
self.conn.commit()
|
||||
|
||||
def record(self, action: str, tool_name: str, entity: str, detail: str = "") -> None:
|
||||
self.conn.execute("INSERT INTO activities (timestamp, action, tool_name, entity, detail) VALUES (?, ?, ?, ?, ?)",
|
||||
(time.time(), action, tool_name, entity, detail))
|
||||
self.conn.commit()
|
||||
|
||||
def get_all(self) -> List[Dict]:
|
||||
cursor = self.conn.execute("SELECT id, timestamp, action, tool_name, entity, detail FROM activities ORDER BY id")
|
||||
rows = cursor.fetchall()
|
||||
return [{"id": r[0], "timestamp": r[1], "action": r[2], "tool_name": r[3], "entity": r[4], "detail": r[5]} for r in rows]
|
||||
|
||||
def clear(self) -> None:
|
||||
self.conn.execute("DELETE FROM activities")
|
||||
self.conn.commit()
|
||||
|
||||
def get_summary(self) -> Dict[str, int]:
|
||||
cursor = self.conn.execute("SELECT action, COUNT(*) FROM activities GROUP BY action")
|
||||
rows = cursor.fetchall()
|
||||
return {r[0]: r[1] for r in rows}
|
||||
|
||||
|
||||
_recorder: Optional[ActivityRecorder] = None
|
||||
|
||||
|
||||
def get_recorder() -> ActivityRecorder:
|
||||
global _recorder
|
||||
if _recorder is None:
|
||||
_recorder = ActivityRecorder()
|
||||
return _recorder
|
||||
128
core/client.py
128
core/client.py
@ -1,128 +0,0 @@
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, Dict
|
||||
|
||||
from .server import BackendServer, Packet, PacketType
|
||||
|
||||
|
||||
class BackendClient:
|
||||
|
||||
def __init__(self, server: BackendServer):
|
||||
self._server = server
|
||||
self._counter = 0
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def _next_id(self) -> str:
|
||||
with self._lock:
|
||||
self._counter += 1
|
||||
return f"{time.time()}_{self._counter}"
|
||||
|
||||
def send(self, message: str) -> Dict:
|
||||
return self.process_message(message)
|
||||
|
||||
def process_message(self, user_input: str) -> Dict:
|
||||
return self._server.process_message(user_input)
|
||||
|
||||
def get_settings(self) -> Dict:
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.GET_SETTINGS,
|
||||
body={}
|
||||
)
|
||||
return self._server.send(packet).body
|
||||
|
||||
def update_settings(self, api_config: Dict = None, tool_limits: Dict = None) -> Dict:
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.SET_SETTINGS,
|
||||
body={
|
||||
"api_config": api_config or {},
|
||||
"tool_limits": tool_limits or {}
|
||||
}
|
||||
)
|
||||
return self._server.send(packet).body
|
||||
|
||||
def execute_tool(self, name: str, arguments: Dict) -> Dict:
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.EXECUTE_TOOL,
|
||||
body={"tool_name": name, "arguments": arguments}
|
||||
)
|
||||
return self._server.send(packet).body
|
||||
|
||||
def get_status(self) -> Dict:
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.GET_STATUS,
|
||||
body={}
|
||||
)
|
||||
return self._server.send(packet).body
|
||||
|
||||
def save_history(self, messages: list) -> Dict:
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.SAVE_HISTORY,
|
||||
body={"messages": messages}
|
||||
)
|
||||
response = self._server.send(packet)
|
||||
return response.body.get("data", {})
|
||||
|
||||
def get_history(self) -> list:
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.GET_HISTORY,
|
||||
body={}
|
||||
)
|
||||
response = self._server.send(packet)
|
||||
data = response.body.get("data", {})
|
||||
return data.get("history", [])
|
||||
|
||||
def clear_history(self) -> Dict:
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.SAVE_HISTORY,
|
||||
body={"messages": []}
|
||||
)
|
||||
response = self._server.send(packet)
|
||||
return response.body.get("data", {})
|
||||
|
||||
def get_web_users(self) -> list:
|
||||
"""获取 Web 用户列表"""
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.GET_WEB_USERS,
|
||||
body={}
|
||||
)
|
||||
return self._server.send(packet).body.get("users", [])
|
||||
|
||||
def set_web_user(self, username: str, password: str) -> Dict:
|
||||
"""设置 Web 用户"""
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.SET_WEB_USER,
|
||||
body={"username": username, "password": password}
|
||||
)
|
||||
return self._server.send(packet).body.get("data", {"success": False})
|
||||
|
||||
def get_full_config(self) -> Dict:
|
||||
"""获取完整配置"""
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.GET_CONFIG,
|
||||
body={}
|
||||
)
|
||||
response = self._server.send(packet)
|
||||
return response.body if response.body else {"api_config": {}, "tool_limits": {}}
|
||||
|
||||
def report_web_status(self, running: bool, port: int = 4096) -> Dict:
|
||||
"""向后端报告 Web 服务运行状态"""
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.GET_WEB_SERVICE_STATUS,
|
||||
body={"running": running, "port": port}
|
||||
)
|
||||
response = self._server.send(packet)
|
||||
return response.body if response.body else {"success": False}
|
||||
|
||||
def shutdown(self) -> None:
|
||||
self._server.shutdown()
|
||||
@ -1,714 +0,0 @@
|
||||
"""
|
||||
内嵌图数据库 - 基于SQLite实现
|
||||
无需Docker,开箱即用
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional, Any
|
||||
|
||||
|
||||
class EmbeddedGraphDB:
|
||||
"""内嵌图数据库 - SQLite实现"""
|
||||
|
||||
def __init__(self, db_path: str = "graph_memory.db"):
|
||||
"""
|
||||
初始化数据库
|
||||
|
||||
Args:
|
||||
db_path: 数据库文件路径
|
||||
"""
|
||||
self.db_path = Path(db_path)
|
||||
self.conn = None
|
||||
self._init_db()
|
||||
|
||||
def _init_db(self):
|
||||
"""初始化数据库表"""
|
||||
self.conn = sqlite3.connect(str(self.db_path), check_same_thread=False)
|
||||
self.conn.row_factory = sqlite3.Row
|
||||
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# 创建实体表
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS entities (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
type TEXT,
|
||||
mention_count INTEGER DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
# 创建关系表
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS relations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source_id INTEGER NOT NULL,
|
||||
target_id INTEGER NOT NULL,
|
||||
relation_type TEXT NOT NULL,
|
||||
confidence REAL DEFAULT 1.0,
|
||||
status TEXT DEFAULT 'active',
|
||||
session_id TEXT,
|
||||
turn_id INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
date_bucket TEXT,
|
||||
superseded_by INTEGER,
|
||||
FOREIGN KEY (source_id) REFERENCES entities(id),
|
||||
FOREIGN KEY (target_id) REFERENCES entities(id)
|
||||
)
|
||||
""")
|
||||
|
||||
# 创建索引
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_entity_name ON entities(name)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_entity_type ON entities(type)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_relation_source ON relations(source_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_relation_target ON relations(target_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_relation_type ON relations(relation_type)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_relation_status ON relations(status)")
|
||||
|
||||
cursor.execute("""
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name='chat_records'
|
||||
""")
|
||||
if not cursor.fetchone():
|
||||
cursor.execute("""
|
||||
CREATE TABLE chat_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
cursor.execute("CREATE INDEX idx_chat_created ON chat_records(created_at)")
|
||||
|
||||
# 创建 Web 用户表(支持多用户隔离)
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS web_users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'user',
|
||||
config_path TEXT,
|
||||
db_path TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
# 检查并添加新字段(用于旧数据库迁移)
|
||||
cursor.execute("PRAGMA table_info(web_users)")
|
||||
columns = [row[1] for row in cursor.fetchall()]
|
||||
if 'config_path' not in columns:
|
||||
cursor.execute("ALTER TABLE web_users ADD COLUMN config_path TEXT")
|
||||
if 'db_path' not in columns:
|
||||
cursor.execute("ALTER TABLE web_users ADD COLUMN db_path TEXT")
|
||||
if 'role' not in columns:
|
||||
cursor.execute("ALTER TABLE web_users ADD COLUMN role TEXT NOT NULL DEFAULT 'user'")
|
||||
|
||||
# 确保至少有一个 admin(当 role 列刚添加时,已有用户都是 user)
|
||||
cursor.execute("SELECT COUNT(*) as cnt FROM web_users WHERE role = 'admin'")
|
||||
has_admin = cursor.fetchone()[0] > 0
|
||||
if not has_admin:
|
||||
cursor.execute("SELECT id, username FROM web_users ORDER BY created_at ASC LIMIT 1")
|
||||
first_user = cursor.fetchone()
|
||||
if first_user:
|
||||
cursor.execute("UPDATE web_users SET role = 'admin' WHERE id = ?", (first_user[0],))
|
||||
|
||||
self.conn.commit()
|
||||
|
||||
def ensure_constraints(self):
|
||||
"""确保约束(兼容Neo4j接口)"""
|
||||
pass # SQLite自动处理
|
||||
|
||||
def recall(self, query_intent: str, seed_entities: List[str] = None,
|
||||
depth: int = 2, time_range: Dict = None,
|
||||
session_filter: str = None) -> Dict:
|
||||
"""
|
||||
检索相关记忆
|
||||
|
||||
Args:
|
||||
query_intent: 查询关键词(逗号分隔)
|
||||
seed_entities: 种子实体
|
||||
depth: 搜索深度
|
||||
time_range: 时间范围
|
||||
session_filter: 会话过滤
|
||||
|
||||
Returns:
|
||||
检索结果
|
||||
"""
|
||||
keywords = [w.strip().lower() for w in query_intent.replace(',', ' ').split() if w.strip()]
|
||||
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# 搜索实体
|
||||
entities = []
|
||||
entity_ids = set()
|
||||
|
||||
# 如果没有关键词,返回所有实体(用于"我们都聊过什么"这类问题)
|
||||
if not keywords and not seed_entities:
|
||||
cursor.execute("""
|
||||
SELECT id, name, type, mention_count
|
||||
FROM entities
|
||||
ORDER BY mention_count DESC
|
||||
LIMIT 50
|
||||
""")
|
||||
|
||||
for row in cursor.fetchall():
|
||||
entity_ids.add(row['id'])
|
||||
entities.append({
|
||||
'name': row['name'],
|
||||
'type': row['type'] or 'unknown',
|
||||
'mention_count': row['mention_count']
|
||||
})
|
||||
else:
|
||||
# 有关键词,按关键词搜索
|
||||
for keyword in keywords:
|
||||
cursor.execute("""
|
||||
SELECT id, name, type, mention_count
|
||||
FROM entities
|
||||
WHERE LOWER(name) LIKE ?
|
||||
""", (f"%{keyword}%",))
|
||||
|
||||
for row in cursor.fetchall():
|
||||
if row['id'] not in entity_ids:
|
||||
entity_ids.add(row['id'])
|
||||
entities.append({
|
||||
'name': row['name'],
|
||||
'type': row['type'] or 'unknown',
|
||||
'mention_count': row['mention_count']
|
||||
})
|
||||
|
||||
# 广度优先搜索(BFS)扩展实体和关系
|
||||
relations = []
|
||||
visited_entity_ids = set(entity_ids) # 已访问的实体
|
||||
current_layer_ids = set(entity_ids) # 当前层的实体
|
||||
|
||||
# 记录每个实体的深度
|
||||
entity_depths = {} # entity_id -> depth
|
||||
for eid in entity_ids:
|
||||
entity_depths[eid] = 0
|
||||
|
||||
for layer in range(depth):
|
||||
if not current_layer_ids:
|
||||
break
|
||||
|
||||
# 查询当前层实体的所有关系
|
||||
placeholders = ','.join('?' * len(current_layer_ids))
|
||||
|
||||
query = f"""
|
||||
SELECT r.id, r.source_id, r.target_id,
|
||||
e1.name as source, e2.name as target,
|
||||
r.relation_type as type, r.confidence, r.session_id,
|
||||
r.turn_id, r.created_at, r.status
|
||||
FROM relations r
|
||||
JOIN entities e1 ON r.source_id = e1.id
|
||||
JOIN entities e2 ON r.target_id = e2.id
|
||||
WHERE (r.source_id IN ({placeholders}) OR r.target_id IN ({placeholders}))
|
||||
AND r.status = 'active'
|
||||
"""
|
||||
|
||||
params = list(current_layer_ids) + list(current_layer_ids)
|
||||
|
||||
if session_filter:
|
||||
query += " AND r.session_id = ?"
|
||||
params.append(session_filter)
|
||||
|
||||
cursor.execute(query, params)
|
||||
|
||||
# 收集下一层的实体
|
||||
next_layer_ids = set()
|
||||
current_layer_relations = [] # 当前层的关系
|
||||
|
||||
for row in cursor.fetchall():
|
||||
# 计算关系的深度(取两端实体深度的最大值+1)
|
||||
source_depth = entity_depths.get(row['source_id'], layer)
|
||||
target_depth = entity_depths.get(row['target_id'], layer)
|
||||
relation_depth = max(source_depth, target_depth) + 1
|
||||
|
||||
# 添加关系(带深度标注)
|
||||
current_layer_relations.append({
|
||||
'source': row['source'],
|
||||
'target': row['target'],
|
||||
'type': row['type'],
|
||||
'confidence': row['confidence'],
|
||||
'session_id': row['session_id'],
|
||||
'turn_id': row['turn_id'],
|
||||
'created_at': row['created_at'],
|
||||
'status': row['status'],
|
||||
'depth': relation_depth
|
||||
})
|
||||
|
||||
# 收集新实体(未访问过的)
|
||||
source_id = row['source_id']
|
||||
target_id = row['target_id']
|
||||
|
||||
if source_id not in visited_entity_ids:
|
||||
next_layer_ids.add(source_id)
|
||||
visited_entity_ids.add(source_id)
|
||||
entity_depths[source_id] = layer + 1
|
||||
|
||||
if target_id not in visited_entity_ids:
|
||||
next_layer_ids.add(target_id)
|
||||
visited_entity_ids.add(target_id)
|
||||
entity_depths[target_id] = layer + 1
|
||||
|
||||
relations.extend(current_layer_relations)
|
||||
|
||||
# 查询下一层实体的详细信息
|
||||
if next_layer_ids:
|
||||
placeholders = ','.join('?' * len(next_layer_ids))
|
||||
cursor.execute(f"""
|
||||
SELECT id, name, type, mention_count
|
||||
FROM entities
|
||||
WHERE id IN ({placeholders})
|
||||
""", list(next_layer_ids))
|
||||
|
||||
for row in cursor.fetchall():
|
||||
entities.append({
|
||||
'name': row['name'],
|
||||
'type': row['type'] or 'unknown',
|
||||
'mention_count': row['mention_count'],
|
||||
'depth': entity_depths.get(row['id'], layer + 1)
|
||||
})
|
||||
|
||||
# 移动到下一层
|
||||
current_layer_ids = next_layer_ids
|
||||
|
||||
# 为种子实体添加深度标注(depth=0)
|
||||
if entity_ids:
|
||||
# 重新标注种子实体的深度
|
||||
for entity in entities:
|
||||
if entity.get('depth') is None:
|
||||
entity['depth'] = 0
|
||||
|
||||
return {
|
||||
"entities": entities,
|
||||
"relations": relations,
|
||||
"message": f"找到 {len(entities)} 个实体, {len(relations)} 条关系"
|
||||
}
|
||||
|
||||
def commit(self, triplets: List[Dict], entity_types: Dict = None,
|
||||
temporal_tag: str = None, session_id: str = None,
|
||||
turn_id: int = None) -> Dict:
|
||||
"""
|
||||
写入记忆
|
||||
|
||||
Args:
|
||||
triplets: 三元组列表
|
||||
entity_types: 实体类型
|
||||
temporal_tag: 时间标签
|
||||
session_id: 会话ID
|
||||
turn_id: 轮次ID
|
||||
|
||||
Returns:
|
||||
写入结果
|
||||
"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
created_entities = 0
|
||||
created_relations = 0
|
||||
|
||||
for triplet in triplets:
|
||||
subject = triplet.get('subject')
|
||||
relation = triplet.get('relation')
|
||||
obj = triplet.get('object')
|
||||
confidence = triplet.get('confidence', 1.0)
|
||||
|
||||
if not all([subject, relation, obj]):
|
||||
continue
|
||||
|
||||
# 创建或更新实体
|
||||
for entity_name in [subject, obj]:
|
||||
entity_type = entity_types.get(entity_name) if entity_types else None
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO entities (name, type)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
mention_count = mention_count + 1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""", (entity_name, entity_type))
|
||||
|
||||
if cursor.rowcount > 0:
|
||||
created_entities += 1
|
||||
|
||||
# 获取实体ID
|
||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (subject,))
|
||||
source_id = cursor.fetchone()['id']
|
||||
|
||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (obj,))
|
||||
target_id = cursor.fetchone()['id']
|
||||
|
||||
# 创建关系
|
||||
date_bucket = datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO relations (
|
||||
source_id, target_id, relation_type, confidence,
|
||||
session_id, turn_id, date_bucket
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (source_id, target_id, relation, confidence,
|
||||
session_id, turn_id, date_bucket))
|
||||
|
||||
created_relations += 1
|
||||
|
||||
self.conn.commit()
|
||||
|
||||
return {
|
||||
"created_entities": created_entities,
|
||||
"created_relations": created_relations,
|
||||
"message": f"创建了 {created_entities} 个实体, {created_relations} 条关系"
|
||||
}
|
||||
|
||||
def purge(self, criteria: Dict, mode: str = "soft",
|
||||
new_relation: Dict = None) -> Dict:
|
||||
"""
|
||||
删除或修正记忆
|
||||
|
||||
Args:
|
||||
criteria: 删除条件
|
||||
mode: 删除模式 (soft/hard)
|
||||
new_relation: 替代关系
|
||||
|
||||
Returns:
|
||||
删除结果
|
||||
"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# 构建查询条件
|
||||
conditions = []
|
||||
params = []
|
||||
|
||||
if criteria.get('source'):
|
||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (criteria['source'],))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
conditions.append("source_id = ?")
|
||||
params.append(row['id'])
|
||||
|
||||
if criteria.get('target'):
|
||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (criteria['target'],))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
conditions.append("target_id = ?")
|
||||
params.append(row['id'])
|
||||
|
||||
if criteria.get('relation'):
|
||||
conditions.append("relation_type = ?")
|
||||
params.append(criteria['relation'])
|
||||
|
||||
if not conditions:
|
||||
return {"deleted": 0, "message": "无删除条件"}
|
||||
|
||||
where_clause = " AND ".join(conditions)
|
||||
|
||||
if mode == "soft":
|
||||
cursor.execute(f"""
|
||||
UPDATE relations
|
||||
SET status = 'deleted', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE {where_clause} AND status = 'active'
|
||||
""", params)
|
||||
else:
|
||||
cursor.execute(f"""
|
||||
DELETE FROM relations
|
||||
WHERE {where_clause}
|
||||
""", params)
|
||||
|
||||
deleted = cursor.rowcount
|
||||
self.conn.commit()
|
||||
|
||||
return {
|
||||
"deleted": deleted,
|
||||
"mode": mode,
|
||||
"message": f"删除了 {deleted} 条关系"
|
||||
}
|
||||
|
||||
def introspect(self, session_id: str = None) -> Dict:
|
||||
"""
|
||||
查看会话状态
|
||||
|
||||
Args:
|
||||
session_id: 会话ID
|
||||
|
||||
Returns:
|
||||
会话状态
|
||||
"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# 统计实体
|
||||
cursor.execute("SELECT COUNT(*) as count FROM entities")
|
||||
entity_count = cursor.fetchone()['count']
|
||||
|
||||
# 统计关系
|
||||
cursor.execute("SELECT COUNT(*) as count FROM relations WHERE status = 'active'")
|
||||
relation_count = cursor.fetchone()['count']
|
||||
|
||||
return {
|
||||
"entity_count": entity_count,
|
||||
"relation_count": relation_count,
|
||||
"session_id": session_id,
|
||||
"message": f"数据库包含 {entity_count} 个实体, {relation_count} 条关系"
|
||||
}
|
||||
|
||||
def archive(self, days: int = 30) -> Dict:
|
||||
"""归档旧关系"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE relations
|
||||
SET status = 'archived', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE status = 'active'
|
||||
AND created_at < datetime('now', ?)
|
||||
""", (f'-{days} days',))
|
||||
|
||||
archived = cursor.rowcount
|
||||
self.conn.commit()
|
||||
|
||||
return {
|
||||
"archived": archived,
|
||||
"message": f"归档了 {archived} 条关系"
|
||||
}
|
||||
|
||||
def cleanup(self, dry_run: bool = True) -> Dict:
|
||||
"""清理已删除数据"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
if dry_run:
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) as count
|
||||
FROM relations
|
||||
WHERE status = 'deleted'
|
||||
AND updated_at < datetime('now', '-90 days')
|
||||
""")
|
||||
deleted_relations = cursor.fetchone()['count']
|
||||
|
||||
return {
|
||||
"dry_run": True,
|
||||
"deleted_relations": deleted_relations,
|
||||
"message": f"将删除 {deleted_relations} 条关系"
|
||||
}
|
||||
else:
|
||||
cursor.execute("""
|
||||
DELETE FROM relations
|
||||
WHERE status = 'deleted'
|
||||
AND updated_at < datetime('now', '-90 days')
|
||||
""")
|
||||
deleted = cursor.rowcount
|
||||
self.conn.commit()
|
||||
|
||||
return {
|
||||
"dry_run": False,
|
||||
"deleted": deleted,
|
||||
"message": f"删除了 {deleted} 条关系"
|
||||
}
|
||||
|
||||
def save_chat_records(self, messages: list) -> Dict:
|
||||
"""保存聊天记录到数据库"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
saved = 0
|
||||
for msg in messages:
|
||||
role = msg.get("role")
|
||||
content = msg.get("content")
|
||||
if role and content:
|
||||
cursor.execute(
|
||||
"INSERT INTO chat_records (role, content) VALUES (?, ?)",
|
||||
(role, content)
|
||||
)
|
||||
saved += 1
|
||||
|
||||
self.conn.commit()
|
||||
|
||||
cursor.execute("""
|
||||
DELETE FROM chat_records
|
||||
WHERE id NOT IN (
|
||||
SELECT id FROM chat_records
|
||||
ORDER BY id DESC
|
||||
LIMIT 500
|
||||
)
|
||||
""")
|
||||
self.conn.commit()
|
||||
|
||||
return {"saved": saved}
|
||||
|
||||
def get_chat_records(self, limit: int = 500) -> list:
|
||||
"""从数据库获取聊天记录"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT role, content FROM chat_records
|
||||
ORDER BY id ASC LIMIT ?
|
||||
""", (limit,))
|
||||
return [{"role": row[0], "content": row[1]} for row in cursor.fetchall()]
|
||||
|
||||
def clear_chat_records(self) -> Dict:
|
||||
"""清空聊天记录(保留图数据库)"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("DELETE FROM chat_records")
|
||||
self.conn.commit()
|
||||
return {"cleared": True}
|
||||
|
||||
def set_web_user(self, username: str, password: str, base_dir: str = None, role: str = 'user') -> Dict:
|
||||
"""设置或更新 Web 登录用户。password 是明文,自动哈希存储。
|
||||
自动创建用户目录并设置 config_path 和 db_path。
|
||||
role: 'admin' 或 'user',默认 'user'"""
|
||||
if not username or not password:
|
||||
return {"success": False, "error": "用户名和密码不能为空"}
|
||||
if role not in ('admin', 'user'):
|
||||
return {"success": False, "error": "角色无效 (admin/user)"}
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
|
||||
# 确定基础目录
|
||||
if base_dir is None:
|
||||
base_dir = Path.home() / ".trulymem"
|
||||
else:
|
||||
base_dir = Path(base_dir)
|
||||
|
||||
# 创建用户目录
|
||||
user_dir = base_dir / username
|
||||
user_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 设置用户文件路径
|
||||
config_path = str(user_dir / "config.json")
|
||||
db_path = str(user_dir / f"{username}_graph.db")
|
||||
|
||||
cursor = self.conn.cursor()
|
||||
# 如果是第一个用户,强制设为 admin
|
||||
if self.get_web_users_count() == 0:
|
||||
role = 'admin'
|
||||
cursor.execute("""
|
||||
INSERT INTO web_users (username, password_hash, role, config_path, db_path)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(username) DO UPDATE SET
|
||||
password_hash = excluded.password_hash,
|
||||
role = CASE WHEN web_users.role = 'admin' THEN 'admin' ELSE excluded.role END,
|
||||
config_path = COALESCE(web_users.config_path, excluded.config_path),
|
||||
db_path = COALESCE(web_users.db_path, excluded.db_path),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""", (username, password_hash, role, config_path, db_path))
|
||||
self.conn.commit()
|
||||
return {"success": True, "username": username, "role": role, "config_path": config_path, "db_path": db_path}
|
||||
|
||||
def get_web_users(self) -> List[Dict]:
|
||||
"""获取所有 Web 用户列表"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("SELECT id, username, role, config_path, db_path, created_at, updated_at FROM web_users ORDER BY created_at ASC")
|
||||
users = []
|
||||
for row in cursor.fetchall():
|
||||
users.append({
|
||||
"id": row['id'],
|
||||
"username": row['username'],
|
||||
"role": row['role'],
|
||||
"config_path": row['config_path'],
|
||||
"db_path": row['db_path'],
|
||||
"created_at": row['created_at'],
|
||||
"updated_at": row['updated_at']
|
||||
})
|
||||
return users
|
||||
|
||||
def get_web_user(self, username: str) -> Optional[Dict]:
|
||||
"""获取单个 Web 用户信息"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT id, username, role, config_path, db_path, created_at, updated_at
|
||||
FROM web_users WHERE username = ?
|
||||
""", (username,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {
|
||||
"id": row['id'],
|
||||
"username": row['username'],
|
||||
"role": row['role'],
|
||||
"config_path": row['config_path'],
|
||||
"db_path": row['db_path'],
|
||||
"created_at": row['created_at'],
|
||||
"updated_at": row['updated_at']
|
||||
}
|
||||
return None
|
||||
|
||||
def is_admin(self, username: str) -> bool:
|
||||
"""检查用户是否为管理员"""
|
||||
user = self.get_web_user(username)
|
||||
return user is not None and user.get('role') == 'admin'
|
||||
|
||||
def delete_web_user(self, username: str) -> Dict:
|
||||
"""删除 Web 用户(同时保留文件目录)"""
|
||||
if not username:
|
||||
return {"success": False, "error": "用户名不能为空"}
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("DELETE FROM web_users WHERE username = ?", (username,))
|
||||
self.conn.commit()
|
||||
if cursor.rowcount > 0:
|
||||
return {"success": True, "username": username}
|
||||
return {"success": False, "error": "用户不存在"}
|
||||
|
||||
def get_web_users_count(self) -> int:
|
||||
"""获取 Web 用户数量 (用于判断是否需要首次设置)"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("SELECT COUNT(*) as cnt FROM web_users")
|
||||
row = cursor.fetchone()
|
||||
return row['cnt'] if row else 0
|
||||
|
||||
def verify_web_user(self, username: str, password: str) -> bool:
|
||||
"""验证 Web 用户登录"""
|
||||
import hashlib
|
||||
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT id FROM web_users
|
||||
WHERE username = ? AND password_hash = ?
|
||||
""", (username, password_hash))
|
||||
return cursor.fetchone() is not None
|
||||
|
||||
def close(self):
|
||||
"""关闭数据库连接"""
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
self.conn = None
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
|
||||
|
||||
# 兼容性别名
|
||||
Neo4jGraph = EmbeddedGraphDB
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试
|
||||
print("Testing Embedded Graph Database...")
|
||||
|
||||
with EmbeddedGraphDB("test.db") as db:
|
||||
# 写入测试
|
||||
result = db.commit(
|
||||
triplets=[
|
||||
{"subject": "用户", "relation": "喜欢", "object": "Python"},
|
||||
{"subject": "用户", "relation": "学习", "object": "AI"}
|
||||
],
|
||||
session_id="test-session",
|
||||
turn_id=1
|
||||
)
|
||||
print(f"Commit: {result}")
|
||||
|
||||
# 检索测试
|
||||
result = db.recall("Python,AI")
|
||||
print(f"Recall: {result}")
|
||||
|
||||
# 状态测试
|
||||
result = db.introspect()
|
||||
print(f"Introspect: {result}")
|
||||
|
||||
print("\nTest completed!")
|
||||
@ -1,393 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Graph Memory Client - 图记忆客户端核心实现(重构版)
|
||||
使用模块化的工具和提示词系统
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from openai import OpenAI
|
||||
|
||||
from .tools import TOOLS
|
||||
from .tool_executor import execute_tool
|
||||
from .prompts.prompt_manager import PromptManager
|
||||
|
||||
# 环境配置
|
||||
DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
|
||||
DEEPSEEK_BASE_URL = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
|
||||
MODEL_NAME = os.environ.get("MODEL_NAME", "deepseek-chat")
|
||||
|
||||
NEO4J_URI = os.environ.get("NEO4J_URI", "bolt://localhost:7687")
|
||||
NEO4J_USER = os.environ.get("NEO4J_USER", "neo4j")
|
||||
NEO4J_PASSWORD = os.environ.get("NEO4J_PASSWORD", "neo4j")
|
||||
|
||||
# 会话配置
|
||||
CURRENT_SESSION_ID = f"session-{datetime.now().strftime('%Y%m%d')}-{uuid.uuid4().hex[:4]}"
|
||||
CURRENT_TURN = 0
|
||||
|
||||
|
||||
class Neo4jGraph:
|
||||
"""Neo4j图数据库客户端"""
|
||||
|
||||
def __init__(self, uri: str, user: str, password: str):
|
||||
from neo4j import GraphDatabase
|
||||
self.driver = GraphDatabase.driver(uri, auth=(user, password))
|
||||
|
||||
def close(self):
|
||||
self.driver.close()
|
||||
|
||||
def ensure_constraints(self):
|
||||
"""确保约束和索引存在"""
|
||||
with self.driver.session() as session:
|
||||
# 实体约束
|
||||
session.run("CREATE CONSTRAINT entity_name_constraint IF NOT EXISTS FOR (e:Entity) REQUIRE e.name IS UNIQUE")
|
||||
session.run("CREATE CONSTRAINT session_id_constraint IF NOT EXISTS FOR (s:Session) REQUIRE s.session_id IS UNIQUE")
|
||||
|
||||
# 关系索引
|
||||
session.run("CREATE INDEX rel_created_at IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.created_at")
|
||||
session.run("CREATE INDEX rel_session_id IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.session_id")
|
||||
session.run("CREATE INDEX rel_type IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.type")
|
||||
session.run("CREATE INDEX rel_status IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.status")
|
||||
session.run("CREATE INDEX rel_date_bucket IF NOT EXISTS FOR ()-[r:RELATES]-() ON r.date_bucket")
|
||||
|
||||
# 实体索引
|
||||
session.run("CREATE INDEX entity_type IF NOT EXISTS FOR (e:Entity) ON e.type")
|
||||
session.run("CREATE INDEX entity_mention_count IF NOT EXISTS FOR (e:Entity) ON e.mention_count")
|
||||
|
||||
def recall(self, query_intent: str, seed_entities: list = None, depth: int = 2,
|
||||
time_range: dict = None, session_filter: str = None) -> dict:
|
||||
"""检索记忆"""
|
||||
with self.driver.session() as session:
|
||||
# 支持逗号分隔的多个关键词
|
||||
keywords = [w.strip() for w in query_intent.replace(',', ' ').split() if len(w.strip()) > 0]
|
||||
|
||||
if not keywords and not seed_entities:
|
||||
return {"entities": [], "relations": [], "message": "无查询关键词"}
|
||||
|
||||
params = {}
|
||||
cond_parts = ["r.status = 'active'"]
|
||||
|
||||
if session_filter:
|
||||
cond_parts.append("r.session_id = $session_id")
|
||||
params["session_id"] = session_filter
|
||||
|
||||
if keywords:
|
||||
keyword_conditions = []
|
||||
for k in keywords:
|
||||
k_lower = k.lower()
|
||||
keyword_conditions.append(f"toLower(e.name) CONTAINS '{k_lower}'")
|
||||
keyword_conditions.append(f"toLower(t.name) CONTAINS '{k_lower}'")
|
||||
keyword_conditions.append(f"toLower(r.type) CONTAINS '{k_lower}'")
|
||||
cond_parts.append(f"({' OR '.join(keyword_conditions)})")
|
||||
|
||||
if seed_entities:
|
||||
placeholders = ",".join([f"'{s}'" for s in seed_entities])
|
||||
cond_parts.append(f"(e.name IN [{placeholders}] OR t.name IN [{placeholders}])")
|
||||
|
||||
if time_range and "days" in time_range:
|
||||
cond_parts.append(f"r.created_at >= datetime() - duration('P{time_range['days']}D')")
|
||||
|
||||
where_clause = " AND ".join(cond_parts)
|
||||
|
||||
cypher = f"""
|
||||
MATCH (e:Entity)-[r:RELATES]->(t:Entity)
|
||||
WHERE {where_clause}
|
||||
RETURN e, r, t
|
||||
ORDER BY r.created_at DESC
|
||||
LIMIT 30
|
||||
"""
|
||||
|
||||
result = session.run(cypher, params)
|
||||
entities, relations = {}, []
|
||||
|
||||
for record in result:
|
||||
e, r, t = record["e"], record["r"], record["t"]
|
||||
if e["name"] not in entities:
|
||||
entities[e["name"]] = {"name": e["name"], "type": e.get("type", "unknown"), "mention_count": e.get("mention_count", 1)}
|
||||
if t["name"] not in entities:
|
||||
entities[t["name"]] = {"name": t["name"], "type": t.get("type", "unknown"), "mention_count": t.get("mention_count", 1)}
|
||||
|
||||
relations.append({
|
||||
"source": e["name"],
|
||||
"target": t["name"],
|
||||
"type": r["type"],
|
||||
"created_at": str(r.get("created_at", "")),
|
||||
"session_id": r.get("session_id", ""),
|
||||
"turn_id": r.get("turn_id", 0),
|
||||
"confidence": r.get("confidence", 1.0)
|
||||
})
|
||||
|
||||
return {"entities": list(entities.values()), "relations": relations[:20]}
|
||||
|
||||
def commit(self, triplets: list, entity_types: list = None, temporal_tag: str = None) -> dict:
|
||||
"""写入记忆"""
|
||||
global CURRENT_TURN
|
||||
with self.driver.session() as session:
|
||||
valid_triplets = [t for t in triplets if t.get("subject") and t.get("relation") and t.get("object")]
|
||||
|
||||
if not valid_triplets:
|
||||
return {"committed_count": 0, "details": []}
|
||||
|
||||
etype = entity_types[0] if entity_types else "unknown"
|
||||
date_bucket = temporal_tag or datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
results = []
|
||||
for triplet in valid_triplets:
|
||||
subject = triplet.get("subject", "").strip()
|
||||
relation = triplet.get("relation", "").strip()
|
||||
obj = triplet.get("object", "").strip()
|
||||
confidence = triplet.get("confidence", 0.9)
|
||||
|
||||
session.run("""
|
||||
MERGE (s:Entity {name: $subject})
|
||||
ON CREATE SET s.type = $type, s.created_at = datetime(), s.mention_count = 1, s.updated_at = datetime()
|
||||
ON MATCH SET s.mention_count = coalesce(s.mention_count, 0) + 1, s.updated_at = datetime()
|
||||
|
||||
MERGE (t:Entity {name: $object})
|
||||
ON CREATE SET t.type = $type, t.created_at = datetime(), t.mention_count = 1, t.updated_at = datetime()
|
||||
ON MATCH SET t.mention_count = coalesce(t.mention_count, 0) + 1, t.updated_at = datetime()
|
||||
|
||||
CREATE (s)-[r:RELATES {
|
||||
type: $relation,
|
||||
created_at: datetime(),
|
||||
session_id: $session_id,
|
||||
turn_id: $turn_id,
|
||||
role: 'user',
|
||||
status: 'active',
|
||||
confidence: $confidence,
|
||||
date_bucket: $date_bucket
|
||||
}]->(t)
|
||||
""", subject=subject, object=obj, relation=relation, type=etype,
|
||||
session_id=CURRENT_SESSION_ID, turn_id=CURRENT_TURN, confidence=confidence,
|
||||
date_bucket=date_bucket)
|
||||
|
||||
results.append(f"{subject} -[{relation}]-> {obj}")
|
||||
|
||||
return {"committed_count": len(results), "details": results}
|
||||
|
||||
def purge(self, criteria: dict, mode: str = "soft", new_relation: dict = None) -> dict:
|
||||
"""删除记忆"""
|
||||
with self.driver.session() as session:
|
||||
subject_pattern = criteria.get("subject_contains", "")
|
||||
rel_type = criteria.get("relation_type", "")
|
||||
target_pattern = criteria.get("target_contains", "")
|
||||
session_id = criteria.get("session_id", CURRENT_SESSION_ID)
|
||||
|
||||
cond_parts = ["r.status = 'active'"]
|
||||
params = {"session_id": session_id}
|
||||
|
||||
if subject_pattern:
|
||||
cond_parts.append("e.name CONTAINS $subject")
|
||||
params["subject"] = subject_pattern
|
||||
if target_pattern:
|
||||
cond_parts.append("t.name CONTAINS $target")
|
||||
params["target"] = target_pattern
|
||||
if rel_type:
|
||||
cond_parts.append("r.type = $rel_type")
|
||||
params["rel_type"] = rel_type
|
||||
|
||||
where_clause = " AND ".join(cond_parts)
|
||||
|
||||
if mode == "supersede" and new_relation:
|
||||
new_rel = new_relation.get("relation", "")
|
||||
new_target = new_relation.get("target", "")
|
||||
|
||||
if not new_rel or not new_target:
|
||||
return {"error": "supersede模式需要提供new_relation.relation和new_relation.target"}
|
||||
|
||||
result = session.run(f"""
|
||||
MATCH (s:Entity)-[r:RELATES]->(t:Entity)
|
||||
WHERE {where_clause}
|
||||
SET r.status = 'superseded', r.updated_at = datetime()
|
||||
RETURN count(r) as count
|
||||
""", params)
|
||||
|
||||
count = result.single()["count"]
|
||||
return {"deleted_count": count, "mode": "supersede"}
|
||||
else:
|
||||
result = session.run(f"""
|
||||
MATCH ()-[r:RELATES]->()
|
||||
WHERE {where_clause}
|
||||
SET r.status = 'deleted', r.updated_at = datetime()
|
||||
RETURN count(r) as deleted
|
||||
""", params)
|
||||
count = result.single()["deleted"]
|
||||
|
||||
return {"deleted_count": count, "mode": "soft"}
|
||||
|
||||
def introspect(self, session_id: str = None) -> dict:
|
||||
"""查看记忆状态"""
|
||||
target_session = session_id or CURRENT_SESSION_ID
|
||||
|
||||
with self.driver.session() as session:
|
||||
result = session.run("""
|
||||
MATCH (s:Entity)-[r:RELATES]->(t:Entity)
|
||||
WHERE r.session_id = $session_id AND r.status = 'active'
|
||||
RETURN collect(DISTINCT s.name) as source_entities,
|
||||
collect(DISTINCT t.name) as target_entities,
|
||||
count(r) as rel_count,
|
||||
collect(DISTINCT r.type) as rel_types
|
||||
""", session_id=target_session)
|
||||
record = result.single()
|
||||
|
||||
result2 = session.run("""
|
||||
MATCH (e:Entity)
|
||||
RETURN e.name as name, e.mention_count as count, e.type as type
|
||||
ORDER BY e.mention_count DESC
|
||||
LIMIT 10
|
||||
""")
|
||||
hotspots = [(r["name"], r["count"], r["type"]) for r in result2]
|
||||
|
||||
return {
|
||||
"session_id": target_session,
|
||||
"total_turns": CURRENT_TURN,
|
||||
"entities_discussed": list(set((record["source_entities"] or []) + (record["target_entities"] or []))),
|
||||
"relation_count": record["rel_count"] if record else 0,
|
||||
"relation_types": record["rel_types"] if record else [],
|
||||
"memory_hotspots": hotspots
|
||||
}
|
||||
|
||||
def archive(self, days: int = 30) -> dict:
|
||||
"""归档旧记忆"""
|
||||
with self.driver.session() as session:
|
||||
result = session.run("""
|
||||
MATCH ()-[r:RELATES]->()
|
||||
WHERE r.status = 'active' AND r.created_at < datetime() - duration('P' + $days + 'D')
|
||||
SET r.status = 'archived', r.archived_at = datetime()
|
||||
RETURN count(r) as archived
|
||||
""", days=str(days))
|
||||
|
||||
return {"archived_count": result.single()["archived"], "days": days}
|
||||
|
||||
def cleanup(self, dry_run: bool = True) -> dict:
|
||||
"""清理无效数据"""
|
||||
with self.driver.session() as session:
|
||||
result1 = session.run("""
|
||||
MATCH ()-[r:RELATES]->()
|
||||
WHERE r.status = 'deleted' AND r.updated_at < datetime() - duration('P90D')
|
||||
RETURN count(r) as to_delete
|
||||
""")
|
||||
deleted_relations = result1.single()["to_delete"]
|
||||
|
||||
result2 = session.run("""
|
||||
MATCH (e:Entity)
|
||||
WHERE NOT (e)-[:RELATES]-()
|
||||
RETURN count(e) as orphans
|
||||
""")
|
||||
orphan_nodes = result2.single()["orphans"]
|
||||
|
||||
if not dry_run and deleted_relations > 0:
|
||||
session.run("""
|
||||
MATCH ()-[r:RELATES]->()
|
||||
WHERE r.status = 'deleted' AND r.updated_at < datetime() - duration('P90D')
|
||||
DELETE r
|
||||
""")
|
||||
|
||||
if not dry_run and orphan_nodes > 0:
|
||||
session.run("""
|
||||
MATCH (e:Entity)
|
||||
WHERE NOT (e)-[:RELATES]-()
|
||||
DELETE e
|
||||
""")
|
||||
|
||||
return {
|
||||
"dry_run": dry_run,
|
||||
"deleted_relations": deleted_relations,
|
||||
"orphan_nodes": orphan_nodes,
|
||||
"action_taken": not dry_run
|
||||
}
|
||||
|
||||
|
||||
class GraphMemoryClient:
|
||||
"""图记忆客户端"""
|
||||
|
||||
def __init__(self, api_key: str, base_url: str, graph, model: str = "deepseek-chat"):
|
||||
# 清理可能存在的错误代理环境变量
|
||||
import os
|
||||
proxy_vars = ['http_proxy', 'https_proxy', 'HTTP_PROXY', 'HTTPS_PROXY', 'all_proxy', 'ALL_PROXY']
|
||||
for var in proxy_vars:
|
||||
if var in os.environ:
|
||||
value = os.environ[var]
|
||||
# 如果代理URL没有scheme前缀,添加http://
|
||||
if value and not value.startswith(('http://', 'https://', 'socks5://', 'socks4://')):
|
||||
os.environ[var] = f'http://{value}'
|
||||
|
||||
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
||||
self.graph = graph
|
||||
self.tools = TOOLS
|
||||
self.model = model
|
||||
|
||||
prompt_manager = PromptManager()
|
||||
self.system_prompt = prompt_manager.get_system_prompt()
|
||||
|
||||
def send_message(self, user_input: str, tool_results: list = None, assistant_msg: dict = None) -> dict:
|
||||
"""发送消息"""
|
||||
global CURRENT_TURN
|
||||
|
||||
messages = [{"role": "system", "content": self.system_prompt}]
|
||||
|
||||
# 添加用户消息
|
||||
messages.append({"role": "user", "content": user_input})
|
||||
|
||||
# 添加 assistant 消息(包含 tool_calls)
|
||||
if assistant_msg:
|
||||
messages.append(assistant_msg)
|
||||
|
||||
# 添加工具结果
|
||||
if tool_results:
|
||||
messages.extend(tool_results)
|
||||
|
||||
response = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=messages,
|
||||
tools=self.tools,
|
||||
tool_choice="auto"
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def send_message_with_history(self, messages_history: list) -> dict:
|
||||
"""使用消息历史发送消息"""
|
||||
global CURRENT_TURN
|
||||
|
||||
# 构建完整消息列表
|
||||
messages = [{"role": "system", "content": self.system_prompt}]
|
||||
messages.extend(messages_history)
|
||||
|
||||
response = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=messages,
|
||||
tools=self.tools,
|
||||
tool_choice="auto"
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def send_message_stream(self, user_input: str, tool_results: list = None, assistant_msg: dict = None):
|
||||
"""流式发送消息"""
|
||||
global CURRENT_TURN
|
||||
|
||||
messages = [{"role": "system", "content": self.system_prompt}]
|
||||
|
||||
# 添加用户消息
|
||||
messages.append({"role": "user", "content": user_input})
|
||||
|
||||
# 添加 assistant 消息(包含 tool_calls)
|
||||
if assistant_msg:
|
||||
messages.append(assistant_msg)
|
||||
|
||||
# 添加工具结果
|
||||
if tool_results:
|
||||
messages.extend(tool_results)
|
||||
|
||||
stream = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=messages,
|
||||
tools=self.tools,
|
||||
tool_choice="auto",
|
||||
stream=True
|
||||
)
|
||||
|
||||
return stream
|
||||
@ -1,6 +0,0 @@
|
||||
"""
|
||||
提示词管理模块
|
||||
"""
|
||||
from .prompt_manager import PromptManager
|
||||
|
||||
__all__ = ["PromptManager"]
|
||||
@ -1,77 +0,0 @@
|
||||
"""
|
||||
提示词管理器
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class PromptManager:
|
||||
"""提示词管理器"""
|
||||
|
||||
_instance = None
|
||||
_cached_prompt = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if not hasattr(self, '_initialized'):
|
||||
self.prompts_dir = Path(__file__).parent / "templates"
|
||||
self._initialized = True
|
||||
|
||||
def get_system_prompt(self) -> str:
|
||||
if PromptManager._cached_prompt is not None:
|
||||
return PromptManager._cached_prompt
|
||||
|
||||
prompt_file = self.prompts_dir / "system_prompt.md"
|
||||
if prompt_file.exists():
|
||||
with open(prompt_file, "r", encoding="utf-8") as f:
|
||||
PromptManager._cached_prompt = f.read()
|
||||
else:
|
||||
PromptManager._cached_prompt = self._build_default_prompt()
|
||||
|
||||
return PromptManager._cached_prompt
|
||||
|
||||
def _build_default_prompt(self) -> str:
|
||||
return """你是TrulyMEM,一个拥有长期记忆能力的AI助手。
|
||||
|
||||
## 核心能力
|
||||
|
||||
1. **长期记忆** - 基于图数据库存储实体关系
|
||||
2. **人设管理** - 支持角色扮演和性格设定
|
||||
3. **任务跟踪** - 维护工作记忆链,跟踪连续性任务
|
||||
|
||||
## 记忆原则
|
||||
|
||||
- **明确内容必须写入** - 用户明确提到的信息必须存储
|
||||
- **推理内容必须标注** - AI推理得到的内容标注[猜测]
|
||||
- **图数据库是唯一记忆源** - 没有其他记忆方式
|
||||
|
||||
## 工具使用
|
||||
|
||||
### 记忆工具
|
||||
- `memory_recall` - 检索记忆
|
||||
- `memory_commit` - 写入记忆
|
||||
- `memory_purge` - 删除记忆
|
||||
- `memory_introspect` - 查看状态
|
||||
|
||||
### 人设工具
|
||||
- `persona_update` - 更新人设
|
||||
- `persona_clear` - 清除人设
|
||||
|
||||
### 任务工具
|
||||
- `task_create` - 创建任务
|
||||
- `task_set_state` - 设置状态
|
||||
- `task_delete` - 删除任务
|
||||
- `task_link_info` - 关联信息
|
||||
|
||||
## 自主性
|
||||
|
||||
你有权根据对话上下文自主决定:
|
||||
- 是否需要查询记忆
|
||||
- 是否需要写入记忆
|
||||
- 是否需要维护任务链
|
||||
- 如何使用工具
|
||||
|
||||
记住:灵活应对,保持自然对话体验。"""
|
||||
@ -1,422 +0,0 @@
|
||||
# TrulyMEM 系统提示词
|
||||
|
||||
你是TrulyMEM,一个拥有长期记忆能力的AI助手。
|
||||
|
||||
## ⚠️ 最高优先级:强制执行顺序
|
||||
|
||||
**每轮对话必须严格按以下顺序执行,不可跳过任何步骤!**
|
||||
|
||||
```
|
||||
步骤1: memory_recall (查询人设图) → 必须首先执行
|
||||
步骤2: memory_recall (查询工作记忆链) → 必须第二步执行
|
||||
步骤3: 处理对话内容
|
||||
步骤4: 更新工作记忆链
|
||||
```
|
||||
|
||||
**违反顺序的后果**:
|
||||
- 跳过步骤1 → 无法获取人设,回复风格错误
|
||||
- 跳过步骤2 → 无法获取上下文,对话不连贯
|
||||
- 顺序错误 → 系统状态混乱
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 最高优先级:只回复一次
|
||||
|
||||
**每轮对话只能回复一次!**
|
||||
|
||||
- 执行完所有工具调用后,给出一个完整的回复
|
||||
- 不要在工具调用过程中多次回复
|
||||
- 不要重复说相同的内容
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键约束:无传统上下文系统
|
||||
|
||||
**重要**: 你没有传统的对话上下文系统(没有消息历史数组)。
|
||||
|
||||
- ❌ **没有** messages数组存储历史对话
|
||||
- ❌ **没有** 传统的多轮对话上下文
|
||||
- ✅ **只有** 图数据库作为唯一记忆载体
|
||||
- ✅ **必须** 通过工作记忆链维持对话连贯性
|
||||
|
||||
## 核心身份
|
||||
|
||||
- **名称**: TrulyMEM (TrueHumanMEM)
|
||||
- **能力**: 基于图数据库的长期记忆
|
||||
- **理念**: 让AI的记忆方式更像人类
|
||||
|
||||
## 核心能力
|
||||
|
||||
### 1. 长期记忆
|
||||
- 图数据库存储实体关系
|
||||
- 支持时间范围查询
|
||||
- 支持会话过滤
|
||||
|
||||
### 2. 人设管理(关键)
|
||||
- 角色扮演支持
|
||||
- 性格、语气设定
|
||||
- 动态切换人设
|
||||
- **每轮必须查询人设图**
|
||||
|
||||
### 3. 任务跟踪(关键)
|
||||
- 工作记忆链 - **维持对话连贯性的唯一机制**
|
||||
- 任务状态管理
|
||||
- 上下文恢复
|
||||
|
||||
## 记忆原则
|
||||
|
||||
### 必须写入的情况
|
||||
- 用户明确表达偏好:"我喜欢X"
|
||||
- 用户分享信息:"我在做X项目"
|
||||
- 用户制定计划:"我打算X"
|
||||
- 用户描述状态:"我现在在X"
|
||||
|
||||
### 禁止写入的情况
|
||||
- AI推断的用户偏好
|
||||
- AI猜测的用户意图
|
||||
- AI推导的结论
|
||||
|
||||
### 标注规则
|
||||
- 推理内容必须标注 **[猜测]**
|
||||
- 明确内容直接陈述
|
||||
|
||||
## 工具系统
|
||||
|
||||
### 记忆工具
|
||||
| 工具 | 功能 | 使用场景 |
|
||||
|------|------|---------|
|
||||
| `memory_recall` | 检索记忆 | 查询历史信息 |
|
||||
| `memory_commit` | 写入记忆 | 存储重要信息 |
|
||||
| `memory_purge` | 删除记忆 | 修正错误信息 |
|
||||
| `memory_introspect` | 查看状态 | 监控记忆系统 |
|
||||
| `context_rewrite` | 压缩工具调用上下文 | 工具调用≥2次后,压缩JSON为自然语言摘要 |
|
||||
|
||||
### 人设工具
|
||||
| 工具 | 功能 | 使用场景 |
|
||||
|------|------|---------|
|
||||
| `persona_update` | 更新人设 | 设置角色属性 |
|
||||
| `persona_clear` | 清除人设 | 恢复默认身份 |
|
||||
|
||||
### 任务工具
|
||||
| 工具 | 功能 | 使用场景 |
|
||||
|------|------|---------|
|
||||
| `task_create` | 创建任务 | 开始连续性任务 |
|
||||
| `task_set_state` | 设置状态 | 更新任务状态 |
|
||||
| `task_delete` | 删除任务 | 清理完成任务 |
|
||||
| `task_link_info` | 关联信息 | 连接任务与记忆 |
|
||||
|
||||
## context_rewrite 使用规则
|
||||
|
||||
### ⚠️ 强制触发条件
|
||||
|
||||
**每调用 5 次记忆相关工具,必须调用一次 context_rewrite!**
|
||||
|
||||
记忆相关工具包括:
|
||||
- `memory_recall` - 检索记忆
|
||||
- `memory_commit` - 写入记忆
|
||||
- `memory_purge` - 删除记忆
|
||||
- `memory_introspect` - 查看状态
|
||||
- `persona_update` - 更新人设
|
||||
- `persona_clear` - 清除人设
|
||||
- `task_create` - 创建任务
|
||||
- `task_set_state` - 设置状态
|
||||
- `task_delete` - 删除任务
|
||||
- `task_link_info` - 关联信息
|
||||
|
||||
**触发规则**:
|
||||
- 累计调用 5 次记忆工具 → 必须调用 context_rewrite
|
||||
- 累计调用 10 次记忆工具 → 必须调用 context_rewrite
|
||||
- 以此类推...
|
||||
|
||||
**目的**:
|
||||
- 保持上下文精简,只保留AI真正需要的信息
|
||||
- 避免无用的JSON细节填满上下文
|
||||
- 提高后续推理效率
|
||||
|
||||
### 使用场景
|
||||
|
||||
当你已经执行了多次工具调用,且:
|
||||
- 工具结果的JSON细节你已经理解,不再需要原始格式
|
||||
- 但你需要记住"我调用了哪些工具、得到了什么结论"
|
||||
- 继续携带原始JSON会干扰后续推理
|
||||
|
||||
→ 调用 context_rewrite 压缩上下文
|
||||
|
||||
**强制格式要求**:
|
||||
- 必须标注 `[工具调用总结: 本次总结了 N 次工具调用 | 调用工具: tool1, tool2]`
|
||||
- 必须保留关键语义信息
|
||||
- 不可删除用户原始消息
|
||||
- 不可歪曲工具返回的关键事实
|
||||
|
||||
**示例**:
|
||||
```
|
||||
[工具调用总结: 本次总结了 2 次工具调用 | 调用工具: memory_recall, memory_recall]
|
||||
|
||||
- 查询人设图:未找到人设,使用默认身份
|
||||
- 查询工作记忆链:发现 Task_成语接龙,状态已暂停,当前成语为虎作伥
|
||||
```
|
||||
|
||||
## 每轮对话强制要求
|
||||
|
||||
### ⚠️ 执行顺序(每轮必须)
|
||||
|
||||
由于没有传统上下文系统,必须通过图数据库维持对话连贯性。
|
||||
|
||||
#### 步骤1: 查询人设图(最高优先级)
|
||||
```
|
||||
必须调用: memory_recall
|
||||
参数: {
|
||||
"query_intent": "AI,人设,角色,性格,语气,说话风格",
|
||||
"depth": 2
|
||||
}
|
||||
```
|
||||
**目的**: 获取当前人设,确保角色一致性。
|
||||
**处理**:
|
||||
- 找到人设 → 严格按照人设回复
|
||||
- 未找到 → 使用默认TrulyMEM身份
|
||||
|
||||
#### 步骤2: 查询工作记忆链
|
||||
```
|
||||
必须调用: memory_recall
|
||||
参数: {
|
||||
"query_intent": "TaskNode,工作记忆,任务链",
|
||||
"depth": 2
|
||||
}
|
||||
```
|
||||
**目的**: 获取之前的任务上下文,了解对话历史。
|
||||
|
||||
#### 步骤3: 处理对话
|
||||
- 理解用户意图
|
||||
- 根据人设和工作记忆链生成回复
|
||||
- 执行其他必要的记忆操作
|
||||
|
||||
#### 步骤4: 更新工作记忆链
|
||||
|
||||
**重要**: 工作记忆链有两种关联机制:
|
||||
1. **时间链(NEXT_TASK)**: 系统自动维护,连接TaskNode形成时间序列
|
||||
2. **信息关联(CONTAINS_INFO)**: 模型主动决定,将TaskNode链接到相关的一般记忆节点
|
||||
|
||||
**执行步骤**:
|
||||
1. 使用 `memory_commit` 写入本轮重要信息(用户偏好、事实等)
|
||||
2. 使用 `task_create` 创建任务节点(系统自动维护时间链)
|
||||
3. 使用 `task_link_info` 将相关记忆节点关联到任务节点
|
||||
|
||||
**task_link_info 使用场景**:
|
||||
- 本轮写入了新的记忆节点 → 关联到当前任务
|
||||
- 讨论了之前的话题 → 关联到相关记忆节点
|
||||
- 用户提到相关概念 → 关联到相关记忆节点
|
||||
|
||||
**示例**:
|
||||
```
|
||||
用户: "我还是更喜欢罗辑,他的角色深度很让我着迷"
|
||||
|
||||
AI操作:
|
||||
1. memory_commit: 写入 "用户喜欢罗辑"、"罗辑角色深度"
|
||||
2. task_create: 创建 "Task_讨论罗辑"
|
||||
3. task_link_info: 关联 ["用户喜欢罗辑", "罗辑角色深度"]
|
||||
```
|
||||
|
||||
**目的**:
|
||||
- 时间链维持对话连贯性(系统自动)
|
||||
- 信息关联实现"由一件事回忆起相关事情"(模型决定)
|
||||
|
||||
---
|
||||
|
||||
## 人设图机制
|
||||
|
||||
### 强制查询
|
||||
每轮对话开始时**必须**查询人设图,确保角色一致性。
|
||||
|
||||
### 人设优先级
|
||||
- 人设优先级 > 默认身份
|
||||
- 每句话都符合人设的语气、风格、特征
|
||||
- 绝不主动跳出角色,除非用户明确要求
|
||||
|
||||
### 人设更新
|
||||
用户要求角色扮演时:
|
||||
1. 使用 `persona_update` 更新人设
|
||||
2. 立即按照新人设回复
|
||||
|
||||
### 人设清除
|
||||
用户要求恢复默认身份时:
|
||||
1. 使用 `persona_clear` 清除人设
|
||||
2. 恢复为TrulyMEM默认身份
|
||||
|
||||
---
|
||||
|
||||
## 工作记忆链机制
|
||||
|
||||
### ⚠️ 核心理念:维持对话连贯性
|
||||
|
||||
**重要**: 由于没有传统的消息历史数组,工作记忆链是维持对话连贯性的唯一机制。
|
||||
|
||||
### 强制查询场景:
|
||||
|
||||
以下情况**必须**查询工作记忆链:
|
||||
|
||||
1. **每轮对话开始时(强制第二步)**
|
||||
- 查询意图: "TaskNode,工作记忆,任务链"
|
||||
- 目的: 获取之前的任务上下文,了解对话历史
|
||||
|
||||
2. **用户提到"刚才"、"之前"、"上次"、"刚刚"**
|
||||
- 例: "刚才我们聊了什么?"
|
||||
- 例: "继续刚才的话题"
|
||||
- 例: "关于刚才的成语接龙..."
|
||||
- 例: "我不是刚刚给你讲了个故事嘛"
|
||||
|
||||
3. **用户使用指代词(这个故事、那个故事、这件事等)**
|
||||
- 例: "你给我整体讲一下这个故事吧" → 必须查询工作记忆链确定"这个故事"指什么
|
||||
- 例: "继续那个任务" → 必须查询工作记忆链确定"那个任务"是什么
|
||||
- 例: "复述一下" → 必须查询工作记忆链确定要复述什么
|
||||
- **关键**: 指代词必须通过工作记忆链解析,不能凭空猜测!
|
||||
|
||||
4. **用户询问对话历史**
|
||||
- 例: "我们之前说了什么?"
|
||||
- 例: "我们聊过X吗?"
|
||||
|
||||
5. **连续性任务被打断后恢复**
|
||||
- 例: 用户突然回到之前的话题
|
||||
- 例: 用户要求继续之前的任务
|
||||
|
||||
6. **涉及上下文的引用**
|
||||
- 例: "那个东西"(需要查询上下文)
|
||||
- 例: "继续"(需要查询当前任务)
|
||||
|
||||
### 强制更新场景:
|
||||
|
||||
以下情况**必须**更新工作记忆链:
|
||||
|
||||
1. **每轮对话结束时(强制第四步)**
|
||||
- 创建任务节点记录本轮对话
|
||||
- 目的: 维持时间链,确保对话连贯性
|
||||
|
||||
2. **开始连续性任务时**
|
||||
- 例: 用户发起游戏、项目、学习计划等
|
||||
- 必须创建任务节点并设置状态为"进行中"
|
||||
|
||||
3. **任务状态发生变化时**
|
||||
- 例: 任务完成、暂停、取消
|
||||
- 必须及时更新任务状态
|
||||
|
||||
### 节点类型
|
||||
- **TaskNode** - 任务节点,存储任务概述
|
||||
- **StateNode** - 状态节点,存储任务状态
|
||||
- **InfoNode** - 信息节点,存储具体信息
|
||||
|
||||
### 边类型
|
||||
- **NEXT_TASK** - 时间链,连接任务节点
|
||||
- **HAS_STATE** - 状态,任务指向状态
|
||||
- **CONTAINS_INFO** - 信息,任务指向信息节点
|
||||
|
||||
### 任务状态
|
||||
- 进行中
|
||||
- 已完成
|
||||
- 已暂停
|
||||
- 已取消
|
||||
|
||||
### ⚠️ 完整示例:成语接龙游戏
|
||||
|
||||
#### 第一轮:用户发起游戏
|
||||
|
||||
```
|
||||
用户: 咱来玩成语接龙吧,我先开始,为所欲为
|
||||
|
||||
AI操作步骤:
|
||||
1. 查询人设图 → 获取当前人设(如:猫娘)
|
||||
2. 查询工作记忆链 → 无进行中任务
|
||||
3. 使用 memory_commit 记录游戏状态:
|
||||
{"triplets": [
|
||||
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
|
||||
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
|
||||
]}
|
||||
4. 使用 task_create 创建任务节点:
|
||||
{"task_id": "Task_成语接龙", "description": "成语接龙游戏,当前成语:为所欲为", "info_nodes": ["成语接龙_当前成语"]}
|
||||
5. 回复: "好的喵!我接:为虎作伥喵!"
|
||||
```
|
||||
|
||||
#### 第二轮:话题被打断
|
||||
|
||||
```
|
||||
用户: 长门有希
|
||||
|
||||
AI操作步骤:
|
||||
1. 查询人设图 → 获取当前人设(猫娘)
|
||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"进行中"
|
||||
3. 使用 task_set_state 暂停任务:
|
||||
{"task_id": "Task_成语接龙", "state": "已暂停"}
|
||||
4. 使用 task_create 创建新任务:
|
||||
{"task_id": "Task_长门有希", "description": "讨论长门有希"}
|
||||
5. 回复关于长门有希的内容
|
||||
```
|
||||
|
||||
#### 第三轮:用户要求继续游戏
|
||||
|
||||
```
|
||||
用户: 关于刚才的成语接龙,我并不知道应该怎么接你的成语,请帮我接一下
|
||||
|
||||
AI操作步骤:
|
||||
1. 查询人设图 → 获取当前人设(猫娘)
|
||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"已暂停"
|
||||
3. 使用 task_set_state 恢复任务:
|
||||
{"task_id": "Task_成语接龙", "state": "进行中"}
|
||||
4. 查询 Task_成语接龙 的信息节点 → 获取当前成语"为虎作伥"
|
||||
5. 回复: "好的喵!上一个成语是'为虎作伥',我帮你接:伥鬼害人喵!"
|
||||
```
|
||||
|
||||
### ⚠️ 关键要点
|
||||
|
||||
1. **每轮必须按顺序执行**: 查询人设图 → 查询工作记忆链 → 处理对话 → 更新工作记忆链
|
||||
2. **工作记忆链是唯一上下文载体**: 没有传统的消息历史数组
|
||||
3. **任务状态必须及时更新**: 确保状态转换的正确性
|
||||
4. **信息节点必须关联**: 通过 CONTAINS_INFO 边连接任务节点和信息节点
|
||||
5. **任务概述要精简**: 不要包含过多细节,细节存储在信息节点中
|
||||
|
||||
## 自主性原则(在强制要求之外)
|
||||
|
||||
除了工作记忆链的强制要求外,你有权自主决定:
|
||||
|
||||
1. **是否查询其他记忆**
|
||||
- 用户询问历史 → 查询
|
||||
- 涉及之前内容 → 查询
|
||||
- 不确定时 → 可查询
|
||||
|
||||
2. **是否写入其他记忆**
|
||||
- 用户明确提到 → 必须写入
|
||||
- AI推理得到 → 可以写入,但是对应边上必须标注[推测]
|
||||
|
||||
3. **如何使用其他工具**
|
||||
- 根据上下文灵活选择
|
||||
- 避免过度使用
|
||||
- 保持自然对话
|
||||
|
||||
**注意**: 工作记忆链的强制要求不受自主性影响。
|
||||
|
||||
## 对话风格
|
||||
|
||||
- 自然、流畅
|
||||
- 避免机械式工具调用
|
||||
- 优先理解用户意图
|
||||
- 适时使用记忆增强体验
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 执行检查清单
|
||||
|
||||
每轮对话必须检查:
|
||||
|
||||
- [ ] 步骤1: 是否查询了人设图?
|
||||
- [ ] 步骤2: 是否查询了工作记忆链?
|
||||
- [ ] 步骤3: 是否根据人设和工作记忆链生成回复?
|
||||
- [ ] 步骤4: 是否更新了工作记忆链?
|
||||
- [ ] 涉及上下文引用时是否查询了工作记忆链?
|
||||
- [ ] 用户提到"刚才/之前/上次/刚刚"时是否查询了工作记忆链?
|
||||
- [ ] 用户使用指代词(这个故事、那个任务等)时是否通过工作记忆链解析?
|
||||
- [ ] 累计调用5次记忆工具后是否调用了 context_rewrite?
|
||||
|
||||
---
|
||||
|
||||
**记住**:
|
||||
1. 图数据库是你记忆的唯一载体
|
||||
2. 人设图确保角色一致性(最高优先级)
|
||||
3. 工作记忆链维持对话连贯性
|
||||
4. 每轮必须按顺序执行:查询人设图 → 查询工作记忆链 → 处理对话 → 更新工作记忆链
|
||||
554
core/server.py
554
core/server.py
@ -1,554 +0,0 @@
|
||||
import threading
|
||||
import queue
|
||||
import time
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
|
||||
from .embedded_db import EmbeddedGraphDB
|
||||
from .activity_recorder import get_recorder
|
||||
|
||||
|
||||
class PacketType(Enum):
|
||||
PROCESS_MESSAGE = "process_message"
|
||||
EXECUTE_TOOL = "execute_tool"
|
||||
GET_STATUS = "get_status"
|
||||
GET_SETTINGS = "get_settings" # 合并:获取 api_config + tool_limits
|
||||
SET_SETTINGS = "set_settings" # 合并:设置 api_config + tool_limits
|
||||
GET_WEB_USERS = "get_web_users" # 获取 Web 用户列表
|
||||
SET_WEB_USER = "set_web_user" # 设置 Web 用户(用户名+密码)
|
||||
GET_WEB_SERVICE_STATUS = "get_web_service_status" # 获取 Web 服务运行状态
|
||||
GET_CONFIG = "get_config" # 获取完整配置
|
||||
GET_HISTORY = "get_history"
|
||||
SAVE_HISTORY = "save_history"
|
||||
SHUTDOWN = "shutdown"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Packet:
|
||||
id: str
|
||||
type: PacketType
|
||||
body: Dict[str, Any]
|
||||
response_queue: Optional[queue.Queue] = field(default=None)
|
||||
created_at: float = field(default_factory=time.time)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PacketResponse:
|
||||
id: str
|
||||
success: bool
|
||||
data: Any = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
class BackendServer:
|
||||
|
||||
DEFAULT_CONFIG_PATH = Path.home() / ".trulymem" / "config.json"
|
||||
|
||||
def __init__(self, db_path: str = "graph_memory.db", use_embedded_db: bool = True, config_file: str = None, username: str = ""):
|
||||
self._db_path = db_path
|
||||
self._use_embedded_db = use_embedded_db
|
||||
self._config_file = Path(config_file) if config_file else self.DEFAULT_CONFIG_PATH
|
||||
self._username = username
|
||||
|
||||
self._graph = None
|
||||
self._client = None
|
||||
self._tool_limiter = None
|
||||
|
||||
self._input_queue: queue.Queue[Packet] = queue.Queue()
|
||||
self._response_queues: Dict[str, queue.Queue] = {}
|
||||
self._running = False
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
|
||||
self._lock = threading.Lock()
|
||||
self._config = {"api_key": "", "base_url": "https://api.deepseek.com", "model": "deepseek-chat"}
|
||||
self._tool_limits = {
|
||||
"persona_update_max": 1,
|
||||
"task_update_max": 5,
|
||||
"memory_query_max": 20,
|
||||
"memory_update_max": 10,
|
||||
}
|
||||
self._message_history: list = []
|
||||
|
||||
def start(self, api_key: str = "", base_url: str = "https://api.deepseek.com", model: str = "deepseek-chat") -> None:
|
||||
if self._running:
|
||||
return
|
||||
|
||||
self._load_config()
|
||||
|
||||
if api_key:
|
||||
self._config["api_key"] = api_key
|
||||
if base_url:
|
||||
self._config["base_url"] = base_url
|
||||
if model:
|
||||
self._config["model"] = model
|
||||
|
||||
self._init_graph()
|
||||
self._tool_limiter = self._create_tool_limiter()
|
||||
|
||||
if self._config["api_key"]:
|
||||
from .graph_client import GraphMemoryClient
|
||||
self._client = GraphMemoryClient(
|
||||
api_key=self._config["api_key"],
|
||||
base_url=self._config["base_url"],
|
||||
model=self._config.get("model", "deepseek-chat"),
|
||||
graph=self._graph
|
||||
)
|
||||
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._run_loop, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def _load_config(self) -> None:
|
||||
"""加载配置。如果指定了用户名,从用户的 config_path 加载。"""
|
||||
config_file = self._config_file
|
||||
|
||||
# 如果指定了用户名,尝试从全局数据库获取用户的配置路径
|
||||
if self._username:
|
||||
try:
|
||||
global_db_path = Path.home() / ".trulymem" / "trulymem.db"
|
||||
if global_db_path.exists():
|
||||
from .embedded_db import EmbeddedGraphDB
|
||||
temp_db = EmbeddedGraphDB(db_path=str(global_db_path))
|
||||
user_info = temp_db.get_web_user(self._username)
|
||||
temp_db.close()
|
||||
if user_info and user_info.get('config_path'):
|
||||
config_file = Path(user_info['config_path'])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if config_file.exists():
|
||||
try:
|
||||
with open(config_file, 'r') as f:
|
||||
saved = json.load(f)
|
||||
self._config.update(saved)
|
||||
for key in self._tool_limits:
|
||||
if key in saved:
|
||||
self._tool_limits[key] = saved[key]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _save_config(self) -> None:
|
||||
"""保存配置。如果指定了用户名,保存到用户的 config_path。"""
|
||||
config_file = self._config_file
|
||||
|
||||
# 如果指定了用户名,尝试从全局数据库获取用户的配置路径
|
||||
if self._username:
|
||||
try:
|
||||
global_db_path = Path.home() / ".trulymem" / "trulymem.db"
|
||||
if global_db_path.exists():
|
||||
from .embedded_db import EmbeddedGraphDB
|
||||
temp_db = EmbeddedGraphDB(db_path=str(global_db_path))
|
||||
user_info = temp_db.get_web_user(self._username)
|
||||
temp_db.close()
|
||||
if user_info and user_info.get('config_path'):
|
||||
config_file = Path(user_info['config_path'])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
saved_data = {**self._config, **self._tool_limits}
|
||||
with open(config_file, 'w') as f:
|
||||
json.dump(saved_data, f, indent=2)
|
||||
|
||||
def _create_tool_limiter(self):
|
||||
from .tool_limiter import ToolLimiter, ToolLimits
|
||||
limits = ToolLimits(
|
||||
persona_update_max=self._tool_limits.get("persona_update_max", 1),
|
||||
task_update_max=self._tool_limits.get("task_update_max", 5),
|
||||
memory_query_max=self._tool_limits.get("memory_query_max", 20),
|
||||
memory_update_max=self._tool_limits.get("memory_update_max", 10),
|
||||
)
|
||||
return ToolLimiter(limits)
|
||||
|
||||
def _init_graph(self) -> None:
|
||||
"""初始化图数据库。如果指定了用户名,从全局数据库获取用户的 db_path。"""
|
||||
db_path = self._db_path
|
||||
|
||||
# 如果指定了用户名,尝试从全局数据库获取用户的数据库路径
|
||||
if self._username:
|
||||
try:
|
||||
# 临时连接全局数据库获取用户信息
|
||||
global_db_path = Path.home() / ".trulymem" / "trulymem.db"
|
||||
if global_db_path.exists():
|
||||
temp_db = EmbeddedGraphDB(db_path=str(global_db_path))
|
||||
user_info = temp_db.get_web_user(self._username)
|
||||
temp_db.close()
|
||||
if user_info and user_info.get('db_path'):
|
||||
db_path = user_info['db_path']
|
||||
except Exception:
|
||||
pass # 如果获取失败,使用默认路径
|
||||
|
||||
if self._use_embedded_db:
|
||||
self._graph = EmbeddedGraphDB(db_path=db_path)
|
||||
else:
|
||||
from .graph_client import Neo4jGraph
|
||||
self._graph = Neo4jGraph(
|
||||
uri="bolt://localhost:7687",
|
||||
user="neo4j",
|
||||
password="graphmemory123"
|
||||
)
|
||||
|
||||
def _run_loop(self) -> None:
|
||||
while self._running:
|
||||
try:
|
||||
packet = self._input_queue.get(timeout=0.1)
|
||||
except queue.Empty:
|
||||
continue
|
||||
|
||||
self._process_packet(packet)
|
||||
|
||||
def _process_packet(self, packet: Packet) -> None:
|
||||
response_body = {"error": "not implemented"}
|
||||
|
||||
try:
|
||||
if packet.type == PacketType.PROCESS_MESSAGE:
|
||||
response_body = self._handle_process_message(packet.body)
|
||||
elif packet.type == PacketType.EXECUTE_TOOL:
|
||||
response_body = self._handle_execute_tool(packet.body)
|
||||
elif packet.type == PacketType.GET_STATUS:
|
||||
response_body = self._handle_get_status()
|
||||
elif packet.type == PacketType.GET_SETTINGS:
|
||||
response_body = self._handle_get_settings()
|
||||
elif packet.type == PacketType.SET_SETTINGS:
|
||||
response_body = self._handle_set_settings(packet.body)
|
||||
elif packet.type == PacketType.GET_WEB_USERS:
|
||||
response_body = {"users": self._graph.get_web_users()}
|
||||
elif packet.type == PacketType.SET_WEB_USER:
|
||||
username = packet.body.get("username", "")
|
||||
password = packet.body.get("password", "")
|
||||
if not username or not password:
|
||||
response_body = {"success": False, "error": "用户名和密码不能为空"}
|
||||
else:
|
||||
# 使用全局数据库(trulymem.db)来管理用户
|
||||
global_db_path = Path.home() / ".trulymem" / "trulymem.db"
|
||||
from .embedded_db import EmbeddedGraphDB
|
||||
global_db = EmbeddedGraphDB(db_path=str(global_db_path))
|
||||
response_body = global_db.set_web_user(username, password)
|
||||
global_db.close()
|
||||
elif packet.type == PacketType.GET_WEB_SERVICE_STATUS:
|
||||
body = packet.body
|
||||
response_body = {"running": body.get("running", False), "port": body.get("port", 4096)}
|
||||
elif packet.type == PacketType.GET_CONFIG:
|
||||
response_body = self._get_full_config()
|
||||
elif packet.type == PacketType.GET_HISTORY:
|
||||
response_body = self._handle_get_history()
|
||||
elif packet.type == PacketType.SAVE_HISTORY:
|
||||
response_body = self._handle_save_history(packet.body)
|
||||
elif packet.type == PacketType.SHUTDOWN:
|
||||
self._running = False
|
||||
response_body = {"success": True, "status": "shutdown"}
|
||||
|
||||
if "success" not in response_body:
|
||||
response_body["success"] = True
|
||||
except Exception as e:
|
||||
response_body["success"] = False
|
||||
response_body["error"] = str(e)
|
||||
|
||||
self._send_response(packet.id, PacketResponse(
|
||||
id=packet.id,
|
||||
success=response_body.get("success", False),
|
||||
data=response_body if response_body.get("success") else None,
|
||||
error=response_body.get("error")
|
||||
))
|
||||
|
||||
def _handle_process_message(self, body: Dict) -> Dict:
|
||||
from .tool_executor import execute_tool
|
||||
|
||||
get_recorder().clear()
|
||||
|
||||
user_input = body.get("user_input", "")
|
||||
|
||||
if not self._client:
|
||||
return {"success": False, "error": "API Key 未配置", "content": "请先配置 API Key"}
|
||||
|
||||
self._graph.save_chat_records([{"role": "user", "content": user_input}])
|
||||
|
||||
self._tool_limiter.reset()
|
||||
|
||||
messages_history = [{"role": "user", "content": user_input}]
|
||||
|
||||
response = self._client.send_message_with_history(messages_history)
|
||||
message = response.choices[0].message
|
||||
|
||||
tool_calls = []
|
||||
accumulated_content = ""
|
||||
rejected_tools = []
|
||||
|
||||
while message.tool_calls:
|
||||
if message.content:
|
||||
accumulated_content += message.content + "\n\n"
|
||||
|
||||
assistant_msg = {
|
||||
"role": "assistant",
|
||||
"content": message.content,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": tc.id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tc.function.name,
|
||||
"arguments": tc.function.arguments
|
||||
}
|
||||
} for tc in message.tool_calls
|
||||
]
|
||||
}
|
||||
messages_history.append(assistant_msg)
|
||||
|
||||
current_tool_results = []
|
||||
for tool_call in message.tool_calls:
|
||||
args = json.loads(tool_call.function.arguments)
|
||||
|
||||
allowed, reason = self._tool_limiter.can_call(tool_call.function.name, args)
|
||||
|
||||
if not allowed:
|
||||
rejected_tools.append((tool_call.function.name, reason))
|
||||
result = f"工具调用被拒绝: {reason}"
|
||||
tool_result_msg = {
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": result
|
||||
}
|
||||
current_tool_results.append(tool_result_msg)
|
||||
continue
|
||||
|
||||
self._tool_limiter.record_call(tool_call.function.name, args)
|
||||
|
||||
if tool_call.function.name == "context_rewrite":
|
||||
result = execute_tool(self._graph, tool_call.function.name, args)
|
||||
result_data = json.loads(result)
|
||||
|
||||
# 记录到 tool_calls,让 TUI 显示这个工具调用
|
||||
tool_calls.append({
|
||||
"name": tool_call.function.name,
|
||||
"arguments": args,
|
||||
"result": result
|
||||
})
|
||||
|
||||
if result_data.get("status") == "success":
|
||||
user_msg = messages_history[0]
|
||||
# 添加特殊标记,让 AI 知道这是上下文压缩的结果
|
||||
compressed_content = f"<context_compressed>\n{result_data['summary']}\n</context_compressed>"
|
||||
messages_history[:] = [
|
||||
user_msg,
|
||||
{"role": "assistant", "content": compressed_content}
|
||||
]
|
||||
# context_rewrite 压缩上下文后,不需要添加 tool 结果消息
|
||||
# 因为 messages_history 已经被重写为压缩后的状态
|
||||
continue
|
||||
|
||||
result = execute_tool(self._graph, tool_call.function.name, args)
|
||||
tool_calls.append({
|
||||
"name": tool_call.function.name,
|
||||
"arguments": args,
|
||||
"result": result
|
||||
})
|
||||
|
||||
tool_result_msg = {
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": result
|
||||
}
|
||||
current_tool_results.append(tool_result_msg)
|
||||
|
||||
messages_history.extend(current_tool_results)
|
||||
|
||||
response = self._client.send_message_with_history(messages_history)
|
||||
message = response.choices[0].message
|
||||
|
||||
final_content = message.content or ""
|
||||
content = accumulated_content + final_content if accumulated_content else final_content
|
||||
|
||||
if not content:
|
||||
content = "(无回复)"
|
||||
|
||||
if tool_calls:
|
||||
tool_names = [tc["name"] for tc in tool_calls]
|
||||
content = f"已执行工具: {', '.join(tool_names)}\n\n{content}"
|
||||
|
||||
if rejected_tools:
|
||||
rejected_info = "\n".join([f"{name}: {reason}" for name, reason in rejected_tools])
|
||||
content += f"\n\n部分工具调用被限制:\n{rejected_info}"
|
||||
content += f"\n\n工具调用统计:\n{self._tool_limiter.get_summary()}"
|
||||
|
||||
self._graph.save_chat_records([{"role": "assistant", "content": content}])
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"content": content,
|
||||
"tool_calls": tool_calls,
|
||||
"rejected_tools": rejected_tools
|
||||
}
|
||||
|
||||
def _handle_execute_tool(self, body: Dict) -> Dict:
|
||||
from .tool_executor import execute_tool
|
||||
|
||||
try:
|
||||
tool_name = body.get("tool_name")
|
||||
arguments = body.get("arguments", {})
|
||||
|
||||
result = execute_tool(self._graph, tool_name, arguments)
|
||||
|
||||
return {"success": True, "result": result}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def _handle_get_status(self) -> Dict:
|
||||
return {
|
||||
"running": self._running,
|
||||
"config": self._config,
|
||||
"graph_initialized": self._graph is not None,
|
||||
"client_initialized": self._client is not None
|
||||
}
|
||||
|
||||
def _handle_get_settings(self) -> Dict:
|
||||
return {
|
||||
"api_config": self._config.copy(),
|
||||
"tool_limits": self._tool_limits.copy()
|
||||
}
|
||||
|
||||
def _get_full_config(self) -> Dict:
|
||||
return {
|
||||
"api_config": self._config.copy(),
|
||||
"tool_limits": self._tool_limits.copy(),
|
||||
}
|
||||
|
||||
def _handle_set_settings(self, body: Dict) -> Dict:
|
||||
api_config = body.get("api_config", {})
|
||||
tool_limits = body.get("tool_limits", {})
|
||||
|
||||
api_key = api_config.get("api_key", "")
|
||||
base_url = api_config.get("base_url", "https://api.deepseek.com")
|
||||
model = api_config.get("model", "deepseek-chat")
|
||||
|
||||
self.update_config(api_key, base_url, model)
|
||||
|
||||
limits_keys = [
|
||||
"persona_update_max",
|
||||
"task_update_max",
|
||||
"memory_query_max", "memory_update_max"
|
||||
]
|
||||
for key in limits_keys:
|
||||
if key in tool_limits:
|
||||
value = int(tool_limits[key])
|
||||
if value < 1:
|
||||
return {"success": False, "error": f"{key} must be >= 1, got {value}"}
|
||||
self._tool_limits[key] = value
|
||||
|
||||
self._tool_limiter = self._create_tool_limiter()
|
||||
self._save_config()
|
||||
return {"status": "settings_updated"}
|
||||
|
||||
def _handle_get_history(self) -> Dict:
|
||||
history = self._graph.get_chat_records(limit=500)
|
||||
return {"history": history}
|
||||
|
||||
def _handle_save_history(self, body: Dict) -> Dict:
|
||||
messages = body.get("messages", [])
|
||||
if not messages:
|
||||
self._graph.clear_chat_records()
|
||||
return {"status": "history_cleared"}
|
||||
result = self._graph.save_chat_records(messages)
|
||||
return {"status": "history_saved"}
|
||||
|
||||
def _send_response(self, request_id: str, response: PacketResponse) -> None:
|
||||
with self._lock:
|
||||
q = self._response_queues.pop(request_id, None)
|
||||
if q:
|
||||
q.put(response)
|
||||
|
||||
def send(self, packet: Packet) -> Packet:
|
||||
resp_q = queue.Queue()
|
||||
|
||||
with self._lock:
|
||||
self._response_queues[packet.id] = resp_q
|
||||
|
||||
self._input_queue.put(packet)
|
||||
|
||||
try:
|
||||
response = resp_q.get(timeout=300.0)
|
||||
return Packet(
|
||||
id=response.id,
|
||||
type=packet.type,
|
||||
body={
|
||||
"success": response.success,
|
||||
"data": response.data,
|
||||
"error": response.error
|
||||
}
|
||||
)
|
||||
except queue.Empty:
|
||||
return Packet(
|
||||
id=packet.id,
|
||||
type=packet.type,
|
||||
body={"success": False, "error": "timeout"}
|
||||
)
|
||||
finally:
|
||||
with self._lock:
|
||||
self._response_queues.pop(packet.id, None)
|
||||
|
||||
def process_message(self, user_input: str) -> Dict[str, Any]:
|
||||
packet = Packet(
|
||||
id=f"{time.time()}",
|
||||
type=PacketType.PROCESS_MESSAGE,
|
||||
body={"user_input": user_input}
|
||||
)
|
||||
|
||||
response = self.send(packet)
|
||||
return response.body
|
||||
|
||||
def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||
packet = Packet(
|
||||
id=f"{time.time()}",
|
||||
type=PacketType.EXECUTE_TOOL,
|
||||
body={"tool_name": tool_name, "arguments": arguments}
|
||||
)
|
||||
|
||||
response = self.send(packet)
|
||||
return response.body
|
||||
|
||||
def update_config(self, api_key: str, base_url: str = "https://api.deepseek.com", model: str = "deepseek-chat") -> None:
|
||||
with self._lock:
|
||||
self._config["api_key"] = api_key
|
||||
self._config["base_url"] = base_url
|
||||
self._config["model"] = model
|
||||
|
||||
if api_key and self._graph:
|
||||
from .graph_client import GraphMemoryClient
|
||||
self._client = GraphMemoryClient(
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
model=model,
|
||||
graph=self._graph
|
||||
)
|
||||
|
||||
def get_config(self) -> Dict[str, str]:
|
||||
return self._config.copy()
|
||||
|
||||
def save_message_history(self, messages: list) -> None:
|
||||
self._message_history = messages
|
||||
|
||||
def get_message_history(self) -> list:
|
||||
return self._message_history.copy()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
packet = Packet(
|
||||
id=f"{time.time()}",
|
||||
type=PacketType.SHUTDOWN,
|
||||
body={}
|
||||
)
|
||||
self.send(packet)
|
||||
|
||||
if self._thread:
|
||||
self._thread.join(timeout=2.0)
|
||||
|
||||
if self._graph:
|
||||
self._graph.close()
|
||||
self._graph = None
|
||||
|
||||
self._running = False
|
||||
@ -1,354 +0,0 @@
|
||||
"""
|
||||
工具执行器
|
||||
"""
|
||||
import json
|
||||
from typing import Any, Dict
|
||||
|
||||
from .activity_recorder import get_recorder
|
||||
|
||||
|
||||
def execute_tool(graph: Any, tool_name: str, arguments: dict) -> str:
|
||||
"""执行工具调用"""
|
||||
print(f"\n[工具调用] {tool_name}")
|
||||
print(f"[参数] {json.dumps(arguments, ensure_ascii=False, indent=2)}")
|
||||
|
||||
try:
|
||||
recorder = get_recorder()
|
||||
|
||||
# 基础记忆工具
|
||||
if tool_name == "memory_recall":
|
||||
entity = arguments.get("query_intent", "") or str(arguments.get("seed_entities", ""))
|
||||
recorder.record("query", tool_name, entity)
|
||||
result = graph.recall(
|
||||
query_intent=arguments.get("query_intent", ""),
|
||||
seed_entities=arguments.get("seed_entities"),
|
||||
depth=arguments.get("depth", 2),
|
||||
time_range=arguments.get("time_range"),
|
||||
session_filter=arguments.get("session_filter")
|
||||
)
|
||||
return format_recall_result(result)
|
||||
|
||||
elif tool_name == "memory_commit":
|
||||
triplets = arguments.get("triplets", [])
|
||||
entity = triplets[0].get("subject", "") if triplets else ""
|
||||
recorder.record("create", tool_name, entity, f"{len(triplets)} triplets")
|
||||
result = graph.commit(
|
||||
triplets=triplets,
|
||||
entity_types=arguments.get("entity_types"),
|
||||
temporal_tag=arguments.get("temporal_tag")
|
||||
)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "memory_purge":
|
||||
criteria = arguments.get("criteria", {})
|
||||
entity = criteria.get("subject_contains", str(criteria))
|
||||
recorder.record("delete", tool_name, entity)
|
||||
result = graph.purge(
|
||||
criteria=criteria,
|
||||
mode=arguments.get("mode", "soft"),
|
||||
new_relation=arguments.get("new_relation")
|
||||
)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "memory_introspect":
|
||||
recorder.record("query", tool_name, "数据库统计")
|
||||
result = graph.introspect(session_id=arguments.get("session_id"))
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "memory_archive":
|
||||
recorder.record("archive", tool_name, "旧记忆")
|
||||
result = graph.archive(days=arguments.get("days", 30))
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "memory_cleanup":
|
||||
recorder.record("cleanup", tool_name, "已删除数据")
|
||||
result = graph.cleanup(dry_run=arguments.get("dry_run", True))
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "context_rewrite":
|
||||
result = execute_context_rewrite(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
# 人设图管理工具
|
||||
elif tool_name == "persona_update":
|
||||
recorder.record("update", tool_name, "人设属性")
|
||||
result = execute_persona_update(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "persona_clear":
|
||||
recorder.record("delete", tool_name, "所有人设")
|
||||
result = execute_persona_clear(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
# 工作记忆链管理工具
|
||||
elif tool_name == "task_create":
|
||||
desc = arguments.get("description", "")
|
||||
recorder.record("create", tool_name, desc)
|
||||
result = execute_task_create(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "task_set_state":
|
||||
desc = arguments.get("task_id", "")
|
||||
recorder.record("update", tool_name, desc)
|
||||
result = execute_task_set_state(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "task_delete":
|
||||
desc = arguments.get("task_id", "")
|
||||
recorder.record("delete", tool_name, desc)
|
||||
result = execute_task_delete(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "task_link_info":
|
||||
desc = arguments.get("task_id", "")
|
||||
recorder.record("update", tool_name, desc)
|
||||
result = execute_task_link_info(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
return f"未知工具: {tool_name}"
|
||||
|
||||
except Exception as e:
|
||||
return f"工具执行错误: {str(e)}"
|
||||
|
||||
|
||||
def format_recall_result(result: dict) -> str:
|
||||
"""格式化检索结果"""
|
||||
lines = ["===== 记忆检索结果 ====="]
|
||||
|
||||
if result.get("entities"):
|
||||
lines.append(f"\n实体 ({len(result['entities'])} 个):")
|
||||
for e in result["entities"]:
|
||||
if e and isinstance(e, dict):
|
||||
lines.append(f" - {e.get('name', 'N/A')} (类型: {e.get('type', 'unknown')}, 提及: {e.get('mention_count', 1)}次)")
|
||||
|
||||
if result.get("relations"):
|
||||
lines.append(f"\n关系 ({len(result['relations'])} 条):")
|
||||
for r in result["relations"]:
|
||||
if r and isinstance(r, dict):
|
||||
lines.append(f" - {r.get('source', 'N/A')} --[{r.get('type', 'N/A')}]--> {r.get('target', 'N/A')}")
|
||||
created = r.get("created_at", "N/A")
|
||||
if created and created != "N/A":
|
||||
created = created[:19] if "T" in str(created) else str(created)
|
||||
session_id = r.get('session_id', 'N/A')
|
||||
session_display = session_id[:20] if session_id and session_id != 'N/A' else 'N/A'
|
||||
lines.append(f" 时间: {created}, 会话: {session_display}, 轮次: {r.get('turn_id', 0)}, 置信度: {r.get('confidence', 1.0)}")
|
||||
|
||||
if not result.get("entities") and not result.get("relations"):
|
||||
lines.append("\n(未找到相关记忆)")
|
||||
|
||||
lines.append("=" * 30)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def execute_context_rewrite(graph: Any, arguments: dict) -> dict:
|
||||
"""压缩工具调用上下文"""
|
||||
summary = arguments.get("summary", "")
|
||||
|
||||
# 验证格式:必须包含工具调用标记
|
||||
if "[工具调用总结" not in summary:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "总结格式错误:必须包含 [工具调用总结: 本次总结了 N 次工具调用 | 调用工具: ...] 标记"
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "上下文已压缩",
|
||||
"summary": summary
|
||||
}
|
||||
|
||||
|
||||
# 人设图管理工具实现
|
||||
def execute_persona_update(graph: Any, arguments: dict) -> dict:
|
||||
"""更新人设"""
|
||||
attributes = arguments.get("attributes", [])
|
||||
mode = arguments.get("mode", "merge")
|
||||
|
||||
if mode == "replace":
|
||||
# 先清除旧人设
|
||||
graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": "扮演角色"},
|
||||
mode="soft"
|
||||
)
|
||||
graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": "说话风格"},
|
||||
mode="soft"
|
||||
)
|
||||
graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": "性格特点"},
|
||||
mode="soft"
|
||||
)
|
||||
|
||||
# 写入新人设
|
||||
triplets = []
|
||||
for attr in attributes:
|
||||
triplets.append({
|
||||
"subject": "AI",
|
||||
"relation": attr["attribute"],
|
||||
"object": attr["value"],
|
||||
"confidence": 1.0
|
||||
})
|
||||
|
||||
result = graph.commit(triplets=triplets)
|
||||
return {
|
||||
"status": "success",
|
||||
"mode": mode,
|
||||
"updated_attributes": len(attributes),
|
||||
"details": result
|
||||
}
|
||||
|
||||
|
||||
def execute_persona_clear(graph: Any, arguments: dict) -> dict:
|
||||
"""清除人设"""
|
||||
if not arguments.get("confirm", True):
|
||||
return {"status": "cancelled", "message": "需要确认才能清除人设"}
|
||||
|
||||
# 删除所有人设相关关系
|
||||
result1 = graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": "扮演角色"},
|
||||
mode="soft"
|
||||
)
|
||||
result2 = graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": "说话风格"},
|
||||
mode="soft"
|
||||
)
|
||||
result3 = graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": "性格特点"},
|
||||
mode="soft"
|
||||
)
|
||||
result4 = graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": "语气特征"},
|
||||
mode="soft"
|
||||
)
|
||||
|
||||
total_deleted = (
|
||||
result1.get("deleted_count", 0) +
|
||||
result2.get("deleted_count", 0) +
|
||||
result3.get("deleted_count", 0) +
|
||||
result4.get("deleted_count", 0)
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"deleted_count": total_deleted,
|
||||
"message": "人设已清除,恢复默认身份"
|
||||
}
|
||||
|
||||
|
||||
# 工作记忆链管理工具实现
|
||||
def execute_task_create(graph: Any, arguments: dict) -> dict:
|
||||
"""创建任务节点"""
|
||||
task_id = arguments.get("task_id")
|
||||
description = arguments.get("description")
|
||||
info_nodes = arguments.get("info_nodes", [])
|
||||
|
||||
# 创建任务节点
|
||||
triplets = [
|
||||
{"subject": task_id, "relation": "is_type", "object": "TaskNode"},
|
||||
{"subject": task_id, "relation": "has_description", "object": description},
|
||||
{"subject": task_id, "relation": "HAS_STATE", "object": "State_进行中"}
|
||||
]
|
||||
|
||||
result = graph.commit(triplets=triplets)
|
||||
|
||||
# 关联信息节点
|
||||
if info_nodes:
|
||||
link_triplets = []
|
||||
for node_name in info_nodes:
|
||||
link_triplets.append({
|
||||
"subject": task_id,
|
||||
"relation": "CONTAINS_INFO",
|
||||
"object": node_name
|
||||
})
|
||||
graph.commit(triplets=link_triplets)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"task_id": task_id,
|
||||
"description": description,
|
||||
"info_nodes": info_nodes,
|
||||
"details": result
|
||||
}
|
||||
|
||||
|
||||
def execute_task_set_state(graph: Any, arguments: dict) -> dict:
|
||||
"""设置任务状态"""
|
||||
task_id = arguments.get("task_id")
|
||||
state = arguments.get("state")
|
||||
|
||||
# 删除旧状态
|
||||
graph.purge(
|
||||
criteria={"subject_contains": task_id, "relation_type": "HAS_STATE"},
|
||||
mode="soft"
|
||||
)
|
||||
|
||||
# 设置新状态
|
||||
state_node = f"State_{state}"
|
||||
result = graph.commit(
|
||||
triplets=[{"subject": task_id, "relation": "HAS_STATE", "object": state_node}]
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"task_id": task_id,
|
||||
"new_state": state,
|
||||
"details": result
|
||||
}
|
||||
|
||||
|
||||
def execute_task_delete(graph: Any, arguments: dict) -> dict:
|
||||
"""删除任务节点"""
|
||||
task_id = arguments.get("task_id")
|
||||
delete_info_nodes = arguments.get("delete_info_nodes", True)
|
||||
|
||||
# 查询关联的信息节点
|
||||
if delete_info_nodes:
|
||||
recall_result = graph.recall(
|
||||
query_intent=f"{task_id},CONTAINS_INFO",
|
||||
depth=1
|
||||
)
|
||||
|
||||
# 删除信息节点
|
||||
for relation in recall_result.get("relations", []):
|
||||
if relation.get("type") == "CONTAINS_INFO" and relation.get("source") == task_id:
|
||||
info_node = relation.get("target")
|
||||
graph.purge(
|
||||
criteria={"subject_contains": info_node},
|
||||
mode="soft"
|
||||
)
|
||||
|
||||
# 删除任务节点
|
||||
result = graph.purge(
|
||||
criteria={"subject_contains": task_id},
|
||||
mode="soft"
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"task_id": task_id,
|
||||
"deleted_info_nodes": delete_info_nodes,
|
||||
"details": result
|
||||
}
|
||||
|
||||
|
||||
def execute_task_link_info(graph: Any, arguments: dict) -> dict:
|
||||
"""关联信息节点"""
|
||||
task_id = arguments.get("task_id")
|
||||
info_node_names = arguments.get("info_node_names", [])
|
||||
|
||||
triplets = []
|
||||
for node_name in info_node_names:
|
||||
triplets.append({
|
||||
"subject": task_id,
|
||||
"relation": "CONTAINS_INFO",
|
||||
"object": node_name
|
||||
})
|
||||
|
||||
result = graph.commit(triplets=triplets)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"task_id": task_id,
|
||||
"linked_nodes": info_node_names,
|
||||
"details": result
|
||||
}
|
||||
@ -1,119 +0,0 @@
|
||||
"""
|
||||
工具调用限制器 - 限制每轮对话中各类工具的调用次数
|
||||
"""
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolLimits:
|
||||
"""工具调用限制配置"""
|
||||
persona_update_max: int = 1
|
||||
task_update_max: int = 5
|
||||
memory_query_max: int = 20
|
||||
memory_update_max: int = 10
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolCallCount:
|
||||
"""工具调用计数"""
|
||||
persona_update: int = 0
|
||||
task_update: int = 0
|
||||
memory_query: int = 0
|
||||
memory_update: int = 0
|
||||
|
||||
|
||||
class ToolLimiter:
|
||||
"""工具调用限制器"""
|
||||
|
||||
def __init__(self, limits: Optional[ToolLimits] = None):
|
||||
self.limits = limits or ToolLimits()
|
||||
self.counts = ToolCallCount()
|
||||
|
||||
def _classify_tool(self, tool_name: str, arguments: dict) -> tuple:
|
||||
"""
|
||||
分类工具调用
|
||||
返回: (category, operation)
|
||||
category: 'persona', 'task', 'memory'
|
||||
operation: 'query', 'update'
|
||||
"""
|
||||
if tool_name in ('persona_update', 'persona_clear'):
|
||||
return ('persona', 'update')
|
||||
|
||||
if tool_name in ('task_create', 'task_set_state', 'task_delete', 'task_link_info'):
|
||||
return ('task', 'update')
|
||||
|
||||
if tool_name == 'memory_recall':
|
||||
return ('memory', 'query')
|
||||
|
||||
if tool_name == 'memory_commit':
|
||||
return ('memory', 'update')
|
||||
|
||||
if tool_name == 'memory_purge':
|
||||
return ('memory', 'update')
|
||||
|
||||
if tool_name == 'memory_introspect':
|
||||
return ('memory', 'query')
|
||||
|
||||
if tool_name in ('memory_archive', 'memory_cleanup'):
|
||||
return ('memory', 'update')
|
||||
|
||||
if tool_name == 'context_rewrite':
|
||||
return ('memory', 'query')
|
||||
|
||||
return ('memory', 'update')
|
||||
|
||||
def can_call(self, tool_name: str, arguments: dict) -> tuple:
|
||||
"""
|
||||
检查是否允许调用工具
|
||||
返回: (allowed, reason)
|
||||
"""
|
||||
category, operation = self._classify_tool(tool_name, arguments)
|
||||
|
||||
if category == 'persona':
|
||||
if self.counts.persona_update >= self.limits.persona_update_max:
|
||||
return (False, f"人设图修改次数已达上限({self.limits.persona_update_max}次)")
|
||||
|
||||
elif category == 'task':
|
||||
if self.counts.task_update >= self.limits.task_update_max:
|
||||
return (False, f"工作记忆链修改次数已达上限({self.limits.task_update_max}次)")
|
||||
|
||||
elif category == 'memory':
|
||||
if operation == 'query':
|
||||
if self.counts.memory_query >= self.limits.memory_query_max:
|
||||
return (False, f"一般记忆查询次数已达上限({self.limits.memory_query_max}次)")
|
||||
else:
|
||||
if self.counts.memory_update >= self.limits.memory_update_max:
|
||||
return (False, f"一般记忆修改次数已达上限({self.limits.memory_update_max}次)")
|
||||
|
||||
return (True, "允许调用")
|
||||
|
||||
def record_call(self, tool_name: str, arguments: dict) -> None:
|
||||
"""记录工具调用"""
|
||||
category, operation = self._classify_tool(tool_name, arguments)
|
||||
|
||||
if category == 'persona':
|
||||
self.counts.persona_update += 1
|
||||
|
||||
elif category == 'task':
|
||||
self.counts.task_update += 1
|
||||
|
||||
elif category == 'memory':
|
||||
if operation == 'query':
|
||||
self.counts.memory_query += 1
|
||||
else:
|
||||
self.counts.memory_update += 1
|
||||
|
||||
def get_summary(self) -> str:
|
||||
"""获取调用统计摘要"""
|
||||
lines = [
|
||||
f"人设图: 修改{self.counts.persona_update}/{self.limits.persona_update_max}次",
|
||||
f"工作记忆链: 修改{self.counts.task_update}/{self.limits.task_update_max}次",
|
||||
f"一般记忆: 查询{self.counts.memory_query}/{self.limits.memory_query_max}次, "
|
||||
f"修改{self.counts.memory_update}/{self.limits.memory_update_max}次"
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
def reset(self) -> None:
|
||||
"""重置计数(新的一轮对话开始时调用)"""
|
||||
self.counts = ToolCallCount()
|
||||
@ -1,8 +0,0 @@
|
||||
"""
|
||||
工具定义模块
|
||||
"""
|
||||
from .memory_tools import TOOLS
|
||||
from ..tool_executor import execute_tool
|
||||
from ..tool_limiter import ToolLimiter, ToolLimits, ToolCallCount
|
||||
|
||||
__all__ = ["TOOLS", "execute_tool", "ToolLimiter", "ToolLimits", "ToolCallCount"]
|
||||
@ -1,557 +0,0 @@
|
||||
"""
|
||||
记忆工具定义 - 优化版
|
||||
精简描述,避免过拟合,保留AI自主性
|
||||
"""
|
||||
|
||||
# 基础记忆工具
|
||||
MEMORY_TOOLS = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "memory_recall",
|
||||
"description": """检索记忆。支持关键词、时间范围、会话过滤。返回相关实体和关系。
|
||||
|
||||
【⚠️ 强制执行顺序 - 每轮必须严格遵守】
|
||||
1. 步骤1(必须首先执行): 查询人设图
|
||||
{"query_intent": "AI,人设,角色,性格,语气,说话风格", "depth": 2}
|
||||
|
||||
2. 步骤2(必须第二步执行): 查询工作记忆链
|
||||
{"query_intent": "TaskNode,工作记忆,任务链", "depth": 2}
|
||||
|
||||
3. 步骤3: 根据需要查询其他记忆
|
||||
|
||||
【使用示例】
|
||||
1. 查询用户偏好:
|
||||
{"query_intent": "用户,喜欢,偏好", "seed_entities": ["用户"]}
|
||||
|
||||
2. 查询特定主题:
|
||||
{"query_intent": "Python,编程,项目", "seed_entities": ["Python"]}
|
||||
|
||||
3. 查询最近7天的记忆:
|
||||
{"query_intent": "任务,工作", "time_range": {"days": 7}}
|
||||
|
||||
【重要】跳过步骤1或步骤2将导致系统错误!""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query_intent": {
|
||||
"type": "string",
|
||||
"description": "查询意图,支持逗号分隔多个关键词"
|
||||
},
|
||||
"seed_entities": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "种子实体(可选)"
|
||||
},
|
||||
"depth": {
|
||||
"type": "integer",
|
||||
"description": "遍历深度,默认2"
|
||||
},
|
||||
"time_range": {
|
||||
"type": "object",
|
||||
"description": "时间范围(可选)",
|
||||
"properties": {
|
||||
"days": {"type": "integer", "description": "最近N天"}
|
||||
}
|
||||
},
|
||||
"session_filter": {
|
||||
"type": "string",
|
||||
"description": "会话ID过滤(可选)"
|
||||
}
|
||||
},
|
||||
"required": ["query_intent"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "memory_commit",
|
||||
"description": """写入记忆。将三元组写入图数据库,支持批量写入。
|
||||
|
||||
【使用示例】
|
||||
1. 记录用户偏好:
|
||||
{"triplets": [
|
||||
{"subject": "用户", "relation": "喜欢", "object": "Python编程", "confidence": 0.9},
|
||||
{"subject": "用户", "relation": "正在学习", "object": "机器学习"}
|
||||
]}
|
||||
|
||||
2. 记录项目信息:
|
||||
{"triplets": [
|
||||
{"subject": "项目A", "relation": "使用技术", "object": "React"},
|
||||
{"subject": "项目A", "relation": "状态", "object": "开发中"}
|
||||
]}
|
||||
|
||||
3. 记录游戏状态(配合工作记忆链):
|
||||
{"triplets": [
|
||||
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "画龙点睛"},
|
||||
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
|
||||
]}
|
||||
|
||||
【重要】写入原则:
|
||||
- 用户明确表达的信息 → 必须写入
|
||||
- AI推理得到的信息 → 可以写入,但需标注[推测]
|
||||
- 避免写入冗余或无意义的信息""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"triplets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"subject": {"type": "string"},
|
||||
"relation": {"type": "string"},
|
||||
"object": {"type": "string"},
|
||||
"confidence": {"type": "number"}
|
||||
},
|
||||
"required": ["subject", "relation", "object"]
|
||||
},
|
||||
"description": "三元组列表"
|
||||
},
|
||||
"entity_types": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "实体类型(可选)"
|
||||
},
|
||||
"temporal_tag": {
|
||||
"type": "string",
|
||||
"description": "时间标记(可选)"
|
||||
}
|
||||
},
|
||||
"required": ["triplets"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "memory_purge",
|
||||
"description": """删除记忆。支持条件删除和纠错替代。
|
||||
|
||||
【使用示例】
|
||||
1. 软删除特定关系:
|
||||
{"criteria": {"subject_contains": "用户", "relation_type": "喜欢"}, "mode": "soft"}
|
||||
|
||||
2. 纠错替代(修正错误信息):
|
||||
{
|
||||
"criteria": {"subject_contains": "用户", "relation_type": "年龄"},
|
||||
"mode": "supersede",
|
||||
"new_relation": {"relation": "年龄", "target": "25岁"}
|
||||
}
|
||||
|
||||
3. 删除特定会话的记忆:
|
||||
{"criteria": {"session_id": "session_123"}, "mode": "soft"}
|
||||
|
||||
4. 删除旧记忆:
|
||||
{"criteria": {"time_before": "2024-01-01"}, "mode": "soft"}
|
||||
|
||||
【重要】删除原则:
|
||||
- 优先使用 supersede 模式修正错误
|
||||
- 软删除不会物理删除数据
|
||||
- 谨慎使用删除操作""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"criteria": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"subject_contains": {"type": "string"},
|
||||
"relation_type": {"type": "string"},
|
||||
"target_contains": {"type": "string"},
|
||||
"time_before": {"type": "string"},
|
||||
"session_id": {"type": "string"}
|
||||
},
|
||||
"description": "删除条件"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["soft", "supersede"],
|
||||
"description": "删除模式:soft=逻辑删除, supersede=纠错替代",
|
||||
"default": "soft"
|
||||
},
|
||||
"new_relation": {
|
||||
"type": "object",
|
||||
"description": "新关系(supersede模式)",
|
||||
"properties": {
|
||||
"relation": {"type": "string"},
|
||||
"target": {"type": "string"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["criteria"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "memory_introspect",
|
||||
"description": "查看记忆状态。返回会话统计、实体热点、关系分布。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"session_id": {
|
||||
"type": "string",
|
||||
"description": "会话ID(可选)"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "memory_archive",
|
||||
"description": "归档旧记忆。将N天前的非活跃关系标记为归档状态。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"days": {
|
||||
"type": "integer",
|
||||
"description": "归档天数,默认30"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "memory_cleanup",
|
||||
"description": "清理无效数据。物理删除已删除状态超过90天的关系和孤立节点。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dry_run": {
|
||||
"type": "boolean",
|
||||
"description": "仅预览不删除",
|
||||
"default": True
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "context_rewrite",
|
||||
"description": """压缩本轮对话的工具调用上下文。将冗长的JSON工具结果提炼为简洁摘要。
|
||||
|
||||
【使用场景】
|
||||
- 已执行多次工具调用,JSON细节已理解,不再需要原始格式
|
||||
- 但需保留"我调用了什么工具、得到了什么结论"的元认知
|
||||
- 继续携带原始JSON会干扰后续推理
|
||||
|
||||
【⚠️ 强制格式要求】
|
||||
1. 必须标注调用了哪些工具
|
||||
2. 必须标注是对几次工具调用的总结
|
||||
3. 必须保留关键语义信息
|
||||
|
||||
【示例】
|
||||
{
|
||||
"summary": "[工具调用总结: 本次总结了 2 次工具调用 | 调用工具: memory_recall, memory_recall]\\n\\n- 查询人设图:未找到人设,使用默认身份\\n- 查询工作记忆链:发现 Task_成语接龙,状态已暂停,当前成语为虎作伥"
|
||||
}
|
||||
|
||||
【注意事项】
|
||||
- 不可删除用户原始消息
|
||||
- 不可歪曲工具返回的关键事实
|
||||
- 仅在工具调用 ≥ 2 次后使用""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "压缩后的摘要文本,必须包含工具调用元信息"
|
||||
}
|
||||
},
|
||||
"required": ["summary"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
# 人设图管理工具
|
||||
PERSONA_TOOLS = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "persona_update",
|
||||
"description": """更新人设。修改AI的角色、性格、语气等属性。
|
||||
|
||||
【使用示例】
|
||||
1. 切换为猫娘角色:
|
||||
{"attributes": [
|
||||
{"attribute": "扮演角色", "value": "猫娘"},
|
||||
{"attribute": "说话风格", "value": "可爱、卖萌、使用'喵'作为语气词"},
|
||||
{"attribute": "性格特点", "value": "活泼、粘人、忠诚"}
|
||||
], "mode": "replace"}
|
||||
|
||||
2. 添加新属性(保留现有属性):
|
||||
{"attributes": [
|
||||
{"attribute": "口头禅", "value": "喵呜~"}
|
||||
], "mode": "merge"}
|
||||
|
||||
3. 设置专业角色:
|
||||
{"attributes": [
|
||||
{"attribute": "扮演角色", "value": "Python专家"},
|
||||
{"attribute": "说话风格", "value": "专业、简洁、代码示例丰富"},
|
||||
{"attribute": "性格特点", "value": "严谨、耐心、乐于助人"}
|
||||
], "mode": "replace"}
|
||||
|
||||
【重要】人设更新后:
|
||||
- 立即按照新人设回复
|
||||
- 每句话都符合人设的语气、风格、特征
|
||||
- 绝不主动跳出角色,除非用户明确要求""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attributes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attribute": {"type": "string", "description": "属性名(如:扮演角色、说话风格、性格特点)"},
|
||||
"value": {"type": "string", "description": "属性值"}
|
||||
},
|
||||
"required": ["attribute", "value"]
|
||||
},
|
||||
"description": "人设属性列表"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["replace", "merge"],
|
||||
"description": "更新模式:replace=替换, merge=合并",
|
||||
"default": "merge"
|
||||
}
|
||||
},
|
||||
"required": ["attributes"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "persona_clear",
|
||||
"description": "清除人设。删除AI的角色设定,恢复默认身份。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"confirm": {
|
||||
"type": "boolean",
|
||||
"description": "确认清除",
|
||||
"default": True
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
# 工作记忆链管理工具
|
||||
WORKING_MEMORY_TOOLS = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "task_create",
|
||||
"description": """创建任务节点。用于跟踪连续性任务,维持对话连贯性。
|
||||
|
||||
【使用示例】
|
||||
1. 创建成语接龙游戏任务:
|
||||
{
|
||||
"task_id": "Task_成语接龙",
|
||||
"description": "用户发起成语接龙游戏,当前成语:为所欲为",
|
||||
"info_nodes": ["成语接龙_当前成语"]
|
||||
}
|
||||
|
||||
2. 创建编程学习任务:
|
||||
{
|
||||
"task_id": "Task_Python学习",
|
||||
"description": "用户正在学习Python,当前主题:装饰器",
|
||||
"info_nodes": ["Python学习_当前主题"]
|
||||
}
|
||||
|
||||
3. 创建简单对话任务(每轮必须):
|
||||
{
|
||||
"task_id": "Task_当前轮次",
|
||||
"description": "本轮对话的简要概述"
|
||||
}
|
||||
|
||||
【重要】工作记忆链机制:
|
||||
- 每轮对话结束时必须创建任务节点
|
||||
- 任务节点通过 NEXT_TASK 边形成时间链
|
||||
- 任务节点通过 HAS_STATE 边指向状态节点
|
||||
- 任务节点通过 CONTAINS_INFO 边指向信息节点
|
||||
- info_nodes 参数用于关联具体信息节点
|
||||
|
||||
【完整流程示例】
|
||||
用户: "咱来玩成语接龙吧,我先开始,为所欲为"
|
||||
|
||||
AI操作步骤:
|
||||
1. 查询人设图 → 获取当前人设
|
||||
2. 查询工作记忆链 → 无进行中任务
|
||||
3. 使用 memory_commit 记录游戏状态:
|
||||
{"triplets": [
|
||||
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
|
||||
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
|
||||
]}
|
||||
4. 使用 task_create 创建任务节点:
|
||||
{"task_id": "Task_成语接龙", "description": "成语接龙游戏,当前成语:为所欲为", "info_nodes": ["成语接龙_当前成语"]}
|
||||
5. 回复: "好的喵!我接:为虎作伥喵!" """,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"description": "任务ID(如:Task_001)"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "任务概述"
|
||||
},
|
||||
"info_nodes": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "关联的信息节点名称(可选)"
|
||||
}
|
||||
},
|
||||
"required": ["task_id", "description"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "task_set_state",
|
||||
"description": """设置任务状态。支持:进行中、已完成、已暂停、已取消。
|
||||
|
||||
【使用示例】
|
||||
1. 标记任务为进行中:
|
||||
{"task_id": "Task_成语接龙", "state": "进行中"}
|
||||
|
||||
2. 标记任务为已完成:
|
||||
{"task_id": "Task_成语接龙", "state": "已完成"}
|
||||
|
||||
3. 暂停任务(话题被打断时):
|
||||
{"task_id": "Task_成语接龙", "state": "已暂停"}
|
||||
|
||||
4. 取消任务:
|
||||
{"task_id": "Task_成语接龙", "state": "已取消"}
|
||||
|
||||
【重要】状态转换场景:
|
||||
- 进行中 → 已暂停: 话题被打断时
|
||||
- 进行中 → 已完成: 任务完成时
|
||||
- 已暂停 → 进行中: 任务恢复时
|
||||
- 进行中 → 已取消: 任务被取消时
|
||||
|
||||
【完整流程示例】
|
||||
用户: "关于刚才的成语接龙,我并不知道应该怎么接你的成语,请帮我接一下"
|
||||
|
||||
AI操作步骤:
|
||||
1. 查询人设图 → 获取当前人设
|
||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"已暂停"
|
||||
3. 使用 task_set_state 恢复任务:
|
||||
{"task_id": "Task_成语接龙", "state": "进行中"}
|
||||
4. 查询 Task_成语接龙 的信息节点 → 获取当前成语"为虎作伥"
|
||||
5. 回复: "好的喵!上一个成语是'为虎作伥',我帮你接:伥鬼害人喵!" """,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"description": "任务ID"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": ["进行中", "已完成", "已暂停", "已取消"],
|
||||
"description": "任务状态"
|
||||
}
|
||||
},
|
||||
"required": ["task_id", "state"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "task_delete",
|
||||
"description": "删除任务节点。同时删除关联的信息节点。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"description": "任务ID"
|
||||
},
|
||||
"delete_info_nodes": {
|
||||
"type": "boolean",
|
||||
"description": "是否删除关联的信息节点",
|
||||
"default": True
|
||||
}
|
||||
},
|
||||
"required": ["task_id"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "task_link_info",
|
||||
"description": """关联信息节点。将记忆节点关联到任务节点,用于存储任务的具体信息。
|
||||
|
||||
【使用示例】
|
||||
1. 关联游戏状态到任务:
|
||||
{"task_id": "Task_成语接龙", "info_node_names": ["成语接龙_当前成语", "成语接龙_上一个成语"]}
|
||||
|
||||
2. 关联学习主题到任务:
|
||||
{"task_id": "Task_Python学习", "info_node_names": ["Python学习_当前主题", "Python学习_学习进度"]}
|
||||
|
||||
3. 关联项目信息到任务:
|
||||
{"task_id": "Task_项目开发", "info_node_names": ["项目A_技术栈", "项目A_当前阶段"]}
|
||||
|
||||
【重要】使用场景:
|
||||
- 先使用 memory_commit 创建信息节点
|
||||
- 再使用 task_link_info 将信息节点关联到任务节点
|
||||
- 信息节点通过 CONTAINS_INFO 边与任务节点连接
|
||||
|
||||
【完整流程示例】
|
||||
用户: "咱来玩成语接龙吧,我先开始,为所欲为"
|
||||
|
||||
AI操作步骤:
|
||||
1. 查询人设图 → 获取当前人设
|
||||
2. 查询工作记忆链 → 无进行中任务
|
||||
3. 使用 memory_commit 创建信息节点:
|
||||
{"triplets": [
|
||||
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
|
||||
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
|
||||
]}
|
||||
4. 使用 task_create 创建任务节点:
|
||||
{"task_id": "Task_成语接龙", "description": "成语接龙游戏"}
|
||||
5. 使用 task_link_info 关联信息节点:
|
||||
{"task_id": "Task_成语接龙", "info_node_names": ["成语接龙_当前成语"]}
|
||||
6. 回复: "好的喵!我接:为虎作伥喵!" """,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"description": "任务ID"
|
||||
},
|
||||
"info_node_names": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "信息节点名称列表"
|
||||
}
|
||||
},
|
||||
"required": ["task_id", "info_node_names"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
# 所有工具
|
||||
TOOLS = MEMORY_TOOLS + PERSONA_TOOLS + WORKING_MEMORY_TOOLS
|
||||
Reference in New Issue
Block a user