feat: add Claude-powered graph engine (Graphify-style)
Add a Claude/Anthropic-driven graph construction engine as a drop-in alternative to the Zep-based one. Each text chunk is sent to Claude with a tool-use schema derived from the generated ontology, extracting only entities/relationships explicitly grounded in the text and merging them into a local JSON graph store. Same service interface and graph data shape as the Zep engine, so the existing D3 visualization works unmodified. - backend/app/services/claude_graph_builder.py: Claude extraction agent - backend/app/models/graph_store.py: local JSON graph persistence - backend/app/api/graph.py: engine selection (claude/zep) on build/data/delete routes - frontend: Claude/Zep engine toggle on the Graph Build step, defaults to Claude - config, requirements, locales, README/.env.example updated accordingly
This commit is contained in:
parent
fa0f6519b1
commit
ff096b72db
13
.env.example
13
.env.example
|
|
@ -5,7 +5,18 @@ LLM_API_KEY=your_api_key_here
|
||||||
LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
|
LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||||
LLM_MODEL_NAME=qwen-plus
|
LLM_MODEL_NAME=qwen-plus
|
||||||
|
|
||||||
# ===== ZEP记忆图谱配置 =====
|
# ===== 图谱构建引擎配置 =====
|
||||||
|
# 默认引擎:"claude" 或 "zep",也可在前端 Step 02 卡片中按项目切换
|
||||||
|
GRAPH_ENGINE_DEFAULT=claude
|
||||||
|
|
||||||
|
# ----- Claude 图谱引擎(默认,Graphify 风格的 Claude Code 智能体抽取) -----
|
||||||
|
# https://console.anthropic.com/
|
||||||
|
ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
||||||
|
CLAUDE_MODEL_NAME=claude-sonnet-5
|
||||||
|
# 如需通过自建网关/代理访问 Anthropic API,可选填
|
||||||
|
# ANTHROPIC_BASE_URL=https://your-proxy.example.com
|
||||||
|
|
||||||
|
# ----- Zep记忆图谱配置(可选,切换引擎为 zep 时需要) -----
|
||||||
# 每月免费额度即可支撑简单使用:https://app.getzep.com/
|
# 每月免费额度即可支撑简单使用:https://app.getzep.com/
|
||||||
ZEP_API_KEY=your_zep_api_key_here
|
ZEP_API_KEY=your_zep_api_key_here
|
||||||
|
|
||||||
|
|
|
||||||
23
README-ZH.md
23
README-ZH.md
|
|
@ -40,6 +40,29 @@ MiroFish 致力于打造映射现实的群体智能镜像,通过捕捉个体
|
||||||
|
|
||||||
从严肃预测到趣味仿真,我们让每一个如果都能看见结果,让预测万物成为可能。
|
从严肃预测到趣味仿真,我们让每一个如果都能看见结果,让预测万物成为可能。
|
||||||
|
|
||||||
|
## 🧠 本 Fork:Claude Code 图谱引擎
|
||||||
|
|
||||||
|
本 Fork 在原有 Zep 图谱引擎之外,新增了一个由 Claude 驱动的图谱构建引擎,采用 **Graphify 风格**的理念:
|
||||||
|
基于本体约束、逐块透明抽取,让图谱的构建过程节点级可见、可追溯。
|
||||||
|
|
||||||
|
- **智能体驱动的抽取**:不再依赖 Zep Cloud,每个文本块会连同由本体(实体类型、关系类型、允许的
|
||||||
|
source/target)动态生成的结构化 `tool_use` schema 一起发送给 Claude,只抽取文本中明确出现的事实。
|
||||||
|
- **本地可检查的图谱存储**:抽取结果(节点、边、事实、来源)以 JSON 形式按项目本地持久化,无需外部图数据库即可体验。
|
||||||
|
- **即插即用,可视化不变**:Claude 引擎实现了与 Zep 引擎完全一致的服务接口(`create_graph`、`set_ontology`、
|
||||||
|
`add_text_batches`、`get_graph_data`、`delete_graph`),因此现有的 D3 图谱面板、实体图例、节点/边详情面板无需改动即可复用。
|
||||||
|
- **按项目切换**:在图谱构建步骤中通过 Claude / Zep 切换按钮选择引擎,默认使用 Claude。
|
||||||
|
|
||||||
|
在 `.env` 中配置:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GRAPH_ENGINE_DEFAULT=claude
|
||||||
|
ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
||||||
|
CLAUDE_MODEL_NAME=claude-sonnet-5
|
||||||
|
```
|
||||||
|
|
||||||
|
详见 `backend/app/services/claude_graph_builder.py`(抽取智能体)与
|
||||||
|
`backend/app/models/graph_store.py`(本地图谱持久化层)。
|
||||||
|
|
||||||
## 🌐 在线体验
|
## 🌐 在线体验
|
||||||
|
|
||||||
欢迎访问在线 Demo 演示环境,体验我们为你准备的一次关于热点舆情事件的推演预测:[mirofish-live-demo](https://666ghj.github.io/mirofish-demo/)
|
欢迎访问在线 Demo 演示环境,体验我们为你准备的一次关于热点舆情事件的推演预测:[mirofish-live-demo](https://666ghj.github.io/mirofish-demo/)
|
||||||
|
|
|
||||||
29
README.md
29
README.md
|
|
@ -40,6 +40,35 @@ MiroFish is dedicated to creating a swarm intelligence mirror that maps reality.
|
||||||
|
|
||||||
From serious predictions to playful simulations, we let every "what if" see its outcome, making it possible to predict anything.
|
From serious predictions to playful simulations, we let every "what if" see its outcome, making it possible to predict anything.
|
||||||
|
|
||||||
|
## 🧠 This Fork: Claude Code Graph Engine
|
||||||
|
|
||||||
|
This fork adds a second, Claude-powered graph construction engine alongside the original Zep-based one, with a
|
||||||
|
**Graphify-style** philosophy: transparent, incremental, ontology-constrained entity/relationship extraction that
|
||||||
|
you can watch build up node by node.
|
||||||
|
|
||||||
|
- **Agent-driven extraction**: instead of delegating extraction to Zep Cloud, each text chunk is sent to Claude
|
||||||
|
with a structured `tool_use` schema derived from your generated ontology (entity types, edge types, allowed
|
||||||
|
source/target pairs). Claude returns only entities and relationships that are explicitly grounded in that
|
||||||
|
fragment — no hallucinated facts, no silent inference.
|
||||||
|
- **Local, inspectable graph store**: the resulting graph (nodes, edges, facts, provenance) is persisted as plain
|
||||||
|
JSON per project — no external graph database required to try it out.
|
||||||
|
- **Drop-in engine, same visualization**: the Claude engine implements the exact same service interface as the
|
||||||
|
Zep engine (`create_graph`, `set_ontology`, `add_text_batches`, `get_graph_data`, `delete_graph`), so the
|
||||||
|
existing D3 graph panel, entity legend, and node/edge inspector work unmodified.
|
||||||
|
- **Pick per project**: choose the engine ("Claude" or "Zep") from a pill toggle on the Graph Build step before
|
||||||
|
building — Claude is the default.
|
||||||
|
|
||||||
|
Configure it via `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GRAPH_ENGINE_DEFAULT=claude
|
||||||
|
ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
||||||
|
CLAUDE_MODEL_NAME=claude-sonnet-5
|
||||||
|
```
|
||||||
|
|
||||||
|
See `backend/app/services/claude_graph_builder.py` for the extraction agent and
|
||||||
|
`backend/app/models/graph_store.py` for the local graph persistence layer.
|
||||||
|
|
||||||
## 🌐 Live Demo
|
## 🌐 Live Demo
|
||||||
|
|
||||||
Welcome to visit our online demo environment and experience a prediction simulation on trending public opinion events we've prepared for you: [mirofish-live-demo](https://666ghj.github.io/mirofish-demo/)
|
Welcome to visit our online demo environment and experience a prediction simulation on trending public opinion events we've prepared for you: [mirofish-live-demo](https://666ghj.github.io/mirofish-demo/)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from . import graph_bp
|
||||||
from ..config import Config
|
from ..config import Config
|
||||||
from ..services.ontology_generator import OntologyGenerator
|
from ..services.ontology_generator import OntologyGenerator
|
||||||
from ..services.graph_builder import GraphBuilderService
|
from ..services.graph_builder import GraphBuilderService
|
||||||
|
from ..services.claude_graph_builder import ClaudeGraphBuilderService
|
||||||
from ..services.text_processor import TextProcessor
|
from ..services.text_processor import TextProcessor
|
||||||
from ..utils.file_parser import FileParser
|
from ..utils.file_parser import FileParser
|
||||||
from ..utils.logger import get_logger
|
from ..utils.logger import get_logger
|
||||||
|
|
@ -22,6 +23,9 @@ from ..models.project import ProjectManager, ProjectStatus
|
||||||
# 获取日志器
|
# 获取日志器
|
||||||
logger = get_logger('mirofish.api')
|
logger = get_logger('mirofish.api')
|
||||||
|
|
||||||
|
# 支持的图谱构建引擎
|
||||||
|
GRAPH_ENGINES = ('claude', 'zep')
|
||||||
|
|
||||||
|
|
||||||
def allowed_file(filename: str) -> bool:
|
def allowed_file(filename: str) -> bool:
|
||||||
"""检查文件扩展名是否允许"""
|
"""检查文件扩展名是否允许"""
|
||||||
|
|
@ -31,6 +35,20 @@ def allowed_file(filename: str) -> bool:
|
||||||
return ext in Config.ALLOWED_EXTENSIONS
|
return ext in Config.ALLOWED_EXTENSIONS
|
||||||
|
|
||||||
|
|
||||||
|
def get_graph_builder(engine: str):
|
||||||
|
"""按引擎名称创建对应的图谱构建服务实例"""
|
||||||
|
if engine == 'claude':
|
||||||
|
return ClaudeGraphBuilderService(api_key=Config.ANTHROPIC_API_KEY)
|
||||||
|
if engine == 'zep':
|
||||||
|
return GraphBuilderService(api_key=Config.ZEP_API_KEY)
|
||||||
|
raise ValueError(t('api.unknownEngine', engine=engine))
|
||||||
|
|
||||||
|
|
||||||
|
def infer_engine_from_graph_id(graph_id: str) -> str:
|
||||||
|
"""从 graph_id 命名规则推断所属引擎(claude 引擎的 id 带有 _claude_ 标记)"""
|
||||||
|
return 'claude' if '_claude_' in graph_id else 'zep'
|
||||||
|
|
||||||
|
|
||||||
# ============== 项目管理接口 ==============
|
# ============== 项目管理接口 ==============
|
||||||
|
|
||||||
@graph_bp.route('/project/<project_id>', methods=['GET'])
|
@graph_bp.route('/project/<project_id>', methods=['GET'])
|
||||||
|
|
@ -107,9 +125,10 @@ def reset_project(project_id: str):
|
||||||
|
|
||||||
project.graph_id = None
|
project.graph_id = None
|
||||||
project.graph_build_task_id = None
|
project.graph_build_task_id = None
|
||||||
|
project.graph_engine = None
|
||||||
project.error = None
|
project.error = None
|
||||||
ProjectManager.save_project(project)
|
ProjectManager.save_project(project)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": t('api.projectReset', id=project_id),
|
"message": t('api.projectReset', id=project_id),
|
||||||
|
|
@ -282,23 +301,32 @@ def build_graph():
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logger.info("=== 开始构建图谱 ===")
|
logger.info("=== 开始构建图谱 ===")
|
||||||
|
|
||||||
# 检查配置
|
# 解析请求
|
||||||
|
data = request.get_json() or {}
|
||||||
|
project_id = data.get('project_id')
|
||||||
|
engine = data.get('engine', Config.GRAPH_ENGINE_DEFAULT)
|
||||||
|
logger.debug(f"请求参数: project_id={project_id}, engine={engine}")
|
||||||
|
|
||||||
|
if engine not in GRAPH_ENGINES:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": t('api.unknownEngine', engine=engine)
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 检查所选引擎所需的配置
|
||||||
errors = []
|
errors = []
|
||||||
if not Config.ZEP_API_KEY:
|
if engine == 'zep' and not Config.ZEP_API_KEY:
|
||||||
errors.append(t('api.zepApiKeyMissing'))
|
errors.append(t('api.zepApiKeyMissing'))
|
||||||
|
if engine == 'claude' and not Config.ANTHROPIC_API_KEY:
|
||||||
|
errors.append(t('api.anthropicApiKeyMissing'))
|
||||||
if errors:
|
if errors:
|
||||||
logger.error(f"配置错误: {errors}")
|
logger.error(f"配置错误: {errors}")
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": t('api.configError', details="; ".join(errors))
|
"error": t('api.configError', details="; ".join(errors))
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
# 解析请求
|
|
||||||
data = request.get_json() or {}
|
|
||||||
project_id = data.get('project_id')
|
|
||||||
logger.debug(f"请求参数: project_id={project_id}")
|
|
||||||
|
|
||||||
if not project_id:
|
if not project_id:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": False,
|
"success": False,
|
||||||
|
|
@ -369,8 +397,9 @@ def build_graph():
|
||||||
# 更新项目状态
|
# 更新项目状态
|
||||||
project.status = ProjectStatus.GRAPH_BUILDING
|
project.status = ProjectStatus.GRAPH_BUILDING
|
||||||
project.graph_build_task_id = task_id
|
project.graph_build_task_id = task_id
|
||||||
|
project.graph_engine = engine
|
||||||
ProjectManager.save_project(project)
|
ProjectManager.save_project(project)
|
||||||
|
|
||||||
# Capture locale before spawning background thread
|
# Capture locale before spawning background thread
|
||||||
current_locale = get_locale()
|
current_locale = get_locale()
|
||||||
|
|
||||||
|
|
@ -386,8 +415,8 @@ def build_graph():
|
||||||
message=t('progress.initGraphService')
|
message=t('progress.initGraphService')
|
||||||
)
|
)
|
||||||
|
|
||||||
# 创建图谱构建服务
|
# 创建图谱构建服务(按所选引擎)
|
||||||
builder = GraphBuilderService(api_key=Config.ZEP_API_KEY)
|
builder = get_graph_builder(engine)
|
||||||
|
|
||||||
# 分块
|
# 分块
|
||||||
task_manager.update_task(
|
task_manager.update_task(
|
||||||
|
|
@ -405,7 +434,7 @@ def build_graph():
|
||||||
# 创建图谱
|
# 创建图谱
|
||||||
task_manager.update_task(
|
task_manager.update_task(
|
||||||
task_id,
|
task_id,
|
||||||
message=t('progress.creatingZepGraph'),
|
message=t('progress.creatingClaudeGraph') if engine == 'claude' else t('progress.creatingZepGraph'),
|
||||||
progress=10
|
progress=10
|
||||||
)
|
)
|
||||||
graph_id = builder.create_graph(name=graph_name)
|
graph_id = builder.create_graph(name=graph_name)
|
||||||
|
|
@ -444,10 +473,10 @@ def build_graph():
|
||||||
progress_callback=add_progress_callback
|
progress_callback=add_progress_callback
|
||||||
)
|
)
|
||||||
|
|
||||||
# 等待Zep处理完成(查询每个episode的processed状态)
|
# 等待处理完成(Zep 引擎需轮询 episode 状态,Claude 引擎是同步抽取)
|
||||||
task_manager.update_task(
|
task_manager.update_task(
|
||||||
task_id,
|
task_id,
|
||||||
message=t('progress.waitingZepProcess'),
|
message=t('progress.claudeExtractionDone') if engine == 'claude' else t('progress.waitingZepProcess'),
|
||||||
progress=55
|
progress=55
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -572,20 +601,27 @@ def get_graph_data(graph_id: str):
|
||||||
获取图谱数据(节点和边)
|
获取图谱数据(节点和边)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if not Config.ZEP_API_KEY:
|
engine = request.args.get('engine') or infer_engine_from_graph_id(graph_id)
|
||||||
|
|
||||||
|
if engine == 'zep' and not Config.ZEP_API_KEY:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": t('api.zepApiKeyMissing')
|
"error": t('api.zepApiKeyMissing')
|
||||||
}), 500
|
}), 500
|
||||||
|
if engine == 'claude' and not Config.ANTHROPIC_API_KEY:
|
||||||
builder = GraphBuilderService(api_key=Config.ZEP_API_KEY)
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": t('api.anthropicApiKeyMissing')
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
builder = get_graph_builder(engine)
|
||||||
graph_data = builder.get_graph_data(graph_id)
|
graph_data = builder.get_graph_data(graph_id)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
"data": graph_data
|
"data": graph_data
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": False,
|
"success": False,
|
||||||
|
|
@ -597,23 +633,30 @@ def get_graph_data(graph_id: str):
|
||||||
@graph_bp.route('/delete/<graph_id>', methods=['DELETE'])
|
@graph_bp.route('/delete/<graph_id>', methods=['DELETE'])
|
||||||
def delete_graph(graph_id: str):
|
def delete_graph(graph_id: str):
|
||||||
"""
|
"""
|
||||||
删除Zep图谱
|
删除图谱(Claude 本地图谱或 Zep 云端图谱)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if not Config.ZEP_API_KEY:
|
engine = request.args.get('engine') or infer_engine_from_graph_id(graph_id)
|
||||||
|
|
||||||
|
if engine == 'zep' and not Config.ZEP_API_KEY:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": t('api.zepApiKeyMissing')
|
"error": t('api.zepApiKeyMissing')
|
||||||
}), 500
|
}), 500
|
||||||
|
if engine == 'claude' and not Config.ANTHROPIC_API_KEY:
|
||||||
builder = GraphBuilderService(api_key=Config.ZEP_API_KEY)
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": t('api.anthropicApiKeyMissing')
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
builder = get_graph_builder(engine)
|
||||||
builder.delete_graph(graph_id)
|
builder.delete_graph(graph_id)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": t('api.graphDeleted', id=graph_id)
|
"message": t('api.graphDeleted', id=graph_id)
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": False,
|
"success": False,
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,15 @@ class Config:
|
||||||
|
|
||||||
# Zep配置
|
# Zep配置
|
||||||
ZEP_API_KEY = os.environ.get('ZEP_API_KEY')
|
ZEP_API_KEY = os.environ.get('ZEP_API_KEY')
|
||||||
|
|
||||||
|
# Claude图谱引擎配置(Anthropic API,作为图谱构建的智能体)
|
||||||
|
ANTHROPIC_API_KEY = os.environ.get('ANTHROPIC_API_KEY')
|
||||||
|
ANTHROPIC_BASE_URL = os.environ.get('ANTHROPIC_BASE_URL')
|
||||||
|
CLAUDE_MODEL_NAME = os.environ.get('CLAUDE_MODEL_NAME', 'claude-sonnet-5')
|
||||||
|
|
||||||
|
# 图谱构建默认引擎:"claude" 或 "zep"
|
||||||
|
GRAPH_ENGINE_DEFAULT = os.environ.get('GRAPH_ENGINE_DEFAULT', 'claude')
|
||||||
|
|
||||||
# 文件上传配置
|
# 文件上传配置
|
||||||
MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 50MB
|
MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 50MB
|
||||||
UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), '../uploads')
|
UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), '../uploads')
|
||||||
|
|
@ -69,7 +77,10 @@ class Config:
|
||||||
errors = []
|
errors = []
|
||||||
if not cls.LLM_API_KEY:
|
if not cls.LLM_API_KEY:
|
||||||
errors.append("LLM_API_KEY 未配置")
|
errors.append("LLM_API_KEY 未配置")
|
||||||
if not cls.ZEP_API_KEY:
|
# 图谱构建引擎所需的密钥按默认引擎校验,另一个引擎仍可在请求时按需选用
|
||||||
|
if cls.GRAPH_ENGINE_DEFAULT == 'zep' and not cls.ZEP_API_KEY:
|
||||||
errors.append("ZEP_API_KEY 未配置")
|
errors.append("ZEP_API_KEY 未配置")
|
||||||
|
if cls.GRAPH_ENGINE_DEFAULT == 'claude' and not cls.ANTHROPIC_API_KEY:
|
||||||
|
errors.append("ANTHROPIC_API_KEY 未配置")
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
"""
|
||||||
|
本地图谱存储
|
||||||
|
为 Claude 图谱引擎提供轻量级的 JSON 持久化存储
|
||||||
|
(Zep 引擎使用 Zep Cloud 托管图谱,Claude 引擎使用本地存储)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
from ..config import Config
|
||||||
|
|
||||||
|
|
||||||
|
class GraphStore:
|
||||||
|
"""基于 JSON 文件的图谱存储,按 graph_id 持久化 nodes/edges/ontology"""
|
||||||
|
|
||||||
|
GRAPHS_DIR = os.path.join(Config.UPLOAD_FOLDER, 'graphs')
|
||||||
|
_lock = threading.Lock()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _ensure_dir(cls):
|
||||||
|
os.makedirs(cls.GRAPHS_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _path(cls, graph_id: str) -> str:
|
||||||
|
return os.path.join(cls.GRAPHS_DIR, f"{graph_id}.json")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, graph_id: str, name: str, description: str = "") -> Dict[str, Any]:
|
||||||
|
cls._ensure_dir()
|
||||||
|
data = {
|
||||||
|
"graph_id": graph_id,
|
||||||
|
"name": name,
|
||||||
|
"description": description,
|
||||||
|
"engine": "claude",
|
||||||
|
"ontology": None,
|
||||||
|
"nodes": {}, # uuid -> node dict
|
||||||
|
"edges": [], # list of edge dicts
|
||||||
|
"created_at": datetime.now().isoformat(),
|
||||||
|
}
|
||||||
|
cls.save(graph_id, data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, graph_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
path = cls._path(graph_id)
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return None
|
||||||
|
with cls._lock:
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def save(cls, graph_id: str, data: Dict[str, Any]) -> None:
|
||||||
|
cls._ensure_dir()
|
||||||
|
path = cls._path(graph_id)
|
||||||
|
with cls._lock:
|
||||||
|
tmp_path = f"{path}.tmp"
|
||||||
|
with open(tmp_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
os.replace(tmp_path, path)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def delete(cls, graph_id: str) -> bool:
|
||||||
|
path = cls._path(graph_id)
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return False
|
||||||
|
os.remove(path)
|
||||||
|
return True
|
||||||
|
|
@ -43,6 +43,7 @@ class Project:
|
||||||
# 图谱信息(接口2完成后填充)
|
# 图谱信息(接口2完成后填充)
|
||||||
graph_id: Optional[str] = None
|
graph_id: Optional[str] = None
|
||||||
graph_build_task_id: Optional[str] = None
|
graph_build_task_id: Optional[str] = None
|
||||||
|
graph_engine: Optional[str] = None # "claude" 或 "zep"
|
||||||
|
|
||||||
# 配置
|
# 配置
|
||||||
simulation_requirement: Optional[str] = None
|
simulation_requirement: Optional[str] = None
|
||||||
|
|
@ -66,6 +67,7 @@ class Project:
|
||||||
"analysis_summary": self.analysis_summary,
|
"analysis_summary": self.analysis_summary,
|
||||||
"graph_id": self.graph_id,
|
"graph_id": self.graph_id,
|
||||||
"graph_build_task_id": self.graph_build_task_id,
|
"graph_build_task_id": self.graph_build_task_id,
|
||||||
|
"graph_engine": self.graph_engine,
|
||||||
"simulation_requirement": self.simulation_requirement,
|
"simulation_requirement": self.simulation_requirement,
|
||||||
"chunk_size": self.chunk_size,
|
"chunk_size": self.chunk_size,
|
||||||
"chunk_overlap": self.chunk_overlap,
|
"chunk_overlap": self.chunk_overlap,
|
||||||
|
|
@ -91,6 +93,7 @@ class Project:
|
||||||
analysis_summary=data.get('analysis_summary'),
|
analysis_summary=data.get('analysis_summary'),
|
||||||
graph_id=data.get('graph_id'),
|
graph_id=data.get('graph_id'),
|
||||||
graph_build_task_id=data.get('graph_build_task_id'),
|
graph_build_task_id=data.get('graph_build_task_id'),
|
||||||
|
graph_engine=data.get('graph_engine'),
|
||||||
simulation_requirement=data.get('simulation_requirement'),
|
simulation_requirement=data.get('simulation_requirement'),
|
||||||
chunk_size=data.get('chunk_size', 500),
|
chunk_size=data.get('chunk_size', 500),
|
||||||
chunk_overlap=data.get('chunk_overlap', 50),
|
chunk_overlap=data.get('chunk_overlap', 50),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,352 @@
|
||||||
|
"""
|
||||||
|
图谱构建服务 - Claude 引擎
|
||||||
|
使用 Claude(Anthropic API)作为图谱构建的智能体,对文本分片进行实体/关系抽取,
|
||||||
|
按照 Graphify 的思路做"增量式、透明化"的图谱构建:每个文本块都会被 Claude
|
||||||
|
以结构化 tool-use 的方式抽取实体与关系,逐步合并进本地图谱存储。
|
||||||
|
|
||||||
|
与 GraphBuilderService(Zep 引擎)保持一致的公开接口,可在 API 层互换使用:
|
||||||
|
create_graph / set_ontology / add_text_batches / _wait_for_episodes /
|
||||||
|
get_graph_data / delete_graph
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, Any, List, Optional, Callable
|
||||||
|
|
||||||
|
import anthropic
|
||||||
|
|
||||||
|
from ..config import Config
|
||||||
|
from ..models.graph_store import GraphStore
|
||||||
|
from ..utils.locale import t, get_language_instruction
|
||||||
|
|
||||||
|
|
||||||
|
def _extraction_tool(ontology: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""根据本体定义动态构建 Claude tool-use 的抽取工具schema"""
|
||||||
|
entity_names = [e["name"] for e in ontology.get("entity_types", [])] or ["Entity"]
|
||||||
|
edge_names = [e["name"] for e in ontology.get("edge_types", [])] or ["RELATED_TO"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": "record_graph_fragment",
|
||||||
|
"description": (
|
||||||
|
"Record the entities and relationships that are explicitly grounded in the "
|
||||||
|
"given text fragment, strictly following the provided ontology types."
|
||||||
|
),
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"entities": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Canonical name of the entity, consistent across mentions in the whole document."
|
||||||
|
},
|
||||||
|
"type": {"type": "string", "enum": entity_names},
|
||||||
|
"summary": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "One-sentence summary of this entity grounded in the text."
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Key/value attributes for this entity matching its ontology type, string values only.",
|
||||||
|
"additionalProperties": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name", "type"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relationships": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"source": {"type": "string", "description": "Name of the source entity, must match one of the entities above."},
|
||||||
|
"target": {"type": "string", "description": "Name of the target entity, must match one of the entities above."},
|
||||||
|
"relation": {"type": "string", "enum": edge_names},
|
||||||
|
"fact": {"type": "string", "description": "The specific fact/sentence from the text that supports this relationship."}
|
||||||
|
},
|
||||||
|
"required": ["source", "target", "relation", "fact"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["entities", "relationships"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _system_prompt(ontology: Dict[str, Any]) -> str:
|
||||||
|
entity_lines = []
|
||||||
|
for e in ontology.get("entity_types", []):
|
||||||
|
entity_lines.append(f"- {e['name']}: {e.get('description', '')}")
|
||||||
|
edge_lines = []
|
||||||
|
for edge in ontology.get("edge_types", []):
|
||||||
|
targets = ", ".join(
|
||||||
|
f"{st.get('source')}->{st.get('target')}" for st in edge.get("source_targets", [])
|
||||||
|
)
|
||||||
|
edge_lines.append(f"- {edge['name']}: {edge.get('description', '')} (allowed: {targets})")
|
||||||
|
|
||||||
|
return f"""You are a precise knowledge-graph extraction agent, acting as the graph-construction engine of MiroFish.
|
||||||
|
|
||||||
|
Your job: read one text fragment at a time and call the `record_graph_fragment` tool with the
|
||||||
|
entities and relationships that are EXPLICITLY grounded in that fragment. Do not invent facts.
|
||||||
|
Reuse entity names exactly as they appear elsewhere so the graph can be merged correctly.
|
||||||
|
|
||||||
|
## Entity types
|
||||||
|
{chr(10).join(entity_lines) or '- Entity: generic entity'}
|
||||||
|
|
||||||
|
## Relationship types
|
||||||
|
{chr(10).join(edge_lines) or '- RELATED_TO: generic relationship'}
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
1. Only extract entities/relationships that are supported by the text fragment given to you.
|
||||||
|
2. Entity `name` must be the canonical, real-world name (e.g. a person's full name), not a pronoun.
|
||||||
|
3. Every relationship's `source` and `target` must refer to an entity you also listed in `entities`.
|
||||||
|
4. If nothing relevant is in the fragment, call the tool with empty `entities` and `relationships` arrays.
|
||||||
|
5. {get_language_instruction()} (this applies to `summary` and `fact` fields only; `name`/`type`/`relation` stay as defined by the ontology).
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ClaudeGraphBuilderService:
|
||||||
|
"""
|
||||||
|
图谱构建服务 - Claude 引擎
|
||||||
|
使用 Anthropic Claude API 作为图谱构建的智能体
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, api_key: Optional[str] = None, model: Optional[str] = None):
|
||||||
|
self.api_key = api_key or Config.ANTHROPIC_API_KEY
|
||||||
|
if not self.api_key:
|
||||||
|
raise ValueError("ANTHROPIC_API_KEY 未配置")
|
||||||
|
|
||||||
|
self.model = model or Config.CLAUDE_MODEL_NAME
|
||||||
|
client_kwargs = {"api_key": self.api_key}
|
||||||
|
if Config.ANTHROPIC_BASE_URL:
|
||||||
|
client_kwargs["base_url"] = Config.ANTHROPIC_BASE_URL
|
||||||
|
|
||||||
|
self.client = anthropic.Anthropic(**client_kwargs)
|
||||||
|
|
||||||
|
# ============== 与 GraphBuilderService 对齐的公开接口 ==============
|
||||||
|
|
||||||
|
def create_graph(self, name: str) -> str:
|
||||||
|
"""创建本地图谱(公开方法,与 Zep 引擎接口对齐)"""
|
||||||
|
graph_id = f"mirofish_claude_{uuid.uuid4().hex[:16]}"
|
||||||
|
GraphStore.create(graph_id, name=name, description="MiroFish Claude-powered Graph")
|
||||||
|
return graph_id
|
||||||
|
|
||||||
|
def set_ontology(self, graph_id: str, ontology: Dict[str, Any]) -> None:
|
||||||
|
"""设置图谱本体(公开方法)"""
|
||||||
|
data = GraphStore.load(graph_id)
|
||||||
|
if data is None:
|
||||||
|
raise ValueError(f"图谱不存在: {graph_id}")
|
||||||
|
data["ontology"] = ontology
|
||||||
|
GraphStore.save(graph_id, data)
|
||||||
|
|
||||||
|
def add_text_batches(
|
||||||
|
self,
|
||||||
|
graph_id: str,
|
||||||
|
chunks: List[str],
|
||||||
|
batch_size: int = 3,
|
||||||
|
progress_callback: Optional[Callable] = None
|
||||||
|
) -> List[str]:
|
||||||
|
"""
|
||||||
|
对每个文本块调用 Claude 进行实体/关系抽取,逐步合并进图谱
|
||||||
|
返回处理过的 episode id 列表(用于与 Zep 引擎接口对齐)
|
||||||
|
"""
|
||||||
|
data = GraphStore.load(graph_id)
|
||||||
|
if data is None:
|
||||||
|
raise ValueError(f"图谱不存在: {graph_id}")
|
||||||
|
|
||||||
|
ontology = data.get("ontology") or {}
|
||||||
|
tool = _extraction_tool(ontology)
|
||||||
|
system_prompt = _system_prompt(ontology)
|
||||||
|
|
||||||
|
episode_uuids = []
|
||||||
|
total_chunks = len(chunks)
|
||||||
|
failures = 0
|
||||||
|
|
||||||
|
for i, chunk in enumerate(chunks):
|
||||||
|
episode_id = f"ep_{uuid.uuid4().hex[:12]}"
|
||||||
|
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(
|
||||||
|
t('progress.claudeExtractingChunk', current=i + 1, total=total_chunks),
|
||||||
|
(i + 1) / total_chunks
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
fragment = self._extract_fragment(chunk, tool, system_prompt)
|
||||||
|
self._merge_fragment(data, fragment, episode_id)
|
||||||
|
GraphStore.save(graph_id, data)
|
||||||
|
episode_uuids.append(episode_id)
|
||||||
|
except Exception as e:
|
||||||
|
failures += 1
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(
|
||||||
|
t('progress.claudeChunkFailed', current=i + 1, error=str(e)),
|
||||||
|
(i + 1) / total_chunks
|
||||||
|
)
|
||||||
|
|
||||||
|
if failures == total_chunks and total_chunks > 0:
|
||||||
|
raise RuntimeError(t('progress.claudeAllChunksFailed'))
|
||||||
|
|
||||||
|
return episode_uuids
|
||||||
|
|
||||||
|
def _wait_for_episodes(
|
||||||
|
self,
|
||||||
|
episode_uuids: List[str],
|
||||||
|
progress_callback: Optional[Callable] = None,
|
||||||
|
timeout: int = 600
|
||||||
|
) -> None:
|
||||||
|
"""Claude 引擎是同步抽取的,无需等待,直接汇报完成"""
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(
|
||||||
|
t('progress.processingComplete', completed=len(episode_uuids), total=len(episode_uuids)),
|
||||||
|
1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_graph_data(self, graph_id: str) -> Dict[str, Any]:
|
||||||
|
"""获取完整图谱数据(nodes/edges),与 Zep 引擎返回格式保持一致"""
|
||||||
|
data = GraphStore.load(graph_id)
|
||||||
|
if data is None:
|
||||||
|
raise ValueError(f"图谱不存在: {graph_id}")
|
||||||
|
|
||||||
|
nodes_data = list(data.get("nodes", {}).values())
|
||||||
|
edges_data = data.get("edges", [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"graph_id": graph_id,
|
||||||
|
"nodes": nodes_data,
|
||||||
|
"edges": edges_data,
|
||||||
|
"node_count": len(nodes_data),
|
||||||
|
"edge_count": len(edges_data),
|
||||||
|
}
|
||||||
|
|
||||||
|
def delete_graph(self, graph_id: str) -> None:
|
||||||
|
"""删除本地图谱"""
|
||||||
|
GraphStore.delete(graph_id)
|
||||||
|
|
||||||
|
# ============== 内部实现 ==============
|
||||||
|
|
||||||
|
def _extract_fragment(
|
||||||
|
self,
|
||||||
|
chunk: str,
|
||||||
|
tool: Dict[str, Any],
|
||||||
|
system_prompt: str
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""调用 Claude,对单个文本块做结构化实体/关系抽取"""
|
||||||
|
message = self.client.messages.create(
|
||||||
|
model=self.model,
|
||||||
|
max_tokens=2048,
|
||||||
|
system=system_prompt,
|
||||||
|
tools=[tool],
|
||||||
|
tool_choice={"type": "tool", "name": "record_graph_fragment"},
|
||||||
|
messages=[{"role": "user", "content": chunk}],
|
||||||
|
)
|
||||||
|
|
||||||
|
for block in message.content:
|
||||||
|
if getattr(block, "type", None) == "tool_use" and block.name == "record_graph_fragment":
|
||||||
|
return block.input
|
||||||
|
|
||||||
|
raise RuntimeError("Claude 未返回有效的图谱抽取结果")
|
||||||
|
|
||||||
|
def _merge_fragment(self, data: Dict[str, Any], fragment: Dict[str, Any], episode_id: str) -> None:
|
||||||
|
"""将单个文本块的抽取结果合并进图谱存储"""
|
||||||
|
nodes = data["nodes"]
|
||||||
|
edges = data["edges"]
|
||||||
|
now = datetime.now().isoformat()
|
||||||
|
|
||||||
|
# 本次抽取内 name -> uuid 的映射,便于关系解析
|
||||||
|
local_name_index: Dict[str, str] = {}
|
||||||
|
|
||||||
|
for entity in fragment.get("entities", []):
|
||||||
|
name = (entity.get("name") or "").strip()
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
entity_type = entity.get("type") or "Entity"
|
||||||
|
|
||||||
|
existing_uuid = self._find_node(nodes, name, entity_type)
|
||||||
|
if existing_uuid:
|
||||||
|
node = nodes[existing_uuid]
|
||||||
|
# 合并属性(新值补充空缺字段)
|
||||||
|
attrs = node.get("attributes") or {}
|
||||||
|
for k, v in (entity.get("attributes") or {}).items():
|
||||||
|
if v and not attrs.get(k):
|
||||||
|
attrs[k] = v
|
||||||
|
node["attributes"] = attrs
|
||||||
|
summary = entity.get("summary")
|
||||||
|
if summary and summary not in (node.get("summary") or ""):
|
||||||
|
node["summary"] = (node.get("summary") or "").strip()
|
||||||
|
node["summary"] = f"{node['summary']} {summary}".strip()
|
||||||
|
local_name_index[name.lower()] = existing_uuid
|
||||||
|
continue
|
||||||
|
|
||||||
|
node_uuid = uuid.uuid4().hex
|
||||||
|
nodes[node_uuid] = {
|
||||||
|
"uuid": node_uuid,
|
||||||
|
"name": name,
|
||||||
|
"labels": ["Entity", entity_type],
|
||||||
|
"summary": entity.get("summary") or "",
|
||||||
|
"attributes": entity.get("attributes") or {},
|
||||||
|
"created_at": now,
|
||||||
|
}
|
||||||
|
local_name_index[name.lower()] = node_uuid
|
||||||
|
|
||||||
|
for rel in fragment.get("relationships", []):
|
||||||
|
source_name = (rel.get("source") or "").strip()
|
||||||
|
target_name = (rel.get("target") or "").strip()
|
||||||
|
relation = rel.get("relation") or "RELATED_TO"
|
||||||
|
fact = rel.get("fact") or ""
|
||||||
|
|
||||||
|
source_uuid = local_name_index.get(source_name.lower()) or self._find_node_by_name(nodes, source_name)
|
||||||
|
target_uuid = local_name_index.get(target_name.lower()) or self._find_node_by_name(nodes, target_name)
|
||||||
|
|
||||||
|
if not source_uuid or not target_uuid:
|
||||||
|
# 关系引用了未抽取到的实体,跳过而不是伪造节点
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self._edge_exists(edges, source_uuid, target_uuid, relation, fact):
|
||||||
|
continue
|
||||||
|
|
||||||
|
edges.append({
|
||||||
|
"uuid": uuid.uuid4().hex,
|
||||||
|
"name": relation,
|
||||||
|
"fact": fact,
|
||||||
|
"fact_type": relation,
|
||||||
|
"source_node_uuid": source_uuid,
|
||||||
|
"target_node_uuid": target_uuid,
|
||||||
|
"attributes": {},
|
||||||
|
"created_at": now,
|
||||||
|
"valid_at": now,
|
||||||
|
"invalid_at": None,
|
||||||
|
"expired_at": None,
|
||||||
|
"episodes": [episode_id],
|
||||||
|
})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _find_node(nodes: Dict[str, Any], name: str, entity_type: str) -> Optional[str]:
|
||||||
|
name_l = name.lower()
|
||||||
|
for node_uuid, node in nodes.items():
|
||||||
|
if node["name"].lower() == name_l and entity_type in (node.get("labels") or []):
|
||||||
|
return node_uuid
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _find_node_by_name(nodes: Dict[str, Any], name: str) -> Optional[str]:
|
||||||
|
name_l = name.lower()
|
||||||
|
for node_uuid, node in nodes.items():
|
||||||
|
if node["name"].lower() == name_l:
|
||||||
|
return node_uuid
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _edge_exists(edges: List[Dict[str, Any]], source_uuid: str, target_uuid: str, relation: str, fact: str) -> bool:
|
||||||
|
for edge in edges:
|
||||||
|
if (
|
||||||
|
edge["source_node_uuid"] == source_uuid
|
||||||
|
and edge["target_node_uuid"] == target_uuid
|
||||||
|
and edge["name"] == relation
|
||||||
|
and edge["fact"] == fact
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
@ -12,6 +12,8 @@ flask-cors>=6.0.0
|
||||||
# ============= LLM 相关 =============
|
# ============= LLM 相关 =============
|
||||||
# OpenAI SDK(统一使用 OpenAI 格式调用 LLM)
|
# OpenAI SDK(统一使用 OpenAI 格式调用 LLM)
|
||||||
openai>=1.0.0
|
openai>=1.0.0
|
||||||
|
# Anthropic SDK(Claude 图谱构建引擎)
|
||||||
|
anthropic>=0.40.0
|
||||||
|
|
||||||
# ============= Zep Cloud =============
|
# ============= Zep Cloud =============
|
||||||
zep-cloud==3.13.0
|
zep-cloud==3.13.0
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,34 @@
|
||||||
<p class="description">
|
<p class="description">
|
||||||
{{ $t('step1.graphRagDesc') }}
|
{{ $t('step1.graphRagDesc') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- Engine Selector (Graphify-style) -->
|
||||||
|
<div class="engine-selector">
|
||||||
|
<span class="engine-label">{{ $t('step1.graphEngine') }}</span>
|
||||||
|
<div class="engine-pills">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="engine-pill"
|
||||||
|
:class="{ active: graphEngine === 'claude' }"
|
||||||
|
:disabled="currentPhase >= 1"
|
||||||
|
@click="$emit('update:graph-engine', 'claude')"
|
||||||
|
>
|
||||||
|
<span class="engine-dot claude"></span>
|
||||||
|
Claude
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="engine-pill"
|
||||||
|
:class="{ active: graphEngine === 'zep' }"
|
||||||
|
:disabled="currentPhase >= 1"
|
||||||
|
@click="$emit('update:graph-engine', 'zep')"
|
||||||
|
>
|
||||||
|
<span class="engine-dot zep"></span>
|
||||||
|
Zep
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Stats Cards -->
|
<!-- Stats Cards -->
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
|
|
@ -201,10 +228,11 @@ const props = defineProps({
|
||||||
ontologyProgress: Object,
|
ontologyProgress: Object,
|
||||||
buildProgress: Object,
|
buildProgress: Object,
|
||||||
graphData: Object,
|
graphData: Object,
|
||||||
systemLogs: { type: Array, default: () => [] }
|
systemLogs: { type: Array, default: () => [] },
|
||||||
|
graphEngine: { type: String, default: 'claude' }
|
||||||
})
|
})
|
||||||
|
|
||||||
defineEmits(['next-step'])
|
defineEmits(['next-step', 'update:graph-engine'])
|
||||||
|
|
||||||
const selectedOntologyItem = ref(null)
|
const selectedOntologyItem = ref(null)
|
||||||
const logContent = ref(null)
|
const logContent = ref(null)
|
||||||
|
|
@ -570,6 +598,76 @@ watch(() => props.systemLogs.length, () => {
|
||||||
color: #BBB;
|
color: #BBB;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Step 02 Engine Selector */
|
||||||
|
.engine-selector {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #999;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-pills {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
background: #F5F5F5;
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid #EAEAEA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-pill {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: transparent;
|
||||||
|
color: #777;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-pill:hover:not(:disabled) {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-pill.active {
|
||||||
|
background: #FFF;
|
||||||
|
color: #000;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-pill:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-dot {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-dot.claude {
|
||||||
|
background: #D97757;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-dot.zep {
|
||||||
|
background: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
/* Step 02 Stats */
|
/* Step 02 Stats */
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
<!-- Right Panel: Step Components -->
|
<!-- Right Panel: Step Components -->
|
||||||
<div class="panel-wrapper right" :style="rightPanelStyle">
|
<div class="panel-wrapper right" :style="rightPanelStyle">
|
||||||
<!-- Step 1: 图谱构建 -->
|
<!-- Step 1: 图谱构建 -->
|
||||||
<Step1GraphBuild
|
<Step1GraphBuild
|
||||||
v-if="currentStep === 1"
|
v-if="currentStep === 1"
|
||||||
:currentPhase="currentPhase"
|
:currentPhase="currentPhase"
|
||||||
:projectData="projectData"
|
:projectData="projectData"
|
||||||
|
|
@ -59,6 +59,8 @@
|
||||||
:buildProgress="buildProgress"
|
:buildProgress="buildProgress"
|
||||||
:graphData="graphData"
|
:graphData="graphData"
|
||||||
:systemLogs="systemLogs"
|
:systemLogs="systemLogs"
|
||||||
|
:graphEngine="graphEngine"
|
||||||
|
@update:graph-engine="val => graphEngine = val"
|
||||||
@next-step="handleNextStep"
|
@next-step="handleNextStep"
|
||||||
/>
|
/>
|
||||||
<!-- Step 2: 环境搭建 -->
|
<!-- Step 2: 环境搭建 -->
|
||||||
|
|
@ -109,6 +111,7 @@ const currentPhase = ref(-1) // -1: Upload, 0: Ontology, 1: Build, 2: Complete
|
||||||
const ontologyProgress = ref(null)
|
const ontologyProgress = ref(null)
|
||||||
const buildProgress = ref(null)
|
const buildProgress = ref(null)
|
||||||
const systemLogs = ref([])
|
const systemLogs = ref([])
|
||||||
|
const graphEngine = ref('claude') // 'claude' | 'zep' - 图谱构建引擎
|
||||||
|
|
||||||
// Polling timers
|
// Polling timers
|
||||||
let pollTimer = null
|
let pollTimer = null
|
||||||
|
|
@ -238,6 +241,7 @@ const loadProject = async () => {
|
||||||
const res = await getProject(currentProjectId.value)
|
const res = await getProject(currentProjectId.value)
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
projectData.value = res.data
|
projectData.value = res.data
|
||||||
|
if (res.data.graph_engine) graphEngine.value = res.data.graph_engine
|
||||||
updatePhaseByStatus(res.data.status)
|
updatePhaseByStatus(res.data.status)
|
||||||
addLog(`Project loaded. Status: ${res.data.status}`)
|
addLog(`Project loaded. Status: ${res.data.status}`)
|
||||||
|
|
||||||
|
|
@ -279,7 +283,7 @@ const startBuildGraph = async () => {
|
||||||
buildProgress.value = { progress: 0, message: 'Starting build...' }
|
buildProgress.value = { progress: 0, message: 'Starting build...' }
|
||||||
addLog('Initiating graph build...')
|
addLog('Initiating graph build...')
|
||||||
|
|
||||||
const res = await buildGraph({ project_id: currentProjectId.value })
|
const res = await buildGraph({ project_id: currentProjectId.value, engine: graphEngine.value })
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
addLog(`Graph build task started. Task ID: ${res.data.task_id}`)
|
addLog(`Graph build task started. Task ID: ${res.data.task_id}`)
|
||||||
startGraphPolling()
|
startGraphPolling()
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,8 @@
|
||||||
"ontologyDesc": "LLM analyzes document content and simulation requirements, extracts reality seeds, and auto-generates a suitable ontology structure",
|
"ontologyDesc": "LLM analyzes document content and simulation requirements, extracts reality seeds, and auto-generates a suitable ontology structure",
|
||||||
"analyzingDocs": "Analyzing documents...",
|
"analyzingDocs": "Analyzing documents...",
|
||||||
"graphRagBuild": "GraphRAG Build",
|
"graphRagBuild": "GraphRAG Build",
|
||||||
"graphRagDesc": "Based on the generated ontology, documents are auto-chunked and sent to Zep to build a knowledge graph, extracting entities and relations, forming temporal memory and community summaries",
|
"graphRagDesc": "Based on the generated ontology, documents are auto-chunked and sent to the selected engine (Claude or Zep) to build a knowledge graph, extracting entities and relations, forming temporal memory and community summaries",
|
||||||
|
"graphEngine": "Engine",
|
||||||
"entityNodes": "Entity Nodes",
|
"entityNodes": "Entity Nodes",
|
||||||
"relationEdges": "Relation Edges",
|
"relationEdges": "Relation Edges",
|
||||||
"schemaTypes": "Schema Types",
|
"schemaTypes": "Schema Types",
|
||||||
|
|
@ -329,6 +330,8 @@
|
||||||
"requireProjectId": "Please provide project_id",
|
"requireProjectId": "Please provide project_id",
|
||||||
"configError": "Configuration error: {details}",
|
"configError": "Configuration error: {details}",
|
||||||
"zepApiKeyMissing": "ZEP_API_KEY not configured",
|
"zepApiKeyMissing": "ZEP_API_KEY not configured",
|
||||||
|
"anthropicApiKeyMissing": "ANTHROPIC_API_KEY not configured",
|
||||||
|
"unknownEngine": "Unknown graph engine: {engine}",
|
||||||
"ontologyNotGenerated": "Ontology not yet generated. Please call /ontology/generate first.",
|
"ontologyNotGenerated": "Ontology not yet generated. Please call /ontology/generate first.",
|
||||||
"graphBuilding": "Graph build in progress. Do not resubmit. To force rebuild, add force: true.",
|
"graphBuilding": "Graph build in progress. Do not resubmit. To force rebuild, add force: true.",
|
||||||
"textNotFound": "Extracted text content not found",
|
"textNotFound": "Extracted text content not found",
|
||||||
|
|
@ -394,6 +397,11 @@
|
||||||
"initGraphService": "Initializing graph build service...",
|
"initGraphService": "Initializing graph build service...",
|
||||||
"textChunking": "Chunking text...",
|
"textChunking": "Chunking text...",
|
||||||
"creatingZepGraph": "Creating Zep graph...",
|
"creatingZepGraph": "Creating Zep graph...",
|
||||||
|
"creatingClaudeGraph": "Creating Claude-powered graph...",
|
||||||
|
"claudeExtractingChunk": "Claude extracting chunk {current}/{total}...",
|
||||||
|
"claudeExtractionDone": "Claude extraction complete, merging graph...",
|
||||||
|
"claudeChunkFailed": "Chunk {current} extraction failed: {error}",
|
||||||
|
"claudeAllChunksFailed": "All chunks failed Claude extraction",
|
||||||
"settingOntology": "Setting ontology definition...",
|
"settingOntology": "Setting ontology definition...",
|
||||||
"addingChunks": "Adding {count} text chunks...",
|
"addingChunks": "Adding {count} text chunks...",
|
||||||
"waitingZepProcess": "Waiting for Zep to process data...",
|
"waitingZepProcess": "Waiting for Zep to process data...",
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,8 @@
|
||||||
"ontologyDesc": "LLM分析文档内容与模拟需求,提取出现实种子,自动生成合适的本体结构",
|
"ontologyDesc": "LLM分析文档内容与模拟需求,提取出现实种子,自动生成合适的本体结构",
|
||||||
"analyzingDocs": "正在分析文档...",
|
"analyzingDocs": "正在分析文档...",
|
||||||
"graphRagBuild": "GraphRAG构建",
|
"graphRagBuild": "GraphRAG构建",
|
||||||
"graphRagDesc": "基于生成的本体,将文档自动分块后调用 Zep 构建知识图谱,提取实体和关系,并形成时序记忆与社区摘要",
|
"graphRagDesc": "基于生成的本体,将文档自动分块后调用所选引擎(Claude 或 Zep)构建知识图谱,提取实体和关系,并形成时序记忆与社区摘要",
|
||||||
|
"graphEngine": "引擎",
|
||||||
"entityNodes": "实体节点",
|
"entityNodes": "实体节点",
|
||||||
"relationEdges": "关系边",
|
"relationEdges": "关系边",
|
||||||
"schemaTypes": "SCHEMA类型",
|
"schemaTypes": "SCHEMA类型",
|
||||||
|
|
@ -329,6 +330,8 @@
|
||||||
"requireProjectId": "请提供 project_id",
|
"requireProjectId": "请提供 project_id",
|
||||||
"configError": "配置错误: {details}",
|
"configError": "配置错误: {details}",
|
||||||
"zepApiKeyMissing": "ZEP_API_KEY未配置",
|
"zepApiKeyMissing": "ZEP_API_KEY未配置",
|
||||||
|
"anthropicApiKeyMissing": "ANTHROPIC_API_KEY未配置",
|
||||||
|
"unknownEngine": "未知的图谱引擎: {engine}",
|
||||||
"ontologyNotGenerated": "项目尚未生成本体,请先调用 /ontology/generate",
|
"ontologyNotGenerated": "项目尚未生成本体,请先调用 /ontology/generate",
|
||||||
"graphBuilding": "图谱正在构建中,请勿重复提交。如需强制重建,请添加 force: true",
|
"graphBuilding": "图谱正在构建中,请勿重复提交。如需强制重建,请添加 force: true",
|
||||||
"textNotFound": "未找到提取的文本内容",
|
"textNotFound": "未找到提取的文本内容",
|
||||||
|
|
@ -394,6 +397,11 @@
|
||||||
"initGraphService": "初始化图谱构建服务...",
|
"initGraphService": "初始化图谱构建服务...",
|
||||||
"textChunking": "文本分块中...",
|
"textChunking": "文本分块中...",
|
||||||
"creatingZepGraph": "创建Zep图谱...",
|
"creatingZepGraph": "创建Zep图谱...",
|
||||||
|
"creatingClaudeGraph": "创建Claude驱动的图谱...",
|
||||||
|
"claudeExtractingChunk": "Claude正在抽取第 {current}/{total} 个文本块...",
|
||||||
|
"claudeExtractionDone": "Claude抽取完成,正在合并图谱...",
|
||||||
|
"claudeChunkFailed": "第 {current} 个文本块抽取失败: {error}",
|
||||||
|
"claudeAllChunksFailed": "所有文本块的Claude抽取均失败",
|
||||||
"settingOntology": "设置本体定义...",
|
"settingOntology": "设置本体定义...",
|
||||||
"addingChunks": "开始添加 {count} 个文本块...",
|
"addingChunks": "开始添加 {count} 个文本块...",
|
||||||
"waitingZepProcess": "等待Zep处理数据...",
|
"waitingZepProcess": "等待Zep处理数据...",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue