Merge branch 'dev' of https://github.com/MoFox-Studio/MoFox_Bot into dev
This commit is contained in:
@@ -17,19 +17,19 @@
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List, Tuple
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import orjson
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
logging.FileHandler('embedding_cleanup.log', encoding='utf-8')
|
||||
logging.FileHandler("embedding_cleanup.log", encoding="utf-8")
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -49,13 +49,13 @@ class EmbeddingCleaner:
|
||||
self.cleaned_files = []
|
||||
self.errors = []
|
||||
self.stats = {
|
||||
'files_processed': 0,
|
||||
'embedings_removed': 0,
|
||||
'bytes_saved': 0,
|
||||
'nodes_processed': 0
|
||||
"files_processed": 0,
|
||||
"embedings_removed": 0,
|
||||
"bytes_saved": 0,
|
||||
"nodes_processed": 0
|
||||
}
|
||||
|
||||
def find_json_files(self) -> List[Path]:
|
||||
def find_json_files(self) -> list[Path]:
|
||||
"""查找可能包含向量数据的 JSON 文件"""
|
||||
json_files = []
|
||||
|
||||
@@ -65,7 +65,7 @@ class EmbeddingCleaner:
|
||||
json_files.append(memory_graph_file)
|
||||
|
||||
# 测试数据文件
|
||||
test_dir = self.data_dir / "test_*"
|
||||
self.data_dir / "test_*"
|
||||
for test_path in self.data_dir.glob("test_*/memory_graph.json"):
|
||||
if test_path.exists():
|
||||
json_files.append(test_path)
|
||||
@@ -82,7 +82,7 @@ class EmbeddingCleaner:
|
||||
logger.info(f"找到 {len(json_files)} 个需要处理的 JSON 文件")
|
||||
return json_files
|
||||
|
||||
def analyze_embedding_in_data(self, data: Dict[str, Any]) -> int:
|
||||
def analyze_embedding_in_data(self, data: dict[str, Any]) -> int:
|
||||
"""
|
||||
分析数据中的 embedding 字段数量
|
||||
|
||||
@@ -97,7 +97,7 @@ class EmbeddingCleaner:
|
||||
def count_embeddings(obj):
|
||||
nonlocal embedding_count
|
||||
if isinstance(obj, dict):
|
||||
if 'embedding' in obj:
|
||||
if "embedding" in obj:
|
||||
embedding_count += 1
|
||||
for value in obj.values():
|
||||
count_embeddings(value)
|
||||
@@ -108,7 +108,7 @@ class EmbeddingCleaner:
|
||||
count_embeddings(data)
|
||||
return embedding_count
|
||||
|
||||
def clean_embedding_from_data(self, data: Dict[str, Any]) -> Tuple[Dict[str, Any], int]:
|
||||
def clean_embedding_from_data(self, data: dict[str, Any]) -> tuple[dict[str, Any], int]:
|
||||
"""
|
||||
从数据中移除 embedding 字段
|
||||
|
||||
@@ -123,8 +123,8 @@ class EmbeddingCleaner:
|
||||
def remove_embeddings(obj):
|
||||
nonlocal removed_count
|
||||
if isinstance(obj, dict):
|
||||
if 'embedding' in obj:
|
||||
del obj['embedding']
|
||||
if "embedding" in obj:
|
||||
del obj["embedding"]
|
||||
removed_count += 1
|
||||
for value in obj.values():
|
||||
remove_embeddings(value)
|
||||
@@ -162,14 +162,14 @@ class EmbeddingCleaner:
|
||||
data = orjson.loads(original_content)
|
||||
except orjson.JSONDecodeError:
|
||||
# 回退到标准 json
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# 分析 embedding 数据
|
||||
embedding_count = self.analyze_embedding_in_data(data)
|
||||
|
||||
if embedding_count == 0:
|
||||
logger.info(f" ✓ 文件中没有 embedding 数据,跳过")
|
||||
logger.info(" ✓ 文件中没有 embedding 数据,跳过")
|
||||
return True
|
||||
|
||||
logger.info(f" 发现 {embedding_count} 个 embedding 字段")
|
||||
@@ -193,30 +193,30 @@ class EmbeddingCleaner:
|
||||
cleaned_data,
|
||||
indent=2,
|
||||
ensure_ascii=False
|
||||
).encode('utf-8')
|
||||
).encode("utf-8")
|
||||
|
||||
cleaned_size = len(cleaned_content)
|
||||
bytes_saved = original_size - cleaned_size
|
||||
|
||||
# 原子写入
|
||||
temp_file = file_path.with_suffix('.tmp')
|
||||
temp_file = file_path.with_suffix(".tmp")
|
||||
temp_file.write_bytes(cleaned_content)
|
||||
temp_file.replace(file_path)
|
||||
|
||||
logger.info(f" ✓ 清理完成:")
|
||||
logger.info(" ✓ 清理完成:")
|
||||
logger.info(f" - 移除 embedding 字段: {removed_count}")
|
||||
logger.info(f" - 节省空间: {bytes_saved:,} 字节 ({bytes_saved/original_size*100:.1f}%)")
|
||||
logger.info(f" - 新文件大小: {cleaned_size:,} 字节")
|
||||
|
||||
# 更新统计
|
||||
self.stats['embedings_removed'] += removed_count
|
||||
self.stats['bytes_saved'] += bytes_saved
|
||||
self.stats["embedings_removed"] += removed_count
|
||||
self.stats["bytes_saved"] += bytes_saved
|
||||
|
||||
else:
|
||||
logger.info(f" [试运行] 将移除 {embedding_count} 个 embedding 字段")
|
||||
self.stats['embedings_removed'] += embedding_count
|
||||
self.stats["embedings_removed"] += embedding_count
|
||||
|
||||
self.stats['files_processed'] += 1
|
||||
self.stats["files_processed"] += 1
|
||||
self.cleaned_files.append(file_path)
|
||||
return True
|
||||
|
||||
@@ -236,12 +236,12 @@ class EmbeddingCleaner:
|
||||
节点数量
|
||||
"""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
node_count = 0
|
||||
if 'nodes' in data and isinstance(data['nodes'], list):
|
||||
node_count = len(data['nodes'])
|
||||
if "nodes" in data and isinstance(data["nodes"], list):
|
||||
node_count = len(data["nodes"])
|
||||
|
||||
return node_count
|
||||
|
||||
@@ -268,7 +268,7 @@ class EmbeddingCleaner:
|
||||
|
||||
# 统计总节点数
|
||||
total_nodes = sum(self.analyze_nodes_in_file(f) for f in json_files)
|
||||
self.stats['nodes_processed'] = total_nodes
|
||||
self.stats["nodes_processed"] = total_nodes
|
||||
|
||||
logger.info(f"总计 {len(json_files)} 个文件,{total_nodes} 个节点")
|
||||
|
||||
@@ -295,8 +295,8 @@ class EmbeddingCleaner:
|
||||
|
||||
if not dry_run:
|
||||
logger.info(f"节省空间: {self.stats['bytes_saved']:,} 字节")
|
||||
if self.stats['bytes_saved'] > 0:
|
||||
mb_saved = self.stats['bytes_saved'] / 1024 / 1024
|
||||
if self.stats["bytes_saved"] > 0:
|
||||
mb_saved = self.stats["bytes_saved"] / 1024 / 1024
|
||||
logger.info(f"节省空间: {mb_saved:.2f} MB")
|
||||
|
||||
if self.errors:
|
||||
@@ -342,7 +342,7 @@ def main():
|
||||
print(" 请确保向量数据库正在正常工作。")
|
||||
print()
|
||||
response = input("确认继续?(yes/no): ")
|
||||
if response.lower() not in ['yes', 'y', '是']:
|
||||
if response.lower() not in ["yes", "y", "是"]:
|
||||
print("操作已取消")
|
||||
return
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ lunar_python
|
||||
fuzzywuzzy
|
||||
python-multipart
|
||||
aiofiles
|
||||
jinja2
|
||||
inkfox
|
||||
soundfile
|
||||
pedalboard
|
||||
|
||||
@@ -22,7 +22,6 @@ import argparse
|
||||
import asyncio
|
||||
import gc
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
@@ -30,7 +29,6 @@ import time
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import psutil
|
||||
|
||||
@@ -101,10 +99,10 @@ async def monitor_bot_process(bot_process: subprocess.Popen, interval: int = 5):
|
||||
if children:
|
||||
print(f" 子进程: {info['children_count']} 个")
|
||||
print(f" 子进程内存: {info['children_mem_mb']:.2f} MB")
|
||||
total_mem = info['rss_mb'] + info['children_mem_mb']
|
||||
total_mem = info["rss_mb"] + info["children_mem_mb"]
|
||||
print(f" 总内存: {total_mem:.2f} MB")
|
||||
|
||||
print(f"\n 📋 子进程详情:")
|
||||
print("\n 📋 子进程详情:")
|
||||
for idx, child in enumerate(children, 1):
|
||||
try:
|
||||
child_mem = child.memory_info().rss / 1024 / 1024
|
||||
@@ -119,13 +117,13 @@ async def monitor_bot_process(bot_process: subprocess.Popen, interval: int = 5):
|
||||
|
||||
if len(history) > 1:
|
||||
prev = history[-2]
|
||||
rss_diff = info['rss_mb'] - prev['rss_mb']
|
||||
print(f"\n变化:")
|
||||
rss_diff = info["rss_mb"] - prev["rss_mb"]
|
||||
print("\n变化:")
|
||||
print(f" RSS: {rss_diff:+.2f} MB")
|
||||
if rss_diff > 10:
|
||||
print(f" ⚠️ 内存增长较快!")
|
||||
if info['rss_mb'] > 1000:
|
||||
print(f" ⚠️ 内存使用超过 1GB!")
|
||||
print(" ⚠️ 内存增长较快!")
|
||||
if info["rss_mb"] > 1000:
|
||||
print(" ⚠️ 内存使用超过 1GB!")
|
||||
|
||||
print(f"{'=' * 80}\n")
|
||||
await asyncio.sleep(interval)
|
||||
@@ -163,7 +161,7 @@ def save_process_history(history: list, pid: int):
|
||||
f.write(f"RSS: {info['rss_mb']:.2f} MB\n")
|
||||
f.write(f"VMS: {info['vms_mb']:.2f} MB\n")
|
||||
f.write(f"占比: {info['percent']:.2f}%\n")
|
||||
if info['children_count'] > 0:
|
||||
if info["children_count"] > 0:
|
||||
f.write(f"子进程: {info['children_count']} 个\n")
|
||||
f.write(f"子进程内存: {info['children_mem_mb']:.2f} MB\n")
|
||||
f.write("\n")
|
||||
@@ -264,7 +262,7 @@ async def run_monitor_mode(interval: int):
|
||||
class ObjectMemoryProfiler:
|
||||
"""对象级内存分析器"""
|
||||
|
||||
def __init__(self, interval: int = 10, output_file: Optional[str] = None, object_limit: int = 20):
|
||||
def __init__(self, interval: int = 10, output_file: str | None = None, object_limit: int = 20):
|
||||
self.interval = interval
|
||||
self.output_file = output_file
|
||||
self.object_limit = object_limit
|
||||
@@ -274,7 +272,7 @@ class ObjectMemoryProfiler:
|
||||
self.tracker = tracker.SummaryTracker()
|
||||
self.iteration = 0
|
||||
|
||||
def get_object_stats(self) -> Dict:
|
||||
def get_object_stats(self) -> dict:
|
||||
"""获取当前进程的对象统计(所有线程)"""
|
||||
if not PYMPLER_AVAILABLE:
|
||||
return {}
|
||||
@@ -317,7 +315,7 @@ class ObjectMemoryProfiler:
|
||||
print(f"❌ 获取对象统计失败: {e}")
|
||||
return {}
|
||||
|
||||
def _get_module_stats(self, all_objects: list) -> Dict:
|
||||
def _get_module_stats(self, all_objects: list) -> dict:
|
||||
"""统计各模块的内存占用"""
|
||||
module_mem = defaultdict(lambda: {"count": 0, "size": 0})
|
||||
|
||||
@@ -329,7 +327,7 @@ class ObjectMemoryProfiler:
|
||||
|
||||
if module_name:
|
||||
# 获取顶级模块名(例如 src.chat.xxx -> src)
|
||||
top_module = module_name.split('.')[0]
|
||||
top_module = module_name.split(".")[0]
|
||||
|
||||
obj_size = sys.getsizeof(obj)
|
||||
module_mem[top_module]["count"] += 1
|
||||
@@ -351,7 +349,7 @@ class ObjectMemoryProfiler:
|
||||
"total_modules": len(module_mem)
|
||||
}
|
||||
|
||||
def print_stats(self, stats: Dict, iteration: int):
|
||||
def print_stats(self, stats: dict, iteration: int):
|
||||
"""打印统计信息"""
|
||||
print("\n" + "=" * 80)
|
||||
print(f"🔍 对象级内存分析 #{iteration} - {time.strftime('%H:%M:%S')}")
|
||||
@@ -374,8 +372,8 @@ class ObjectMemoryProfiler:
|
||||
|
||||
print(f"{obj_type:<50} {obj_count:>12,} {size_str:>15}")
|
||||
|
||||
if "module_stats" in stats and stats["module_stats"]:
|
||||
print(f"\n📚 模块内存占用 (前 20 个模块):\n")
|
||||
if stats.get("module_stats"):
|
||||
print("\n📚 模块内存占用 (前 20 个模块):\n")
|
||||
print(f"{'模块名':<40} {'对象数':>12} {'总内存':>15}")
|
||||
print("-" * 80)
|
||||
|
||||
@@ -402,7 +400,7 @@ class ObjectMemoryProfiler:
|
||||
|
||||
if "gc_stats" in stats:
|
||||
gc_stats = stats["gc_stats"]
|
||||
print(f"\n🗑️ 垃圾回收:")
|
||||
print("\n🗑️ 垃圾回收:")
|
||||
print(f" 代 0: {gc_stats['collections'][0]:,} 次")
|
||||
print(f" 代 1: {gc_stats['collections'][1]:,} 次")
|
||||
print(f" 代 2: {gc_stats['collections'][2]:,} 次")
|
||||
@@ -423,7 +421,7 @@ class ObjectMemoryProfiler:
|
||||
self.tracker.print_diff()
|
||||
print("-" * 80)
|
||||
|
||||
def save_to_file(self, stats: Dict):
|
||||
def save_to_file(self, stats: dict):
|
||||
"""保存统计信息到文件"""
|
||||
if not self.output_file:
|
||||
return
|
||||
@@ -441,7 +439,7 @@ class ObjectMemoryProfiler:
|
||||
for obj_type, obj_count, obj_size in stats["summary"]:
|
||||
f.write(f" {obj_type}: {obj_count:,} 个, {obj_size:,} 字节\n")
|
||||
|
||||
if "module_stats" in stats and stats["module_stats"]:
|
||||
if stats.get("module_stats"):
|
||||
f.write("\n模块统计 (前 20 个):\n")
|
||||
for module_name, obj_count, obj_size in stats["module_stats"]["top_modules"]:
|
||||
f.write(f" {module_name}: {obj_count:,} 个对象, {obj_size:,} 字节\n")
|
||||
@@ -452,7 +450,7 @@ class ObjectMemoryProfiler:
|
||||
# 保存 JSONL
|
||||
jsonl_path = str(self.output_file) + ".jsonl"
|
||||
record = {
|
||||
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"iteration": self.iteration,
|
||||
"total_objects": stats.get("total_objects", 0),
|
||||
"threads": stats.get("threads", []),
|
||||
@@ -479,7 +477,7 @@ class ObjectMemoryProfiler:
|
||||
self.running = True
|
||||
|
||||
def monitor_loop():
|
||||
print(f"🚀 对象分析器已启动")
|
||||
print("🚀 对象分析器已启动")
|
||||
print(f" 监控间隔: {self.interval} 秒")
|
||||
print(f" 对象类型限制: {self.object_limit}")
|
||||
print(f" 输出文件: {self.output_file or '无'}")
|
||||
@@ -506,14 +504,14 @@ class ObjectMemoryProfiler:
|
||||
|
||||
monitor_thread = threading.Thread(target=monitor_loop, daemon=True)
|
||||
monitor_thread.start()
|
||||
print(f"✓ 监控线程已启动\n")
|
||||
print("✓ 监控线程已启动\n")
|
||||
|
||||
def stop(self):
|
||||
"""停止监控"""
|
||||
self.running = False
|
||||
|
||||
|
||||
def run_objects_mode(interval: int, output: Optional[str], object_limit: int):
|
||||
def run_objects_mode(interval: int, output: str | None, object_limit: int):
|
||||
"""对象分析模式主函数"""
|
||||
if not PYMPLER_AVAILABLE:
|
||||
print("❌ pympler 未安装,无法使用对象分析模式")
|
||||
@@ -549,9 +547,9 @@ def run_objects_mode(interval: int, output: Optional[str], object_limit: int):
|
||||
try:
|
||||
import bot
|
||||
|
||||
if hasattr(bot, 'main_async'):
|
||||
if hasattr(bot, "main_async"):
|
||||
asyncio.run(bot.main_async())
|
||||
elif hasattr(bot, 'main'):
|
||||
elif hasattr(bot, "main"):
|
||||
bot.main()
|
||||
else:
|
||||
print("⚠️ bot.py 未找到 main_async() 或 main() 函数")
|
||||
@@ -577,10 +575,10 @@ def run_objects_mode(interval: int, output: Optional[str], object_limit: int):
|
||||
# 可视化模式
|
||||
# ============================================================================
|
||||
|
||||
def load_jsonl(path: Path) -> List[Dict]:
|
||||
def load_jsonl(path: Path) -> list[dict]:
|
||||
"""加载 JSONL 文件"""
|
||||
snapshots = []
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
@@ -592,7 +590,7 @@ def load_jsonl(path: Path) -> List[Dict]:
|
||||
return snapshots
|
||||
|
||||
|
||||
def aggregate_top_types(snapshots: List[Dict], top_n: int = 10):
|
||||
def aggregate_top_types(snapshots: list[dict], top_n: int = 10):
|
||||
"""聚合前 N 个对象类型的时间序列"""
|
||||
type_max = defaultdict(int)
|
||||
for snap in snapshots:
|
||||
@@ -622,7 +620,7 @@ def aggregate_top_types(snapshots: List[Dict], top_n: int = 10):
|
||||
return times, series
|
||||
|
||||
|
||||
def plot_series(times: List, series: Dict, output: Path, top_n: int):
|
||||
def plot_series(times: list, series: dict, output: Path, top_n: int):
|
||||
"""绘制时间序列图"""
|
||||
plt.figure(figsize=(14, 8))
|
||||
|
||||
|
||||
361
src/api/memory_visualizer_router.py
Normal file
361
src/api/memory_visualizer_router.py
Normal file
@@ -0,0 +1,361 @@
|
||||
"""
|
||||
记忆图可视化 - API 路由模块
|
||||
|
||||
提供 Web API 用于可视化记忆图数据
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import orjson
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
# 调整项目根目录的计算方式
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
data_dir = project_root / "data" / "memory_graph"
|
||||
|
||||
# 缓存
|
||||
graph_data_cache = None
|
||||
current_data_file = None
|
||||
|
||||
# FastAPI 路由
|
||||
router = APIRouter()
|
||||
|
||||
# Jinja2 模板引擎
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates"))
|
||||
|
||||
|
||||
def find_available_data_files() -> List[Path]:
|
||||
"""查找所有可用的记忆图数据文件"""
|
||||
files = []
|
||||
if not data_dir.exists():
|
||||
return files
|
||||
|
||||
possible_files = ["graph_store.json", "memory_graph.json", "graph_data.json"]
|
||||
for filename in possible_files:
|
||||
file_path = data_dir / filename
|
||||
if file_path.exists():
|
||||
files.append(file_path)
|
||||
|
||||
for pattern in ["graph_store_*.json", "memory_graph_*.json", "graph_data_*.json"]:
|
||||
for backup_file in data_dir.glob(pattern):
|
||||
if backup_file not in files:
|
||||
files.append(backup_file)
|
||||
|
||||
backups_dir = data_dir / "backups"
|
||||
if backups_dir.exists():
|
||||
for backup_file in backups_dir.glob("**/*.json"):
|
||||
if backup_file not in files:
|
||||
files.append(backup_file)
|
||||
|
||||
backup_dir = data_dir.parent / "backup"
|
||||
if backup_dir.exists():
|
||||
for pattern in ["**/graph_*.json", "**/memory_*.json"]:
|
||||
for backup_file in backup_dir.glob(pattern):
|
||||
if backup_file not in files:
|
||||
files.append(backup_file)
|
||||
|
||||
return sorted(files, key=lambda f: f.stat().st_mtime, reverse=True)
|
||||
|
||||
|
||||
def load_graph_data_from_file(file_path: Optional[Path] = None) -> Dict[str, Any]:
|
||||
"""从磁盘加载图数据"""
|
||||
global graph_data_cache, current_data_file
|
||||
|
||||
if file_path and file_path != current_data_file:
|
||||
graph_data_cache = None
|
||||
current_data_file = file_path
|
||||
|
||||
if graph_data_cache:
|
||||
return graph_data_cache
|
||||
|
||||
try:
|
||||
graph_file = current_data_file
|
||||
if not graph_file:
|
||||
available_files = find_available_data_files()
|
||||
if not available_files:
|
||||
return {"error": "未找到数据文件", "nodes": [], "edges": [], "stats": {}}
|
||||
graph_file = available_files[0]
|
||||
current_data_file = graph_file
|
||||
|
||||
if not graph_file.exists():
|
||||
return {"error": f"文件不存在: {graph_file}", "nodes": [], "edges": [], "stats": {}}
|
||||
|
||||
with open(graph_file, "r", encoding="utf-8") as f:
|
||||
data = orjson.loads(f.read())
|
||||
|
||||
nodes = data.get("nodes", [])
|
||||
edges = data.get("edges", [])
|
||||
metadata = data.get("metadata", {})
|
||||
|
||||
nodes_dict = {
|
||||
node["id"]: {
|
||||
**node,
|
||||
"label": node.get("content", ""),
|
||||
"group": node.get("node_type", ""),
|
||||
"title": f"{node.get('node_type', '')}: {node.get('content', '')}",
|
||||
}
|
||||
for node in nodes
|
||||
if node.get("id")
|
||||
}
|
||||
|
||||
edges_list = [
|
||||
{
|
||||
**edge,
|
||||
"from": edge.get("source", edge.get("source_id")),
|
||||
"to": edge.get("target", edge.get("target_id")),
|
||||
"label": edge.get("relation", ""),
|
||||
"arrows": "to",
|
||||
}
|
||||
for edge in edges
|
||||
]
|
||||
|
||||
stats = metadata.get("statistics", {})
|
||||
total_memories = stats.get("total_memories", 0)
|
||||
|
||||
graph_data_cache = {
|
||||
"nodes": list(nodes_dict.values()),
|
||||
"edges": edges_list,
|
||||
"memories": [],
|
||||
"stats": {
|
||||
"total_nodes": len(nodes_dict),
|
||||
"total_edges": len(edges_list),
|
||||
"total_memories": total_memories,
|
||||
},
|
||||
"current_file": str(graph_file),
|
||||
"file_size": graph_file.stat().st_size,
|
||||
"file_modified": datetime.fromtimestamp(graph_file.stat().st_mtime).isoformat(),
|
||||
}
|
||||
return graph_data_cache
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
raise HTTPException(status_code=500, detail=f"加载图数据失败: {e}")
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request):
|
||||
"""主页面"""
|
||||
return templates.TemplateResponse("visualizer.html", {"request": request})
|
||||
|
||||
|
||||
def _format_graph_data_from_manager(memory_manager) -> Dict[str, Any]:
|
||||
"""从 MemoryManager 提取并格式化图数据"""
|
||||
if not memory_manager.graph_store:
|
||||
return {"nodes": [], "edges": [], "memories": [], "stats": {}}
|
||||
|
||||
all_memories = memory_manager.graph_store.get_all_memories()
|
||||
nodes_dict = {}
|
||||
edges_list = []
|
||||
memory_info = []
|
||||
|
||||
for memory in all_memories:
|
||||
memory_info.append(
|
||||
{
|
||||
"id": memory.id,
|
||||
"type": memory.memory_type.value,
|
||||
"importance": memory.importance,
|
||||
"text": memory.to_text(),
|
||||
}
|
||||
)
|
||||
for node in memory.nodes:
|
||||
if node.id not in nodes_dict:
|
||||
nodes_dict[node.id] = {
|
||||
"id": node.id,
|
||||
"label": node.content,
|
||||
"type": node.node_type.value,
|
||||
"group": node.node_type.name,
|
||||
"title": f"{node.node_type.value}: {node.content}",
|
||||
}
|
||||
for edge in memory.edges:
|
||||
edges_list.append( # noqa: PERF401
|
||||
{
|
||||
"id": edge.id,
|
||||
"from": edge.source_id,
|
||||
"to": edge.target_id,
|
||||
"label": edge.relation,
|
||||
"arrows": "to",
|
||||
"memory_id": memory.id,
|
||||
}
|
||||
)
|
||||
|
||||
stats = memory_manager.get_statistics()
|
||||
return {
|
||||
"nodes": list(nodes_dict.values()),
|
||||
"edges": edges_list,
|
||||
"memories": memory_info,
|
||||
"stats": {
|
||||
"total_nodes": stats.get("total_nodes", 0),
|
||||
"total_edges": stats.get("total_edges", 0),
|
||||
"total_memories": stats.get("total_memories", 0),
|
||||
},
|
||||
"current_file": "memory_manager (实时数据)",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/api/graph/full")
|
||||
async def get_full_graph():
|
||||
"""获取完整记忆图数据"""
|
||||
try:
|
||||
from src.memory_graph.manager_singleton import get_memory_manager
|
||||
|
||||
memory_manager = get_memory_manager()
|
||||
|
||||
data = {}
|
||||
if memory_manager and memory_manager._initialized:
|
||||
data = _format_graph_data_from_manager(memory_manager)
|
||||
else:
|
||||
# 如果内存管理器不可用,则从文件加载
|
||||
data = load_graph_data_from_file()
|
||||
|
||||
return JSONResponse(content={"success": True, "data": data})
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return JSONResponse(content={"success": False, "error": str(e)}, status_code=500)
|
||||
|
||||
|
||||
@router.get("/api/files")
|
||||
async def list_files_api():
|
||||
"""列出所有可用的数据文件"""
|
||||
try:
|
||||
files = find_available_data_files()
|
||||
file_list = []
|
||||
for f in files:
|
||||
stat = f.stat()
|
||||
file_list.append(
|
||||
{
|
||||
"path": str(f),
|
||||
"name": f.name,
|
||||
"size": stat.st_size,
|
||||
"size_kb": round(stat.st_size / 1024, 2),
|
||||
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
|
||||
"modified_readable": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"is_current": str(f) == str(current_data_file) if current_data_file else False,
|
||||
}
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": True,
|
||||
"files": file_list,
|
||||
"count": len(file_list),
|
||||
"current_file": str(current_data_file) if current_data_file else None,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
# 增加日志记录
|
||||
# logger.error(f"列出数据文件失败: {e}", exc_info=True)
|
||||
return JSONResponse(content={"success": False, "error": str(e)}, status_code=500)
|
||||
|
||||
|
||||
@router.post("/select_file")
|
||||
async def select_file(request: Request):
|
||||
"""选择要加载的数据文件"""
|
||||
global graph_data_cache, current_data_file
|
||||
try:
|
||||
data = await request.json()
|
||||
file_path = data.get("file_path")
|
||||
if not file_path:
|
||||
raise HTTPException(status_code=400, detail="未提供文件路径")
|
||||
|
||||
file_to_load = Path(file_path)
|
||||
if not file_to_load.exists():
|
||||
raise HTTPException(status_code=404, detail=f"文件不存在: {file_path}")
|
||||
|
||||
graph_data_cache = None
|
||||
current_data_file = file_to_load
|
||||
graph_data = load_graph_data_from_file(file_to_load)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": True,
|
||||
"message": f"已切换到文件: {file_to_load.name}",
|
||||
"stats": graph_data.get("stats", {}),
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
return JSONResponse(content={"success": False, "error": str(e)}, status_code=500)
|
||||
|
||||
|
||||
@router.get("/reload")
|
||||
async def reload_data():
|
||||
"""重新加载数据"""
|
||||
global graph_data_cache
|
||||
graph_data_cache = None
|
||||
data = load_graph_data_from_file()
|
||||
return JSONResponse(content={"success": True, "message": "数据已重新加载", "stats": data.get("stats", {})})
|
||||
|
||||
|
||||
@router.get("/api/search")
|
||||
async def search_memories(q: str, limit: int = 50):
|
||||
"""搜索记忆"""
|
||||
try:
|
||||
from src.memory_graph.manager_singleton import get_memory_manager
|
||||
|
||||
memory_manager = get_memory_manager()
|
||||
|
||||
results = []
|
||||
if memory_manager and memory_manager._initialized and memory_manager.graph_store:
|
||||
# 从 memory_manager 搜索
|
||||
all_memories = memory_manager.graph_store.get_all_memories()
|
||||
for memory in all_memories:
|
||||
if q.lower() in memory.to_text().lower():
|
||||
results.append(
|
||||
{
|
||||
"id": memory.id,
|
||||
"type": memory.memory_type.value,
|
||||
"importance": memory.importance,
|
||||
"text": memory.to_text(),
|
||||
}
|
||||
)
|
||||
else:
|
||||
# 从文件加载的数据中搜索 (降级方案)
|
||||
data = load_graph_data_from_file()
|
||||
for memory in data.get("memories", []):
|
||||
if q.lower() in memory.get("text", "").lower():
|
||||
results.append(memory)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": True,
|
||||
"data": {
|
||||
"results": results[:limit],
|
||||
"count": len(results),
|
||||
},
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
return JSONResponse(content={"success": False, "error": str(e)}, status_code=500)
|
||||
|
||||
|
||||
@router.get("/api/stats")
|
||||
async def get_statistics():
|
||||
"""获取统计信息"""
|
||||
try:
|
||||
data = load_graph_data_from_file()
|
||||
|
||||
node_types = {}
|
||||
memory_types = {}
|
||||
|
||||
for node in data["nodes"]:
|
||||
node_type = node.get("type", "Unknown")
|
||||
node_types[node_type] = node_types.get(node_type, 0) + 1
|
||||
|
||||
for memory in data.get("memories", []):
|
||||
mem_type = memory.get("type", "Unknown")
|
||||
memory_types[mem_type] = memory_types.get(mem_type, 0) + 1
|
||||
|
||||
stats = data.get("stats", {})
|
||||
stats["node_types"] = node_types
|
||||
stats["memory_types"] = memory_types
|
||||
|
||||
return JSONResponse(content={"success": True, "data": stats})
|
||||
except Exception as e:
|
||||
return JSONResponse(content={"success": False, "error": str(e)}, status_code=500)
|
||||
@@ -4,10 +4,10 @@ from typing import Any, Literal
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
|
||||
from src.common.database.compatibility import db_get
|
||||
from src.common.database.core.models import LLMUsage
|
||||
from src.chat.utils.statistic import (
|
||||
StatisticOutputTask,
|
||||
)
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import model_config
|
||||
|
||||
logger = get_logger("LLM统计API")
|
||||
|
||||
@@ -37,108 +37,6 @@ COST_BY_USER = "costs_by_user"
|
||||
COST_BY_MODEL = "costs_by_model"
|
||||
COST_BY_MODULE = "costs_by_module"
|
||||
|
||||
|
||||
async def _collect_stats_in_period(start_time: datetime, end_time: datetime) -> dict[str, Any]:
|
||||
"""在指定时间段内收集LLM使用统计信息"""
|
||||
records = await db_get(
|
||||
model_class=LLMUsage,
|
||||
filters={"timestamp": {"$gte": start_time, "$lt": end_time}},
|
||||
)
|
||||
if not records:
|
||||
return {}
|
||||
|
||||
# 创建一个从 model_identifier 到 name 的映射
|
||||
model_identifier_to_name_map = {model.model_identifier: model.name for model in model_config.models}
|
||||
|
||||
stats: dict[str, Any] = {
|
||||
TOTAL_REQ_CNT: 0,
|
||||
TOTAL_COST: 0.0,
|
||||
REQ_CNT_BY_TYPE: defaultdict(int),
|
||||
REQ_CNT_BY_USER: defaultdict(int),
|
||||
REQ_CNT_BY_MODEL: defaultdict(int),
|
||||
REQ_CNT_BY_MODULE: defaultdict(int),
|
||||
IN_TOK_BY_TYPE: defaultdict(int),
|
||||
IN_TOK_BY_USER: defaultdict(int),
|
||||
IN_TOK_BY_MODEL: defaultdict(int),
|
||||
IN_TOK_BY_MODULE: defaultdict(int),
|
||||
OUT_TOK_BY_TYPE: defaultdict(int),
|
||||
OUT_TOK_BY_USER: defaultdict(int),
|
||||
OUT_TOK_BY_MODEL: defaultdict(int),
|
||||
OUT_TOK_BY_MODULE: defaultdict(int),
|
||||
TOTAL_TOK_BY_TYPE: defaultdict(int),
|
||||
TOTAL_TOK_BY_USER: defaultdict(int),
|
||||
TOTAL_TOK_BY_MODEL: defaultdict(int),
|
||||
TOTAL_TOK_BY_MODULE: defaultdict(int),
|
||||
COST_BY_TYPE: defaultdict(float),
|
||||
COST_BY_USER: defaultdict(float),
|
||||
COST_BY_MODEL: defaultdict(float),
|
||||
COST_BY_MODULE: defaultdict(float),
|
||||
}
|
||||
|
||||
for record in records:
|
||||
if not isinstance(record, dict):
|
||||
continue
|
||||
|
||||
stats[TOTAL_REQ_CNT] += 1
|
||||
|
||||
request_type = record.get("request_type") or "unknown"
|
||||
user_id = record.get("user_id") or "unknown"
|
||||
# 从数据库获取的是真实模型名 (model_identifier)
|
||||
real_model_name = record.get("model_name") or "unknown"
|
||||
module_name = request_type.split(".")[0] if "." in request_type else request_type
|
||||
|
||||
# 尝试通过真实模型名找到配置文件中的模型名
|
||||
config_model_name = model_identifier_to_name_map.get(real_model_name, real_model_name)
|
||||
|
||||
prompt_tokens = record.get("prompt_tokens") or 0
|
||||
completion_tokens = record.get("completion_tokens") or 0
|
||||
total_tokens = prompt_tokens + completion_tokens
|
||||
|
||||
cost = 0.0
|
||||
try:
|
||||
# 使用配置文件中的模型名来获取模型信息
|
||||
model_info = model_config.get_model_info(config_model_name)
|
||||
if model_info:
|
||||
input_cost = (prompt_tokens / 1000000) * model_info.price_in
|
||||
output_cost = (completion_tokens / 1000000) * model_info.price_out
|
||||
cost = round(input_cost + output_cost, 6)
|
||||
except KeyError as e:
|
||||
logger.info(str(e))
|
||||
logger.warning(f"模型 '{config_model_name}' (真实名称: '{real_model_name}') 在配置中未找到,成本计算将使用默认值 0.0")
|
||||
|
||||
stats[TOTAL_COST] += cost
|
||||
|
||||
# 按类型统计
|
||||
stats[REQ_CNT_BY_TYPE][request_type] += 1
|
||||
stats[IN_TOK_BY_TYPE][request_type] += prompt_tokens
|
||||
stats[OUT_TOK_BY_TYPE][request_type] += completion_tokens
|
||||
stats[TOTAL_TOK_BY_TYPE][request_type] += total_tokens
|
||||
stats[COST_BY_TYPE][request_type] += cost
|
||||
|
||||
# 按用户统计
|
||||
stats[REQ_CNT_BY_USER][user_id] += 1
|
||||
stats[IN_TOK_BY_USER][user_id] += prompt_tokens
|
||||
stats[OUT_TOK_BY_USER][user_id] += completion_tokens
|
||||
stats[TOTAL_TOK_BY_USER][user_id] += total_tokens
|
||||
stats[COST_BY_USER][user_id] += cost
|
||||
|
||||
# 按模型统计 (使用配置文件中的名称)
|
||||
stats[REQ_CNT_BY_MODEL][config_model_name] += 1
|
||||
stats[IN_TOK_BY_MODEL][config_model_name] += prompt_tokens
|
||||
stats[OUT_TOK_BY_MODEL][config_model_name] += completion_tokens
|
||||
stats[TOTAL_TOK_BY_MODEL][config_model_name] += total_tokens
|
||||
stats[COST_BY_MODEL][config_model_name] += cost
|
||||
|
||||
# 按模块统计
|
||||
stats[REQ_CNT_BY_MODULE][module_name] += 1
|
||||
stats[IN_TOK_BY_MODULE][module_name] += prompt_tokens
|
||||
stats[OUT_TOK_BY_MODULE][module_name] += completion_tokens
|
||||
stats[TOTAL_TOK_BY_MODULE][module_name] += total_tokens
|
||||
stats[COST_BY_MODULE][module_name] += cost
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
@router.get("/llm/stats")
|
||||
async def get_llm_stats(
|
||||
period_type: Literal[
|
||||
@@ -179,7 +77,8 @@ async def get_llm_stats(
|
||||
if start_time is None:
|
||||
raise HTTPException(status_code=400, detail="无法确定查询的起始时间")
|
||||
|
||||
period_stats = await _collect_stats_in_period(start_time, end_time)
|
||||
stats_data = await StatisticOutputTask._collect_model_request_for_period([("custom", start_time)])
|
||||
period_stats = stats_data.get("custom", {})
|
||||
|
||||
if not period_stats:
|
||||
return {"period": {"start": start_time.isoformat(), "end": end_time.isoformat()}, "data": {}}
|
||||
|
||||
@@ -658,7 +658,7 @@
|
||||
try {
|
||||
document.getElementById('loading').style.display = 'block';
|
||||
|
||||
const response = await fetch('/api/graph/full');
|
||||
const response = await fetch('/visualizer/api/graph/full');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
@@ -748,7 +748,7 @@
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}&limit=50`);
|
||||
const response = await fetch(`/visualizer/api/search?q=${encodeURIComponent(query)}&limit=50`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
@@ -1041,7 +1041,7 @@
|
||||
// 文件选择功能
|
||||
async function loadFileList() {
|
||||
try {
|
||||
const response = await fetch('/api/files');
|
||||
const response = await fetch('/visualizer/api/files');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
@@ -1130,7 +1130,7 @@
|
||||
document.getElementById('loading').style.display = 'block';
|
||||
closeFileSelector();
|
||||
|
||||
const response = await fetch('/api/select_file', {
|
||||
const response = await fetch('/visualizer/api/select_file', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
@@ -127,7 +127,7 @@ class StyleLearner:
|
||||
|
||||
# 最后使用时间(越近越好)
|
||||
last_used = self.learning_stats["style_last_used"].get(style_id, 0)
|
||||
time_since_used = current_time - last_used if last_used > 0 else float('inf')
|
||||
time_since_used = current_time - last_used if last_used > 0 else float("inf")
|
||||
|
||||
# 综合分数:使用次数越多越好,距离上次使用时间越短越好
|
||||
# 使用对数来平滑使用次数的影响
|
||||
|
||||
@@ -8,7 +8,6 @@ from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import orjson
|
||||
from sqlalchemy import select
|
||||
|
||||
from src.common.config_helpers import resolve_embedding_dimension
|
||||
@@ -605,7 +604,7 @@ class BotInterestManager:
|
||||
)
|
||||
|
||||
# 如果有新生成的扩展embedding,保存到缓存文件
|
||||
if hasattr(self, '_new_expanded_embeddings_generated') and self._new_expanded_embeddings_generated:
|
||||
if hasattr(self, "_new_expanded_embeddings_generated") and self._new_expanded_embeddings_generated:
|
||||
await self._save_embedding_cache_to_file(self.current_interests.personality_id)
|
||||
self._new_expanded_embeddings_generated = False
|
||||
logger.debug("💾 已保存新生成的扩展embedding到缓存文件")
|
||||
@@ -670,59 +669,59 @@ class BotInterestManager:
|
||||
tag_lower = tag_name.lower()
|
||||
|
||||
# 技术编程类标签(具体化描述)
|
||||
if any(word in tag_lower for word in ['python', 'java', 'code', '代码', '编程', '脚本', '算法', '开发']):
|
||||
if 'python' in tag_lower:
|
||||
return f"讨论Python编程语言、写Python代码、Python脚本开发、Python技术问题"
|
||||
elif '算法' in tag_lower:
|
||||
return f"讨论算法题目、数据结构、编程竞赛、刷LeetCode题目、代码优化"
|
||||
elif '代码' in tag_lower or '被窝' in tag_lower:
|
||||
return f"讨论写代码、编程开发、代码实现、技术方案、编程技巧"
|
||||
if any(word in tag_lower for word in ["python", "java", "code", "代码", "编程", "脚本", "算法", "开发"]):
|
||||
if "python" in tag_lower:
|
||||
return "讨论Python编程语言、写Python代码、Python脚本开发、Python技术问题"
|
||||
elif "算法" in tag_lower:
|
||||
return "讨论算法题目、数据结构、编程竞赛、刷LeetCode题目、代码优化"
|
||||
elif "代码" in tag_lower or "被窝" in tag_lower:
|
||||
return "讨论写代码、编程开发、代码实现、技术方案、编程技巧"
|
||||
else:
|
||||
return f"讨论编程开发、软件技术、代码编写、技术实现"
|
||||
return "讨论编程开发、软件技术、代码编写、技术实现"
|
||||
|
||||
# 情感表达类标签(具体化为真实对话场景)
|
||||
elif any(word in tag_lower for word in ['治愈', '撒娇', '安慰', '呼噜', '蹭', '卖萌']):
|
||||
return f"想要获得安慰、寻求温暖关怀、撒娇卖萌、表达亲昵、求抱抱求陪伴的对话"
|
||||
elif any(word in tag_lower for word in ["治愈", "撒娇", "安慰", "呼噜", "蹭", "卖萌"]):
|
||||
return "想要获得安慰、寻求温暖关怀、撒娇卖萌、表达亲昵、求抱抱求陪伴的对话"
|
||||
|
||||
# 游戏娱乐类标签(具体游戏场景)
|
||||
elif any(word in tag_lower for word in ['游戏', '网游', 'mmo', '游', '玩']):
|
||||
return f"讨论网络游戏、MMO游戏、游戏玩法、组队打副本、游戏攻略心得"
|
||||
elif any(word in tag_lower for word in ["游戏", "网游", "mmo", "游", "玩"]):
|
||||
return "讨论网络游戏、MMO游戏、游戏玩法、组队打副本、游戏攻略心得"
|
||||
|
||||
# 动漫影视类标签(具体观看行为)
|
||||
elif any(word in tag_lower for word in ['番', '动漫', '视频', 'b站', '弹幕', '追番', '云新番']):
|
||||
elif any(word in tag_lower for word in ["番", "动漫", "视频", "b站", "弹幕", "追番", "云新番"]):
|
||||
# 特别处理"云新番" - 它的意思是在网上看新动漫,不是泛泛的"新东西"
|
||||
if '云' in tag_lower or '新番' in tag_lower:
|
||||
return f"讨论正在播出的新动漫、新番剧集、动漫剧情、追番心得、动漫角色"
|
||||
if "云" in tag_lower or "新番" in tag_lower:
|
||||
return "讨论正在播出的新动漫、新番剧集、动漫剧情、追番心得、动漫角色"
|
||||
else:
|
||||
return f"讨论动漫番剧内容、B站视频、弹幕文化、追番体验"
|
||||
return "讨论动漫番剧内容、B站视频、弹幕文化、追番体验"
|
||||
|
||||
# 社交平台类标签(具体平台行为)
|
||||
elif any(word in tag_lower for word in ['小红书', '贴吧', '论坛', '社区', '吃瓜', '八卦']):
|
||||
if '吃瓜' in tag_lower:
|
||||
return f"聊八卦爆料、吃瓜看热闹、网络热点事件、社交平台热议话题"
|
||||
elif any(word in tag_lower for word in ["小红书", "贴吧", "论坛", "社区", "吃瓜", "八卦"]):
|
||||
if "吃瓜" in tag_lower:
|
||||
return "聊八卦爆料、吃瓜看热闹、网络热点事件、社交平台热议话题"
|
||||
else:
|
||||
return f"讨论社交平台内容、网络社区话题、论坛讨论、分享生活"
|
||||
return "讨论社交平台内容、网络社区话题、论坛讨论、分享生活"
|
||||
|
||||
# 生活日常类标签(具体萌宠场景)
|
||||
elif any(word in tag_lower for word in ['猫', '宠物', '尾巴', '耳朵', '毛绒']):
|
||||
return f"讨论猫咪宠物、晒猫分享、萌宠日常、可爱猫猫、养猫心得"
|
||||
elif any(word in tag_lower for word in ["猫", "宠物", "尾巴", "耳朵", "毛绒"]):
|
||||
return "讨论猫咪宠物、晒猫分享、萌宠日常、可爱猫猫、养猫心得"
|
||||
|
||||
# 状态心情类标签(具体情绪状态)
|
||||
elif any(word in tag_lower for word in ['社恐', '隐身', '流浪', '深夜', '被窝']):
|
||||
if '社恐' in tag_lower:
|
||||
return f"表达社交焦虑、不想见人、想躲起来、害怕社交的心情"
|
||||
elif '深夜' in tag_lower:
|
||||
return f"深夜睡不着、熬夜、夜猫子、深夜思考人生的对话"
|
||||
elif any(word in tag_lower for word in ["社恐", "隐身", "流浪", "深夜", "被窝"]):
|
||||
if "社恐" in tag_lower:
|
||||
return "表达社交焦虑、不想见人、想躲起来、害怕社交的心情"
|
||||
elif "深夜" in tag_lower:
|
||||
return "深夜睡不着、熬夜、夜猫子、深夜思考人生的对话"
|
||||
else:
|
||||
return f"表达当前心情状态、个人感受、生活状态"
|
||||
return "表达当前心情状态、个人感受、生活状态"
|
||||
|
||||
# 物品装备类标签(具体使用场景)
|
||||
elif any(word in tag_lower for word in ['键盘', '耳机', '装备', '设备']):
|
||||
return f"讨论键盘耳机装备、数码产品、使用体验、装备推荐评测"
|
||||
elif any(word in tag_lower for word in ["键盘", "耳机", "装备", "设备"]):
|
||||
return "讨论键盘耳机装备、数码产品、使用体验、装备推荐评测"
|
||||
|
||||
# 互动关系类标签
|
||||
elif any(word in tag_lower for word in ['拾风', '互怼', '互动']):
|
||||
return f"聊天互动、开玩笑、友好互怼、日常对话交流"
|
||||
elif any(word in tag_lower for word in ["拾风", "互怼", "互动"]):
|
||||
return "聊天互动、开玩笑、友好互怼、日常对话交流"
|
||||
|
||||
# 默认:尽量具体化
|
||||
else:
|
||||
@@ -1011,9 +1010,10 @@ class BotInterestManager:
|
||||
async def _load_embedding_cache_from_file(self, personality_id: str) -> dict[str, list[float]] | None:
|
||||
"""从文件加载embedding缓存"""
|
||||
try:
|
||||
import orjson
|
||||
from pathlib import Path
|
||||
|
||||
import orjson
|
||||
|
||||
cache_dir = Path("data/embedding")
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
cache_file = cache_dir / f"{personality_id}_embeddings.json"
|
||||
@@ -1053,9 +1053,10 @@ class BotInterestManager:
|
||||
async def _save_embedding_cache_to_file(self, personality_id: str):
|
||||
"""保存embedding缓存到文件(包括扩展标签的embedding)"""
|
||||
try:
|
||||
import orjson
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import orjson
|
||||
|
||||
cache_dir = Path("data/embedding")
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -9,8 +9,8 @@ from .scheduler_dispatcher import SchedulerDispatcher, scheduler_dispatcher
|
||||
|
||||
__all__ = [
|
||||
"MessageManager",
|
||||
"SingleStreamContextManager",
|
||||
"SchedulerDispatcher",
|
||||
"SingleStreamContextManager",
|
||||
"message_manager",
|
||||
"scheduler_dispatcher",
|
||||
]
|
||||
|
||||
@@ -73,7 +73,7 @@ class SingleStreamContextManager:
|
||||
cache_enabled = global_config.chat.enable_message_cache
|
||||
use_cache_system = message_manager.is_running and cache_enabled
|
||||
if not cache_enabled:
|
||||
logger.debug(f"消息缓存系统已在配置中禁用")
|
||||
logger.debug("消息缓存系统已在配置中禁用")
|
||||
except Exception as e:
|
||||
logger.debug(f"MessageManager不可用,使用直接添加: {e}")
|
||||
use_cache_system = False
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
import time
|
||||
from collections import defaultdict, deque
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from src.chat.chatter_manager import ChatterManager
|
||||
from src.chat.message_receive.chat_stream import ChatStream
|
||||
from src.chat.planner_actions.action_manager import ChatterActionManager
|
||||
from src.common.data_models.database_data_model import DatabaseMessages
|
||||
from src.common.data_models.message_manager_data_model import MessageManagerStats, StreamStats
|
||||
|
||||
@@ -32,7 +32,14 @@ class SchedulerDispatcher:
|
||||
def __init__(self):
|
||||
# 追踪每个流的 schedule_id
|
||||
self.stream_schedules: dict[str, str] = {} # stream_id -> schedule_id
|
||||
<<<<<<< HEAD
|
||||
|
||||
=======
|
||||
|
||||
# 用于保护 schedule 创建/删除的锁,避免竞态条件
|
||||
self.schedule_locks: dict[str, asyncio.Lock] = {} # stream_id -> Lock
|
||||
|
||||
>>>>>>> b0ee26652efd8d03c2e407575b8c5eea78f6afec
|
||||
# Chatter 管理器
|
||||
self.chatter_manager: ChatterManager | None = None
|
||||
|
||||
@@ -81,6 +88,15 @@ class SchedulerDispatcher:
|
||||
"""设置 Chatter 管理器"""
|
||||
self.chatter_manager = chatter_manager
|
||||
logger.debug(f"设置 Chatter 管理器: {chatter_manager.__class__.__name__}")
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
def _get_schedule_lock(self, stream_id: str) -> asyncio.Lock:
|
||||
"""获取流的 schedule 锁"""
|
||||
if stream_id not in self.schedule_locks:
|
||||
self.schedule_locks[stream_id] = asyncio.Lock()
|
||||
return self.schedule_locks[stream_id]
|
||||
>>>>>>> b0ee26652efd8d03c2e407575b8c5eea78f6afec
|
||||
|
||||
async def on_message_received(self, stream_id: str) -> None:
|
||||
"""消息接收时的处理逻辑
|
||||
|
||||
@@ -367,7 +367,7 @@ class ChatBot:
|
||||
message_segment = message_data.get("message_segment")
|
||||
if message_segment and isinstance(message_segment, dict):
|
||||
if message_segment.get("type") == "adapter_response":
|
||||
logger.info(f"[DEBUG bot.py message_process] 检测到adapter_response,立即处理")
|
||||
logger.info("[DEBUG bot.py message_process] 检测到adapter_response,立即处理")
|
||||
await self._handle_adapter_response_from_dict(message_segment.get("data"))
|
||||
return
|
||||
|
||||
|
||||
@@ -557,7 +557,7 @@ class DefaultReplyer:
|
||||
participants = []
|
||||
try:
|
||||
# 尝试从聊天流中获取参与者信息
|
||||
if hasattr(stream, 'chat_history_manager'):
|
||||
if hasattr(stream, "chat_history_manager"):
|
||||
history_manager = stream.chat_history_manager
|
||||
# 获取最近的参与者列表
|
||||
recent_records = history_manager.get_memory_chat_history(
|
||||
@@ -586,9 +586,9 @@ class DefaultReplyer:
|
||||
formatted_history = ""
|
||||
if chat_history:
|
||||
# 移除过长的历史记录,只保留最近部分
|
||||
lines = chat_history.strip().split('\n')
|
||||
lines = chat_history.strip().split("\n")
|
||||
recent_lines = lines[-10:] if len(lines) > 10 else lines
|
||||
formatted_history = '\n'.join(recent_lines)
|
||||
formatted_history = "\n".join(recent_lines)
|
||||
|
||||
query_context = {
|
||||
"chat_history": formatted_history,
|
||||
@@ -725,7 +725,7 @@ class DefaultReplyer:
|
||||
for tool_result in tool_results:
|
||||
tool_name = tool_result.get("tool_name", "unknown")
|
||||
content = tool_result.get("content", "")
|
||||
result_type = tool_result.get("type", "tool_result")
|
||||
tool_result.get("type", "tool_result")
|
||||
|
||||
# 不进行截断,让工具自己处理结果长度
|
||||
current_results_parts.append(f"- **{tool_name}**: {content}")
|
||||
@@ -1913,7 +1913,6 @@ class DefaultReplyer:
|
||||
return
|
||||
|
||||
# 使用统一记忆系统存储记忆
|
||||
from src.chat.memory_system import get_memory_system
|
||||
|
||||
stream = self.chat_stream
|
||||
user_info_obj = getattr(stream, "user_info", None)
|
||||
@@ -2036,7 +2035,7 @@ class DefaultReplyer:
|
||||
timestamp=time.time(),
|
||||
limit=int(global_config.chat.max_context_size),
|
||||
)
|
||||
chat_history = await build_readable_messages(
|
||||
await build_readable_messages(
|
||||
message_list_before_short,
|
||||
replace_bot_name=True,
|
||||
merge_messages=False,
|
||||
|
||||
@@ -91,13 +91,11 @@ def is_mentioned_bot_in_message(message) -> tuple[bool, float]:
|
||||
# 1. 判断是否为私聊(强提及)
|
||||
group_info = getattr(message, "group_info", None)
|
||||
if not group_info or not getattr(group_info, "group_id", None):
|
||||
is_private = True
|
||||
mention_type = 2
|
||||
logger.debug("检测到私聊消息 - 强提及")
|
||||
|
||||
# 2. 判断是否被@(强提及)
|
||||
if re.search(rf"@<(.+?):{global_config.bot.qq_account}>", processed_text):
|
||||
is_at = True
|
||||
mention_type = 2
|
||||
logger.debug("检测到@提及 - 强提及")
|
||||
|
||||
@@ -108,7 +106,6 @@ def is_mentioned_bot_in_message(message) -> tuple[bool, float]:
|
||||
rf"\[回复<(.+?)(?=:{global_config.bot.qq_account!s}>)\:{global_config.bot.qq_account!s}>:(.+?)\],说:",
|
||||
processed_text,
|
||||
):
|
||||
is_replied = True
|
||||
mention_type = 2
|
||||
logger.debug("检测到回复消息 - 强提及")
|
||||
|
||||
@@ -122,14 +119,12 @@ def is_mentioned_bot_in_message(message) -> tuple[bool, float]:
|
||||
|
||||
# 检查bot主名字
|
||||
if global_config.bot.nickname in message_content:
|
||||
is_text_mentioned = True
|
||||
mention_type = 1
|
||||
logger.debug(f"检测到文本提及bot主名字 '{global_config.bot.nickname}' - 弱提及")
|
||||
# 如果主名字没匹配,再检查别名
|
||||
elif nicknames:
|
||||
for alias_name in nicknames:
|
||||
if alias_name in message_content:
|
||||
is_text_mentioned = True
|
||||
mention_type = 1
|
||||
logger.debug(f"检测到文本提及bot别名 '{alias_name}' - 弱提及")
|
||||
break
|
||||
|
||||
@@ -374,7 +374,7 @@ class CacheManager:
|
||||
# 简化的健康统计,不包含内存监控(因为相关属性未定义)
|
||||
return {
|
||||
"l1_count": len(self.l1_kv_cache),
|
||||
"l1_vector_count": self.l1_vector_index.ntotal if hasattr(self.l1_vector_index, 'ntotal') else 0,
|
||||
"l1_vector_count": self.l1_vector_index.ntotal if hasattr(self.l1_vector_index, "ntotal") else 0,
|
||||
"tool_stats": {
|
||||
"total_tool_calls": self.tool_stats.get("total_tool_calls", 0),
|
||||
"tracked_tools": len(self.tool_stats.get("most_used_tools", {})),
|
||||
@@ -397,7 +397,7 @@ class CacheManager:
|
||||
warnings.append(f"⚠️ L1缓存条目数较多: {l1_size}")
|
||||
|
||||
# 检查向量索引大小
|
||||
vector_count = self.l1_vector_index.ntotal if hasattr(self.l1_vector_index, 'ntotal') else 0
|
||||
vector_count = self.l1_vector_index.ntotal if hasattr(self.l1_vector_index, "ntotal") else 0
|
||||
if isinstance(vector_count, int) and vector_count > 500:
|
||||
warnings.append(f"⚠️ 向量索引条目数较多: {vector_count}")
|
||||
|
||||
|
||||
@@ -539,7 +539,6 @@ class AdaptiveBatchScheduler:
|
||||
|
||||
def _set_cache(self, cache_key: str, result: Any) -> None:
|
||||
"""设置缓存(改进版,带大小限制和内存统计)"""
|
||||
import sys
|
||||
|
||||
# 🔧 检查缓存大小限制
|
||||
if len(self._result_cache) >= self._cache_max_size:
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
提供比 sys.getsizeof() 更准确的内存占用估算方法
|
||||
"""
|
||||
|
||||
import sys
|
||||
import pickle
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@@ -44,15 +45,15 @@ def get_accurate_size(obj: Any, seen: set | None = None) -> int:
|
||||
for k, v in obj.items())
|
||||
|
||||
# 列表、元组、集合:递归计算所有元素
|
||||
elif isinstance(obj, (list, tuple, set, frozenset)):
|
||||
elif isinstance(obj, list | tuple | set | frozenset):
|
||||
size += sum(get_accurate_size(item, seen) for item in obj)
|
||||
|
||||
# 有 __dict__ 的对象:递归计算属性
|
||||
elif hasattr(obj, '__dict__'):
|
||||
elif hasattr(obj, "__dict__"):
|
||||
size += get_accurate_size(obj.__dict__, seen)
|
||||
|
||||
# 其他可迭代对象
|
||||
elif hasattr(obj, '__iter__') and not isinstance(obj, (str, bytes, bytearray)):
|
||||
elif hasattr(obj, "__iter__") and not isinstance(obj, str | bytes | bytearray):
|
||||
try:
|
||||
size += sum(get_accurate_size(item, seen) for item in obj)
|
||||
except:
|
||||
@@ -116,7 +117,7 @@ def _estimate_recursive(obj: Any, depth: int, seen: set, sample_large: bool) ->
|
||||
size = sys.getsizeof(obj)
|
||||
|
||||
# 简单类型直接返回
|
||||
if isinstance(obj, (int, float, bool, type(None), str, bytes, bytearray)):
|
||||
if isinstance(obj, int | float | bool | type(None) | str | bytes | bytearray):
|
||||
return size
|
||||
|
||||
# NumPy 数组特殊处理
|
||||
@@ -144,7 +145,7 @@ def _estimate_recursive(obj: Any, depth: int, seen: set, sample_large: bool) ->
|
||||
return size
|
||||
|
||||
# 列表、元组、集合递归
|
||||
if isinstance(obj, (list, tuple, set, frozenset)):
|
||||
if isinstance(obj, list | tuple | set | frozenset):
|
||||
items = list(obj)
|
||||
if sample_large and len(items) > 100:
|
||||
# 大容器采样:前50 + 中间50 + 最后50
|
||||
@@ -162,7 +163,7 @@ def _estimate_recursive(obj: Any, depth: int, seen: set, sample_large: bool) ->
|
||||
return size
|
||||
|
||||
# 有 __dict__ 的对象
|
||||
if hasattr(obj, '__dict__'):
|
||||
if hasattr(obj, "__dict__"):
|
||||
size += _estimate_recursive(obj.__dict__, depth - 1, seen, sample_large)
|
||||
|
||||
return size
|
||||
|
||||
@@ -2,7 +2,8 @@ import os
|
||||
import socket
|
||||
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware # 新增导入
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from rich.traceback import install
|
||||
from uvicorn import Config
|
||||
from uvicorn import Server as UvicornServer
|
||||
|
||||
@@ -2,7 +2,6 @@ import os
|
||||
import shutil
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
import tomlkit
|
||||
from pydantic import Field
|
||||
@@ -381,7 +380,7 @@ class Config(ValidatedConfigBase):
|
||||
notice: NoticeConfig = Field(..., description="Notice消息配置")
|
||||
emoji: EmojiConfig = Field(..., description="表情配置")
|
||||
expression: ExpressionConfig = Field(..., description="表达配置")
|
||||
memory: Optional[MemoryConfig] = Field(default=None, description="记忆配置")
|
||||
memory: MemoryConfig | None = Field(default=None, description="记忆配置")
|
||||
mood: MoodConfig = Field(..., description="情绪配置")
|
||||
reaction: ReactionConfig = Field(default_factory=ReactionConfig, description="反应规则配置")
|
||||
chinese_typo: ChineseTypoConfig = Field(..., description="中文错别字配置")
|
||||
|
||||
@@ -637,6 +637,7 @@ class WebSearchConfig(ValidatedConfigBase):
|
||||
exa_api_keys: list[str] = Field(default_factory=lambda: [], description="exa API密钥列表,支持轮询机制")
|
||||
searxng_instances: list[str] = Field(default_factory=list, description="SearXNG 实例 URL 列表")
|
||||
searxng_api_keys: list[str] = Field(default_factory=list, description="SearXNG 实例 API 密钥列表")
|
||||
serper_api_keys: list[str] = Field(default_factory=list, description="serper API 密钥列表")
|
||||
enabled_engines: list[str] = Field(default_factory=lambda: ["ddg"], description="启用的搜索引擎")
|
||||
search_strategy: Literal["fallback", "single", "parallel"] = Field(default="single", description="搜索策略")
|
||||
|
||||
|
||||
@@ -534,7 +534,7 @@ class _RequestExecutor:
|
||||
model_name = model_info.name
|
||||
retry_interval = api_provider.retry_interval
|
||||
|
||||
if isinstance(e, (NetworkConnectionError, ReqAbortException)):
|
||||
if isinstance(e, NetworkConnectionError | ReqAbortException):
|
||||
return await self._check_retry(remain_try, retry_interval, "连接异常", model_name)
|
||||
elif isinstance(e, RespNotOkException):
|
||||
return await self._handle_resp_not_ok(e, model_info, api_provider, remain_try, messages_info)
|
||||
|
||||
@@ -423,15 +423,16 @@ MoFox_Bot(第三方修改版)
|
||||
|
||||
# 注册API路由
|
||||
try:
|
||||
from src.api.memory_visualizer_router import router as visualizer_router
|
||||
from src.api.message_router import router as message_router
|
||||
from src.api.statistic_router import router as llm_statistic_router
|
||||
|
||||
self.server.register_router(message_router, prefix="/api")
|
||||
self.server.register_router(llm_statistic_router, prefix="/api")
|
||||
self.server.register_router(visualizer_router, prefix="/visualizer")
|
||||
logger.info("API路由注册成功")
|
||||
except Exception as e:
|
||||
logger.error(f"注册API路由失败: {e}")
|
||||
|
||||
# 初始化统一调度器
|
||||
try:
|
||||
from src.schedule.unified_scheduler import initialize_scheduler
|
||||
|
||||
@@ -100,10 +100,10 @@ class VectorStore:
|
||||
|
||||
# 处理额外的元数据,将 list 转换为 JSON 字符串
|
||||
for key, value in node.metadata.items():
|
||||
if isinstance(value, (list, dict)):
|
||||
if isinstance(value, list | dict):
|
||||
import orjson
|
||||
metadata[key] = orjson.dumps(value, option=orjson.OPT_NON_STR_KEYS).decode("utf-8")
|
||||
elif isinstance(value, (str, int, float, bool)) or value is None:
|
||||
elif isinstance(value, str | int | float | bool) or value is None:
|
||||
metadata[key] = value
|
||||
else:
|
||||
metadata[key] = str(value)
|
||||
@@ -149,9 +149,9 @@ class VectorStore:
|
||||
"created_at": n.created_at.isoformat(),
|
||||
}
|
||||
for key, value in n.metadata.items():
|
||||
if isinstance(value, (list, dict)):
|
||||
if isinstance(value, list | dict):
|
||||
metadata[key] = orjson.dumps(value, option=orjson.OPT_NON_STR_KEYS).decode("utf-8")
|
||||
elif isinstance(value, (str, int, float, bool)) or value is None:
|
||||
elif isinstance(value, str | int | float | bool) or value is None:
|
||||
metadata[key] = value # type: ignore
|
||||
else:
|
||||
metadata[key] = str(value)
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import numpy as np
|
||||
|
||||
from src.common.logger import get_logger
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
"""
|
||||
|
||||
import atexit
|
||||
import orjson
|
||||
import os
|
||||
import threading
|
||||
from typing import Any, ClassVar
|
||||
|
||||
import orjson
|
||||
|
||||
from src.common.logger import get_logger
|
||||
|
||||
# 获取日志记录器
|
||||
@@ -125,7 +126,7 @@ class PluginStorage:
|
||||
|
||||
try:
|
||||
with open(self.file_path, "w", encoding="utf-8") as f:
|
||||
f.write(orjson.dumps(self._data, option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS).decode('utf-8'))
|
||||
f.write(orjson.dumps(self._data, option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS).decode("utf-8"))
|
||||
self._dirty = False # 保存后重置标志
|
||||
logger.debug(f"插件 '{self.name}' 的数据已成功保存到磁盘。")
|
||||
except Exception as e:
|
||||
|
||||
@@ -5,12 +5,12 @@ MCP Client Manager
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import orjson
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import mcp.types
|
||||
import orjson
|
||||
from fastmcp.client import Client, StdioTransport, StreamableHttpTransport
|
||||
|
||||
from src.common.logger import get_logger
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Any, Optional
|
||||
from dataclasses import dataclass, asdict, field
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
import orjson
|
||||
from src.common.logger import get_logger
|
||||
|
||||
from src.common.cache_manager import tool_cache
|
||||
from src.common.logger import get_logger
|
||||
|
||||
logger = get_logger("stream_tool_history")
|
||||
|
||||
@@ -18,10 +20,10 @@ class ToolCallRecord:
|
||||
"""工具调用记录"""
|
||||
tool_name: str
|
||||
args: dict[str, Any]
|
||||
result: Optional[dict[str, Any]] = None
|
||||
result: dict[str, Any] | None = None
|
||||
status: str = "success" # success, error, pending
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
execution_time: Optional[float] = None # 执行耗时(秒)
|
||||
execution_time: float | None = None # 执行耗时(秒)
|
||||
cache_hit: bool = False # 是否命中缓存
|
||||
result_preview: str = "" # 结果预览
|
||||
error_message: str = "" # 错误信息
|
||||
@@ -32,9 +34,9 @@ class ToolCallRecord:
|
||||
content = self.result.get("content", "")
|
||||
if isinstance(content, str):
|
||||
self.result_preview = content[:500] + ("..." if len(content) > 500 else "")
|
||||
elif isinstance(content, (list, dict)):
|
||||
elif isinstance(content, list | dict):
|
||||
try:
|
||||
self.result_preview = orjson.dumps(content, option=orjson.OPT_NON_STR_KEYS).decode('utf-8')[:500] + "..."
|
||||
self.result_preview = orjson.dumps(content, option=orjson.OPT_NON_STR_KEYS).decode("utf-8")[:500] + "..."
|
||||
except Exception:
|
||||
self.result_preview = str(content)[:500] + "..."
|
||||
else:
|
||||
@@ -105,7 +107,7 @@ class StreamToolHistoryManager:
|
||||
|
||||
logger.debug(f"[{self.chat_id}] 添加工具调用记录: {record.tool_name}, 缓存命中: {record.cache_hit}")
|
||||
|
||||
async def get_cached_result(self, tool_name: str, args: dict[str, Any]) -> Optional[dict[str, Any]]:
|
||||
async def get_cached_result(self, tool_name: str, args: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""从缓存或历史记录中获取结果
|
||||
|
||||
Args:
|
||||
@@ -160,9 +162,9 @@ class StreamToolHistoryManager:
|
||||
return None
|
||||
|
||||
async def cache_result(self, tool_name: str, args: dict[str, Any], result: dict[str, Any],
|
||||
execution_time: Optional[float] = None,
|
||||
tool_file_path: Optional[str] = None,
|
||||
ttl: Optional[int] = None) -> None:
|
||||
execution_time: float | None = None,
|
||||
tool_file_path: str | None = None,
|
||||
ttl: int | None = None) -> None:
|
||||
"""缓存工具调用结果
|
||||
|
||||
Args:
|
||||
@@ -207,7 +209,7 @@ class StreamToolHistoryManager:
|
||||
except Exception as e:
|
||||
logger.warning(f"[{self.chat_id}] 缓存设置失败: {e}")
|
||||
|
||||
async def get_recent_history(self, count: int = 5, status_filter: Optional[str] = None) -> list[ToolCallRecord]:
|
||||
async def get_recent_history(self, count: int = 5, status_filter: str | None = None) -> list[ToolCallRecord]:
|
||||
"""获取最近的历史记录
|
||||
|
||||
Args:
|
||||
@@ -295,7 +297,7 @@ class StreamToolHistoryManager:
|
||||
self._history.clear()
|
||||
logger.info(f"[{self.chat_id}] 工具历史记录已清除")
|
||||
|
||||
def _search_memory_cache(self, tool_name: str, args: dict[str, Any]) -> Optional[dict[str, Any]]:
|
||||
def _search_memory_cache(self, tool_name: str, args: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""在内存历史记录中搜索缓存
|
||||
|
||||
Args:
|
||||
@@ -333,7 +335,7 @@ class StreamToolHistoryManager:
|
||||
|
||||
return tool_path_mapping.get(tool_name, f"src/plugins/tools/{tool_name}.py")
|
||||
|
||||
def _extract_semantic_query(self, tool_name: str, args: dict[str, Any]) -> Optional[str]:
|
||||
def _extract_semantic_query(self, tool_name: str, args: dict[str, Any]) -> str | None:
|
||||
"""提取语义查询参数
|
||||
|
||||
Args:
|
||||
@@ -370,7 +372,7 @@ class StreamToolHistoryManager:
|
||||
return ""
|
||||
|
||||
try:
|
||||
args_str = orjson.dumps(args, option=orjson.OPT_SORT_KEYS).decode('utf-8')
|
||||
args_str = orjson.dumps(args, option=orjson.OPT_SORT_KEYS).decode("utf-8")
|
||||
if len(args_str) > max_length:
|
||||
args_str = args_str[:max_length] + "..."
|
||||
return args_str
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import inspect
|
||||
import time
|
||||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
from src.chat.utils.prompt import Prompt, global_prompt_manager
|
||||
@@ -10,8 +11,7 @@ from src.llm_models.utils_model import LLMRequest
|
||||
from src.plugin_system.apis.tool_api import get_llm_available_tool_definitions, get_tool_instance
|
||||
from src.plugin_system.base.base_tool import BaseTool
|
||||
from src.plugin_system.core.global_announcement_manager import global_announcement_manager
|
||||
from src.plugin_system.core.stream_tool_history import get_stream_tool_history_manager, ToolCallRecord
|
||||
from dataclasses import asdict
|
||||
from src.plugin_system.core.stream_tool_history import ToolCallRecord, get_stream_tool_history_manager
|
||||
|
||||
logger = get_logger("tool_use")
|
||||
|
||||
@@ -338,9 +338,8 @@ class ToolExecutor:
|
||||
if tool_instance and result and tool_instance.enable_cache:
|
||||
try:
|
||||
tool_file_path = inspect.getfile(tool_instance.__class__)
|
||||
semantic_query = None
|
||||
if tool_instance.semantic_cache_query_key:
|
||||
semantic_query = function_args.get(tool_instance.semantic_cache_query_key)
|
||||
function_args.get(tool_instance.semantic_cache_query_key)
|
||||
|
||||
await self.history_manager.cache_result(
|
||||
tool_name=tool_call.func_name,
|
||||
|
||||
@@ -217,7 +217,7 @@ class AffinityInterestCalculator(BaseInterestCalculator):
|
||||
return 0.0
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(f"⏱️ 兴趣匹配计算超时(>1.5秒),返回默认分值0.5以保留其他分数")
|
||||
logger.warning("⏱️ 兴趣匹配计算超时(>1.5秒),返回默认分值0.5以保留其他分数")
|
||||
return 0.5 # 超时时返回默认分值,避免丢失提及分和关系分
|
||||
except Exception as e:
|
||||
logger.warning(f"智能兴趣匹配失败: {e}")
|
||||
|
||||
@@ -4,10 +4,10 @@ AffinityFlow Chatter 规划器模块
|
||||
包含计划生成、过滤、执行等规划相关功能
|
||||
"""
|
||||
|
||||
from . import planner_prompts
|
||||
from .plan_executor import ChatterPlanExecutor
|
||||
from .plan_filter import ChatterPlanFilter
|
||||
from .plan_generator import ChatterPlanGenerator
|
||||
from .planner import ChatterActionPlanner
|
||||
from . import planner_prompts
|
||||
|
||||
__all__ = ["ChatterActionPlanner", "planner_prompts", "ChatterPlanGenerator", "ChatterPlanFilter", "ChatterPlanExecutor"]
|
||||
__all__ = ["ChatterActionPlanner", "ChatterPlanExecutor", "ChatterPlanFilter", "ChatterPlanGenerator", "planner_prompts"]
|
||||
|
||||
@@ -14,9 +14,7 @@ from json_repair import repair_json
|
||||
# 旧的Hippocampus系统已被移除,现在使用增强记忆系统
|
||||
# from src.chat.memory_system.enhanced_memory_manager import enhanced_memory_manager
|
||||
from src.chat.utils.chat_message_builder import (
|
||||
build_readable_actions,
|
||||
build_readable_messages_with_id,
|
||||
get_actions_by_timestamp_with_chat,
|
||||
)
|
||||
from src.chat.utils.prompt import global_prompt_manager
|
||||
from src.common.data_models.info_data_model import ActionPlannerInfo, Plan
|
||||
|
||||
@@ -21,7 +21,6 @@ if TYPE_CHECKING:
|
||||
from src.common.data_models.message_manager_data_model import StreamContext
|
||||
|
||||
# 导入提示词模块以确保其被初始化
|
||||
from src.plugins.built_in.affinity_flow_chatter.planner import planner_prompts
|
||||
|
||||
logger = get_logger("planner")
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ from .proactive_thinking_executor import execute_proactive_thinking
|
||||
from .proactive_thinking_scheduler import ProactiveThinkingScheduler, proactive_thinking_scheduler
|
||||
|
||||
__all__ = [
|
||||
"ProactiveThinkingReplyHandler",
|
||||
"ProactiveThinkingMessageHandler",
|
||||
"execute_proactive_thinking",
|
||||
"ProactiveThinkingReplyHandler",
|
||||
"ProactiveThinkingScheduler",
|
||||
"execute_proactive_thinking",
|
||||
"proactive_thinking_scheduler",
|
||||
]
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
当定时任务触发时,负责搜集信息、调用LLM决策、并根据决策生成回复
|
||||
"""
|
||||
|
||||
import orjson
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ from maim_message import UserInfo
|
||||
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from src.common.logger import get_logger
|
||||
from src.config.api_ada_configs import TaskConfig
|
||||
from src.llm_models.utils_model import LLMRequest
|
||||
from src.plugin_system.apis import config_api, generator_api, llm_api
|
||||
|
||||
|
||||
@@ -136,10 +136,10 @@ class QZoneService:
|
||||
logger.info(f"[DEBUG] 准备获取API客户端,qq_account={qq_account}")
|
||||
api_client = await self._get_api_client(qq_account, stream_id)
|
||||
if not api_client:
|
||||
logger.error(f"[DEBUG] API客户端获取失败,返回错误")
|
||||
logger.error("[DEBUG] API客户端获取失败,返回错误")
|
||||
return {"success": False, "message": "获取QZone API客户端失败"}
|
||||
|
||||
logger.info(f"[DEBUG] API客户端获取成功,准备读取说说")
|
||||
logger.info("[DEBUG] API客户端获取成功,准备读取说说")
|
||||
num_to_read = self.get_config("read.read_number", 5)
|
||||
|
||||
# 尝试执行,如果Cookie失效则自动重试一次
|
||||
@@ -186,7 +186,7 @@ class QZoneService:
|
||||
|
||||
# 检查是否是Cookie失效(-3000错误)
|
||||
if "错误码: -3000" in error_msg and retry_count == 0:
|
||||
logger.warning(f"检测到Cookie失效(-3000错误),准备删除缓存并重试...")
|
||||
logger.warning("检测到Cookie失效(-3000错误),准备删除缓存并重试...")
|
||||
|
||||
# 删除Cookie缓存文件
|
||||
cookie_file = self.cookie_service._get_cookie_file_path(qq_account)
|
||||
@@ -623,7 +623,7 @@ class QZoneService:
|
||||
logger.error(f"获取API客户端失败:Cookie中缺少关键的 'p_skey'。Cookie内容: {cookies}")
|
||||
return None
|
||||
|
||||
logger.info(f"[DEBUG] p_skey获取成功")
|
||||
logger.info("[DEBUG] p_skey获取成功")
|
||||
|
||||
gtk = self._generate_gtk(p_skey)
|
||||
uin = cookies.get("uin", "").lstrip("o")
|
||||
@@ -1230,7 +1230,7 @@ class QZoneService:
|
||||
logger.error(f"监控好友动态失败: {e}", exc_info=True)
|
||||
return []
|
||||
|
||||
logger.info(f"[DEBUG] API客户端构造完成,返回包含6个方法的字典")
|
||||
logger.info("[DEBUG] API客户端构造完成,返回包含6个方法的字典")
|
||||
return {
|
||||
"publish": _publish,
|
||||
"list_feeds": _list_feeds,
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
负责记录和管理已回复过的评论ID,避免重复回复
|
||||
"""
|
||||
|
||||
import orjson
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import orjson
|
||||
|
||||
from src.common.logger import get_logger
|
||||
|
||||
logger = get_logger("MaiZone.ReplyTrackerService")
|
||||
@@ -117,8 +118,8 @@ class ReplyTrackerService:
|
||||
temp_file = self.reply_record_file.with_suffix(".tmp")
|
||||
|
||||
# 先写入临时文件
|
||||
with open(temp_file, "w", encoding="utf-8") as f:
|
||||
orjson.dumps(self.replied_comments, option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS).decode('utf-8')
|
||||
with open(temp_file, "w", encoding="utf-8"):
|
||||
orjson.dumps(self.replied_comments, option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS).decode("utf-8")
|
||||
|
||||
# 如果写入成功,重命名为正式文件
|
||||
if temp_file.stat().st_size > 0: # 确保写入成功
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import orjson
|
||||
import random
|
||||
import time
|
||||
import random
|
||||
import websockets as Server
|
||||
import uuid
|
||||
from maim_message import (
|
||||
@@ -205,7 +204,7 @@ class SendHandler:
|
||||
# 发送响应回MoFox-Bot
|
||||
logger.debug(f"[DEBUG handle_adapter_command] 即将调用send_adapter_command_response, request_id={request_id}")
|
||||
await self.send_adapter_command_response(raw_message_base, response, request_id)
|
||||
logger.debug(f"[DEBUG handle_adapter_command] send_adapter_command_response调用完成")
|
||||
logger.debug("[DEBUG handle_adapter_command] send_adapter_command_response调用完成")
|
||||
|
||||
if response.get("status") == "ok":
|
||||
logger.info(f"适配器命令 {action} 执行成功")
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""
|
||||
Metaso Search Engine (Chat Completions Mode)
|
||||
"""
|
||||
import orjson
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import orjson
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system.apis import config_api
|
||||
|
||||
@@ -3,9 +3,10 @@ Serper search engine implementation
|
||||
Google Search via Serper.dev API
|
||||
"""
|
||||
|
||||
import aiohttp
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system.apis import config_api
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ Web Search Tool Plugin
|
||||
"""
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system import BasePlugin, ComponentInfo, ConfigField, PythonDependency, register_plugin
|
||||
from src.plugin_system import BasePlugin, ComponentInfo, ConfigField, register_plugin
|
||||
from src.plugin_system.apis import config_api
|
||||
|
||||
from .tools.url_parser import URLParserTool
|
||||
|
||||
@@ -113,7 +113,7 @@ class WebSurfingTool(BaseTool):
|
||||
custom_args["num_results"] = custom_args.get("num_results", 5)
|
||||
|
||||
# 如果启用了answer模式且是Exa引擎,使用answer_search方法
|
||||
if answer_mode and engine_name == "exa" and hasattr(engine, 'answer_search'):
|
||||
if answer_mode and engine_name == "exa" and hasattr(engine, "answer_search"):
|
||||
search_tasks.append(engine.answer_search(custom_args))
|
||||
else:
|
||||
search_tasks.append(engine.search(custom_args))
|
||||
@@ -162,7 +162,7 @@ class WebSurfingTool(BaseTool):
|
||||
custom_args["num_results"] = custom_args.get("num_results", 5)
|
||||
|
||||
# 如果启用了answer模式且是Exa引擎,使用answer_search方法
|
||||
if answer_mode and engine_name == "exa" and hasattr(engine, 'answer_search'):
|
||||
if answer_mode and engine_name == "exa" and hasattr(engine, "answer_search"):
|
||||
logger.info("使用Exa答案模式进行搜索(fallback策略)")
|
||||
results = await engine.answer_search(custom_args)
|
||||
else:
|
||||
@@ -195,7 +195,7 @@ class WebSurfingTool(BaseTool):
|
||||
custom_args["num_results"] = custom_args.get("num_results", 5)
|
||||
|
||||
# 如果启用了answer模式且是Exa引擎,使用answer_search方法
|
||||
if answer_mode and engine_name == "exa" and hasattr(engine, 'answer_search'):
|
||||
if answer_mode and engine_name == "exa" and hasattr(engine, "answer_search"):
|
||||
logger.info("使用Exa答案模式进行搜索")
|
||||
results = await engine.answer_search(custom_args)
|
||||
else:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[inner]
|
||||
version = "7.6.4"
|
||||
version = "7.6.5"
|
||||
|
||||
#----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读----
|
||||
#如果你想要修改配置文件,请递增version的值
|
||||
@@ -478,9 +478,10 @@ exa_api_keys = ["None"]# EXA API密钥列表,支持轮询机制
|
||||
metaso_api_keys = ["None"]# Metaso API密钥列表,支持轮询机制
|
||||
searxng_instances = [] # SearXNG 实例 URL 列表
|
||||
searxng_api_keys = []# SearXNG 实例 API 密钥列表
|
||||
serper_api_keys = []# serper API 密钥列表
|
||||
|
||||
# 搜索引擎配置
|
||||
enabled_engines = ["ddg"] # 启用的搜索引擎列表,可选: "exa", "tavily", "ddg","bing", "metaso"
|
||||
enabled_engines = ["ddg"] # 启用的搜索引擎列表,可选: "exa", "tavily", "ddg","bing", "metaso","serper"
|
||||
search_strategy = "single" # 搜索策略: "single"(使用第一个可用引擎), "parallel"(并行使用所有启用的引擎), "fallback"(按顺序尝试,失败则尝试下一个)
|
||||
|
||||
[cross_context] # 跨群聊/私聊上下文共享配置
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
# 🔄 更新日志 - 记忆图可视化工具
|
||||
|
||||
## v1.1 - 2025-11-06
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
1. **📂 文件选择器**
|
||||
- 自动搜索所有可用的记忆图数据文件
|
||||
- 支持在Web界面中切换不同的数据文件
|
||||
- 显示文件大小、修改时间等信息
|
||||
- 高亮显示当前使用的文件
|
||||
|
||||
2. **🔍 智能文件搜索**
|
||||
- 自动查找 `data/memory_graph/graph_store.json`
|
||||
- 搜索所有备份文件 `graph_store_*.json`
|
||||
- 搜索 `data/backup/` 目录下的历史数据
|
||||
- 按修改时间排序,自动使用最新文件
|
||||
|
||||
3. **📊 增强的文件信息显示**
|
||||
- 在侧边栏显示当前文件信息
|
||||
- 包含文件名、大小、修改时间
|
||||
- 实时更新,方便追踪
|
||||
|
||||
### 🔧 改进
|
||||
|
||||
- 更友好的错误提示
|
||||
- 无数据文件时显示引导信息
|
||||
- 优化用户体验
|
||||
|
||||
### 🎯 使用方法
|
||||
|
||||
```bash
|
||||
# 启动可视化工具
|
||||
python run_visualizer_simple.py
|
||||
|
||||
# 或直接运行
|
||||
python tools/memory_visualizer/visualizer_simple.py
|
||||
```
|
||||
|
||||
在Web界面中:
|
||||
1. 点击侧边栏的 "选择文件" 按钮
|
||||
2. 浏览所有可用的数据文件
|
||||
3. 点击任意文件切换数据源
|
||||
4. 图形会自动重新加载
|
||||
|
||||
### 📸 新界面预览
|
||||
|
||||
侧边栏新增:
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ 📂 数据文件 │
|
||||
│ ┌──────────┬──────────┐ │
|
||||
│ │ 选择文件 │ 刷新列表 │ │
|
||||
│ └──────────┴──────────┘ │
|
||||
│ ┌─────────────────────┐ │
|
||||
│ │ 📄 graph_store.json │ │
|
||||
│ │ 大小: 125 KB │ │
|
||||
│ │ 修改: 2025-11-06 │ │
|
||||
│ └─────────────────────┘ │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
文件选择对话框:
|
||||
```
|
||||
┌────────────────────────────────┐
|
||||
│ 📂 选择数据文件 [×] │
|
||||
├────────────────────────────────┤
|
||||
│ ┌────────────────────────────┐ │
|
||||
│ │ 📄 graph_store.json [当前] │ │
|
||||
│ │ 125 KB | 2025-11-06 09:30 │ │
|
||||
│ └────────────────────────────┘ │
|
||||
│ ┌────────────────────────────┐ │
|
||||
│ │ 📄 graph_store_backup.json │ │
|
||||
│ │ 120 KB | 2025-11-05 18:00 │ │
|
||||
│ └────────────────────────────┘ │
|
||||
└────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## v1.0 - 2025-11-06 (初始版本)
|
||||
|
||||
### 🎉 首次发布
|
||||
|
||||
- ✅ 基于Vis.js的交互式图形可视化
|
||||
- ✅ 节点类型颜色分类
|
||||
- ✅ 搜索和过滤功能
|
||||
- ✅ 统计信息显示
|
||||
- ✅ 节点详情查看
|
||||
- ✅ 数据导出功能
|
||||
- ✅ 独立版服务器(快速启动)
|
||||
- ✅ 完整版服务器(实时数据)
|
||||
|
||||
---
|
||||
|
||||
## 🔮 计划中的功能 (v1.2+)
|
||||
|
||||
- [ ] 时间轴视图 - 查看记忆随时间的变化
|
||||
- [ ] 3D可视化模式
|
||||
- [ ] 记忆重要性热力图
|
||||
- [ ] 关系强度可视化
|
||||
- [ ] 导出为图片/PDF
|
||||
- [ ] 记忆路径追踪
|
||||
- [ ] 多文件对比视图
|
||||
- [ ] 性能优化 - 支持更大规模图形
|
||||
- [ ] 移动端适配
|
||||
|
||||
欢迎提出建议和需求! 🚀
|
||||
@@ -1,163 +0,0 @@
|
||||
# 📁 可视化工具文件整理完成
|
||||
|
||||
## ✅ 整理结果
|
||||
|
||||
### 新的目录结构
|
||||
|
||||
```
|
||||
tools/memory_visualizer/
|
||||
├── visualizer.ps1 ⭐ 统一启动脚本(主入口)
|
||||
├── visualizer_simple.py # 独立版服务器
|
||||
├── visualizer_server.py # 完整版服务器
|
||||
├── generate_sample_data.py # 测试数据生成器
|
||||
├── test_visualizer.py # 测试脚本
|
||||
├── run_visualizer.py # Python 运行脚本(独立版)
|
||||
├── run_visualizer_simple.py # Python 运行脚本(简化版)
|
||||
├── start_visualizer.bat # Windows 批处理启动脚本
|
||||
├── start_visualizer.ps1 # PowerShell 启动脚本
|
||||
├── start_visualizer.sh # Linux/Mac 启动脚本
|
||||
├── requirements.txt # Python 依赖
|
||||
├── templates/ # HTML 模板
|
||||
│ └── visualizer.html # 可视化界面
|
||||
├── docs/ # 文档目录
|
||||
│ ├── VISUALIZER_README.md
|
||||
│ ├── VISUALIZER_GUIDE.md
|
||||
│ └── VISUALIZER_INSTALL_COMPLETE.md
|
||||
├── README.md # 主说明文档
|
||||
├── QUICKSTART.md # 快速开始指南
|
||||
└── CHANGELOG.md # 更新日志
|
||||
```
|
||||
|
||||
### 根目录保留文件
|
||||
|
||||
```
|
||||
项目根目录/
|
||||
├── visualizer.ps1 # 快捷启动脚本(指向 tools/memory_visualizer/visualizer.ps1)
|
||||
└── tools/memory_visualizer/ # 所有可视化工具文件
|
||||
```
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
### 推荐方式:使用统一启动脚本
|
||||
|
||||
```powershell
|
||||
# 在项目根目录
|
||||
.\visualizer.ps1
|
||||
|
||||
# 或在工具目录
|
||||
cd tools\memory_visualizer
|
||||
.\visualizer.ps1
|
||||
```
|
||||
|
||||
### 命令行参数
|
||||
|
||||
```powershell
|
||||
# 直接启动独立版(推荐)
|
||||
.\visualizer.ps1 -Simple
|
||||
|
||||
# 启动完整版
|
||||
.\visualizer.ps1 -Full
|
||||
|
||||
# 生成测试数据
|
||||
.\visualizer.ps1 -Generate
|
||||
|
||||
# 运行测试
|
||||
.\visualizer.ps1 -Test
|
||||
```
|
||||
|
||||
## 📋 整理内容
|
||||
|
||||
### 已移动的文件
|
||||
|
||||
从项目根目录移动到 `tools/memory_visualizer/`:
|
||||
|
||||
1. **脚本文件**
|
||||
- `generate_sample_data.py`
|
||||
- `run_visualizer.py`
|
||||
- `run_visualizer_simple.py`
|
||||
- `test_visualizer.py`
|
||||
- `start_visualizer.bat`
|
||||
- `start_visualizer.ps1`
|
||||
- `start_visualizer.sh`
|
||||
- `visualizer.ps1`
|
||||
|
||||
2. **文档文件** → `docs/` 子目录
|
||||
- `VISUALIZER_GUIDE.md`
|
||||
- `VISUALIZER_INSTALL_COMPLETE.md`
|
||||
- `VISUALIZER_README.md`
|
||||
|
||||
### 已创建的新文件
|
||||
|
||||
1. **统一启动脚本**
|
||||
- `tools/memory_visualizer/visualizer.ps1` - 功能齐全的统一入口
|
||||
|
||||
2. **快捷脚本**
|
||||
- `visualizer.ps1`(根目录)- 快捷方式,指向实际脚本
|
||||
|
||||
3. **更新的文档**
|
||||
- `tools/memory_visualizer/README.md` - 更新为反映新结构
|
||||
|
||||
## 🎯 优势
|
||||
|
||||
### 整理前的问题
|
||||
- ❌ 文件散落在根目录
|
||||
- ❌ 多个启动脚本功能重复
|
||||
- ❌ 文档分散不便管理
|
||||
- ❌ 不清楚哪个是主入口
|
||||
|
||||
### 整理后的改进
|
||||
- ✅ 所有文件集中在 `tools/memory_visualizer/`
|
||||
- ✅ 单一统一的启动脚本 `visualizer.ps1`
|
||||
- ✅ 文档集中在 `docs/` 子目录
|
||||
- ✅ 清晰的主入口和快捷方式
|
||||
- ✅ 更好的可维护性
|
||||
|
||||
## 📝 功能对比
|
||||
|
||||
### 旧的方式(整理前)
|
||||
```powershell
|
||||
# 需要记住多个脚本名称
|
||||
.\start_visualizer.ps1
|
||||
.\run_visualizer.py
|
||||
.\run_visualizer_simple.py
|
||||
.\generate_sample_data.py
|
||||
```
|
||||
|
||||
### 新的方式(整理后)
|
||||
```powershell
|
||||
# 只需要一个统一的脚本
|
||||
.\visualizer.ps1 # 交互式菜单
|
||||
.\visualizer.ps1 -Simple # 启动独立版
|
||||
.\visualizer.ps1 -Generate # 生成数据
|
||||
.\visualizer.ps1 -Test # 运行测试
|
||||
```
|
||||
|
||||
## 🔧 维护说明
|
||||
|
||||
### 添加新功能
|
||||
1. 在 `tools/memory_visualizer/` 目录下添加新文件
|
||||
2. 如需启动选项,在 `visualizer.ps1` 中添加新参数
|
||||
3. 更新 `README.md` 文档
|
||||
|
||||
### 更新文档
|
||||
1. 主文档:`tools/memory_visualizer/README.md`
|
||||
2. 详细文档:`tools/memory_visualizer/docs/`
|
||||
|
||||
## ✅ 测试结果
|
||||
|
||||
- ✅ 统一启动脚本正常工作
|
||||
- ✅ 独立版服务器成功启动(端口 5001)
|
||||
- ✅ 数据加载成功(725 节点,769 边)
|
||||
- ✅ Web 界面正常访问
|
||||
- ✅ 所有文件已整理到位
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [README](tools/memory_visualizer/README.md) - 主要说明文档
|
||||
- [QUICKSTART](tools/memory_visualizer/QUICKSTART.md) - 快速开始指南
|
||||
- [CHANGELOG](tools/memory_visualizer/CHANGELOG.md) - 更新日志
|
||||
- [详细指南](tools/memory_visualizer/docs/VISUALIZER_GUIDE.md) - 完整使用指南
|
||||
|
||||
---
|
||||
|
||||
整理完成时间:2025-11-06
|
||||
@@ -1,279 +0,0 @@
|
||||
# 记忆图可视化工具 - 快速入门指南
|
||||
|
||||
## 🎯 方案选择
|
||||
|
||||
我为你创建了**两个版本**的可视化工具:
|
||||
|
||||
### 1️⃣ 独立版 (推荐 ⭐)
|
||||
- **文件**: `tools/memory_visualizer/visualizer_simple.py`
|
||||
- **优点**:
|
||||
- 直接读取存储文件,无需初始化完整系统
|
||||
- 启动快速
|
||||
- 占用资源少
|
||||
- **适用**: 快速查看已有记忆数据
|
||||
|
||||
### 2️⃣ 完整版
|
||||
- **文件**: `tools/memory_visualizer/visualizer_server.py`
|
||||
- **优点**:
|
||||
- 实时数据
|
||||
- 支持更多功能
|
||||
- **缺点**:
|
||||
- 需要完整初始化记忆管理器
|
||||
- 启动较慢
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 步骤 1: 安装依赖
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
# 依赖会自动检查和安装
|
||||
.\start_visualizer.ps1
|
||||
```
|
||||
|
||||
**Windows (CMD):**
|
||||
```cmd
|
||||
start_visualizer.bat
|
||||
```
|
||||
|
||||
**Linux/Mac:**
|
||||
```bash
|
||||
chmod +x start_visualizer.sh
|
||||
./start_visualizer.sh
|
||||
```
|
||||
|
||||
**手动安装依赖:**
|
||||
```bash
|
||||
# 使用虚拟环境
|
||||
.\.venv\Scripts\python.exe -m pip install flask flask-cors
|
||||
|
||||
# 或全局安装
|
||||
pip install flask flask-cors
|
||||
```
|
||||
|
||||
### 步骤 2: 确保有数据
|
||||
|
||||
如果还没有记忆数据,可以:
|
||||
|
||||
**选项A**: 运行Bot生成实际数据
|
||||
```bash
|
||||
python bot.py
|
||||
# 与Bot交互一会儿,让它积累一些记忆
|
||||
```
|
||||
|
||||
**选项B**: 生成测试数据 (如果测试脚本可用)
|
||||
```bash
|
||||
python test_visualizer.py
|
||||
# 选择选项 1: 生成测试数据
|
||||
```
|
||||
|
||||
### 步骤 3: 启动可视化服务器
|
||||
|
||||
**方式一: 使用启动脚本 (推荐 ⭐)**
|
||||
|
||||
Windows PowerShell:
|
||||
```powershell
|
||||
.\start_visualizer.ps1
|
||||
```
|
||||
|
||||
Windows CMD:
|
||||
```cmd
|
||||
start_visualizer.bat
|
||||
```
|
||||
|
||||
Linux/Mac:
|
||||
```bash
|
||||
./start_visualizer.sh
|
||||
```
|
||||
|
||||
**方式二: 手动启动**
|
||||
|
||||
使用虚拟环境:
|
||||
```bash
|
||||
# Windows
|
||||
.\.venv\Scripts\python.exe tools/memory_visualizer/visualizer_simple.py
|
||||
|
||||
# Linux/Mac
|
||||
.venv/bin/python tools/memory_visualizer/visualizer_simple.py
|
||||
```
|
||||
|
||||
或使用系统Python:
|
||||
```bash
|
||||
python tools/memory_visualizer/visualizer_simple.py
|
||||
```
|
||||
|
||||
服务器将在 http://127.0.0.1:5001 启动
|
||||
|
||||
### 步骤 4: 打开浏览器
|
||||
|
||||
访问对应的地址,开始探索记忆图! 🎉
|
||||
|
||||
## 🎨 界面功能
|
||||
|
||||
### 左侧栏
|
||||
|
||||
1. **🔍 搜索框**
|
||||
- 输入关键词搜索相关记忆
|
||||
- 结果会在图中高亮显示
|
||||
|
||||
2. **📊 统计信息**
|
||||
- 节点总数
|
||||
- 边总数
|
||||
- 记忆总数
|
||||
- 图密度
|
||||
|
||||
3. **🎨 节点类型图例**
|
||||
- 🔴 主体 (SUBJECT) - 记忆的主语
|
||||
- 🔵 主题 (TOPIC) - 动作或状态
|
||||
- 🟢 客体 (OBJECT) - 宾语
|
||||
- 🟠 属性 (ATTRIBUTE) - 延伸属性
|
||||
- 🟣 值 (VALUE) - 属性的具体值
|
||||
|
||||
4. **🔧 过滤器**
|
||||
- 勾选/取消勾选来显示/隐藏特定类型的节点
|
||||
- 实时更新图形
|
||||
|
||||
5. **ℹ️ 节点信息**
|
||||
- 点击任意节点查看详细信息
|
||||
- 显示节点类型、内容、创建时间等
|
||||
|
||||
### 右侧主区域
|
||||
|
||||
1. **控制按钮**
|
||||
- 🔄 刷新图形: 重新加载最新数据
|
||||
- 📐 适应窗口: 自动调整图形大小
|
||||
- 💾 导出数据: 下载JSON格式的图数据
|
||||
|
||||
2. **交互式图形**
|
||||
- **拖动节点**: 点击并拖动单个节点
|
||||
- **拖动画布**: 按住空白处拖动整个图形
|
||||
- **缩放**: 使用鼠标滚轮放大/缩小
|
||||
- **点击节点**: 查看详细信息
|
||||
- **物理模拟**: 节点会自动排列,避免重叠
|
||||
|
||||
## 🎮 操作技巧
|
||||
|
||||
### 查看特定类型的节点
|
||||
1. 在左侧过滤器中取消勾选不需要的类型
|
||||
2. 图形会自动更新,只显示选中的类型
|
||||
|
||||
### 查找特定记忆
|
||||
1. 在搜索框输入关键词(如: "小明", "吃饭")
|
||||
2. 点击"搜索"按钮
|
||||
3. 相关节点会被选中并自动聚焦
|
||||
|
||||
### 整理混乱的图形
|
||||
1. 点击"适应窗口"按钮
|
||||
2. 或者刷新页面重新初始化布局
|
||||
|
||||
### 导出数据进行分析
|
||||
1. 点击"导出数据"按钮
|
||||
2. JSON文件会自动下载
|
||||
3. 可以用于进一步的数据分析或备份
|
||||
|
||||
## 🎯 示例场景
|
||||
|
||||
### 场景1: 了解记忆图整体结构
|
||||
1. 启动可视化工具
|
||||
2. 观察不同颜色的节点分布
|
||||
3. 查看统计信息了解数量
|
||||
4. 使用过滤器逐个类型查看
|
||||
|
||||
### 场景2: 追踪特定主题的记忆
|
||||
1. 在搜索框输入主题关键词(如: "学习")
|
||||
2. 点击搜索
|
||||
3. 查看高亮的相关节点
|
||||
4. 点击节点查看详情
|
||||
|
||||
### 场景3: 调试记忆系统
|
||||
1. 创建一条新记忆
|
||||
2. 刷新可视化页面
|
||||
3. 查看新节点和边是否正确创建
|
||||
4. 验证节点类型和关系
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### Q: 页面显示空白或没有数据?
|
||||
**A**:
|
||||
1. 检查是否有记忆数据: 查看 `data/memory_graph/` 目录
|
||||
2. 确保记忆系统已启用: 检查 `config/bot_config.toml` 中 `[memory] enable = true`
|
||||
3. 尝试生成一些测试数据
|
||||
|
||||
### Q: 节点太多,看不清楚?
|
||||
**A**:
|
||||
1. 使用过滤器只显示某些类型
|
||||
2. 使用搜索功能定位特定节点
|
||||
3. 调整浏览器窗口大小,点击"适应窗口"
|
||||
|
||||
### Q: 如何更新数据?
|
||||
**A**:
|
||||
- **独立版**: 点击"刷新图形"或访问 `/api/reload`
|
||||
- **完整版**: 点击"刷新图形"会自动加载最新数据
|
||||
|
||||
### Q: 端口被占用怎么办?
|
||||
**A**: 修改启动脚本中的端口号:
|
||||
```python
|
||||
run_server(host='127.0.0.1', port=5002, debug=True) # 改为其他端口
|
||||
```
|
||||
|
||||
## 🎨 自定义配置
|
||||
|
||||
### 修改节点颜色
|
||||
|
||||
编辑 `templates/visualizer.html`,找到:
|
||||
|
||||
```javascript
|
||||
const nodeColors = {
|
||||
'SUBJECT': '#FF6B6B', // 改为你喜欢的颜色
|
||||
'TOPIC': '#4ECDC4',
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
### 修改物理引擎参数
|
||||
|
||||
在同一文件中找到 `physics` 配置:
|
||||
|
||||
```javascript
|
||||
physics: {
|
||||
barnesHut: {
|
||||
gravitationalConstant: -8000, // 调整引力
|
||||
springLength: 150, // 调整弹簧长度
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 修改数据加载限制
|
||||
|
||||
编辑对应的服务器文件,修改 `get_all_memories()` 的limit参数。
|
||||
|
||||
## 📝 文件结构
|
||||
|
||||
```
|
||||
tools/memory_visualizer/
|
||||
├── README.md # 详细文档
|
||||
├── requirements.txt # 依赖列表
|
||||
├── visualizer_server.py # 完整版服务器
|
||||
├── visualizer_simple.py # 独立版服务器 ⭐
|
||||
└── templates/
|
||||
└── visualizer.html # Web界面模板
|
||||
|
||||
run_visualizer.py # 快速启动脚本
|
||||
test_visualizer.py # 测试和演示脚本
|
||||
```
|
||||
|
||||
## 🚀 下一步
|
||||
|
||||
现在你可以:
|
||||
|
||||
1. ✅ 启动可视化工具查看现有数据
|
||||
2. ✅ 与Bot交互生成更多记忆
|
||||
3. ✅ 使用可视化工具验证记忆结构
|
||||
4. ✅ 根据需要自定义样式和配置
|
||||
|
||||
祝你使用愉快! 🎉
|
||||
|
||||
---
|
||||
|
||||
如有问题,请查看 `tools/memory_visualizer/README.md` 获取更多帮助。
|
||||
@@ -1,201 +0,0 @@
|
||||
# 🦊 记忆图可视化工具
|
||||
|
||||
一个交互式的 Web 可视化工具,用于查看和分析 MoFox Bot 的记忆图结构。
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
tools/memory_visualizer/
|
||||
├── visualizer.ps1 # 统一启动脚本(主入口)⭐
|
||||
├── visualizer_simple.py # 独立版服务器(推荐)
|
||||
├── visualizer_server.py # 完整版服务器
|
||||
├── generate_sample_data.py # 测试数据生成器
|
||||
├── test_visualizer.py # 测试脚本
|
||||
├── requirements.txt # Python 依赖
|
||||
├── templates/ # HTML 模板
|
||||
│ └── visualizer.html # 可视化界面
|
||||
├── docs/ # 文档目录
|
||||
│ ├── VISUALIZER_README.md
|
||||
│ ├── VISUALIZER_GUIDE.md
|
||||
│ └── VISUALIZER_INSTALL_COMPLETE.md
|
||||
├── README.md # 本文件
|
||||
├── QUICKSTART.md # 快速开始指南
|
||||
└── CHANGELOG.md # 更新日志
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 方式 1:交互式菜单(推荐)
|
||||
|
||||
```powershell
|
||||
# 在项目根目录运行
|
||||
.\visualizer.ps1
|
||||
|
||||
# 或在工具目录运行
|
||||
cd tools\memory_visualizer
|
||||
.\visualizer.ps1
|
||||
```
|
||||
|
||||
### 方式 2:命令行参数
|
||||
|
||||
```powershell
|
||||
# 启动独立版(推荐,快速)
|
||||
.\visualizer.ps1 -Simple
|
||||
|
||||
# 启动完整版(需要 MemoryManager)
|
||||
.\visualizer.ps1 -Full
|
||||
|
||||
# 生成测试数据
|
||||
.\visualizer.ps1 -Generate
|
||||
|
||||
# 运行测试
|
||||
.\visualizer.ps1 -Test
|
||||
|
||||
# 查看帮助
|
||||
.\visualizer.ps1 -Help
|
||||
```
|
||||
|
||||
## 📊 两个版本的区别
|
||||
|
||||
### 独立版(Simple)- 推荐
|
||||
- ✅ **快速启动**:直接读取数据文件,无需初始化 MemoryManager
|
||||
- ✅ **轻量级**:只依赖 Flask 和 vis.js
|
||||
- ✅ **稳定**:不依赖主系统运行状态
|
||||
- 📌 **端口**:5001
|
||||
- 📁 **数据源**:`data/memory_graph/*.json`
|
||||
|
||||
### 完整版(Full)
|
||||
- 🔄 **实时数据**:使用 MemoryManager 获取最新数据
|
||||
- 🔌 **集成**:与主系统深度集成
|
||||
- ⚡ **功能完整**:支持所有高级功能
|
||||
- 📌 **端口**:5000
|
||||
- 📁 **数据源**:MemoryManager
|
||||
|
||||
## ✨ 主要功能
|
||||
|
||||
1. **交互式图形可视化**
|
||||
- 🎨 5 种节点类型(主体、主题、客体、属性、值)
|
||||
- 🔗 完整路径高亮显示
|
||||
- 🔍 点击节点查看连接关系
|
||||
- 📐 自动布局和缩放
|
||||
|
||||
2. **高级筛选**
|
||||
- ☑️ 按节点类型筛选
|
||||
- 🔎 关键词搜索
|
||||
- 📊 统计信息实时更新
|
||||
|
||||
3. **智能高亮**
|
||||
- 💡 点击节点高亮所有连接路径(递归探索)
|
||||
- 👻 无关节点变为半透明
|
||||
- 🎯 自动聚焦到相关子图
|
||||
|
||||
4. **物理引擎优化**
|
||||
- 🚀 智能布局算法
|
||||
- ⏱️ 自动停止防止持续运行
|
||||
- 🔄 筛选后自动重新布局
|
||||
|
||||
5. **数据管理**
|
||||
- 📂 多文件选择器
|
||||
- 💾 导出图形数据
|
||||
- 🔄 实时刷新
|
||||
|
||||
## 🔧 依赖安装
|
||||
|
||||
脚本会自动检查并安装依赖,也可以手动安装:
|
||||
|
||||
```powershell
|
||||
# 激活虚拟环境
|
||||
.\.venv\Scripts\Activate.ps1
|
||||
|
||||
# 安装依赖
|
||||
pip install -r tools/memory_visualizer/requirements.txt
|
||||
```
|
||||
|
||||
**所需依赖:**
|
||||
- Flask >= 2.3.0
|
||||
- flask-cors >= 4.0.0
|
||||
|
||||
## 📖 使用说明
|
||||
|
||||
### 1. 查看记忆图
|
||||
1. 启动服务器(推荐独立版)
|
||||
2. 在浏览器打开 http://127.0.0.1:5001
|
||||
3. 等待数据加载完成
|
||||
|
||||
### 2. 探索连接关系
|
||||
1. **点击节点**:查看与该节点相关的所有连接路径
|
||||
2. **点击空白处**:恢复所有节点显示
|
||||
3. **使用筛选器**:按类型过滤节点
|
||||
|
||||
### 3. 搜索记忆
|
||||
1. 在搜索框输入关键词
|
||||
2. 点击搜索按钮
|
||||
3. 相关节点会自动高亮
|
||||
|
||||
### 4. 查看统计
|
||||
- 左侧面板显示实时统计信息
|
||||
- 节点数、边数、记忆数
|
||||
- 图密度等指标
|
||||
|
||||
## 🎨 节点颜色说明
|
||||
|
||||
- 🔴 **主体(SUBJECT)**:红色 (#FF6B6B)
|
||||
- 🔵 **主题(TOPIC)**:青色 (#4ECDC4)
|
||||
- 🟦 **客体(OBJECT)**:蓝色 (#45B7D1)
|
||||
- 🟠 **属性(ATTRIBUTE)**:橙色 (#FFA07A)
|
||||
- 🟢 **值(VALUE)**:绿色 (#98D8C8)
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### 问题 1:没有数据显示
|
||||
**解决方案:**
|
||||
1. 检查 `data/memory_graph/` 目录是否存在数据文件
|
||||
2. 运行 `.\visualizer.ps1 -Generate` 生成测试数据
|
||||
3. 确保 Bot 已经运行过并生成了记忆数据
|
||||
|
||||
### 问题 2:物理引擎一直运行
|
||||
**解决方案:**
|
||||
- 新版本已修复此问题
|
||||
- 物理引擎会在稳定后自动停止(最多 5 秒)
|
||||
|
||||
### 问题 3:筛选后节点排版错乱
|
||||
**解决方案:**
|
||||
- 新版本已修复此问题
|
||||
- 筛选后会自动重新布局
|
||||
|
||||
### 问题 4:无法查看完整连接路径
|
||||
**解决方案:**
|
||||
- 新版本使用 BFS 算法递归探索所有连接
|
||||
- 点击节点即可查看完整路径
|
||||
|
||||
## 📝 开发说明
|
||||
|
||||
### 添加新功能
|
||||
1. 编辑 `visualizer_simple.py` 或 `visualizer_server.py`
|
||||
2. 修改 `templates/visualizer.html` 更新界面
|
||||
3. 更新 `requirements.txt` 添加新依赖
|
||||
4. 运行测试:`.\visualizer.ps1 -Test`
|
||||
|
||||
### 调试
|
||||
```powershell
|
||||
# 启动 Flask 调试模式
|
||||
$env:FLASK_DEBUG = "1"
|
||||
python tools/memory_visualizer/visualizer_simple.py
|
||||
```
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [快速开始指南](QUICKSTART.md)
|
||||
- [更新日志](CHANGELOG.md)
|
||||
- [详细使用指南](docs/VISUALIZER_GUIDE.md)
|
||||
|
||||
## 🆘 获取帮助
|
||||
|
||||
遇到问题?
|
||||
1. 查看 [常见问题](#常见问题)
|
||||
2. 运行 `.\visualizer.ps1 -Help` 查看帮助
|
||||
3. 查看项目文档目录
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
与 MoFox Bot 主项目相同
|
||||
@@ -1,163 +0,0 @@
|
||||
# 🦊 MoFox Bot 记忆图可视化工具
|
||||
|
||||
这是一个交互式的Web界面,用于可视化和探索MoFox Bot的记忆图结构。
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
- **交互式图形可视化**: 使用Vis.js展示节点和边的关系
|
||||
- **实时数据**: 直接从记忆管理器读取最新数据
|
||||
- **节点类型分类**: 不同颜色区分不同类型的节点
|
||||
- 🔴 主体 (SUBJECT)
|
||||
- 🔵 主题 (TOPIC)
|
||||
- 🟢 客体 (OBJECT)
|
||||
- 🟠 属性 (ATTRIBUTE)
|
||||
- 🟣 值 (VALUE)
|
||||
- **搜索功能**: 快速查找相关记忆
|
||||
- **过滤器**: 按节点类型过滤显示
|
||||
- **统计信息**: 实时显示图的统计数据
|
||||
- **节点详情**: 点击节点查看详细信息
|
||||
- **自由缩放拖动**: 支持图形的交互式操作
|
||||
- **数据导出**: 导出当前图形数据为JSON
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
pip install flask flask-cors
|
||||
```
|
||||
|
||||
### 2. 启动服务器
|
||||
|
||||
在项目根目录运行:
|
||||
|
||||
```bash
|
||||
python tools/memory_visualizer/visualizer_server.py
|
||||
```
|
||||
|
||||
或者使用便捷脚本:
|
||||
|
||||
```bash
|
||||
python run_visualizer.py
|
||||
```
|
||||
|
||||
### 3. 打开浏览器
|
||||
|
||||
访问: http://127.0.0.1:5000
|
||||
|
||||
## 📊 界面说明
|
||||
|
||||
### 主界面布局
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ 侧边栏 │ 主内容区 │
|
||||
│ - 搜索框 │ - 控制按钮 │
|
||||
│ - 统计信息 │ - 图形显示 │
|
||||
│ - 节点类型图例 │ │
|
||||
│ - 过滤器 │ │
|
||||
│ - 节点详情 │ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 操作说明
|
||||
|
||||
- **🔍 搜索**: 在搜索框输入关键词,点击"搜索"按钮查找相关记忆
|
||||
- **🔄 刷新图形**: 重新加载最新的记忆图数据
|
||||
- **📐 适应窗口**: 自动调整图形大小以适应窗口
|
||||
- **💾 导出数据**: 将当前图形数据导出为JSON文件
|
||||
- **✅ 过滤器**: 勾选/取消勾选不同类型的节点来过滤显示
|
||||
- **👆 点击节点**: 点击任意节点查看详细信息
|
||||
- **🖱️ 拖动**: 按住鼠标拖动节点或整个图形
|
||||
- **🔍 缩放**: 使用鼠标滚轮缩放图形
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 修改服务器配置
|
||||
|
||||
在 `visualizer_server.py` 的最后:
|
||||
|
||||
```python
|
||||
if __name__ == '__main__':
|
||||
run_server(
|
||||
host='127.0.0.1', # 监听地址
|
||||
port=5000, # 端口号
|
||||
debug=True # 调试模式
|
||||
)
|
||||
```
|
||||
|
||||
### API端点
|
||||
|
||||
- `GET /` - 主页面
|
||||
- `GET /api/graph/full` - 获取完整记忆图数据
|
||||
- `GET /api/memory/<memory_id>` - 获取特定记忆详情
|
||||
- `GET /api/search?q=<query>&limit=<n>` - 搜索记忆
|
||||
- `GET /api/stats` - 获取统计信息
|
||||
|
||||
## 📝 技术栈
|
||||
|
||||
- **后端**: Flask (Python Web框架)
|
||||
- **前端**:
|
||||
- Vis.js (图形可视化库)
|
||||
- 原生JavaScript
|
||||
- CSS3 (渐变、动画、响应式布局)
|
||||
- **数据**: 直接从MoFox Bot记忆管理器读取
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 问题: 无法启动服务器
|
||||
|
||||
**原因**: 记忆系统未启用或配置错误
|
||||
|
||||
**解决**: 检查 `config/bot_config.toml` 确保:
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
enable = true
|
||||
data_dir = "data/memory_graph"
|
||||
```
|
||||
|
||||
### 问题: 图形显示空白
|
||||
|
||||
**原因**: 没有记忆数据
|
||||
|
||||
**解决**:
|
||||
1. 先运行Bot让其生成一些记忆
|
||||
2. 或者运行测试脚本生成测试数据
|
||||
|
||||
### 问题: 节点太多,图形混乱
|
||||
|
||||
**解决**:
|
||||
1. 使用过滤器只显示某些类型的节点
|
||||
2. 使用搜索功能定位特定记忆
|
||||
3. 调整物理引擎参数(在visualizer.html中)
|
||||
|
||||
## 🎨 自定义样式
|
||||
|
||||
修改 `templates/visualizer.html` 中的样式定义:
|
||||
|
||||
```javascript
|
||||
const nodeColors = {
|
||||
'SUBJECT': '#FF6B6B', // 主体颜色
|
||||
'TOPIC': '#4ECDC4', // 主题颜色
|
||||
'OBJECT': '#45B7D1', // 客体颜色
|
||||
'ATTRIBUTE': '#FFA07A', // 属性颜色
|
||||
'VALUE': '#98D8C8' // 值颜色
|
||||
};
|
||||
```
|
||||
|
||||
## 📈 性能优化
|
||||
|
||||
对于大型图形(>1000节点):
|
||||
|
||||
1. **禁用物理引擎**: 在stabilization完成后自动禁用
|
||||
2. **限制显示节点**: 使用过滤器或搜索
|
||||
3. **分页加载**: 修改API使用分页
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交Issue和Pull Request!
|
||||
|
||||
## 📄 许可
|
||||
|
||||
与MoFox Bot主项目相同的许可证
|
||||
@@ -1,210 +0,0 @@
|
||||
# ✅ 记忆图可视化工具 - 安装完成
|
||||
|
||||
## 🎉 恭喜!可视化工具已成功创建!
|
||||
|
||||
---
|
||||
|
||||
## 📦 已创建的文件
|
||||
|
||||
```
|
||||
Bot/
|
||||
├── visualizer.ps1 ⭐⭐⭐ # 统一启动脚本 (推荐使用)
|
||||
├── start_visualizer.ps1 # 独立版快速启动
|
||||
├── start_visualizer.bat # CMD版启动脚本
|
||||
├── generate_sample_data.py # 示例数据生成器
|
||||
├── VISUALIZER_README.md ⭐ # 快速参考指南
|
||||
├── VISUALIZER_GUIDE.md # 完整使用指南
|
||||
└── tools/memory_visualizer/
|
||||
├── visualizer_simple.py ⭐ # 独立版服务器 (推荐)
|
||||
├── visualizer_server.py # 完整版服务器
|
||||
├── README.md # 详细文档
|
||||
├── QUICKSTART.md # 快速入门
|
||||
├── CHANGELOG.md # 更新日志
|
||||
└── templates/
|
||||
└── visualizer.html ⭐ # 精美Web界面
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 立即开始 (3秒)
|
||||
|
||||
### 方法 1: 使用统一启动脚本 (最简单 ⭐⭐⭐)
|
||||
|
||||
```powershell
|
||||
.\visualizer.ps1
|
||||
```
|
||||
|
||||
然后按提示选择:
|
||||
- **1** = 独立版 (推荐,快速)
|
||||
- **2** = 完整版 (实时数据)
|
||||
- **3** = 生成示例数据
|
||||
|
||||
### 方法 2: 直接启动
|
||||
|
||||
```powershell
|
||||
# 如果还没有数据,先生成
|
||||
.\.venv\Scripts\python.exe generate_sample_data.py
|
||||
|
||||
# 启动可视化
|
||||
.\start_visualizer.ps1
|
||||
|
||||
# 打开浏览器
|
||||
# http://127.0.0.1:5001
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 功能亮点
|
||||
|
||||
### ✨ 核心功能
|
||||
- 🎯 **交互式图形**: 拖动、缩放、点击
|
||||
- 🎨 **颜色分类**: 5种节点类型自动上色
|
||||
- 🔍 **智能搜索**: 快速定位相关记忆
|
||||
- 🔧 **灵活过滤**: 按节点类型筛选
|
||||
- 📊 **实时统计**: 节点、边、记忆数量
|
||||
- 💾 **数据导出**: JSON格式导出
|
||||
|
||||
### 📂 独立版特色 (推荐)
|
||||
- ⚡ **秒速启动**: 2秒内完成
|
||||
- 📁 **文件切换**: 浏览所有历史数据
|
||||
- 🔄 **自动搜索**: 智能查找数据文件
|
||||
- 💚 **低资源**: 占用资源极少
|
||||
|
||||
### 🔥 完整版特色
|
||||
- 🔴 **实时数据**: 与Bot同步
|
||||
- 🔄 **自动更新**: 无需刷新
|
||||
- 🛠️ **完整功能**: 使用全部API
|
||||
|
||||
---
|
||||
|
||||
## 📊 界面预览
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 侧边栏 │ 主区域 │
|
||||
│ ┌─────────────────────┐ │ ┌───────────────────────┐ │
|
||||
│ │ 📂 数据文件 │ │ │ 🔄 📐 💾 控制按钮 │ │
|
||||
│ │ [选择] [刷新] │ │ └───────────────────────┘ │
|
||||
│ │ 📄 当前: xxx.json │ │ ┌───────────────────────┐ │
|
||||
│ └─────────────────────┘ │ │ │ │
|
||||
│ │ │ 交互式图形可视化 │ │
|
||||
│ ┌─────────────────────┐ │ │ │ │
|
||||
│ │ 🔍 搜索记忆 │ │ │ 🔴 主体 🔵 主题 │ │
|
||||
│ │ [...........] [搜索] │ │ │ 🟢 客体 🟠 属性 │ │
|
||||
│ └─────────────────────┘ │ │ 🟣 值 │ │
|
||||
│ │ │ │ │
|
||||
│ 📊 统计: 12节点 15边 │ │ 可拖动、缩放、点击 │ │
|
||||
│ │ │ │ │
|
||||
│ 🎨 节点类型图例 │ └───────────────────────┘ │
|
||||
│ 🔧 过滤器 │ │
|
||||
│ ℹ️ 节点信息 │ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 快速命令
|
||||
|
||||
```powershell
|
||||
# 统一启动 (推荐)
|
||||
.\visualizer.ps1
|
||||
|
||||
# 生成示例数据
|
||||
.\.venv\Scripts\python.exe generate_sample_data.py
|
||||
|
||||
# 独立版 (端口 5001)
|
||||
.\start_visualizer.ps1
|
||||
|
||||
# 完整版 (端口 5000)
|
||||
.\.venv\Scripts\python.exe tools/memory_visualizer/visualizer_server.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 文档索引
|
||||
|
||||
### 快速参考 (必读 ⭐)
|
||||
- **VISUALIZER_README.md** - 快速参考卡片
|
||||
- **VISUALIZER_GUIDE.md** - 完整使用指南
|
||||
|
||||
### 详细文档
|
||||
- **tools/memory_visualizer/README.md** - 技术文档
|
||||
- **tools/memory_visualizer/QUICKSTART.md** - 快速入门
|
||||
- **tools/memory_visualizer/CHANGELOG.md** - 版本历史
|
||||
|
||||
---
|
||||
|
||||
## 💡 使用建议
|
||||
|
||||
### 🎯 对于首次使用者
|
||||
1. 运行 `.\visualizer.ps1`
|
||||
2. 选择 `3` 生成示例数据
|
||||
3. 选择 `1` 启动独立版
|
||||
4. 打开浏览器访问 http://127.0.0.1:5001
|
||||
5. 开始探索!
|
||||
|
||||
### 🔧 对于开发者
|
||||
1. 运行Bot积累真实数据
|
||||
2. 启动完整版可视化: `.\visualizer.ps1` → `2`
|
||||
3. 实时查看记忆图变化
|
||||
4. 调试和优化
|
||||
|
||||
### 📊 对于数据分析
|
||||
1. 使用独立版查看历史数据
|
||||
2. 切换不同时期的数据文件
|
||||
3. 使用搜索和过滤功能
|
||||
4. 导出数据进行分析
|
||||
|
||||
---
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### Q: 未找到数据文件?
|
||||
**A**: 运行 `.\visualizer.ps1` 选择 `3` 生成示例数据
|
||||
|
||||
### Q: 端口被占用?
|
||||
**A**: 修改对应服务器文件中的端口号,或关闭占用端口的程序
|
||||
|
||||
### Q: 两个版本有什么区别?
|
||||
**A**:
|
||||
- **独立版**: 快速,读文件,可切换,推荐日常使用
|
||||
- **完整版**: 实时,用内存,完整功能,推荐开发调试
|
||||
|
||||
### Q: 图形显示混乱?
|
||||
**A**:
|
||||
1. 使用过滤器减少节点
|
||||
2. 点击"适应窗口"
|
||||
3. 刷新页面
|
||||
|
||||
---
|
||||
|
||||
## 🎉 开始使用
|
||||
|
||||
### 立即启动
|
||||
```powershell
|
||||
.\visualizer.ps1
|
||||
```
|
||||
|
||||
### 访问地址
|
||||
- 独立版: http://127.0.0.1:5001
|
||||
- 完整版: http://127.0.0.1:5000
|
||||
|
||||
---
|
||||
|
||||
## 🤝 反馈与支持
|
||||
|
||||
如有问题或建议,请查看:
|
||||
- 📖 `VISUALIZER_GUIDE.md` - 完整使用指南
|
||||
- 📝 `tools/memory_visualizer/README.md` - 技术文档
|
||||
|
||||
---
|
||||
|
||||
## 🌟 特别感谢
|
||||
|
||||
感谢你使用 MoFox Bot 记忆图可视化工具!
|
||||
|
||||
**享受探索记忆图的乐趣!** 🚀🦊
|
||||
|
||||
---
|
||||
|
||||
_最后更新: 2025-11-06_
|
||||
@@ -1,159 +0,0 @@
|
||||
# 🎯 记忆图可视化工具 - 快速参考
|
||||
|
||||
## 🚀 快速启动
|
||||
|
||||
### 推荐方式 (交互式菜单)
|
||||
```powershell
|
||||
.\visualizer.ps1
|
||||
```
|
||||
|
||||
然后选择:
|
||||
- **选项 1**: 独立版 (快速,推荐) ⭐
|
||||
- **选项 2**: 完整版 (实时数据)
|
||||
- **选项 3**: 生成示例数据
|
||||
|
||||
---
|
||||
|
||||
## 📋 各版本对比
|
||||
|
||||
| 特性 | 独立版 ⭐ | 完整版 |
|
||||
|------|---------|--------|
|
||||
| **启动速度** | 🚀 快速 (2秒) | ⏱️ 较慢 (5-10秒) |
|
||||
| **数据源** | 📂 文件 | 💾 内存 (实时) |
|
||||
| **文件切换** | ✅ 支持 | ❌ 不支持 |
|
||||
| **资源占用** | 💚 低 | 💛 中等 |
|
||||
| **端口** | 5001 | 5000 |
|
||||
| **适用场景** | 查看历史数据、调试 | 实时监控、开发 |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 手动启动命令
|
||||
|
||||
### 独立版 (推荐)
|
||||
```powershell
|
||||
# Windows
|
||||
.\start_visualizer.ps1
|
||||
|
||||
# 或直接运行
|
||||
.\.venv\Scripts\python.exe tools/memory_visualizer/visualizer_simple.py
|
||||
```
|
||||
访问: http://127.0.0.1:5001
|
||||
|
||||
### 完整版
|
||||
```powershell
|
||||
.\.venv\Scripts\python.exe tools/memory_visualizer/visualizer_server.py
|
||||
```
|
||||
访问: http://127.0.0.1:5000
|
||||
|
||||
### 生成示例数据
|
||||
```powershell
|
||||
.\.venv\Scripts\python.exe generate_sample_data.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 功能一览
|
||||
|
||||
### 🎨 可视化功能
|
||||
- ✅ 交互式图形 (拖动、缩放、点击)
|
||||
- ✅ 节点类型颜色分类
|
||||
- ✅ 实时搜索和过滤
|
||||
- ✅ 统计信息展示
|
||||
- ✅ 节点详情查看
|
||||
|
||||
### 📂 数据管理
|
||||
- ✅ 自动搜索数据文件
|
||||
- ✅ 多文件切换 (独立版)
|
||||
- ✅ 数据导出 (JSON格式)
|
||||
- ✅ 文件信息显示
|
||||
|
||||
---
|
||||
|
||||
## 🎯 使用场景
|
||||
|
||||
### 1️⃣ 首次使用
|
||||
```powershell
|
||||
# 1. 生成示例数据
|
||||
.\visualizer.ps1
|
||||
# 选择: 3
|
||||
|
||||
# 2. 启动可视化
|
||||
.\visualizer.ps1
|
||||
# 选择: 1
|
||||
|
||||
# 3. 打开浏览器
|
||||
# 访问: http://127.0.0.1:5001
|
||||
```
|
||||
|
||||
### 2️⃣ 查看实际数据
|
||||
```powershell
|
||||
# 先运行Bot生成记忆
|
||||
# 然后启动可视化
|
||||
.\visualizer.ps1
|
||||
# 选择: 1 (独立版) 或 2 (完整版)
|
||||
```
|
||||
|
||||
### 3️⃣ 调试记忆系统
|
||||
```powershell
|
||||
# 使用完整版,实时查看变化
|
||||
.\visualizer.ps1
|
||||
# 选择: 2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### ❌ 问题: 未找到数据文件
|
||||
**解决**:
|
||||
```powershell
|
||||
.\visualizer.ps1
|
||||
# 选择 3 生成示例数据
|
||||
```
|
||||
|
||||
### ❌ 问题: 端口被占用
|
||||
**解决**:
|
||||
- 独立版: 修改 `visualizer_simple.py` 中的 `port=5001`
|
||||
- 完整版: 修改 `visualizer_server.py` 中的 `port=5000`
|
||||
|
||||
### ❌ 问题: 数据加载失败
|
||||
**可能原因**:
|
||||
- 数据文件格式不正确
|
||||
- 文件损坏
|
||||
|
||||
**解决**:
|
||||
1. 检查 `data/memory_graph/` 目录
|
||||
2. 重新生成示例数据
|
||||
3. 查看终端错误信息
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- **完整指南**: `VISUALIZER_GUIDE.md`
|
||||
- **快速入门**: `tools/memory_visualizer/QUICKSTART.md`
|
||||
- **详细文档**: `tools/memory_visualizer/README.md`
|
||||
- **更新日志**: `tools/memory_visualizer/CHANGELOG.md`
|
||||
|
||||
---
|
||||
|
||||
## 💡 提示
|
||||
|
||||
1. **首次使用**: 先生成示例数据 (选项 3)
|
||||
2. **查看历史**: 使用独立版,可以切换不同数据文件
|
||||
3. **实时监控**: 使用完整版,与Bot同时运行
|
||||
4. **性能优化**: 大型图使用过滤器和搜索
|
||||
5. **快捷键**:
|
||||
- `Ctrl + 滚轮`: 缩放
|
||||
- 拖动空白: 移动画布
|
||||
- 点击节点: 查看详情
|
||||
|
||||
---
|
||||
|
||||
## 🎉 开始探索!
|
||||
|
||||
```powershell
|
||||
.\visualizer.ps1
|
||||
```
|
||||
|
||||
享受你的记忆图之旅!🚀🦊
|
||||
@@ -1,9 +0,0 @@
|
||||
# 记忆图可视化工具依赖
|
||||
|
||||
# Web框架
|
||||
flask>=2.3.0
|
||||
flask-cors>=4.0.0
|
||||
|
||||
# 其他依赖由主项目提供
|
||||
# - src.memory_graph
|
||||
# - src.config
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
记忆图可视化工具启动脚本
|
||||
|
||||
快速启动记忆图可视化Web服务器
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到路径
|
||||
project_root = Path(__file__).parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from tools.memory_visualizer.visualizer_server import run_server
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 60)
|
||||
print("🦊 MoFox Bot - 记忆图可视化工具")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print("📊 启动可视化服务器...")
|
||||
print("🌐 访问地址: http://127.0.0.1:5000")
|
||||
print("⏹️ 按 Ctrl+C 停止服务器")
|
||||
print()
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
run_server(
|
||||
host='127.0.0.1',
|
||||
port=5000,
|
||||
debug=True
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n👋 服务器已停止")
|
||||
except Exception as e:
|
||||
print(f"\n❌ 启动失败: {e}")
|
||||
sys.exit(1)
|
||||
@@ -1,39 +0,0 @@
|
||||
"""
|
||||
快速启动脚本 - 记忆图可视化工具 (独立版)
|
||||
|
||||
使用说明:
|
||||
1. 直接运行此脚本启动可视化服务器
|
||||
2. 工具会自动搜索可用的数据文件
|
||||
3. 如果找到多个文件,会使用最新的文件
|
||||
4. 你也可以在Web界面中选择其他文件
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录
|
||||
project_root = Path(__file__).parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 70)
|
||||
print("🦊 MoFox Bot - 记忆图可视化工具 (独立版)")
|
||||
print("=" * 70)
|
||||
print()
|
||||
print("✨ 特性:")
|
||||
print(" • 自动搜索可用的数据文件")
|
||||
print(" • 支持在Web界面中切换文件")
|
||||
print(" • 快速启动,无需完整初始化")
|
||||
print()
|
||||
print("=" * 70)
|
||||
|
||||
try:
|
||||
from tools.memory_visualizer.visualizer_simple import run_server
|
||||
run_server(host='127.0.0.1', port=5001, debug=True)
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n👋 服务器已停止")
|
||||
except Exception as e:
|
||||
print(f"\n❌ 启动失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@@ -1,53 +0,0 @@
|
||||
@echo off
|
||||
REM 记忆图可视化工具启动脚本 - CMD版本
|
||||
|
||||
echo ======================================================================
|
||||
echo 🦊 MoFox Bot - 记忆图可视化工具
|
||||
echo ======================================================================
|
||||
echo.
|
||||
|
||||
REM 检查虚拟环境
|
||||
set VENV_PYTHON=.venv\Scripts\python.exe
|
||||
if not exist "%VENV_PYTHON%" (
|
||||
echo ❌ 未找到虚拟环境: %VENV_PYTHON%
|
||||
echo.
|
||||
echo 请先创建虚拟环境:
|
||||
echo python -m venv .venv
|
||||
echo .venv\Scripts\activate.bat
|
||||
echo pip install -r requirements.txt
|
||||
echo.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo ✅ 使用虚拟环境: %VENV_PYTHON%
|
||||
echo.
|
||||
|
||||
REM 检查依赖
|
||||
echo 🔍 检查依赖...
|
||||
"%VENV_PYTHON%" -c "import flask; import flask_cors" 2>nul
|
||||
if errorlevel 1 (
|
||||
echo ⚠️ 缺少依赖,正在安装...
|
||||
"%VENV_PYTHON%" -m pip install flask flask-cors --quiet
|
||||
if errorlevel 1 (
|
||||
echo ❌ 安装依赖失败
|
||||
exit /b 1
|
||||
)
|
||||
echo ✅ 依赖安装完成
|
||||
)
|
||||
|
||||
echo ✅ 依赖检查完成
|
||||
echo.
|
||||
|
||||
REM 显示信息
|
||||
echo 📊 启动可视化服务器...
|
||||
echo 🌐 访问地址: http://127.0.0.1:5001
|
||||
echo ⏹️ 按 Ctrl+C 停止服务器
|
||||
echo.
|
||||
echo ======================================================================
|
||||
echo.
|
||||
|
||||
REM 启动服务器
|
||||
"%VENV_PYTHON%" "tools\memory_visualizer\visualizer_simple.py"
|
||||
|
||||
echo.
|
||||
echo 👋 服务器已停止
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# 记忆图可视化工具启动脚本 - PowerShell版本
|
||||
|
||||
Write-Host "=" -NoNewline -ForegroundColor Cyan
|
||||
Write-Host ("=" * 69) -ForegroundColor Cyan
|
||||
Write-Host "🦊 MoFox Bot - 记忆图可视化工具" -ForegroundColor Yellow
|
||||
Write-Host "=" -NoNewline -ForegroundColor Cyan
|
||||
Write-Host ("=" * 69) -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# 检查虚拟环境
|
||||
$venvPath = ".venv\Scripts\python.exe"
|
||||
if (-not (Test-Path $venvPath)) {
|
||||
Write-Host "❌ 未找到虚拟环境: $venvPath" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
Write-Host "请先创建虚拟环境:" -ForegroundColor Yellow
|
||||
Write-Host " python -m venv .venv" -ForegroundColor Cyan
|
||||
Write-Host " .\.venv\Scripts\Activate.ps1" -ForegroundColor Cyan
|
||||
Write-Host " pip install -r requirements.txt" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "✅ 使用虚拟环境: $venvPath" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# 检查依赖
|
||||
Write-Host "🔍 检查依赖..." -ForegroundColor Cyan
|
||||
& $venvPath -c "import flask; import flask_cors" 2>$null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "⚠️ 缺少依赖,正在安装..." -ForegroundColor Yellow
|
||||
& $venvPath -m pip install flask flask-cors --quiet
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "❌ 安装依赖失败" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host "✅ 依赖安装完成" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host "✅ 依赖检查完成" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# 显示信息
|
||||
Write-Host "📊 启动可视化服务器..." -ForegroundColor Cyan
|
||||
Write-Host "🌐 访问地址: " -NoNewline -ForegroundColor White
|
||||
Write-Host "http://127.0.0.1:5001" -ForegroundColor Blue
|
||||
Write-Host "⏹️ 按 Ctrl+C 停止服务器" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "=" -NoNewline -ForegroundColor Cyan
|
||||
Write-Host ("=" * 69) -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# 启动服务器
|
||||
try {
|
||||
& $venvPath "tools\memory_visualizer\visualizer_simple.py"
|
||||
}
|
||||
catch {
|
||||
Write-Host ""
|
||||
Write-Host "❌ 启动失败: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
finally {
|
||||
Write-Host ""
|
||||
Write-Host "👋 服务器已停止" -ForegroundColor Yellow
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 记忆图可视化工具启动脚本 - Bash版本 (Linux/Mac)
|
||||
|
||||
echo "======================================================================"
|
||||
echo "🦊 MoFox Bot - 记忆图可视化工具"
|
||||
echo "======================================================================"
|
||||
echo ""
|
||||
|
||||
# 检查虚拟环境
|
||||
VENV_PYTHON=".venv/bin/python"
|
||||
if [ ! -f "$VENV_PYTHON" ]; then
|
||||
echo "❌ 未找到虚拟环境: $VENV_PYTHON"
|
||||
echo ""
|
||||
echo "请先创建虚拟环境:"
|
||||
echo " python -m venv .venv"
|
||||
echo " source .venv/bin/activate"
|
||||
echo " pip install -r requirements.txt"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ 使用虚拟环境: $VENV_PYTHON"
|
||||
echo ""
|
||||
|
||||
# 检查依赖
|
||||
echo "🔍 检查依赖..."
|
||||
$VENV_PYTHON -c "import flask; import flask_cors" 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "⚠️ 缺少依赖,正在安装..."
|
||||
$VENV_PYTHON -m pip install flask flask-cors --quiet
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ 安装依赖失败"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ 依赖安装完成"
|
||||
fi
|
||||
|
||||
echo "✅ 依赖检查完成"
|
||||
echo ""
|
||||
|
||||
# 显示信息
|
||||
echo "📊 启动可视化服务器..."
|
||||
echo "🌐 访问地址: http://127.0.0.1:5001"
|
||||
echo "⏹️ 按 Ctrl+C 停止服务器"
|
||||
echo ""
|
||||
echo "======================================================================"
|
||||
echo ""
|
||||
|
||||
# 启动服务器
|
||||
$VENV_PYTHON "tools/memory_visualizer/visualizer_simple.py"
|
||||
|
||||
echo ""
|
||||
echo "👋 服务器已停止"
|
||||
@@ -1,59 +0,0 @@
|
||||
# 记忆图可视化工具统一启动脚本
|
||||
param(
|
||||
[switch]$Simple,
|
||||
[switch]$Full,
|
||||
[switch]$Generate,
|
||||
[switch]$Test
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$ProjectRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
|
||||
Set-Location $ProjectRoot
|
||||
|
||||
function Get-Python {
|
||||
$paths = @(".venv\Scripts\python.exe", "venv\Scripts\python.exe")
|
||||
foreach ($p in $paths) {
|
||||
if (Test-Path $p) { return $p }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
$python = Get-Python
|
||||
if (-not $python) {
|
||||
Write-Host "ERROR: Virtual environment not found" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Simple) {
|
||||
Write-Host "Starting Simple Server on http://127.0.0.1:5001" -ForegroundColor Green
|
||||
& $python "$ScriptDir\visualizer_simple.py"
|
||||
}
|
||||
elseif ($Full) {
|
||||
Write-Host "Starting Full Server on http://127.0.0.1:5000" -ForegroundColor Green
|
||||
& $python "$ScriptDir\visualizer_server.py"
|
||||
}
|
||||
elseif ($Generate) {
|
||||
& $python "$ScriptDir\generate_sample_data.py"
|
||||
}
|
||||
elseif ($Test) {
|
||||
& $python "$ScriptDir\test_visualizer.py"
|
||||
}
|
||||
else {
|
||||
Write-Host "MoFox Bot - Memory Graph Visualizer" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "[1] Start Simple Server (Recommended)"
|
||||
Write-Host "[2] Start Full Server"
|
||||
Write-Host "[3] Generate Test Data"
|
||||
Write-Host "[4] Run Tests"
|
||||
Write-Host "[Q] Quit"
|
||||
Write-Host ""
|
||||
$choice = Read-Host "Select"
|
||||
|
||||
switch ($choice) {
|
||||
"1" { & $python "$ScriptDir\visualizer_simple.py" }
|
||||
"2" { & $python "$ScriptDir\visualizer_server.py" }
|
||||
"3" { & $python "$ScriptDir\generate_sample_data.py" }
|
||||
"4" { & $python "$ScriptDir\test_visualizer.py" }
|
||||
default { exit 0 }
|
||||
}
|
||||
}
|
||||
@@ -1,356 +0,0 @@
|
||||
"""
|
||||
记忆图可视化服务器
|
||||
|
||||
提供 Web API 用于可视化记忆图数据
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import orjson
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from flask import Flask, jsonify, render_template, request
|
||||
from flask_cors import CORS
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
import sys
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.memory_graph.manager import MemoryManager
|
||||
from src.memory_graph.models import EdgeType, MemoryType, NodeType
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app) # 允许跨域请求
|
||||
|
||||
# 全局记忆管理器
|
||||
memory_manager: Optional[MemoryManager] = None
|
||||
|
||||
|
||||
def init_memory_manager():
|
||||
"""初始化记忆管理器"""
|
||||
global memory_manager
|
||||
if memory_manager is None:
|
||||
try:
|
||||
memory_manager = MemoryManager()
|
||||
# 在新的事件循环中初始化
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(memory_manager.initialize())
|
||||
logger.info("记忆管理器初始化成功")
|
||||
except Exception as e:
|
||||
logger.error(f"初始化记忆管理器失败: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""主页面"""
|
||||
return render_template('visualizer.html')
|
||||
|
||||
|
||||
@app.route('/api/graph/full')
|
||||
def get_full_graph():
|
||||
"""
|
||||
获取完整记忆图数据
|
||||
|
||||
返回所有节点和边,格式化为前端可用的结构
|
||||
"""
|
||||
try:
|
||||
if memory_manager is None:
|
||||
init_memory_manager()
|
||||
|
||||
# 获取所有记忆
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# 获取所有记忆
|
||||
all_memories = memory_manager.graph_store.get_all_memories()
|
||||
|
||||
# 构建节点和边数据
|
||||
nodes_dict = {} # {node_id: node_data}
|
||||
edges_dict = {} # {edge_id: edge_data} - 使用字典去重
|
||||
memory_info = []
|
||||
|
||||
for memory in all_memories:
|
||||
# 添加记忆信息
|
||||
memory_info.append({
|
||||
'id': memory.id,
|
||||
'type': memory.memory_type.value,
|
||||
'importance': memory.importance,
|
||||
'activation': memory.activation,
|
||||
'status': memory.status.value,
|
||||
'created_at': memory.created_at.isoformat(),
|
||||
'text': memory.to_text(),
|
||||
'access_count': memory.access_count,
|
||||
})
|
||||
|
||||
# 处理节点
|
||||
for node in memory.nodes:
|
||||
if node.id not in nodes_dict:
|
||||
nodes_dict[node.id] = {
|
||||
'id': node.id,
|
||||
'label': node.content,
|
||||
'type': node.node_type.value,
|
||||
'group': node.node_type.name, # 用于颜色分组
|
||||
'title': f"{node.node_type.value}: {node.content}",
|
||||
'metadata': node.metadata,
|
||||
'created_at': node.created_at.isoformat(),
|
||||
}
|
||||
|
||||
# 处理边 - 使用字典自动去重
|
||||
for edge in memory.edges:
|
||||
edge_id = edge.id
|
||||
# 如果ID已存在,生成唯一ID
|
||||
counter = 1
|
||||
original_edge_id = edge_id
|
||||
while edge_id in edges_dict:
|
||||
edge_id = f"{original_edge_id}_{counter}"
|
||||
counter += 1
|
||||
|
||||
edges_dict[edge_id] = {
|
||||
'id': edge_id,
|
||||
'from': edge.source_id,
|
||||
'to': edge.target_id,
|
||||
'label': edge.relation,
|
||||
'type': edge.edge_type.value,
|
||||
'importance': edge.importance,
|
||||
'title': f"{edge.edge_type.value}: {edge.relation}",
|
||||
'arrows': 'to',
|
||||
'memory_id': memory.id,
|
||||
}
|
||||
|
||||
nodes_list = list(nodes_dict.values())
|
||||
edges_list = list(edges_dict.values())
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'nodes': nodes_list,
|
||||
'edges': edges_list,
|
||||
'memories': memory_info,
|
||||
'stats': {
|
||||
'total_nodes': len(nodes_list),
|
||||
'total_edges': len(edges_list),
|
||||
'total_memories': len(all_memories),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取图数据失败: {e}", exc_info=True)
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/memory/<memory_id>')
|
||||
def get_memory_detail(memory_id: str):
|
||||
"""
|
||||
获取特定记忆的详细信息
|
||||
|
||||
Args:
|
||||
memory_id: 记忆ID
|
||||
"""
|
||||
try:
|
||||
if memory_manager is None:
|
||||
init_memory_manager()
|
||||
|
||||
memory = memory_manager.graph_store.get_memory_by_id(memory_id)
|
||||
|
||||
if memory is None:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': '记忆不存在'
|
||||
}), 404
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': memory.to_dict()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取记忆详情失败: {e}", exc_info=True)
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/search')
|
||||
def search_memories():
|
||||
"""
|
||||
搜索记忆
|
||||
|
||||
Query参数:
|
||||
- q: 搜索关键词
|
||||
- type: 记忆类型过滤
|
||||
- limit: 返回数量限制
|
||||
"""
|
||||
try:
|
||||
if memory_manager is None:
|
||||
init_memory_manager()
|
||||
|
||||
query = request.args.get('q', '')
|
||||
memory_type = request.args.get('type', None)
|
||||
limit = int(request.args.get('limit', 50))
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# 执行搜索
|
||||
results = loop.run_until_complete(
|
||||
memory_manager.search_memories(
|
||||
query=query,
|
||||
top_k=limit
|
||||
)
|
||||
)
|
||||
|
||||
# 构建返回数据
|
||||
memories = []
|
||||
for memory in results:
|
||||
memories.append({
|
||||
'id': memory.id,
|
||||
'text': memory.to_text(),
|
||||
'type': memory.memory_type.value,
|
||||
'importance': memory.importance,
|
||||
'created_at': memory.created_at.isoformat(),
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'results': memories,
|
||||
'count': len(memories),
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"搜索失败: {e}", exc_info=True)
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/stats')
|
||||
def get_statistics():
|
||||
"""
|
||||
获取记忆图统计信息
|
||||
"""
|
||||
try:
|
||||
if memory_manager is None:
|
||||
init_memory_manager()
|
||||
|
||||
# 获取统计信息
|
||||
all_memories = memory_manager.graph_store.get_all_memories()
|
||||
all_nodes = set()
|
||||
all_edges = 0
|
||||
|
||||
for memory in all_memories:
|
||||
for node in memory.nodes:
|
||||
all_nodes.add(node.id)
|
||||
all_edges += len(memory.edges)
|
||||
|
||||
stats = {
|
||||
'total_memories': len(all_memories),
|
||||
'total_nodes': len(all_nodes),
|
||||
'total_edges': all_edges,
|
||||
'node_types': {},
|
||||
'memory_types': {},
|
||||
}
|
||||
|
||||
# 统计节点类型分布
|
||||
for memory in all_memories:
|
||||
mem_type = memory.memory_type.value
|
||||
stats['memory_types'][mem_type] = stats['memory_types'].get(mem_type, 0) + 1
|
||||
|
||||
for node in memory.nodes:
|
||||
node_type = node.node_type.value
|
||||
stats['node_types'][node_type] = stats['node_types'].get(node_type, 0) + 1
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': stats
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取统计信息失败: {e}", exc_info=True)
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/files')
|
||||
def list_files():
|
||||
"""
|
||||
列出所有可用的数据文件
|
||||
注意: 完整版服务器直接使用内存中的数据,不支持文件切换
|
||||
"""
|
||||
try:
|
||||
from pathlib import Path
|
||||
data_dir = Path("data/memory_graph")
|
||||
|
||||
files = []
|
||||
if data_dir.exists():
|
||||
for f in data_dir.glob("*.json"):
|
||||
stat = f.stat()
|
||||
files.append({
|
||||
'path': str(f),
|
||||
'name': f.name,
|
||||
'size': stat.st_size,
|
||||
'size_kb': round(stat.st_size / 1024, 2),
|
||||
'modified': datetime.fromtimestamp(stat.st_mtime).isoformat(),
|
||||
'modified_readable': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'is_current': True # 完整版始终使用内存数据
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'files': files,
|
||||
'count': len(files),
|
||||
'current_file': 'memory_manager (实时数据)',
|
||||
'note': '完整版服务器使用实时内存数据,如需切换文件请使用独立版服务器'
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"获取文件列表失败: {e}", exc_info=True)
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/reload')
|
||||
def reload_data():
|
||||
"""
|
||||
重新加载数据
|
||||
"""
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '完整版服务器使用实时数据,无需重新加载',
|
||||
'note': '数据始终是最新的'
|
||||
})
|
||||
|
||||
|
||||
def run_server(host: str = '127.0.0.1', port: int = 5000, debug: bool = False):
|
||||
"""
|
||||
启动可视化服务器
|
||||
|
||||
Args:
|
||||
host: 服务器地址
|
||||
port: 端口号
|
||||
debug: 是否开启调试模式
|
||||
"""
|
||||
logger.info(f"启动记忆图可视化服务器: http://{host}:{port}")
|
||||
app.run(host=host, port=port, debug=debug)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_server(debug=True)
|
||||
@@ -1,480 +0,0 @@
|
||||
"""
|
||||
记忆图可视化 - 独立版本
|
||||
|
||||
直接从存储的数据文件生成可视化,无需启动完整的记忆管理器
|
||||
"""
|
||||
|
||||
import orjson
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Set
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
|
||||
# 添加项目根目录
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from flask import Flask, jsonify, render_template_string, request, send_from_directory
|
||||
from flask_cors import CORS
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
# 数据缓存
|
||||
graph_data_cache = None
|
||||
data_dir = project_root / "data" / "memory_graph"
|
||||
current_data_file = None # 当前选择的数据文件
|
||||
|
||||
|
||||
def find_available_data_files() -> List[Path]:
|
||||
"""查找所有可用的记忆图数据文件"""
|
||||
files = []
|
||||
|
||||
if not data_dir.exists():
|
||||
return files
|
||||
|
||||
# 查找多种可能的文件名
|
||||
possible_files = [
|
||||
"graph_store.json",
|
||||
"memory_graph.json",
|
||||
"graph_data.json",
|
||||
]
|
||||
|
||||
for filename in possible_files:
|
||||
file_path = data_dir / filename
|
||||
if file_path.exists():
|
||||
files.append(file_path)
|
||||
|
||||
# 查找所有备份文件
|
||||
for pattern in ["graph_store_*.json", "memory_graph_*.json", "graph_data_*.json"]:
|
||||
for backup_file in data_dir.glob(pattern):
|
||||
if backup_file not in files:
|
||||
files.append(backup_file)
|
||||
|
||||
# 查找backups子目录
|
||||
backups_dir = data_dir / "backups"
|
||||
if backups_dir.exists():
|
||||
for backup_file in backups_dir.glob("**/*.json"):
|
||||
if backup_file not in files:
|
||||
files.append(backup_file)
|
||||
|
||||
# 查找data/backup目录
|
||||
backup_dir = data_dir.parent / "backup"
|
||||
if backup_dir.exists():
|
||||
for backup_file in backup_dir.glob("**/graph_*.json"):
|
||||
if backup_file not in files:
|
||||
files.append(backup_file)
|
||||
for backup_file in backup_dir.glob("**/memory_*.json"):
|
||||
if backup_file not in files:
|
||||
files.append(backup_file)
|
||||
|
||||
return sorted(files, key=lambda f: f.stat().st_mtime, reverse=True)
|
||||
|
||||
|
||||
def load_graph_data(file_path: Optional[Path] = None) -> Dict[str, Any]:
|
||||
"""从磁盘加载图数据"""
|
||||
global graph_data_cache, current_data_file
|
||||
|
||||
# 如果指定了新文件,清除缓存
|
||||
if file_path is not None and file_path != current_data_file:
|
||||
graph_data_cache = None
|
||||
current_data_file = file_path
|
||||
|
||||
if graph_data_cache is not None:
|
||||
return graph_data_cache
|
||||
|
||||
try:
|
||||
# 确定要加载的文件
|
||||
if current_data_file is not None:
|
||||
graph_file = current_data_file
|
||||
else:
|
||||
# 尝试查找可用的数据文件
|
||||
available_files = find_available_data_files()
|
||||
if not available_files:
|
||||
print(f"⚠️ 未找到任何图数据文件")
|
||||
print(f"📂 搜索目录: {data_dir}")
|
||||
return {
|
||||
"nodes": [],
|
||||
"edges": [],
|
||||
"memories": [],
|
||||
"stats": {"total_nodes": 0, "total_edges": 0, "total_memories": 0},
|
||||
"error": "未找到数据文件",
|
||||
"available_files": []
|
||||
}
|
||||
|
||||
# 使用最新的文件
|
||||
graph_file = available_files[0]
|
||||
current_data_file = graph_file
|
||||
print(f"📂 自动选择最新文件: {graph_file}")
|
||||
|
||||
if not graph_file.exists():
|
||||
print(f"⚠️ 图数据文件不存在: {graph_file}")
|
||||
return {
|
||||
"nodes": [],
|
||||
"edges": [],
|
||||
"memories": [],
|
||||
"stats": {"total_nodes": 0, "total_edges": 0, "total_memories": 0},
|
||||
"error": f"文件不存在: {graph_file}"
|
||||
}
|
||||
|
||||
print(f"📂 加载图数据: {graph_file}")
|
||||
with open(graph_file, 'r', encoding='utf-8') as f:
|
||||
data = orjson.loads(f.read())
|
||||
|
||||
# 解析数据
|
||||
nodes_dict = {}
|
||||
edges_list = []
|
||||
memory_info = []
|
||||
|
||||
# 实际文件格式是 {nodes: [], edges: [], metadata: {}}
|
||||
# 不是 {memories: [{nodes: [], edges: []}]}
|
||||
nodes = data.get("nodes", [])
|
||||
edges = data.get("edges", [])
|
||||
metadata = data.get("metadata", {})
|
||||
|
||||
print(f"✅ 找到 {len(nodes)} 个节点, {len(edges)} 条边")
|
||||
|
||||
# 处理节点
|
||||
for node in nodes:
|
||||
node_id = node.get('id', '')
|
||||
if node_id and node_id not in nodes_dict:
|
||||
memory_ids = node.get('metadata', {}).get('memory_ids', [])
|
||||
nodes_dict[node_id] = {
|
||||
'id': node_id,
|
||||
'label': node.get('content', ''),
|
||||
'type': node.get('node_type', ''),
|
||||
'group': extract_group_from_type(node.get('node_type', '')),
|
||||
'title': f"{node.get('node_type', '')}: {node.get('content', '')}",
|
||||
'metadata': node.get('metadata', {}),
|
||||
'created_at': node.get('created_at', ''),
|
||||
'memory_ids': memory_ids,
|
||||
}
|
||||
|
||||
# 处理边 - 使用集合去重,避免重复的边ID
|
||||
existing_edge_ids = set()
|
||||
for edge in edges:
|
||||
# 边的ID字段可能是 'id' 或 'edge_id'
|
||||
edge_id = edge.get('edge_id') or edge.get('id', '')
|
||||
# 如果ID为空或已存在,跳过这条边
|
||||
if not edge_id or edge_id in existing_edge_ids:
|
||||
continue
|
||||
|
||||
existing_edge_ids.add(edge_id)
|
||||
memory_id = edge.get('metadata', {}).get('memory_id', '')
|
||||
|
||||
# 注意: GraphStore 保存的格式使用 'source'/'target', 不是 'source_id'/'target_id'
|
||||
edges_list.append({
|
||||
'id': edge_id,
|
||||
'from': edge.get('source', edge.get('source_id', '')),
|
||||
'to': edge.get('target', edge.get('target_id', '')),
|
||||
'label': edge.get('relation', ''),
|
||||
'type': edge.get('edge_type', ''),
|
||||
'importance': edge.get('importance', 0.5),
|
||||
'title': f"{edge.get('edge_type', '')}: {edge.get('relation', '')}",
|
||||
'arrows': 'to',
|
||||
'memory_id': memory_id,
|
||||
})
|
||||
|
||||
# 从元数据中获取统计信息
|
||||
stats = metadata.get('statistics', {})
|
||||
total_memories = stats.get('total_memories', 0)
|
||||
|
||||
# TODO: 如果需要记忆详细信息,需要从其他地方加载
|
||||
# 目前只有节点和边的数据
|
||||
|
||||
graph_data_cache = {
|
||||
'nodes': list(nodes_dict.values()),
|
||||
'edges': edges_list,
|
||||
'memories': memory_info, # 空列表,因为文件中没有记忆详情
|
||||
'stats': {
|
||||
'total_nodes': len(nodes_dict),
|
||||
'total_edges': len(edges_list),
|
||||
'total_memories': total_memories,
|
||||
},
|
||||
'current_file': str(graph_file),
|
||||
'file_size': graph_file.stat().st_size,
|
||||
'file_modified': datetime.fromtimestamp(graph_file.stat().st_mtime).isoformat(),
|
||||
}
|
||||
|
||||
print(f"📊 统计: {len(nodes_dict)} 个节点, {len(edges_list)} 条边, {total_memories} 条记忆")
|
||||
print(f"📄 数据文件: {graph_file} ({graph_file.stat().st_size / 1024:.2f} KB)")
|
||||
return graph_data_cache
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 加载失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {"nodes": [], "edges": [], "memories": [], "stats": {}}
|
||||
|
||||
|
||||
def extract_group_from_type(node_type: str) -> str:
|
||||
"""从节点类型提取分组名"""
|
||||
# 假设类型格式为 "主体" 或 "SUBJECT"
|
||||
type_mapping = {
|
||||
'主体': 'SUBJECT',
|
||||
'主题': 'TOPIC',
|
||||
'客体': 'OBJECT',
|
||||
'属性': 'ATTRIBUTE',
|
||||
'值': 'VALUE',
|
||||
}
|
||||
return type_mapping.get(node_type, node_type)
|
||||
|
||||
|
||||
def generate_memory_text(memory: Dict[str, Any]) -> str:
|
||||
"""生成记忆的文本描述"""
|
||||
try:
|
||||
nodes = {n['id']: n for n in memory.get('nodes', [])}
|
||||
edges = memory.get('edges', [])
|
||||
|
||||
subject_id = memory.get('subject_id', '')
|
||||
if not subject_id or subject_id not in nodes:
|
||||
return f"[记忆 {memory.get('id', '')[:8]}]"
|
||||
|
||||
parts = [nodes[subject_id]['content']]
|
||||
|
||||
# 找主题节点
|
||||
for edge in edges:
|
||||
if edge.get('edge_type') == '记忆类型' and edge.get('source_id') == subject_id:
|
||||
topic_id = edge.get('target_id', '')
|
||||
if topic_id in nodes:
|
||||
parts.append(nodes[topic_id]['content'])
|
||||
|
||||
# 找客体
|
||||
for e2 in edges:
|
||||
if e2.get('edge_type') == '核心关系' and e2.get('source_id') == topic_id:
|
||||
obj_id = e2.get('target_id', '')
|
||||
if obj_id in nodes:
|
||||
parts.append(f"{e2.get('relation', '')} {nodes[obj_id]['content']}")
|
||||
break
|
||||
break
|
||||
|
||||
return " ".join(parts)
|
||||
except Exception:
|
||||
return f"[记忆 {memory.get('id', '')[:8]}]"
|
||||
|
||||
|
||||
# 使用内嵌的HTML模板(与之前相同)
|
||||
HTML_TEMPLATE = open(project_root / "tools" / "memory_visualizer" / "templates" / "visualizer.html", 'r', encoding='utf-8').read()
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""主页面"""
|
||||
return render_template_string(HTML_TEMPLATE)
|
||||
|
||||
|
||||
@app.route('/api/graph/full')
|
||||
def get_full_graph():
|
||||
"""获取完整记忆图数据"""
|
||||
try:
|
||||
data = load_graph_data()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': data
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/memory/<memory_id>')
|
||||
def get_memory_detail(memory_id: str):
|
||||
"""获取记忆详情"""
|
||||
try:
|
||||
data = load_graph_data()
|
||||
memory = next((m for m in data['memories'] if m['id'] == memory_id), None)
|
||||
|
||||
if memory is None:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': '记忆不存在'
|
||||
}), 404
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': memory
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/search')
|
||||
def search_memories():
|
||||
"""搜索记忆"""
|
||||
try:
|
||||
query = request.args.get('q', '').lower()
|
||||
limit = int(request.args.get('limit', 50))
|
||||
|
||||
data = load_graph_data()
|
||||
|
||||
# 简单的文本匹配搜索
|
||||
results = []
|
||||
for memory in data['memories']:
|
||||
text = memory.get('text', '').lower()
|
||||
if query in text:
|
||||
results.append(memory)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'results': results[:limit],
|
||||
'count': len(results),
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/stats')
|
||||
def get_statistics():
|
||||
"""获取统计信息"""
|
||||
try:
|
||||
data = load_graph_data()
|
||||
|
||||
# 扩展统计信息
|
||||
node_types = {}
|
||||
memory_types = {}
|
||||
|
||||
for node in data['nodes']:
|
||||
node_type = node.get('type', 'Unknown')
|
||||
node_types[node_type] = node_types.get(node_type, 0) + 1
|
||||
|
||||
for memory in data['memories']:
|
||||
mem_type = memory.get('type', 'Unknown')
|
||||
memory_types[mem_type] = memory_types.get(mem_type, 0) + 1
|
||||
|
||||
stats = data.get('stats', {})
|
||||
stats['node_types'] = node_types
|
||||
stats['memory_types'] = memory_types
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': stats
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/reload')
|
||||
def reload_data():
|
||||
"""重新加载数据"""
|
||||
global graph_data_cache
|
||||
graph_data_cache = None
|
||||
data = load_graph_data()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '数据已重新加载',
|
||||
'stats': data.get('stats', {})
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/files')
|
||||
def list_files():
|
||||
"""列出所有可用的数据文件"""
|
||||
try:
|
||||
files = find_available_data_files()
|
||||
file_list = []
|
||||
|
||||
for f in files:
|
||||
stat = f.stat()
|
||||
file_list.append({
|
||||
'path': str(f),
|
||||
'name': f.name,
|
||||
'size': stat.st_size,
|
||||
'size_kb': round(stat.st_size / 1024, 2),
|
||||
'modified': datetime.fromtimestamp(stat.st_mtime).isoformat(),
|
||||
'modified_readable': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'is_current': str(f) == str(current_data_file) if current_data_file else False
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'files': file_list,
|
||||
'count': len(file_list),
|
||||
'current_file': str(current_data_file) if current_data_file else None
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/select_file', methods=['POST'])
|
||||
def select_file():
|
||||
"""选择要加载的数据文件"""
|
||||
global graph_data_cache, current_data_file
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
file_path = data.get('file_path')
|
||||
|
||||
if not file_path:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': '未提供文件路径'
|
||||
}), 400
|
||||
|
||||
file_path = Path(file_path)
|
||||
if not file_path.exists():
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'文件不存在: {file_path}'
|
||||
}), 404
|
||||
|
||||
# 清除缓存并加载新文件
|
||||
graph_data_cache = None
|
||||
current_data_file = file_path
|
||||
graph_data = load_graph_data(file_path)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'已切换到文件: {file_path.name}',
|
||||
'stats': graph_data.get('stats', {})
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
|
||||
|
||||
def run_server(host: str = '127.0.0.1', port: int = 5001, debug: bool = False):
|
||||
"""启动服务器"""
|
||||
print("=" * 60)
|
||||
print("🦊 MoFox Bot - 记忆图可视化工具 (独立版)")
|
||||
print("=" * 60)
|
||||
print(f"📂 数据目录: {data_dir}")
|
||||
print(f"🌐 访问地址: http://{host}:{port}")
|
||||
print("⏹️ 按 Ctrl+C 停止服务器")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# 预加载数据
|
||||
load_graph_data()
|
||||
|
||||
app.run(host=host, port=port, debug=debug)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
run_server(debug=True)
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n👋 服务器已停止")
|
||||
except Exception as e:
|
||||
print(f"\n❌ 启动失败: {e}")
|
||||
sys.exit(1)
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# ======================================================================
|
||||
# 记忆图可视化工具 - 快捷启动脚本
|
||||
# ======================================================================
|
||||
# 此脚本是快捷方式,实际脚本位于 tools/memory_visualizer/ 目录
|
||||
# ======================================================================
|
||||
|
||||
$visualizerScript = Join-Path $PSScriptRoot "tools\memory_visualizer\visualizer.ps1"
|
||||
|
||||
if (Test-Path $visualizerScript) {
|
||||
& $visualizerScript @args
|
||||
} else {
|
||||
Write-Host "❌ 错误:找不到可视化工具脚本" -ForegroundColor Red
|
||||
Write-Host " 预期位置: $visualizerScript" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Reference in New Issue
Block a user