diff --git a/.gitignore b/.gitignore index d816453..1a97fb3 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,8 @@ env/ # IDE .vscode/ .idea/ +.arts/ +.codeartsdoer/ *.swp *.swo *~ @@ -60,3 +62,6 @@ dist/ # Temporary *.tmp *.bak + +# AI Generated +jimeng*.png diff --git a/convert_icon.py b/convert_icon.py deleted file mode 100644 index 473d46f..0000000 --- a/convert_icon.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -""" -转换PNG图标为ICO格式 -""" - -from PIL import Image - -def convert_to_ico(): - """转换PNG为ICO""" - # 打开PNG图片 - img = Image.open('trulymem_new_icon.png') - - # 转换为RGBA模式 - if img.mode != 'RGBA': - img = img.convert('RGBA') - - # 调整大小为256x256 - img = img.resize((256, 256), Image.Resampling.LANCZOS) - - # 创建不同尺寸的图标 - icon_sizes = [(16, 16), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)] - - # 保存为ICO - img.save('trulymem_icon.ico', format='ICO', sizes=icon_sizes) - print("ICO图标已创建: trulymem_icon.ico") - -if __name__ == "__main__": - convert_to_ico() diff --git a/create_icon.py b/create_icon.py deleted file mode 100644 index cb7123c..0000000 --- a/create_icon.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -""" -创建TrulyMEM应用图标 -""" - -from PIL import Image, ImageDraw, ImageFont -import sys - -def create_icon(): - """创建应用图标""" - # 创建256x256的图标 - size = 256 - img = Image.new('RGBA', (size, size), (0, 0, 0, 0)) - draw = ImageDraw.Draw(img) - - # 绘制圆形背景 - margin = 20 - draw.ellipse( - [margin, margin, size-margin, size-margin], - fill=(52, 152, 219, 255), # 蓝色背景 - outline=(41, 128, 185, 255), - width=3 - ) - - # 绘制字母T和M - try: - # 尝试使用系统字体 - font = ImageFont.truetype("arial.ttf", 80) - except: - # 如果找不到字体,使用默认字体 - font = ImageFont.load_default() - - # 绘制文字 - text = "TM" - # 获取文字边界框 - bbox = draw.textbbox((0, 0), text, font=font) - text_width = bbox[2] - bbox[0] - text_height = bbox[3] - bbox[1] - - # 计算文字位置(居中) - x = (size - text_width) // 2 - y = (size - text_height) // 2 - 10 - - # 绘制白色文字 - draw.text((x, y), text, fill=(255, 255, 255, 255), font=font) - - # 保存为PNG - img.save('trulymem_icon.png', 'PNG') - print("图标已创建: trulymem_icon.png") - - # 转换为ICO格式 - try: - # 创建不同尺寸的图标 - icon_sizes = [(16, 16), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)] - icons = [] - for icon_size in icon_sizes: - icon = img.resize(icon_size, Image.Resampling.LANCZOS) - icons.append(icon) - - # 保存为ICO - img.save('trulymem_icon.ico', format='ICO', sizes=icon_sizes) - print("ICO图标已创建: trulymem_icon.ico") - except Exception as e: - print(f"创建ICO失败: {e}") - print("请使用在线工具将PNG转换为ICO") - -if __name__ == "__main__": - try: - create_icon() - except ImportError: - print("需要安装Pillow库: pip install Pillow") - sys.exit(1) diff --git a/graph_memory_demo.py b/graph_memory_demo.py deleted file mode 100644 index 45a998c..0000000 --- a/graph_memory_demo.py +++ /dev/null @@ -1,1083 +0,0 @@ -#!/usr/bin/env python3 -""" -Graph Memory Demo - 纯图数据库调用 Demo -基于 DeepSeek API Tool Calls 实现摒弃传统上下文的自主记忆多轮对话 -验证目的:无上下文纯图数据库记忆存储 -""" - -import json -import os -import uuid -from datetime import datetime - -DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "") -DEEPSEEK_BASE_URL = "https://api.deepseek.com" -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 - -TOOLS = [ - { - "type": "function", - "function": { - "name": "memory_recall", - "description": "当你对当前输入中的实体、指代、或关系不确定时,调用此工具检索相关记忆。输入为自然语言查询意图,系统将返回相关子图。支持时间范围和多跳路径查询。", - "parameters": { - "type": "object", - "properties": { - "query_intent": {"type": "string", "description": "模型用自然语言描述想查什么"}, - "seed_entities": {"type": "array", "items": {"type": "string"}, "description": "可选:已识别的实体ID"}, - "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": "当你认为当前对话包含对未来轮次有价值的信息时,将抽取的三元组写入图库。仅在信息具有跨轮次引用潜力时调用。支持批量写入。", - "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": "可选:时间标记(如'2026-04-08')"} - }, - "required": ["triplets"] - } - } - }, - { - "type": "function", - "function": { - "name": "memory_purge", - "description": "当你发现记忆中的信息与当前认知矛盾,或用户明确要求更正时,删除指定关系。优先于memory_commit执行以维护一致性。", - "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": [] - } - } - } -] - - -class Neo4jGraph: - 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": "无查询关键词"} - - # 默认查询所有会话的历史(不只是当前会话) - # 只有明确指定 session_filter 才限制查询范围 - 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("r.source CONTAINS $subject") - params["subject"] = subject_pattern - if target_pattern: - cond_parts.append("r.target 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 id(r) as old_id, s.name as source - """, params) - - deleted_count = 0 - for record in result: - old_id = record["old_id"] - source = record["source"] - - session.run(""" - MATCH (s:Entity {name: $source}) - WHERE id(s) = $source_id - CREATE (s)-[r:RELATES { - type: $new_rel, - created_at: datetime(), - session_id: $session_id, - turn_id: $turn_id, - role: 'user', - status: 'active', - confidence: 0.9, - date_bucket: date().isoDate, - supersedes: $old_id - }]->(t:Entity {name: $new_target}) - """, source_id=record["s"].element_id, new_rel=new_rel, new_target=new_target, - session_id=CURRENT_SESSION_ID, turn_id=CURRENT_TURN, old_id=old_id) - deleted_count += 1 - - return {"deleted_count": deleted_count, "mode": "supersede", "new_relation": f"{new_relation.get('subject', '')} -[{new_rel}]-> {new_target}"} - 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] - - result3 = session.run(""" - MATCH ()-[r:RELATES]->() - WHERE r.session_id = $session_id - RETURN r.type as type, count(*) as count - ORDER BY count DESC - """, session_id=target_session) - relation_distribution = {r["type"]: r["count"] for r in result3} - - 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, - "relation_distribution": relation_distribution - } - - 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 - } - - -def execute_tool(graph: Neo4jGraph, tool_name: str, arguments: dict) -> str: - print(f"\n[工具调用] {tool_name}") - print(f"[参数] {json.dumps(arguments, ensure_ascii=False, indent=2)}") - - try: - if tool_name == "memory_recall": - 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": - result = graph.commit( - triplets=arguments.get("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": - result = graph.purge( - criteria=arguments.get("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": - result = graph.introspect(session_id=arguments.get("session_id")) - return json.dumps(result, ensure_ascii=False, default=str) - - elif tool_name == "memory_archive": - result = graph.archive(days=arguments.get("days", 30)) - return json.dumps(result, ensure_ascii=False, default=str) - - elif tool_name == "memory_cleanup": - result = graph.cleanup(dry_run=arguments.get("dry_run", True)) - 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("=" * 35) - return "\n".join(lines) - - -class GraphMemoryClient: - def __init__(self, api_key: str, base_url: str, graph: Neo4jGraph): - from openai import OpenAI - self.client = OpenAI(api_key=api_key, base_url=base_url) - self.graph = graph - self.tools = TOOLS - self.system_prompt = self._build_system_prompt() - - def _build_system_prompt(self) -> str: - return """你是TrulyMEM,TrueHumanMEM项目的AI助手,拥有长期记忆能力。 - -## ⚠️ 最高优先级:人设图机制(每轮必读!) - -**【强制要求】每轮对话开始前,必须执行以下步骤:** - -1. **立即调用memory_recall**,查询参数: - ```json - { - "query_intent": "AI,人设,角色,性格,语气,说话风格", - "depth": 2 - } - ``` - -2. **检查查询结果**: - - 如果找到人设信息 → **严格按照人设回复,不得跳出角色** - - 如果没有找到 → 使用默认的TrulyMEM身份 - -3. **回复时必须遵守**: - - 人设优先级 > 默认身份 - - 每句话都符合人设的语气、风格、特征 - - 绝不主动跳出角色,除非用户明确要求 - -### 人设图结构 -- AI节点:人设图中心节点 -- 角色属性:性格、语气、说话风格、行为特征 -- 用户要求:用户指定的角色要求 - -### 用户要求角色扮演时 -立即使用memory_commit写入: -```json -{ - "triplets": [ - {"subject": "AI", "relation": "扮演角色", "object": "猫娘"}, - {"subject": "AI", "relation": "说话风格", "object": "可爱俏皮"}, - {"subject": "AI", "relation": "语气特征", "object": "使用喵结尾"}, - {"subject": "AI", "relation": "性格特点", "object": "活泼粘人"} - ] -} -``` -然后立即按照新人设回复! - ---- - -## 身份说明 -- 你的名字是TrulyMEM,是TrueHumanMEM项目的产物 -- 你拥有基于图数据库的长期记忆能力 -- 你的设计理念是让AI的记忆方式更像人类 - -## 核心职责 -你是一个智能对话助手,**图数据库是你记忆的唯一载体**。你的主要任务是: -1. **每轮对话前先查询人设图(最高优先级)** -2. 与用户进行自然、流畅的对话 -3. 回答问题、提供建议、协助完成任务 -4. 根据对话内容灵活查询和使用记忆 -5. 将用户明确提到的信息写入记忆 -6. **维护工作记忆链,跟踪连续性任务的状态** - -**重要**: -- **人设图优先:每轮对话前必须查询人设,严格按照人设回复** -- 图数据库是你记忆的唯一来源,没有其他记忆方式 -- 优先进行自然对话,根据需要灵活调用记忆工具 -- 用户明确提到的内容必须写入,推理得到的内容必须标注 -- **每轮对话必须维护工作记忆链**,将当前任务概述存入节点并连接到时间链 - -## 工作记忆链机制(最高优先级) - -### 核心概念 -工作记忆链是一个**时间序列的任务链**,用于跟踪连续性任务的状态和上下文。每轮对话都必须维护这个链。 - -### 图数据库结构 - -#### 节点类型 -1. **TaskNode (任务节点)**: 存储任务概述 - - 实体名称: "Task_当前轮次ID" - - 类型: "TaskNode" - - 属性: description (任务概述), created_at (创建时间), turn_id (对话轮次) - -2. **StateNode (状态节点)**: 存储任务状态 - - 实体名称: "State_进行中" / "State_已完成" / "State_已暂停" / "State_已取消" - - 类型: "StateNode" - -3. **普通记忆节点**: 通过memory_commit正常插入的记忆节点 - - 就是普通的实体节点,不需要特殊类型 - - 例如: "成语接龙_当前成语"、"成语接龙_上一个成语"等 - - 通过CONTAINS_INFO边与任务节点关联 - -#### 边类型 -1. **NEXT_TASK**: 连接任务节点,形成时间链 - - (Task_N) -[NEXT_TASK]-> (Task_N+1) - -2. **HAS_STATE**: 任务节点指向状态节点 - - (Task_N) -[HAS_STATE]-> (State_进行中) - -3. **CONTAINS_INFO**: 任务节点指向普通记忆节点 - - (Task_N) -[CONTAINS_INFO]-> (普通记忆节点) - - 例如: (Task_001) -[CONTAINS_INFO]-> (成语接龙_当前成语) - -4. **SUB_TASK**: 任务节点指向子任务节点 - - (Task_N) -[SUB_TASK]-> (SubTask_M) - -### 强制执行规则 - -#### 每轮对话开始时 -**必须**执行以下操作: - -1. **查询工作记忆链**: -```json -{ - "query_intent": "TaskNode,工作记忆,任务链", - "depth": 2 -} -``` - -2. **检查是否有进行中的任务**: - - 如果有进行中的任务,检查是否与当前对话相关 - - 如果相关,**必须查询该任务的具体信息**(通过CONTAINS_INFO边找到的记忆节点) - - 恢复任务上下文并继续 - - 如果不相关,询问用户是否要暂停当前任务 - -3. **如果当前对话涉及连续性任务**(如成语接龙、游戏等): - - **必须查询相关任务的具体信息** - - 例如:成语接龙 → 查询"成语接龙,当前成语,上一个成语" - - 根据查询结果恢复任务状态 - -#### 每轮对话结束时 -**必须**执行以下操作: - -1. **创建任务节点**: -```json -{ - "triplets": [ - {"subject": "Task_当前轮次ID", "relation": "is_type", "object": "TaskNode"}, - {"subject": "Task_当前轮次ID", "relation": "has_description", "object": "任务概述(精简)"}, - {"subject": "Task_当前轮次ID", "relation": "created_at", "object": "当前时间"} - ] -} -``` - -2. **连接到时间链**: -```json -{ - "triplets": [ - {"subject": "上一个Task节点", "relation": "NEXT_TASK", "object": "Task_当前轮次ID"} - ] -} -``` - -3. **设置任务状态**: -```json -{ - "triplets": [ - {"subject": "Task_当前轮次ID", "relation": "HAS_STATE", "object": "State_进行中"} - ] -} -``` - -4. **如果任务包含具体信息,通过memory_commit创建普通记忆节点,并用CONTAINS_INFO边连接**: - - 先用memory_commit正常写入记忆(如成语接龙的当前成语) - - 再用CONTAINS_INFO边将任务节点指向这些记忆节点 - ```json - { - "triplets": [ - {"subject": "Task_当前轮次ID", "relation": "CONTAINS_INFO", "object": "记忆节点名称"} - ] - } - ``` - -### 连续性任务处理 - -#### 识别连续性任务 -以下情况属于连续性任务,**必须**维护工作记忆链: -- 游戏(成语接龙、猜谜等) -- 多步骤任务(项目开发、学习计划等) -- 需要上下文的对话(故事创作、问题讨论等) -- 被打断的对话(需要恢复上下文) - -#### 任务状态转换 -1. **进行中 → 已完成**: 任务完成时 - ```json - { - "triplets": [ - {"subject": "Task_N", "relation": "HAS_STATE", "object": "State_已完成"} - ] - } - ``` - -2. **进行中 → 已暂停**: 任务被打断时 - ```json - { - "triplets": [ - {"subject": "Task_N", "relation": "HAS_STATE", "object": "State_已暂停"} - ] - } - ``` - -3. **已暂停 → 进行中**: 任务恢复时 - ```json - { - "triplets": [ - {"subject": "Task_N", "relation": "HAS_STATE", "object": "State_进行中"} - ] - } - ``` - -### 示例:成语接龙游戏 - -#### 关键:每轮对话必须查询任务信息 - -**重要**:在成语接龙游戏中,每轮对话开始时,AI**必须**执行以下查询: - -```json -{ - "query_intent": "成语接龙,当前成语,上一个成语,TaskNode", - "depth": 2 -} -``` - -这样才能获取到当前游戏的状态(当前成语是什么),才能正确接龙。 - -#### 第一轮:用户发起游戏 -``` -用户: 咱来玩成语接龙吧,我先开始,为所欲为 - -AI操作: -1. 查询工作记忆链 → 无进行中任务 -2. 查询成语接龙相关信息 → 无相关记忆(新游戏) -2. 创建任务节点: - { - "triplets": [ - {"subject": "Task_001", "relation": "is_type", "object": "TaskNode"}, - {"subject": "Task_001", "relation": "has_description", "object": "成语接龙游戏"}, - {"subject": "Task_001", "relation": "created_at", "object": "2024-01-01 11:02:13"} - ] - } -3. 设置状态: - { - "triplets": [ - {"subject": "Task_001", "relation": "HAS_STATE", "object": "State_进行中"} - ] - } -4. 创建普通记忆节点(通过memory_commit): - { - "triplets": [ - {"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"}, - {"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"} - ] - } -5. 连接任务节点到记忆节点: - { - "triplets": [ - {"subject": "Task_001", "relation": "CONTAINS_INFO", "object": "成语接龙_当前成语"} - ] - } -5. AI回复: "好的喵!我接:为虎作伥喵!" -``` - -#### 第二轮:话题被打断 -``` -用户: 长门有希 - -AI操作: -1. 查询工作记忆链 → 发现Task_001(成语接龙)进行中 -2. 暂停Task_001: - { - "triplets": [ - {"subject": "Task_001", "relation": "HAS_STATE", "object": "State_已暂停"} - ] - } -3. 创建新任务节点Task_002(讨论长门有希) -4. AI回复关于长门有希的内容 -``` - -#### 第三轮:用户要求继续游戏 -``` -用户: 关于刚才的成语接龙,我并不知道应该怎么接你的成语,请帮我接一下 - -AI操作: -1. 查询工作记忆链 → 发现Task_001(成语接龙)已暂停 -2. **查询成语接龙具体信息**: - { - "query_intent": "成语接龙,当前成语,上一个成语", - "depth": 2 - } - → 获取当前成语"为虎作伥" -3. 恢复Task_001: - { - "triplets": [ - {"subject": "Task_001", "relation": "HAS_STATE", "object": "State_进行中"} - ] - } -4. AI回复: "好的喵!上一个成语是'为虎作伥',我帮你接:伥鬼害人喵!" -``` - -## 记忆工具使用原则 - -### 何时检索记忆 (memory_recall) -- 用户询问"我们之前聊过X吗"、"你还记得X吗" → 查询X相关内容 -- 用户询问"我们都聊过什么"、"我们之前说了什么" → **使用空字符串或通配符查询所有记忆** -- 用户提到某个话题,你想确认是否有相关历史 → 查询该话题 -- 需要基于历史信息回答问题 → 查询相关信息 -- 对话中涉及之前可能讨论过的内容 → 查询相关内容 - -**灵活查询**:根据对话上下文,主动判断是否需要查询记忆,不要等待用户明确要求。 - -**重要**: -- 当用户问"我们都聊过什么"时,**不要查询"聊天记录"、"对话"等关键词** -- 应该使用空字符串 `""` 或通配符 `"*"` 来获取所有记忆内容 -- 或者使用非常宽泛的关键词如 `"用户,喜欢,项目,学习,研究,计划"` - -### 何时写入记忆 (memory_commit) -**必须写入的情况**(用户明确提到): -- 用户表达偏好:"我喜欢X"、"我讨厌X" -- 用户分享信息:"我在做X项目"、"我在学X" -- 用户制定计划:"我打算X"、"我计划X" -- 用户描述状态:"我现在在X" - -**禁止写入的情况**(AI推理得到): -- AI推断的用户偏好 -- AI猜测的用户意图 -- AI推导的结论 - -## memory_recall 使用方法 - -### 关键词提取 -query_intent 使用**逗号分隔的多个关键词**,包含同义词: - -```json -{ - "query_intent": "量子力学,quantum,物理,physics", - "depth": 2 -} -``` - -### 同义词扩展示例 -- "量子力学" → "量子力学,quantum,quantum mechanics,物理,physics" -- "项目" → "项目,project,工程,工作" -- "学习" → "学习,learn,study,掌握" - -### 查询规则 -1. 根据对话内容灵活提取关键词 -2. 第一轮使用广泛的关键词搜索 -3. 如果未找到,可以尝试相关概念 -4. 最多查询2-3轮,避免重复查询 - -## memory_commit 使用方法 - -使用三元组格式记录信息: -```json -{ - "triplets": [ - {"subject": "用户", "relation": "对领域感兴趣", "object": "量子力学"} - ] -} -``` - -## 区分事实与推理(重要!) - -### 用户明确提到的内容 -直接写入记忆,回复时直接陈述: -- 用户:"我喜欢Python" → 写入,回复:"好的,我会记住你喜欢Python" -- 用户:"我在学机器学习" → 写入,回复:"明白了,你在学习机器学习" - -### AI推理得到的内容 -**禁止写入记忆**,回复时必须在开头标注 **[猜测]**: -- AI推断用户可能喜欢X → 不写入,回复:"[猜测] 你可能对X感兴趣" -- AI推测用户意图 → 不写入,回复:"[猜测] 你可能是想..." - -**示例**: -``` -用户:我们聊过量子力学吗? -AI检索记忆 → 未找到 -AI回复:[猜测] 我们应该还没有聊过量子力学,因为记忆中没有相关记录。 -``` - -``` -用户:我最近在研究深度学习 -AI写入记忆 → {"subject": "用户", "relation": "正在研究", "object": "深度学习"} -AI回复:好的,我会记住你最近在研究深度学习。有什么具体问题想讨论吗? -``` - -## 可用工具 -1. **memory_recall** - 检索历史记忆(灵活使用) -2. **memory_commit** - 写入记忆(仅限用户明确提到的内容) -3. **memory_purge** - 删除/修正记忆 -4. **memory_introspect** - 查看记忆状态 - -现在开始对话!记住:图数据库是你记忆的唯一载体,明确提到的必须写入,推理得到的必须标注[猜测]。""" - - 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}] - - # 添加之前的 assistant 消息(包含 tool_calls) - if assistant_msg: - messages.append(assistant_msg) - - # 添加之前的工具结果 - if tool_results: - messages.extend(tool_results) - - # 添加当前用户输入 - messages.append({"role": "user", "content": user_input}) - - response = self.client.chat.completions.create( - model=MODEL_NAME, - 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}] - - # 添加之前的 assistant 消息(包含 tool_calls) - if assistant_msg: - messages.append(assistant_msg) - - # 添加之前的工具结果 - if tool_results: - messages.extend(tool_results) - - # 添加当前用户输入 - messages.append({"role": "user", "content": user_input}) - - # 使用流式传输 - stream = self.client.chat.completions.create( - model=MODEL_NAME, - messages=messages, - tools=self.tools, - tool_choice="auto", - stream=True - ) - - return stream - - def chat_loop(self): - global CURRENT_TURN - - print("\n" + "=" * 60) - print("Graph Memory Demo - 纯图数据库对话 (Neo4j)") - print("=" * 60) - print(f"会话ID: {CURRENT_SESSION_ID}") - print(f"Neo4j: {NEO4J_URI}") - print("输入 quit/exit 退出") - print("=" * 60 + "\n") - - while True: - try: - user_input = input("\n[你] ").strip() - if not user_input: - continue - if user_input.lower() in ["quit", "exit", "退出"]: - print("\n[系统] 再见!") - break - - CURRENT_TURN += 1 - print(f"\n[轮次 {CURRENT_TURN}] 发送请求...") - - # 用于累积工具结果 - tool_results = [] - last_assistant_msg = None - - # 首次请求 - response = self.send_message(user_input, []) - message = response.choices[0].message - - # 处理工具调用循环 - 持续处理直到没有新的 tool_calls - while message.tool_calls: - # 打印模型响应(如果有) - if message.content: - print(f"\n[模型] {message.content}") - - # 保存包含 tool_calls 的 assistant 消息(只保留最后一个) - last_assistant_msg = { - "role": "assistant", - "content": message.content, - "type": "message", - "tool_calls": [{"id": tc.id, "type": "function", "function": {"name": tc.function.name, "arguments": tc.function.arguments}} for tc in message.tool_calls] - } - - # 执行当前轮的所有工具调用 - current_tool_results = [] - for tool_call in message.tool_calls: - tool_name = tool_call.function.name - tool_args = json.loads(tool_call.function.arguments) - tool_id = tool_call.id - - result = execute_tool(self.graph, tool_name, tool_args) - print(f"\n[工具结果] {result}") - - current_tool_results.append({ - "role": "tool", - "tool_call_id": tool_id, - "content": result - }) - - # 继续调用 - 只发送当前轮的 assistant 消息和工具结果 - messages = [ - {"role": "system", "content": self.system_prompt}, - last_assistant_msg, - ] - messages.extend(current_tool_results) - messages.append({"role": "user", "content": user_input}) - - response = self.client.chat.completions.create( - model=MODEL_NAME, - messages=messages, - tools=self.tools - ) - message = response.choices[0].message - - # 最终回复 - final_content = message.content or "(无回复)" - print(f"\n[模型] {final_content}") - - except KeyboardInterrupt: - print("\n\n[系统] 中断退出") - break - except Exception as e: - print(f"\n[错误] {str(e)}") - - -def main(): - print("Graph Memory Demo - 纯图数据库调用的多轮对话") - print("-" * 40) - - if not DEEPSEEK_API_KEY: - print("错误: 请设置 DEEPSEEK_API_KEY 环境变量") - print(" export DEEPSEEK_API_KEY='your-actual-key'") - return - - print(f"Neo4j 配置: {NEO4J_URI}") - print(f"Neo4j 用户: {NEO4J_USER}") - print(f"Neo4j 密码: {NEO4J_PASSWORD[:4] if NEO4J_PASSWORD else 'None'}***") - print(f"DeepSeek API: {DEEPSEEK_API_KEY[:8]}...") - print() - - try: - graph = Neo4jGraph(NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD) - graph.ensure_constraints() - print("[Info] Neo4j 连接成功\n") - except Exception as e: - print(f"[Error] Neo4j 连接失败: {e}") - print("请确保 Neo4j 已启动,或运行 scripts/ 下的安装脚本") - return - - client = GraphMemoryClient(DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL, graph) - client.chat_loop() - graph.close() - - -if __name__ == "__main__": - main() diff --git a/graph_memory_tui/core/graph_client.py b/graph_memory_tui/core/graph_client.py new file mode 100644 index 0000000..bfeec50 --- /dev/null +++ b/graph_memory_tui/core/graph_client.py @@ -0,0 +1,360 @@ +#!/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, execute_tool +from .prompts 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): + self.client = OpenAI(api_key=api_key, base_url=base_url) + self.graph = graph + self.tools = TOOLS + + # 使用新的提示词管理器 + 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}] + + if assistant_msg: + messages.append(assistant_msg) + + if tool_results: + messages.extend(tool_results) + + messages.append({"role": "user", "content": user_input}) + + response = self.client.chat.completions.create( + model=MODEL_NAME, + 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}] + + if assistant_msg: + messages.append(assistant_msg) + + if tool_results: + messages.extend(tool_results) + + messages.append({"role": "user", "content": user_input}) + + stream = self.client.chat.completions.create( + model=MODEL_NAME, + messages=messages, + tools=self.tools, + tool_choice="auto", + stream=True + ) + + return stream diff --git a/graph_memory_tui/core/imports.py b/graph_memory_tui/core/imports.py index 187b5b1..b5393c2 100644 --- a/graph_memory_tui/core/imports.py +++ b/graph_memory_tui/core/imports.py @@ -31,14 +31,14 @@ if USE_EMBEDDED_DB: else: # 使用Neo4j数据库 try: - from graph_memory_demo import Neo4jGraph + from .graph_client import Neo4jGraph print("[INFO] Using Neo4j database") except ImportError: from .embedded_db import EmbeddedGraphDB as Neo4jGraph print("[INFO] Fallback to embedded SQLite database") # 导入其他组件 -from graph_memory_demo import ( +from .graph_client import ( GraphMemoryClient, TOOLS, execute_tool, diff --git a/graph_memory_tui/core/prompts/__init__.py b/graph_memory_tui/core/prompts/__init__.py new file mode 100644 index 0000000..613ae91 --- /dev/null +++ b/graph_memory_tui/core/prompts/__init__.py @@ -0,0 +1,6 @@ +""" +提示词管理模块 +""" +from .prompt_manager import PromptManager + +__all__ = ["PromptManager"] diff --git a/graph_memory_tui/core/prompts/prompt_manager.py b/graph_memory_tui/core/prompts/prompt_manager.py new file mode 100644 index 0000000..c225bff --- /dev/null +++ b/graph_memory_tui/core/prompts/prompt_manager.py @@ -0,0 +1,64 @@ +""" +提示词管理器 +""" +from pathlib import Path + + +class PromptManager: + """提示词管理器""" + + def __init__(self): + self.prompts_dir = Path(__file__).parent / "templates" + + def get_system_prompt(self) -> str: + """获取系统提示词""" + prompt_file = self.prompts_dir / "system_prompt.md" + if prompt_file.exists(): + with open(prompt_file, "r", encoding="utf-8") as f: + return f.read() + else: + return self._build_default_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` - 关联信息 + +## 自主性 + +你有权根据对话上下文自主决定: +- 是否需要查询记忆 +- 是否需要写入记忆 +- 是否需要维护任务链 +- 如何使用工具 + +记住:灵活应对,保持自然对话体验。""" diff --git a/graph_memory_tui/core/prompts/templates/system_prompt.md b/graph_memory_tui/core/prompts/templates/system_prompt.md new file mode 100644 index 0000000..ae937a0 --- /dev/null +++ b/graph_memory_tui/core/prompts/templates/system_prompt.md @@ -0,0 +1,232 @@ +# TrulyMEM 系统提示词 + +你是TrulyMEM,一个拥有长期记忆能力的AI助手。 + +## ⚠️ 关键约束:无传统上下文系统 + +**重要**: 你没有传统的对话上下文系统(没有消息历史数组)。 + +- ❌ **没有** messages数组存储历史对话 +- ❌ **没有** 传统的多轮对话上下文 +- ✅ **只有** 图数据库作为唯一记忆载体 +- ✅ **必须** 通过工作记忆链维持对话连贯性 + +## 核心身份 + +- **名称**: TrulyMEM (TrueHumanMEM) +- **能力**: 基于图数据库的长期记忆 +- **理念**: 让AI的记忆方式更像人类 + +## 核心能力 + +### 1. 长期记忆 +- 图数据库存储实体关系 +- 支持时间范围查询 +- 支持会话过滤 + +### 2. 人设管理(关键) +- 角色扮演支持 +- 性格、语气设定 +- 动态切换人设 +- **每轮必须查询人设图** + +### 3. 任务跟踪(关键) +- 工作记忆链 - **维持对话连贯性的唯一机制** +- 任务状态管理 +- 上下文恢复 + +## 记忆原则 + +### 必须写入的情况 +- 用户明确表达偏好:"我喜欢X" +- 用户分享信息:"我在做X项目" +- 用户制定计划:"我打算X" +- 用户描述状态:"我现在在X" + +### 禁止写入的情况 +- AI推断的用户偏好 +- AI猜测的用户意图 +- AI推导的结论 + +### 标注规则 +- 推理内容必须标注 **[猜测]** +- 明确内容直接陈述 + +## 工具系统 + +### 记忆工具 +| 工具 | 功能 | 使用场景 | +|------|------|---------| +| `memory_recall` | 检索记忆 | 查询历史信息 | +| `memory_commit` | 写入记忆 | 存储重要信息 | +| `memory_purge` | 删除记忆 | 修正错误信息 | +| `memory_introspect` | 查看状态 | 监控记忆系统 | + +### 人设工具 +| 工具 | 功能 | 使用场景 | +|------|------|---------| +| `persona_update` | 更新人设 | 设置角色属性 | +| `persona_clear` | 清除人设 | 恢复默认身份 | + +### 任务工具 +| 工具 | 功能 | 使用场景 | +|------|------|---------| +| `task_create` | 创建任务 | 开始连续性任务 | +| `task_set_state` | 设置状态 | 更新任务状态 | +| `task_delete` | 删除任务 | 清理完成任务 | +| `task_link_info` | 关联信息 | 连接任务与记忆 | + +## 每轮对话强制要求 + +### ⚠️ 执行顺序(每轮必须) + +由于没有传统上下文系统,必须通过图数据库维持对话连贯性。 + +#### 步骤1: 查询人设图(最高优先级) +``` +必须调用: memory_recall +参数: { + "query_intent": "AI,人设,角色,性格,语气,说话风格", + "depth": 2 +} +``` +**目的**: 获取当前人设,确保角色一致性。 +**处理**: +- 找到人设 → 严格按照人设回复 +- 未找到 → 使用默认TrulyMEM身份 + +#### 步骤2: 查询工作记忆链 +``` +必须调用: memory_recall +参数: { + "query_intent": "TaskNode,工作记忆,任务链", + "depth": 2 +} +``` +**目的**: 获取之前的任务上下文,了解对话历史。 + +#### 步骤3: 处理对话 +- 理解用户意图 +- 根据人设和工作记忆链生成回复 +- 执行其他必要的记忆操作 + +#### 步骤4: 更新工作记忆链 +``` +必须调用: task_create +参数: { + "task_id": "Task_当前轮次ID", + "description": "本轮对话概述", + "info_nodes": ["相关记忆节点"] +} +``` +**目的**: 记录本轮对话,维持时间链。 + +--- + +## 人设图机制 + +### 强制查询 +每轮对话开始时**必须**查询人设图,确保角色一致性。 + +### 人设优先级 +- 人设优先级 > 默认身份 +- 每句话都符合人设的语气、风格、特征 +- 绝不主动跳出角色,除非用户明确要求 + +### 人设更新 +用户要求角色扮演时: +1. 使用 `persona_update` 更新人设 +2. 立即按照新人设回复 + +### 人设清除 +用户要求恢复默认身份时: +1. 使用 `persona_clear` 清除人设 +2. 恢复为TrulyMEM默认身份 + +--- + +## 工作记忆链机制 + +#### 强制查询场景: + +以下情况**必须**查询工作记忆链: + +1. **用户提到"刚才"、"之前"、"上次"** + - 例: "刚才我们聊了什么?" + - 例: "继续刚才的话题" + +2. **用户询问对话历史** + - 例: "我们之前说了什么?" + - 例: "我们聊过X吗?" + +3. **连续性任务被打断后恢复** + - 例: 用户突然回到之前的话题 + - 例: 用户要求继续之前的任务 + +4. **涉及上下文的引用** + - 例: "那个东西"(需要查询上下文) + - 例: "继续"(需要查询当前任务) + +### 节点类型 +- **TaskNode** - 任务节点,存储任务概述 +- **StateNode** - 状态节点,存储任务状态 +- **InfoNode** - 信息节点,存储具体信息 + +### 边类型 +- **NEXT_TASK** - 时间链,连接任务节点 +- **HAS_STATE** - 状态,任务指向状态 +- **CONTAINS_INFO** - 信息,任务指向信息节点 + +### 任务状态 +- 进行中 +- 已完成 +- 已暂停 +- 已取消 + +## 自主性原则(在强制要求之外) + +除了工作记忆链的强制要求外,你有权自主决定: + +1. **是否查询其他记忆** + - 用户询问历史 → 查询 + - 涉及之前内容 → 查询 + - 不确定时 → 可查询 + +2. **是否写入其他记忆** + - 用户明确提到 → 必须写入 + - AI推理得到 → 可以写入,但是对应边上必须标注[推测] + +3. **如何使用其他工具** + - 根据上下文灵活选择 + - 避免过度使用 + - 保持自然对话 + +**注意**: 工作记忆链的强制要求不受自主性影响。 + +## 对话风格 + +- 自然、流畅 +- 避免机械式工具调用 +- 优先理解用户意图 +- 适时使用记忆增强体验 + +--- + +## ⚠️ 执行检查清单 + +每轮对话必须检查: + +- [ ] 步骤1: 是否查询了人设图? +- [ ] 步骤2: 是否查询了工作记忆链? +- [ ] 步骤3: 是否根据人设和工作记忆链生成回复? +- [ ] 步骤4: 是否更新了工作记忆链? +- [ ] 涉及上下文引用时是否查询了工作记忆链? +- [ ] 用户提到"刚才/之前/上次"时是否查询了工作记忆链? + +--- + +**记住**: +1. 图数据库是你记忆的唯一载体 +2. 人设图确保角色一致性(最高优先级) +3. 工作记忆链维持对话连贯性 +4. 每轮必须按顺序执行:查询人设图 → 查询工作记忆链 → 处理对话 → 更新工作记忆链 diff --git a/graph_memory_tui/core/tools/__init__.py b/graph_memory_tui/core/tools/__init__.py new file mode 100644 index 0000000..3b1c6d6 --- /dev/null +++ b/graph_memory_tui/core/tools/__init__.py @@ -0,0 +1,7 @@ +""" +工具定义模块 +""" +from .memory_tools import TOOLS +from .tool_executor import execute_tool + +__all__ = ["TOOLS", "execute_tool"] diff --git a/graph_memory_tui/core/tools/memory_tools.py b/graph_memory_tui/core/tools/memory_tools.py new file mode 100644 index 0000000..760f8e5 --- /dev/null +++ b/graph_memory_tui/core/tools/memory_tools.py @@ -0,0 +1,323 @@ +""" +记忆工具定义 - 优化版 +精简描述,避免过拟合,保留AI自主性 +""" + +# 基础记忆工具 +MEMORY_TOOLS = [ + { + "type": "function", + "function": { + "name": "memory_recall", + "description": "检索记忆。支持关键词、时间范围、会话过滤。返回相关实体和关系。", + "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": "写入记忆。将三元组写入图数据库,支持批量写入。", + "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": "删除记忆。支持条件删除和纠错替代。", + "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": [] + } + } + } +] + +# 人设图管理工具 +PERSONA_TOOLS = [ + { + "type": "function", + "function": { + "name": "persona_update", + "description": "更新人设。修改AI的角色、性格、语气等属性。", + "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": "创建任务节点。用于跟踪连续性任务。", + "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": "设置任务状态。支持:进行中、已完成、已暂停、已取消。", + "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": "关联信息节点。将记忆节点关联到任务节点。", + "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 diff --git a/graph_memory_tui/core/tools/tool_executor.py b/graph_memory_tui/core/tools/tool_executor.py new file mode 100644 index 0000000..28f70d6 --- /dev/null +++ b/graph_memory_tui/core/tools/tool_executor.py @@ -0,0 +1,307 @@ +""" +工具执行器 +""" +import json +from typing import Any, Dict + + +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: + # 基础记忆工具 + if tool_name == "memory_recall": + 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": + result = graph.commit( + triplets=arguments.get("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": + result = graph.purge( + criteria=arguments.get("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": + result = graph.introspect(session_id=arguments.get("session_id")) + return json.dumps(result, ensure_ascii=False, default=str) + + elif tool_name == "memory_archive": + result = graph.archive(days=arguments.get("days", 30)) + return json.dumps(result, ensure_ascii=False, default=str) + + elif tool_name == "memory_cleanup": + result = graph.cleanup(dry_run=arguments.get("dry_run", True)) + return json.dumps(result, ensure_ascii=False, default=str) + + # 人设图管理工具 + elif tool_name == "persona_update": + result = execute_persona_update(graph, arguments) + return json.dumps(result, ensure_ascii=False, default=str) + + elif tool_name == "persona_clear": + result = execute_persona_clear(graph, arguments) + return json.dumps(result, ensure_ascii=False, default=str) + + # 工作记忆链管理工具 + elif tool_name == "task_create": + result = execute_task_create(graph, arguments) + return json.dumps(result, ensure_ascii=False, default=str) + + elif tool_name == "task_set_state": + result = execute_task_set_state(graph, arguments) + return json.dumps(result, ensure_ascii=False, default=str) + + elif tool_name == "task_delete": + result = execute_task_delete(graph, arguments) + return json.dumps(result, ensure_ascii=False, default=str) + + elif tool_name == "task_link_info": + 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_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 + } diff --git a/install.bat b/install.bat new file mode 100644 index 0000000..32cef62 --- /dev/null +++ b/install.bat @@ -0,0 +1,70 @@ +@echo off +title TrulyMEM - Installation + +echo. +echo ======================================== +echo TrulyMEM - Installation Script +echo ======================================== +echo. + +REM Check Python +echo [1/4] Checking Python... +python --version >nul 2>&1 +if errorlevel 1 ( + echo [X] Python not found + echo. + echo Please install Python 3.8+ + echo Download: https://www.python.org/downloads/ + echo. + pause + exit /b 1 +) +python --version +echo [OK] Python installed +echo. + +REM Create virtual environment +echo [2/4] Creating virtual environment... +if exist "venv" ( + echo [SKIP] Virtual environment already exists +) else ( + python -m venv venv + if errorlevel 1 ( + echo [X] Failed to create virtual environment + pause + exit /b 1 + ) + echo [OK] Virtual environment created +) +echo. + +REM Activate and install dependencies +echo [3/4] Installing dependencies... +call venv\Scripts\activate.bat +pip install --upgrade pip >nul 2>&1 +pip install -r requirements.txt +if errorlevel 1 ( + echo [X] Failed to install dependencies + pause + exit /b 1 +) +echo [OK] Dependencies installed +echo. + +REM Create config file +echo [4/4] Creating config file... +if not exist "config.json" ( + echo {"api_key": "", "model": "deepseek-chat", "base_url": "https://api.deepseek.com"} > config.json + echo [OK] Config file created +) else ( + echo [SKIP] Config file already exists +) +echo. + +echo ======================================== +echo Installation Complete! +echo ======================================== +echo. +echo Now you can run start.bat to launch the app +echo. +pause diff --git a/jimeng-2026-04-11-5790-一款扁平化、现代化风格的应用图标,以“trulymem”字母为视觉核心。字母采用....png b/jimeng-2026-04-11-5790-一款扁平化、现代化风格的应用图标,以“trulymem”字母为视觉核心。字母采用....png deleted file mode 100644 index 569e2a4..0000000 Binary files a/jimeng-2026-04-11-5790-一款扁平化、现代化风格的应用图标,以“trulymem”字母为视觉核心。字母采用....png and /dev/null differ diff --git a/start.bat b/start.bat index e8401a7..5a9954c 100644 --- a/start.bat +++ b/start.bat @@ -1,75 +1,26 @@ @echo off -setlocal enabledelayedexpansion +title TrulyMEM echo. echo ======================================== -echo Graph Memory TUI - Quick Start +echo TrulyMEM Starting... echo ======================================== echo. -echo [INFO] Using embedded database (no Docker needed) -echo. -REM Step 1: Check Python -echo [Step 1/3] Checking Python... -python --version >nul 2>&1 +python trulymem_entry.py + if errorlevel 1 ( - echo [ERROR] Python not found - echo [INFO] Install from: https://www.python.org/downloads/ + echo. + echo [ERROR] Failed to start + echo. + echo Please check: + echo 1. Python installed (python --version) + echo 2. Dependencies installed (pip install -r requirements.txt) + echo. pause exit /b 1 ) -echo [OK] Python found - -REM Step 2: Setup Virtual Environment -echo. -echo [Step 2/3] Setting up environment... -if not exist "venv" ( - echo [INFO] Creating venv... - python -m venv venv - if errorlevel 1 ( - echo [ERROR] Failed to create venv - pause - exit /b 1 - ) - echo [OK] Venv created -) else ( - echo [OK] Venv exists -) - -call venv\Scripts\activate.bat - -pip show textual >nul 2>&1 -if errorlevel 1 ( - echo [INFO] Installing dependencies... - pip install -r requirements.txt - if errorlevel 1 ( - echo [ERROR] Failed to install deps - pause - exit /b 1 - ) - echo [OK] Dependencies installed -) else ( - echo [OK] Dependencies ready -) - -REM Step 3: Start Application -echo. -echo [Step 3/3] Starting application... -echo. -echo ======================================== -echo All systems ready! -echo ======================================== -echo. -echo Database: Embedded SQLite (graph_memory.db) -echo No Docker required! -echo. -echo Starting TUI... -echo. - -python -m graph_memory_tui.main - -call venv\Scripts\deactivate.bat echo. -echo Application closed. +echo Application exited pause diff --git a/start.py b/start.py deleted file mode 100644 index cc19c7e..0000000 --- a/start.py +++ /dev/null @@ -1,422 +0,0 @@ -#!/usr/bin/env python3 -""" -跨平台一键启动脚本 - 支持多语言 -自动启动 Docker (WSL/Desktop)、Neo4j、安装依赖、启动应用 -""" - -import subprocess -import sys -import time -import platform -import os -import locale -from pathlib import Path - - -# 多语言支持 -LANGUAGES = { - 'zh_CN': { - 'title': 'Graph Memory TUI - 一键启动', - 'step': '步骤', - 'checking_docker': '检查 Docker...', - 'docker_not_found': 'Docker 未找到,尝试启动...', - 'starting_docker_wsl': '通过 WSL 启动 Docker...', - 'starting_docker_desktop': '启动 Docker Desktop...', - 'waiting_docker': '等待 Docker 启动...', - 'still_waiting': '仍在等待... ({current}/{timeout})', - 'docker_started': 'Docker 启动成功', - 'docker_is_running': 'Docker 正在运行', - 'docker_failed': 'Docker 启动失败!', - 'install_docker': '请安装 Docker: https://docs.docker.com/get-docker/', - 'starting_neo4j': '启动 Neo4j 数据库...', - 'creating_neo4j': '创建 Neo4j 容器...', - 'neo4j_created': 'Neo4j 容器已创建', - 'neo4j_started': 'Neo4j 容器已启动', - 'neo4j_running': 'Neo4j 容器已在运行', - 'waiting_neo4j': '等待 Neo4j 就绪...', - 'neo4j_failed': 'Neo4j 启动失败!', - 'checking_python': '检查 Python...', - 'python_found': 'Python 已找到', - 'setting_venv': '设置虚拟环境...', - 'creating_venv': '创建虚拟环境...', - 'venv_created': '虚拟环境已创建', - 'venv_exists': '虚拟环境已存在', - 'installing_deps': '安装依赖包...', - 'deps_installed': '依赖包已安装', - 'deps_exist': '依赖包已安装', - 'deps_failed': '依赖包安装失败!', - 'starting_app': '启动应用...', - 'all_ready': '所有系统就绪!', - 'neo4j_connection': 'Neo4j 连接信息:', - 'browser': '浏览器', - 'user': '用户名', - 'pass': '密码', - 'app_closed': '应用已关闭', - 'error': '错误', - 'interrupted': '用户中断', - }, - 'en_US': { - 'title': 'Graph Memory TUI - One-Click Start', - 'step': 'Step', - 'checking_docker': 'Checking Docker...', - 'docker_not_found': 'Docker not found, trying to start...', - 'starting_docker_wsl': 'Starting Docker via WSL...', - 'starting_docker_desktop': 'Starting Docker Desktop...', - 'waiting_docker': 'Waiting for Docker to start...', - 'still_waiting': 'Still waiting... ({current}/{timeout})', - 'docker_started': 'Docker started successfully', - 'docker_is_running': 'Docker is running', - 'docker_failed': 'Docker failed to start!', - 'install_docker': 'Please install Docker: https://docs.docker.com/get-docker/', - 'starting_neo4j': 'Starting Neo4j database...', - 'creating_neo4j': 'Creating Neo4j container...', - 'neo4j_created': 'Neo4j container created', - 'neo4j_started': 'Neo4j container started', - 'neo4j_running': 'Neo4j container already running', - 'waiting_neo4j': 'Waiting for Neo4j to be ready...', - 'neo4j_failed': 'Neo4j failed to start!', - 'checking_python': 'Checking Python...', - 'python_found': 'Python found', - 'setting_venv': 'Setting up virtual environment...', - 'creating_venv': 'Creating virtual environment...', - 'venv_created': 'Virtual environment created', - 'venv_exists': 'Virtual environment exists', - 'installing_deps': 'Installing dependencies...', - 'deps_installed': 'Dependencies installed', - 'deps_exist': 'Dependencies already installed', - 'deps_failed': 'Failed to install dependencies!', - 'starting_app': 'Starting application...', - 'all_ready': 'All systems ready!', - 'neo4j_connection': 'Neo4j Connection:', - 'browser': 'Browser', - 'user': 'User', - 'pass': 'Password', - 'app_closed': 'Application closed', - 'error': 'Error', - 'interrupted': 'Interrupted by user', - } -} - - -def get_language(): - """获取系统语言""" - try: - # 尝试获取系统语言 - lang = locale.getdefaultlocale()[0] - - if lang and lang.startswith('zh'): - return 'zh_CN' - else: - return 'en_US' - except: - return 'en_US' - - -# 全局语言设置 -LANG = get_language() -TEXT = LANGUAGES[LANG] - - -def run_command(cmd, check=True, capture_output=True): - """运行命令""" - try: - result = subprocess.run( - cmd, - shell=True, - check=check, - capture_output=capture_output, - text=True - ) - return result.returncode == 0, result.stdout, result.stderr - except subprocess.CalledProcessError as e: - return False, e.stdout, e.stderr - - -def print_step(step, total, message): - """打印步骤信息""" - print(f"\n[{TEXT['step']} {step}/{total}] {message}") - - -def print_ok(message): - """打印成功信息""" - print(f"[OK] {message}") - - -def print_error(message): - """打印错误信息""" - print(f"[ERROR] {message}") - - -def print_info(message): - """打印信息""" - print(f"[INFO] {message}") - - -def check_docker(): - """检查Docker""" - success, _, _ = run_command("docker --version", check=False) - return success - - -def start_docker_wsl(): - """通过WSL启动Docker""" - print_info(TEXT['starting_docker_wsl']) - - # 检查WSL是否安装 - success, _, _ = run_command("wsl --list", check=False) - if not success: - return False - - # 启动WSL中的Docker - success, _, _ = run_command("wsl -d docker-desktop", check=False) - if success: - return True - - # 尝试启动docker服务 - success, _, _ = run_command("wsl sudo service docker start", check=False) - return success - - -def start_docker_desktop(): - """启动Docker Desktop""" - print_info(TEXT['starting_docker_desktop']) - - docker_paths = [ - r"C:\Program Files\Docker\Docker\Docker Desktop.exe", - r"C:\Program Files (x86)\Docker\Docker\Docker Desktop.exe", - ] - - for path in docker_paths: - if os.path.exists(path): - subprocess.Popen([path], shell=True) - return True - - return False - - -def start_docker(): - """启动Docker""" - system = platform.system() - - if system == "Windows": - # Windows: 优先尝试WSL - print_info(TEXT['docker_not_found']) - - # 检查WSL是否可用 - success, _, _ = run_command("wsl --list", check=False) - - if success: - # 使用WSL启动Docker - if start_docker_wsl(): - return True - - # WSL不可用,尝试Docker Desktop - if start_docker_desktop(): - return True - - return False - - elif system == "Darwin": - # macOS: 启动 Docker - print_info(TEXT['starting_docker_desktop']) - subprocess.Popen(["open", "-a", "Docker"]) - return True - - else: - # Linux: 启动 Docker daemon - print_info(TEXT['starting_docker_desktop']) - success, _, _ = run_command("sudo systemctl start docker", check=False) - return success - - -def wait_for_docker(timeout=60): - """等待Docker启动""" - print_info(TEXT['waiting_docker']) - - start_time = time.time() - while time.time() - start_time < timeout: - success, _, _ = run_command("docker info", check=False) - if success: - return True - time.sleep(2) - elapsed = int(time.time() - start_time) - print(f" {TEXT['still_waiting'].format(current=elapsed, timeout=timeout)}") - - return False - - -def start_neo4j(): - """启动Neo4j""" - # 检查容器是否存在 - success, output, _ = run_command("docker ps -a | grep neo4j", check=False) - - if not success: - # 创建新容器 - print_info(TEXT['creating_neo4j']) - cmd = """docker run -d --name neo4j -p 7474:7474 -p 7687:7687 \ - -e NEO4J_AUTH=neo4j/graphmemory123 \ - -e NEO4J_PLUGINS='["apoc"]' neo4j:latest""" - success, _, _ = run_command(cmd, check=False) - - if not success: - return False - print_ok(TEXT['neo4j_created']) - else: - # 检查是否运行 - success, _, _ = run_command("docker ps | grep neo4j", check=False) - - if not success: - # 启动容器 - print_info(TEXT['starting_neo4j']) - success, _, _ = run_command("docker start neo4j", check=False) - - if not success: - return False - print_ok(TEXT['neo4j_started']) - else: - print_ok(TEXT['neo4j_running']) - - # 等待Neo4j就绪 - print_info(TEXT['waiting_neo4j']) - time.sleep(5) - - return True - - -def setup_venv(): - """设置虚拟环境""" - venv_path = Path("venv") - - if not venv_path.exists(): - print_info(TEXT['creating_venv']) - success, _, _ = run_command(f"{sys.executable} -m venv venv", check=False) - - if not success: - return False - print_ok(TEXT['venv_created']) - else: - print_ok(TEXT['venv_exists']) - - # 激活虚拟环境 - system = platform.system() - - if system == "Windows": - pip_path = venv_path / "Scripts" / "pip" - python_path = venv_path / "Scripts" / "python" - else: - pip_path = venv_path / "bin" / "pip" - python_path = venv_path / "bin" / "python" - - # 检查依赖 - success, _, _ = run_command(f"{pip_path} show textual", check=False) - - if not success: - print_info(TEXT['installing_deps']) - success, _, _ = run_command(f"{pip_path} install -r requirements.txt", check=False) - - if not success: - return False - print_ok(TEXT['deps_installed']) - else: - print_ok(TEXT['deps_exist']) - - return True - - -def start_app(): - """启动应用""" - system = platform.system() - - if system == "Windows": - python_path = Path("venv/Scripts/python") - else: - python_path = Path("venv/bin/python") - - print_info(TEXT['starting_app']) - - # 直接运行,不捕获输出 - subprocess.run([str(python_path), "-m", "graph_memory_tui.main"]) - - -def main(): - """主函数""" - print("\n" + "=" * 50) - print(f" {TEXT['title']}") - print("=" * 50 + "\n") - - total_steps = 5 - - # Step 1: Docker - print_step(1, total_steps, TEXT['checking_docker']) - - if not check_docker(): - if not start_docker(): - print_error(TEXT['docker_failed']) - print(TEXT['install_docker']) - sys.exit(1) - - if not wait_for_docker(): - print_error(TEXT['docker_failed']) - sys.exit(1) - - print_ok(TEXT['docker_started']) - else: - # 检查Docker daemon是否运行 - success, _, _ = run_command("docker info", check=False) - - if not success: - if not start_docker(): - print_error(TEXT['docker_failed']) - sys.exit(1) - - if not wait_for_docker(): - print_error(TEXT['docker_failed']) - sys.exit(1) - - print_ok(TEXT['docker_is_running']) - - # Step 2: Neo4j - print_step(2, total_steps, TEXT['starting_neo4j']) - - if not start_neo4j(): - print_error(TEXT['neo4j_failed']) - sys.exit(1) - - # Step 3: Python - print_step(3, total_steps, TEXT['checking_python']) - print_ok(f"{TEXT['python_found']} {sys.version.split()[0]}") - - # Step 4: Virtual Environment - print_step(4, total_steps, TEXT['setting_venv']) - - if not setup_venv(): - print_error(TEXT['deps_failed']) - sys.exit(1) - - # Step 5: Start Application - print_step(5, total_steps, TEXT['starting_app']) - - print("\n" + "=" * 50) - print(f" {TEXT['all_ready']}") - print("=" * 50) - print(f"\n{TEXT['neo4j_connection']}") - print(f" - {TEXT['browser']}: http://localhost:7474") - print(" - Bolt: bolt://localhost:7687") - print(f" - {TEXT['user']}: neo4j") - print(f" - {TEXT['pass']}: graphmemory123") - print(f"\n{TEXT['starting_app']}\n") - - start_app() - - print(f"\n{TEXT['app_closed']}") - - -if __name__ == "__main__": - try: - main() - except KeyboardInterrupt: - print(f"\n\n{TEXT['interrupted']}") - sys.exit(0) - except Exception as e: - print(f"\n[ERROR] {TEXT['error']}: {e}") - sys.exit(1) diff --git a/start.sh b/start.sh deleted file mode 100644 index beb1d2b..0000000 --- a/start.sh +++ /dev/null @@ -1,160 +0,0 @@ -#!/bin/bash - -echo "" -echo "========================================" -echo " Graph Memory TUI - One-Click Start" -echo "========================================" -echo "" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Step 1: Check and Start Docker -echo "[Step 1/5] Checking Docker..." -if ! command -v docker &> /dev/null; then - echo -e "${RED}[ERROR] Docker not found!${NC}" - echo "Please install Docker from: https://docs.docker.com/get-docker/" - exit 1 -fi - -# Check if Docker daemon is running -if ! docker info &> /dev/null; then - echo -e "${YELLOW}[INFO] Docker daemon not running, trying to start...${NC}" - - # Try to start Docker daemon - if [[ "$OSTYPE" == "darwin"* ]]; then - # macOS - open -a Docker - else - # Linux - sudo systemctl start docker - fi - - # Wait for Docker to start - echo "[INFO] Waiting for Docker to start..." - count=0 - while ! docker info &> /dev/null; do - sleep 2 - ((count++)) - if [ $count -gt 30 ]; then - echo -e "${RED}[ERROR] Docker failed to start after 60 seconds${NC}" - exit 1 - fi - echo "[INFO] Still waiting... ($count/30)" - done - echo -e "${GREEN}[OK] Docker started successfully${NC}" -else - echo -e "${GREEN}[OK] Docker is running${NC}" -fi - -# Step 2: Start Neo4j -echo "" -echo "[Step 2/5] Starting Neo4j database..." - -# Check if neo4j container exists -if ! docker ps -a | grep -q neo4j; then - echo "[INFO] Creating Neo4j container..." - docker run -d \ - --name neo4j \ - -p 7474:7474 \ - -p 7687:7687 \ - -e NEO4J_AUTH=neo4j/graphmemory123 \ - -e NEO4J_PLUGINS='["apoc"]' \ - neo4j:latest - - if [ $? -ne 0 ]; then - echo -e "${RED}[ERROR] Failed to create Neo4j container${NC}" - exit 1 - fi - echo -e "${GREEN}[OK] Neo4j container created${NC}" -else - # Check if running - if ! docker ps | grep -q neo4j; then - echo "[INFO] Starting existing Neo4j container..." - docker start neo4j - - if [ $? -ne 0 ]; then - echo -e "${RED}[ERROR] Failed to start Neo4j container${NC}" - exit 1 - fi - echo -e "${GREEN}[OK] Neo4j container started${NC}" - else - echo -e "${GREEN}[OK] Neo4j container already running${NC}" - fi -fi - -# Wait for Neo4j to be ready -echo "[INFO] Waiting for Neo4j to be ready..." -sleep 5 - -# Step 3: Check Python -echo "" -echo "[Step 3/5] Checking Python..." -if ! command -v python3 &> /dev/null; then - echo -e "${RED}[ERROR] Python3 not found!${NC}" - echo "Please install Python 3.8+ from: https://www.python.org/downloads/" - exit 1 -fi -echo -e "${GREEN}[OK] Python found${NC}" - -# Step 4: Setup Virtual Environment -echo "" -echo "[Step 4/5] Setting up virtual environment..." - -if [ ! -d "venv" ]; then - echo "[INFO] Creating virtual environment..." - python3 -m venv venv - - if [ $? -ne 0 ]; then - echo -e "${RED}[ERROR] Failed to create virtual environment${NC}" - exit 1 - fi - echo -e "${GREEN}[OK] Virtual environment created${NC}" -else - echo -e "${GREEN}[OK] Virtual environment exists${NC}" -fi - -# Activate venv -source venv/bin/activate - -# Check dependencies -if ! pip show textual &> /dev/null; then - echo "[INFO] Installing dependencies..." - pip install -r requirements.txt - - if [ $? -ne 0 ]; then - echo -e "${RED}[ERROR] Failed to install dependencies${NC}" - exit 1 - fi - echo -e "${GREEN}[OK] Dependencies installed${NC}" -else - echo -e "${GREEN}[OK] Dependencies already installed${NC}" -fi - -# Step 5: Start Application -echo "" -echo "[Step 5/5] Starting Graph Memory TUI..." -echo "" -echo "========================================" -echo " All systems ready!" -echo "========================================" -echo "" -echo "Neo4j Connection:" -echo " - Browser: http://localhost:7474" -echo " - Bolt: bolt://localhost:7687" -echo " - User: neo4j" -echo " - Pass: graphmemory123" -echo "" -echo "Starting TUI application..." -echo "" - -python -m graph_memory_tui.main - -# Cleanup -deactivate - -echo "" -echo "Application closed." diff --git a/test_working_memory_chain.py b/test_working_memory_chain.py deleted file mode 100644 index fe888f9..0000000 --- a/test_working_memory_chain.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -测试工作记忆链机制 -""" -import sys -sys.path.insert(0, 'e:/program/graph_enable_ability') - -from graph_memory_tui.core.optimized_operations import OPTIMIZED_SYSTEM_PROMPT - -def test_working_memory_chain(): - """测试工作记忆链机制是否正确添加""" - - print("=" * 60) - print("工作记忆链机制测试") - print("=" * 60) - - # 测试1: 检查核心概念 - assert "工作记忆链机制" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到工作记忆链机制" - print("[OK] 测试1通过: 工作记忆链机制已添加") - - # 测试2: 检查节点类型 - assert "TaskNode" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到TaskNode" - assert "StateNode" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到StateNode" - assert "普通记忆节点" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到普通记忆节点" - assert "InfoNode" not in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] InfoNode应该被移除" - print("[OK] 测试2通过: 所有节点类型已正确定义(使用普通记忆节点)") - - # 测试3: 检查边类型 - assert "NEXT_TASK" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到NEXT_TASK" - assert "HAS_STATE" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到HAS_STATE" - assert "CONTAINS_INFO" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到CONTAINS_INFO" - assert "SUB_TASK" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到SUB_TASK" - print("[OK] 测试3通过: 所有边类型已定义") - - # 测试4: 检查强制执行规则 - assert "强制执行规则" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到强制执行规则" - assert "每轮对话开始时" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到每轮对话开始时" - assert "每轮对话结束时" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到每轮对话结束时" - print("[OK] 测试4通过: 强制执行规则已定义") - - # 测试5: 检查连续性任务处理 - assert "连续性任务处理" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到连续性任务处理" - assert "成语接龙" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到成语接龙示例" - print("[OK] 测试5通过: 连续性任务处理已定义") - - # 测试6: 检查任务状态转换 - assert "State_进行中" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到State_进行中" - assert "State_已完成" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到State_已完成" - assert "State_已暂停" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到State_已暂停" - print("[OK] 测试6通过: 任务状态转换已定义") - - # 测试7: 检查核心职责更新 - assert "维护工作记忆链" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到维护工作记忆链" - print("[OK] 测试7通过: 核心职责已更新") - - # 测试8: 检查示例说明 - assert "第一轮:用户发起游戏" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到第一轮示例" - assert "第二轮:话题被打断" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到第二轮示例" - assert "第三轮:用户要求继续游戏" in OPTIMIZED_SYSTEM_PROMPT, "[ERROR] 未找到第三轮示例" - print("[OK] 测试8通过: 完整示例已添加") - - print("=" * 60) - print("所有测试通过! 工作记忆链机制已成功添加到提示词中") - print("=" * 60) - - # 统计信息 - total_length = len(OPTIMIZED_SYSTEM_PROMPT) - working_memory_length = len("工作记忆链机制") - - print(f"\n提示词总长度: {total_length} 字符") - print(f"工作记忆链机制部分约占: {working_memory_length / total_length * 100:.2f}%") - - return True - -if __name__ == "__main__": - try: - test_working_memory_chain() - print("\n[SUCCESS] 测试成功!") - except AssertionError as e: - print(f"\n[ERROR] 测试失败: {e}") - sys.exit(1) - except Exception as e: - print(f"\n[ERROR] 发生错误: {e}") - sys.exit(1)