#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ RAG Manager - RAG 检索与管理模式 功能: 1. 检索 - 查询项目/系统/插件各层 RAG 2. 添加 - 将学习到的模式存入指定层 3. 删除 - 从指定层删除模式 4. 列表 - 列出各层的所有模式 5. 同步 - 将项目学习成果同步到系统共享层 用法: python rag_manager.py --project-root . list --layer system python rag_manager.py --project-root . search "打脸爽点" python rag_manager.py --project-root . add --pattern-id xxx --layer project python rag_manager.py --project-root . delete --pattern-id xxx --layer system python rag_manager.py --project-root . sync --from project --to system """ import argparse import asyncio import json import sys from dataclasses import asdict from datetime import datetime from pathlib import Path from typing import Optional try: from runtime_compat import enable_windows_utf8_stdio except ImportError: enable_windows_utf8_stdio = lambda: None # 添加 scripts 目录到路径 sys.path.insert(0, str(Path(__file__).parent.parent)) from data_modules.cross_project_rag import CrossProjectRAG, RAGLayer, LearnedPattern from data_modules.chapter_analyzer import ChapterAnalyzer, ChapterAnalysisResult class RAGManager: """ RAG 管理器 提供: - 检索:跨三层 RAG 检索 - 添加:存储模式到指定层 - 删除:删除指定模式 - 列表:列出各层模式 - 同步:将模式从项目层同步到系统层 """ def __init__(self, project_root: Path): self.project_root = project_root self.cross_rag = CrossProjectRAG(project_root) self.analyzer = ChapterAnalyzer(project_root) # ==================== 检索 ==================== async def search( self, query: str, top_k: int = 5, layers: Optional[list[str]] = None ) -> list: """检索 RAG""" if layers is None: layer_list = [RAGLayer.PROJECT, RAGLayer.SYSTEM, RAGLayer.PLUGIN] else: layer_map = { "project": RAGLayer.PROJECT, "system": RAGLayer.SYSTEM, "plugin": RAGLayer.PLUGIN } layer_list = [layer_map[l] for l in layers if l in layer_map] results = await self.cross_rag.search(query, top_k, layer_list) return [ { "chunk_id": r.chunk_id, "title": r.content.split('\n')[0][:50] if r.content else r.chunk_id, "content": r.content[:200] + "..." if len(r.content) > 200 else r.content, "score": r.score, "layer": r.source_layer.value, "project": r.source_project, "chapter": r.chapter, "type": r.chunk_type } for r in results ] # ==================== 添加 ==================== def add_pattern( self, pattern_id: str, pattern_type: str, title: str, description: str, catharsis_model: str, source_chapter: int, layer: str = "project", tension_curve: Optional[list] = None, structure: Optional[dict] = None, style_tags: Optional[list] = None ) -> bool: """添加模式""" pattern = LearnedPattern( pattern_id=pattern_id, pattern_type=pattern_type, title=title, description=description, tension_curve=tension_curve or [], catharsis_model=catharsis_model, structure=structure or {}, hot_spots=[], style_tags=style_tags or [], source_project=self.project_root.name, source_chapter=source_chapter, learned_at=datetime.now().isoformat() ) rag_layer = RAGLayer.SYSTEM if layer == "system" else RAGLayer.PROJECT return self.cross_rag.store_learned_pattern(pattern, rag_layer) def learn_and_add( self, chapter_file: Path, layer: str = "project" ) -> Optional[str]: """从章节学习并添加模式""" # 分析章节 result = self.analyzer.analyze_chapter(chapter_file) # 生成模式 pattern = self.analyzer.learn_pattern(result, layer) # 存储 rag_layer = RAGLayer.SYSTEM if layer == "system" else RAGLayer.PROJECT success = self.cross_rag.store_learned_pattern(pattern, rag_layer) if success: return pattern.pattern_id return None # ==================== 删除 ==================== def delete_pattern(self, pattern_id: str, layer: str) -> bool: """删除模式""" if layer == "project": return self._delete_from_project_db(pattern_id) elif layer == "system": return self._delete_from_system(pattern_id) return False def _delete_from_project_db(self, pattern_id: str) -> bool: """从项目数据库删除""" db_path = self.project_root / ".noma" / "rag" / "learned.db" if not db_path.exists(): return False try: import sqlite3 conn = sqlite3.connect(str(db_path)) cursor = conn.cursor() cursor.execute("DELETE FROM learned_patterns WHERE pattern_id = ?", (pattern_id,)) affected = cursor.rowcount conn.commit() conn.close() return affected > 0 except Exception: return False def _delete_from_system(self, pattern_id: str) -> bool: """从系统目录删除""" pattern_file = self.cross_rag.system_learned_dir / f"{pattern_id}.json" if pattern_file.exists(): pattern_file.unlink() return True return False # ==================== 列表 ==================== def list_patterns(self, layer: str) -> list: """列出指定层的模式""" if layer == "project": return self._list_project_patterns() elif layer == "system": return self._list_system_patterns() elif layer == "plugin": return self._list_plugin_patterns() return [] def _list_project_patterns(self) -> list: """列出项目层模式""" db_path = self.project_root / ".noma" / "rag" / "learned.db" if not db_path.exists(): return [] try: import sqlite3 conn = sqlite3.connect(str(db_path)) cursor = conn.cursor() cursor.execute(""" SELECT pattern_id, pattern_type, title, description, catharsis_model, source_chapter, learned_at, usage_count FROM learned_patterns ORDER BY learned_at DESC """) rows = cursor.fetchall() conn.close() return [ { "pattern_id": r[0], "type": r[1], "title": r[2], "description": r[3][:100] + "..." if r[3] and len(r[3]) > 100 else r[3], "catharsis_model": r[4], "source_chapter": r[5], "learned_at": r[6], "usage_count": r[7] } for r in rows ] except Exception: return [] def _list_system_patterns(self) -> list: """列出系统层模式""" if not self.cross_rag.system_learned_dir.exists(): return [] patterns = [] for f in self.cross_rag.system_learned_dir.glob("*.json"): try: data = json.loads(f.read_text(encoding="utf-8")) patterns.append({ "pattern_id": data.get("pattern_id", f.stem), "type": data.get("pattern_type", "unknown"), "title": data.get("title", f.stem), "description": data.get("description", "")[:100], "catharsis_model": data.get("catharsis_model", "unknown"), "source_project": data.get("source_project", "unknown"), "source_chapter": data.get("source_chapter", 0), "learned_at": data.get("learned_at", "") }) except Exception: continue return sorted(patterns, key=lambda x: x.get("learned_at", ""), reverse=True) def _list_plugin_patterns(self) -> list: """列出插件层模式""" patterns = [] plugin_dir = self.cross_rag.plugin_matrices_dir # catharsis models catharsis_dir = plugin_dir / "catharsis_models" if catharsis_dir.exists(): for f in catharsis_dir.glob("*.md"): patterns.append({ "pattern_id": f"plugin:{f.stem}", "type": "catharsis_model", "title": f.stem, "description": "内置爽感模型", "catharsis_model": f.stem, "source": "noma_plugin" }) return patterns # ==================== 同步 ==================== def sync_to_system(self, pattern_id: str) -> bool: """将项目模式同步到系统层""" # 从项目数据库读取 db_path = self.project_root / ".noma" / "rag" / "learned.db" if not db_path.exists(): return False try: import sqlite3 conn = sqlite3.connect(str(db_path)) cursor = conn.cursor() cursor.execute(""" SELECT pattern_id, pattern_type, title, description, tension_curve, catharsis_model, structure, hot_spots, style_tags, source_project, source_chapter, learned_at, usage_count FROM learned_patterns WHERE pattern_id = ? """, (pattern_id,)) row = cursor.fetchone() conn.close() if not row: return False pattern = LearnedPattern( pattern_id=row[0], pattern_type=row[1], title=row[2], description=row[3] or "", tension_curve=json.loads(row[4]) if row[4] else [], catharsis_model=row[5] or "unknown", structure=json.loads(row[6]) if row[6] else {}, hot_spots=json.loads(row[7]) if row[7] else [], style_tags=json.loads(row[8]) if row[8] else [], source_project=row[9] or self.project_root.name, source_chapter=row[10] or 0, learned_at=row[11] or datetime.now().isoformat(), usage_count=row[12] or 0 ) return self.cross_rag.store_learned_pattern(pattern, RAGLayer.SYSTEM) except Exception: return False def sync_all_to_system(self) -> dict: """同步所有项目模式到系统层""" project_patterns = self._list_project_patterns() synced = 0 failed = 0 for p in project_patterns: if self.sync_to_system(p["pattern_id"]): synced += 1 else: failed += 1 return {"synced": synced, "failed": failed, "total": len(project_patterns)} # ==================== CLI ==================== def main(): if sys.platform == "win32": enable_windows_utf8_stdio() parser = argparse.ArgumentParser(description="RAG Manager - RAG 检索与管理") parser.add_argument("--project-root", type=str, default=".", help="项目根目录") subparsers = parser.add_subparsers(dest="command") # 搜索 search_parser = subparsers.add_parser("search", help="检索 RAG") search_parser.add_argument("query", help="检索 query") search_parser.add_argument("--top-k", type=int, default=5) search_parser.add_argument("--layers", type=str, default="project,system", help="检索层级,逗号分隔") # 列表 list_parser = subparsers.add_parser("list", help="列出模式") list_parser.add_argument("--layer", choices=["project", "system", "plugin"], default="project", help="RAG 层") # 添加 add_parser = subparsers.add_parser("add", help="添加模式") add_parser.add_argument("--pattern-id", required=True) add_parser.add_argument("--pattern-type", required=True) add_parser.add_argument("--title", required=True) add_parser.add_argument("--description", required=True) add_parser.add_argument("--catharsis-model", required=True) add_parser.add_argument("--source-chapter", type=int, required=True) add_parser.add_argument("--layer", choices=["project", "system"], default="project") add_parser.add_argument("--learn", help="从章节文件学习") add_parser.add_argument("--chapter-file", help="章节文件路径") # 删除 delete_parser = subparsers.add_parser("delete", help="删除模式") delete_parser.add_argument("--pattern-id", required=True) delete_parser.add_argument("--layer", choices=["project", "system"], required=True) # 同步 sync_parser = subparsers.add_parser("sync", help="同步到系统层") sync_parser.add_argument("--pattern-id", help="同步单个模式(可选)") sync_parser.add_argument("--all", action="store_true", help="同步所有") args = parser.parse_args() if not args.project_root: print("Error: --project-root is required") sys.exit(1) project_root = Path(args.project_root).resolve() manager = RAGManager(project_root) if args.command == "search": layers = [l.strip() for l in args.layers.split(",")] results = asyncio.run(manager.search(args.query, args.top_k, layers)) print(f"\n=== Search Results ({len(results)}) ===") for r in results: print(f"\n[{r['layer']}] {r['title']}") print(f" Type: {r['type']}, Chapter: {r['chapter']}") print(f" Score: {r['score']:.2f}") print(f" Content: {r['content']}") elif args.command == "list": patterns = manager.list_patterns(args.layer) print(f"\n=== {args.layer.upper()} Patterns ({len(patterns)}) ===") for p in patterns: print(f"\n[{p['pattern_id']}] {p['title']}") print(f" Type: {p['type']}, Catharsis: {p.get('catharsis_model', 'N/A')}") print(f" Source: {p.get('source_project', 'N/A')} Ch.{p.get('source_chapter', 0)}") if p.get("description"): print(f" Desc: {p['description'][:100]}") elif args.command == "add": if hasattr(args, 'learn') and args.learn: # 从章节学习 chapter_file = project_root / args.chapter_file pattern_id = manager.learn_and_add(chapter_file, args.layer) if pattern_id: print(f"✓ Pattern learned and added: {pattern_id}") else: print("✗ Failed to learn pattern") sys.exit(1) else: success = manager.add_pattern( args.pattern_id, args.pattern_type, args.title, args.description, args.catharsis_model, args.source_chapter, args.layer ) if success: print(f"✓ Pattern added to {args.layer}") else: print("✗ Failed to add pattern") sys.exit(1) elif args.command == "delete": success = manager.delete_pattern(args.pattern_id, args.layer) if success: print(f"✓ Pattern deleted from {args.layer}") else: print("✗ Failed to delete pattern") sys.exit(1) elif args.command == "sync": if args.all: result = manager.sync_all_to_system() print(f"✓ Synced {result['synced']}/{result['total']} patterns") if result['failed'] > 0: print(f" Failed: {result['failed']}") elif args.pattern_id: success = manager.sync_to_system(args.pattern_id) if success: print(f"✓ Pattern synced to system") else: print("✗ Failed to sync pattern") sys.exit(1) else: print("Specify --pattern-id or --all") sys.exit(1) else: parser.print_help() if __name__ == "__main__": main()