马一丁

Add Comments

@@ -598,6 +598,7 @@ class ReportAgent: @@ -598,6 +598,7 @@ class ReportAgent:
598 emit('stage', {'stage': 'storage_ready', 'run_dir': str(run_dir)}) 598 emit('stage', {'stage': 'storage_ready', 'run_dir': str(run_dir)})
599 599
600 # ==================== GraphRAG 初始化 ==================== 600 # ==================== GraphRAG 初始化 ====================
  601 + # 根据配置开关决定是否启用图谱构建/查询(需 .env 设置 GRAPHRAG_ENABLED=True)
601 graphrag_enabled = getattr(self.config, 'GRAPHRAG_ENABLED', False) 602 graphrag_enabled = getattr(self.config, 'GRAPHRAG_ENABLED', False)
602 knowledge_graph = None 603 knowledge_graph = None
603 graphrag_query_node = None 604 graphrag_query_node = None
@@ -607,6 +608,7 @@ class ReportAgent: @@ -607,6 +608,7 @@ class ReportAgent:
607 emit('stage', {'stage': 'graphrag_building', 'message': '正在构建知识图谱'}) 608 emit('stage', {'stage': 'graphrag_building', 'message': '正在构建知识图谱'})
608 609
609 try: 610 try:
  611 + # 将 state_*.json + forum.log 转为结构化图谱,并立即落盘 graphrag.json
610 knowledge_graph = self._build_knowledge_graph( 612 knowledge_graph = self._build_knowledge_graph(
611 query, normalized_reports, forum_logs, run_dir 613 query, normalized_reports, forum_logs, run_dir
612 ) 614 )
@@ -687,6 +689,7 @@ class ReportAgent: @@ -687,6 +689,7 @@ class ReportAgent:
687 'emphasis': emphasis_value 689 'emphasis': emphasis_value
688 } 690 }
689 691
  692 + # 先让 GraphRAG 节点多轮查询,再把结果附加到章节上下文
690 graph_results = graphrag_query_node.run( 693 graph_results = graphrag_query_node.run(
691 section_info, 694 section_info,
692 { 695 {
@@ -699,7 +702,7 @@ class ReportAgent: @@ -699,7 +702,7 @@ class ReportAgent:
699 ) 702 )
700 703
701 if graph_results and graph_results.get('total_nodes', 0) > 0: 704 if graph_results and graph_results.get('total_nodes', 0) > 0:
702 - # 将图谱结果注入生成上下文 705 + # 将图谱结果注入生成上下文,后续章节 LLM 自动使用增强提示词
703 chapter_context['graph_results'] = graph_results 706 chapter_context['graph_results'] = graph_results
704 chapter_context['graph_enhancement_prompt'] = format_graph_results_for_prompt(graph_results) 707 chapter_context['graph_enhancement_prompt'] = format_graph_results_for_prompt(graph_results)
705 logger.info(f"章节 {section.title} GraphRAG 查询完成: {graph_results.get('total_nodes', 0)} 节点") 708 logger.info(f"章节 {section.title} GraphRAG 查询完成: {graph_results.get('total_nodes', 0)} 节点")
@@ -2,6 +2,11 @@ @@ -2,6 +2,11 @@
2 GraphRAG 知识图谱模块 2 GraphRAG 知识图谱模块
3 3
4 提供基于结构化数据的知识图谱构建、存储与查询功能。 4 提供基于结构化数据的知识图谱构建、存储与查询功能。
  5 +典型用法:
  6 +1) 使用 `StateParser`/`ForumParser` 解析三引擎 state JSON 与 forum.log;
  7 +2) 调用 `GraphBuilder.build` 生成纯结构化的图对象;
  8 +3) 通过 `GraphStorage.save/load` 持久化或读取图数据;
  9 +4) 以 `QueryEngine` 在章节侧执行多轮图查询。
5 """ 10 """
6 11
7 from .state_parser import StateParser, ParsedState, ParsedSection, SearchRecord 12 from .state_parser import StateParser, ParsedState, ParsedSection, SearchRecord
@@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
2 Forum 日志解析器 2 Forum 日志解析器
3 3
4 解析 forum.log 文件,提取结构化的讨论记录用于构建知识图谱。 4 解析 forum.log 文件,提取结构化的讨论记录用于构建知识图谱。
  5 +日志与 GraphRAG 的关系:仅将主持人/三引擎发言转为结构化节点,
  6 +用于补充 Host 总结或跨引擎观点。
5 """ 7 """
6 8
7 from dataclasses import dataclass 9 from dataclasses import dataclass
@@ -41,6 +43,7 @@ class ForumParser: @@ -41,6 +43,7 @@ class ForumParser:
41 43
42 解析 forum.log,提取结构化的讨论记录。 44 解析 forum.log,提取结构化的讨论记录。
43 日志格式: [HH:MM:SS] [SPEAKER] content 45 日志格式: [HH:MM:SS] [SPEAKER] content
  46 + SPEAKER 需属于 VALID_SPEAKERS;非规范行会被忽略,确保图谱不被噪音污染。
44 """ 47 """
45 48
46 # 匹配日志行的正则表达式 49 # 匹配日志行的正则表达式
@@ -19,6 +19,10 @@ class GraphBuilder: @@ -19,6 +19,10 @@ class GraphBuilder:
19 基于已有的结构化数据(State JSON、Forum 日志)构建图谱, 19 基于已有的结构化数据(State JSON、Forum 日志)构建图谱,
20 无需 LLM 进行实体/关系提取。 20 无需 LLM 进行实体/关系提取。
21 21
  22 + ReportAgent 在 _build_knowledge_graph 中调用本构建器,将 load_input_files
  23 + 提前解析好的 ParsedState / ForumEntry 转为 Graph 对象,再交由 GraphStorage
  24 + 落盘并供 GraphRAGQueryNode 查询。
  25 +
22 节点类型(5种): 26 节点类型(5种):
23 - topic: 用户查询主题 27 - topic: 用户查询主题
24 - engine: 四个引擎来源 (insight/media/query/host) 28 - engine: 四个引擎来源 (insight/media/query/host)
@@ -108,7 +112,7 @@ class GraphBuilder: @@ -108,7 +112,7 @@ class GraphBuilder:
108 if not search.query: 112 if not search.query:
109 continue 113 continue
110 114
111 - # 搜索词去重 115 + # 搜索词去重(同一段落相同查询仅保留首条,避免图谱冗余)
112 query_key = search.query.strip().lower() 116 query_key = search.query.strip().lower()
113 if query_key in seen_queries: 117 if query_key in seen_queries:
114 continue 118 continue
@@ -107,7 +107,12 @@ class Edge: @@ -107,7 +107,12 @@ class Edge:
107 107
108 108
109 class Graph: 109 class Graph:
110 - """知识图谱""" 110 + """
  111 + 知识图谱
  112 +
  113 + 仅负责存储节点/边与邻接表,不依赖外部数据库,便于在章节侧内存查询。
  114 + 邻接表 _adjacency 用于 QueryEngine 按深度扩展邻居节点。
  115 + """
111 116
112 def __init__(self): 117 def __init__(self):
113 self._nodes: Dict[str, Node] = {} 118 self._nodes: Dict[str, Node] = {}
@@ -290,7 +295,13 @@ class Graph: @@ -290,7 +295,13 @@ class Graph:
290 295
291 296
292 class GraphStorage: 297 class GraphStorage:
293 - """图谱存储管理器""" 298 + """
  299 + 图谱存储管理器
  300 +
  301 + 将 Graph 对象序列化为 JSON(graphrag.json),路径与 ChapterStorage 输出目录一致,
  302 + 便于 Web/Report 引擎共享。支持按报告ID查找、列举最新图谱,供 Flask API 或
  303 + GraphRAGQueryNode 直接读取。
  304 + """
294 305
295 FILENAME = "graphrag.json" 306 FILENAME = "graphrag.json"
296 307
@@ -394,6 +405,11 @@ class GraphStorage: @@ -394,6 +405,11 @@ class GraphStorage:
394 405
395 Returns: 406 Returns:
396 图谱文件路径,未找到返回 None 407 图谱文件路径,未找到返回 None
  408 +
  409 + 工作方式:
  410 + 1) 优先匹配目录名是否含 report_id(兼容 _/- 差异);
  411 + 2) 否则读取 graphrag.json 内 task_id/report_id 做兜底匹配;
  412 + 适配 Agent 运行目录命名不一致的场景。
397 """ 413 """
398 # 在章节目录中搜索(与 ChapterStorage 保持一致) 414 # 在章节目录中搜索(与 ChapterStorage 保持一致)
399 chapters_dir = self.chapters_dir 415 chapters_dir = self.chapters_dir
@@ -442,6 +458,8 @@ class GraphStorage: @@ -442,6 +458,8 @@ class GraphStorage:
442 458
443 Returns: 459 Returns:
444 最新图谱文件路径,未找到返回 None 460 最新图谱文件路径,未找到返回 None
  461 +
  462 + 根据文件修改时间排序,用于前端“最近一次生成”快速预览。
445 """ 463 """
446 chapters_dir = self.chapters_dir 464 chapters_dir = self.chapters_dir
447 if not chapters_dir.exists(): 465 if not chapters_dir.exists():
@@ -121,6 +121,9 @@ def format_graph_results_for_prompt(graph_results: dict) -> str: @@ -121,6 +121,9 @@ def format_graph_results_for_prompt(graph_results: dict) -> str:
121 121
122 Returns: 122 Returns:
123 格式化的字符串 123 格式化的字符串
  124 +
  125 + 供 ReportAgent 在章节生成前注入 `graph_enhancement_prompt`,
  126 + 将多轮查询结果以结构化文本交给章节 LLM,避免直接传递大 JSON。
124 """ 127 """
125 if not graph_results: 128 if not graph_results:
126 return "" 129 return ""
@@ -12,7 +12,15 @@ from .graph_storage import Graph, Node @@ -12,7 +12,15 @@ from .graph_storage import Graph, Node
12 12
13 @dataclass 13 @dataclass
14 class QueryParams: 14 class QueryParams:
15 - """查询参数""" 15 + """
  16 + 查询参数
  17 +
  18 + 由 GraphRAGQueryNode 或 Flask API 注入,控制查询范围:
  19 + - keywords: 关键词列表,可为空(空时默认返回各引擎 section 摘要);
  20 + - node_types: 限定节点类型;None 表示全量;
  21 + - engine_filter: 仅保留指定引擎来源;
  22 + - depth: 匹配节点向外扩展的层级。
  23 + """
16 keywords: List[str] = field(default_factory=list) 24 keywords: List[str] = field(default_factory=list)
17 node_types: Optional[List[str]] = None # None 表示全部类型 25 node_types: Optional[List[str]] = None # None 表示全部类型
18 engine_filter: Optional[List[str]] = None # 限定引擎来源 26 engine_filter: Optional[List[str]] = None # 限定引擎来源
@@ -3,6 +3,10 @@ State JSON 解析器 @@ -3,6 +3,10 @@ State JSON 解析器
3 3
4 解析 Insight/Media/Query 三引擎的 State JSON 文件, 4 解析 Insight/Media/Query 三引擎的 State JSON 文件,
5 提取结构化数据用于构建知识图谱。 5 提取结构化数据用于构建知识图谱。
  6 +
  7 +默认假设 state_* 文件结构与三引擎输出一致:
  8 +- 顶层包含 query/report_title/paragraphs;
  9 +- 段落内的 research.search_history 记录搜索关键词、URL与摘要。
6 """ 10 """
7 11
8 from dataclasses import dataclass, field 12 from dataclasses import dataclass, field
@@ -45,6 +49,8 @@ class StateParser: @@ -45,6 +49,8 @@ class StateParser:
45 State JSON 解析器 49 State JSON 解析器
46 50
47 解析三引擎的 State JSON,提取用于构建知识图谱的结构化数据。 51 解析三引擎的 State JSON,提取用于构建知识图谱的结构化数据。
  52 + 适用于 load_input_files 阶段:先查找与 MD 同目录的 state_*.json,
  53 + 若存在则转为 ParsedState 供 GraphBuilder 直接消费。
48 """ 54 """
49 55
50 def parse(self, engine_name: str, state_json: Dict[str, Any]) -> ParsedState: 56 def parse(self, engine_name: str, state_json: Dict[str, Any]) -> ParsedState:
@@ -84,7 +90,7 @@ class StateParser: @@ -84,7 +90,7 @@ class StateParser:
84 timestamp=search.get('timestamp', '') 90 timestamp=search.get('timestamp', '')
85 )) 91 ))
86 92
87 - # 获取摘要,优先使用 latest_summary 93 + # 获取摘要,优先使用 latest_summary;若缺失则回退到段落正文
88 summary = research.get('latest_summary', '') 94 summary = research.get('latest_summary', '')
89 if not summary: 95 if not summary:
90 summary = para.get('content', '') 96 summary = para.get('content', '')
@@ -124,6 +130,7 @@ class StateParser: @@ -124,6 +130,7 @@ class StateParser:
124 根据 Markdown 报告路径查找对应的 State JSON 文件 130 根据 Markdown 报告路径查找对应的 State JSON 文件
125 131
126 State JSON 通常与 MD 文件在同一目录下,命名格式为 state_*.json 132 State JSON 通常与 MD 文件在同一目录下,命名格式为 state_*.json
  133 + 用于 GraphRAG:在 load_input_files 时自动匹配最新或同名 state 文件。
127 134
128 Args: 135 Args:
129 md_path: Markdown 文件路径 136 md_path: Markdown 文件路径
@@ -133,7 +133,7 @@ class GraphRAGQueryNode(BaseNode): @@ -133,7 +133,7 @@ class GraphRAGQueryNode(BaseNode):
133 for round_idx in range(max_queries): 133 for round_idx in range(max_queries):
134 self.log_info(f"查询轮次 {round_idx + 1}/{max_queries}") 134 self.log_info(f"查询轮次 {round_idx + 1}/{max_queries}")
135 135
136 - # 1. 构建决策提示词 136 + # 1. 构建决策提示词:将章节目标+图谱概览+查询历史一起交给 LLM
137 prompt = self._build_decision_prompt( 137 prompt = self._build_decision_prompt(
138 section, context, query_engine, history 138 section, context, query_engine, history
139 ) 139 )
@@ -145,12 +145,12 @@ class GraphRAGQueryNode(BaseNode): @@ -145,12 +145,12 @@ class GraphRAGQueryNode(BaseNode):
145 self.log_error("LLM 返回无效决策,终止查询") 145 self.log_error("LLM 返回无效决策,终止查询")
146 break 146 break
147 147
148 - # 3. 检查是否停止 148 + # 3. 检查是否停止:LLM 可主动返回 should_query=false 以节省轮次
149 if not decision.get('should_query', False): 149 if not decision.get('should_query', False):
150 self.log_info(f"LLM 决定停止查询: {decision.get('reasoning', '无原因')}") 150 self.log_info(f"LLM 决定停止查询: {decision.get('reasoning', '无原因')}")
151 break 151 break
152 152
153 - # 4. 执行查询 153 + # 4. 执行查询:按 LLM 给出的参数查询本地图谱
154 params = QueryParams( 154 params = QueryParams(
155 keywords=decision.get('keywords', []), 155 keywords=decision.get('keywords', []),
156 node_types=decision.get('node_types'), 156 node_types=decision.get('node_types'),
@@ -222,7 +222,12 @@ class GraphRAGQueryNode(BaseNode): @@ -222,7 +222,12 @@ class GraphRAGQueryNode(BaseNode):
222 context: Dict[str, Any], 222 context: Dict[str, Any],
223 query_engine: QueryEngine, 223 query_engine: QueryEngine,
224 history: QueryHistory) -> Dict[str, str]: 224 history: QueryHistory) -> Dict[str, str]:
225 - """构建查询决策提示词""" 225 + """
  226 + 构建查询决策提示词
  227 +
  228 + 将章节目标、模板章节概览、图谱统计、历史查询摘要整合为
  229 + system/user prompt,指导 LLM 生成下一轮 QueryParams。
  230 + """
226 # 获取图谱概览 231 # 获取图谱概览
227 summary = query_engine.get_node_summary() 232 summary = query_engine.get_node_summary()
228 stats = summary.get('stats', {}) 233 stats = summary.get('stats', {})
@@ -268,7 +273,12 @@ class GraphRAGQueryNode(BaseNode): @@ -268,7 +273,12 @@ class GraphRAGQueryNode(BaseNode):
268 } 273 }
269 274
270 def _get_query_decision(self, prompt: Dict[str, str]) -> Optional[Dict[str, Any]]: 275 def _get_query_decision(self, prompt: Dict[str, str]) -> Optional[Dict[str, Any]]:
271 - """调用 LLM 获取查询决策""" 276 + """
  277 + 调用 LLM 获取查询决策
  278 +
  279 + 返回的 JSON 将被转换为 QueryParams;任何解析失败都会终止后续轮次,
  280 + 避免章节生成被异常输出阻断。
  281 + """
272 try: 282 try:
273 response = self.llm_client.invoke( 283 response = self.llm_client.invoke(
274 system_prompt=prompt['system'], 284 system_prompt=prompt['system'],
@@ -1307,6 +1307,8 @@ def shutdown_system(): @@ -1307,6 +1307,8 @@ def shutdown_system():
1307 return jsonify({'success': False, 'message': f'系统关闭异常: {exc}'}), 500 1307 return jsonify({'success': False, 'message': f'系统关闭异常: {exc}'}), 500
1308 1308
1309 # ==================== GraphRAG API 端点 ==================== 1309 # ==================== GraphRAG API 端点 ====================
  1310 +# 前端控制台与 /graph-viewer 调用,均依赖 ReportEngine 在章节目录落盘的 graphrag.json。
  1311 +# 若 GRAPHRAG_ENABLED 关闭,这些接口仅返回“未找到图谱”提示。
1310 1312
1311 @app.route('/api/graph/<report_id>') 1313 @app.route('/api/graph/<report_id>')
1312 def get_graph_data(report_id): 1314 def get_graph_data(report_id):
@@ -1493,6 +1495,7 @@ def query_graph(): @@ -1493,6 +1495,7 @@ def query_graph():
1493 if report_id: 1495 if report_id:
1494 graph_path = storage.find_graph_by_report_id(report_id) 1496 graph_path = storage.find_graph_by_report_id(report_id)
1495 else: 1497 else:
  1498 + # 未指定报告ID时默认取最近一次生成的图谱,便于快速试用
1496 graph_path = storage.find_latest_graph() 1499 graph_path = storage.find_latest_graph()
1497 1500
1498 if not graph_path or not graph_path.exists(): 1501 if not graph_path or not graph_path.exists():
@@ -2282,6 +2282,7 @@ @@ -2282,6 +2282,7 @@
2282 query: 8503 2282 query: 8503
2283 }; 2283 };
2284 2284
  2285 + // ---------------- GraphRAG 开关与配置同步 ----------------
2285 function syncGraphragFlag(config) { 2286 function syncGraphragFlag(config) {
2286 if (!config || !Object.prototype.hasOwnProperty.call(config, 'GRAPHRAG_ENABLED')) { 2287 if (!config || !Object.prototype.hasOwnProperty.call(config, 'GRAPHRAG_ENABLED')) {
2287 return; 2288 return;
@@ -2290,6 +2291,7 @@ @@ -2290,6 +2291,7 @@
2290 graphragSettingLoaded = true; 2291 graphragSettingLoaded = true;
2291 } 2292 }
2292 2293
  2294 + // 前端懒加载配置:初次访问或强制刷新时请求 /api/config,决定是否展示图谱面板
2293 async function ensureGraphragSetting(force = false) { 2295 async function ensureGraphragSetting(force = false) {
2294 if (!force && graphragSettingLoaded) { 2296 if (!force && graphragSettingLoaded) {
2295 return graphragEnabled; 2297 return graphragEnabled;
@@ -5151,6 +5153,7 @@ function getConsoleContainer() { @@ -5151,6 +5153,7 @@ function getConsoleContainer() {
5151 } 5153 }
5152 } 5154 }
5153 5155
  5156 + // 入口:在报告界面渲染后初始化图谱面板,若 GraphRAG 关闭则隐藏
5154 async function initializeGraphPanel(statusData) { 5157 async function initializeGraphPanel(statusData) {
5155 const panel = document.getElementById('graphPanel'); 5158 const panel = document.getElementById('graphPanel');
5156 if (!panel) return; 5159 if (!panel) return;
@@ -5175,6 +5178,7 @@ function getConsoleContainer() { @@ -5175,6 +5178,7 @@ function getConsoleContainer() {
5175 refreshGraphPanel(graphPanelTaskId, true); 5178 refreshGraphPanel(graphPanelTaskId, true);
5176 } 5179 }
5177 5180
  5181 + // 注册图谱面板按钮/筛选/搜索事件,只绑定一次
5178 function bindGraphPanelEvents() { 5182 function bindGraphPanelEvents() {
5179 const refreshBtn = document.getElementById('graphRefreshBtn'); 5183 const refreshBtn = document.getElementById('graphRefreshBtn');
5180 const collapseBtn = document.getElementById('graphCollapseBtn'); 5184 const collapseBtn = document.getElementById('graphCollapseBtn');
@@ -5342,6 +5346,7 @@ function getConsoleContainer() { @@ -5342,6 +5346,7 @@ function getConsoleContainer() {
5342 }); 5346 });
5343 } 5347 }
5344 5348
  5349 + // 将 graphrag.json 转换的节点/边渲染成 mini 版 vis.js 图谱
5345 function renderGraphPanel(graph, resetPlaceholder = true) { 5350 function renderGraphPanel(graph, resetPlaceholder = true) {
5346 const panel = document.getElementById('graphPanel'); 5351 const panel = document.getElementById('graphPanel');
5347 const canvasWrapper = document.getElementById('graphPanelCanvas'); 5352 const canvasWrapper = document.getElementById('graphPanelCanvas');