diff --git a/README.md b/README.md index 57863e4..747cbeb 100644 --- a/README.md +++ b/README.md @@ -122,14 +122,21 @@ POST /api/v1/projects/{project_id}/align-entities?threshold=0.85 - [ ] 图谱数据同步 - [ ] 复杂图查询支持 -## Phase 5: 高级功能 - 规划中 📋 +## Phase 5: 高级功能 - 进行中 🚧 -- [ ] 知识推理与问答增强 -- [ ] 实体属性扩展 -- [ ] 时间线视图 -- [ ] 导出功能(PDF/图片) -- [ ] 协作功能(多用户) -- [ ] API 开放平台 +### 开发任务清单 + +1. **知识推理与问答增强** ✅ (已完成) + - 后端推理引擎 `knowledge_reasoner.py` + - 因果/对比/时序/关联推理 + - 智能项目总结 API + - 实体关联路径发现 + - 前端推理面板 UI + +2. **实体属性扩展** - 待开发 +3. **时间线视图** ✅ (已完成) +4. **导出功能** - 待开发 +5. **协作功能** - 待开发 --- diff --git a/STATUS.md b/STATUS.md index 5e33b98..bb2fbd0 100644 --- a/STATUS.md +++ b/STATUS.md @@ -78,7 +78,12 @@ Phase 4: Agent 助手与知识溯源 - **开发中 🚧** - [ ] 支持复杂图查询 ### Phase 5 - 高级功能 (进行中 🚧) -- [ ] 知识推理与问答增强 +- [x] 知识推理与问答增强 ✅ (2026-02-19 完成) + - 后端 API: `/api/v1/projects/{id}/reasoning/query` + - 支持因果/对比/时序/关联推理 + - 智能项目总结 (全面/高管/技术/风险) + - 实体关联路径发现 + - 前端推理面板 UI - [ ] 实体属性扩展 - [x] 时间线视图 ✅ (2026-02-19 完成) - [ ] 导出功能 diff --git a/backend/__pycache__/db_manager.cpython-312.pyc b/backend/__pycache__/db_manager.cpython-312.pyc index 016ecc9..b06fe2e 100644 Binary files a/backend/__pycache__/db_manager.cpython-312.pyc and b/backend/__pycache__/db_manager.cpython-312.pyc differ diff --git a/backend/__pycache__/document_processor.cpython-312.pyc b/backend/__pycache__/document_processor.cpython-312.pyc index aef023d..6fe9caa 100644 Binary files a/backend/__pycache__/document_processor.cpython-312.pyc and b/backend/__pycache__/document_processor.cpython-312.pyc differ diff --git a/backend/__pycache__/entity_aligner.cpython-312.pyc b/backend/__pycache__/entity_aligner.cpython-312.pyc index 3045fd4..41f18b2 100644 Binary files a/backend/__pycache__/entity_aligner.cpython-312.pyc and b/backend/__pycache__/entity_aligner.cpython-312.pyc differ diff --git a/backend/__pycache__/knowledge_reasoner.cpython-312.pyc b/backend/__pycache__/knowledge_reasoner.cpython-312.pyc new file mode 100644 index 0000000..2f9e237 Binary files /dev/null and b/backend/__pycache__/knowledge_reasoner.cpython-312.pyc differ diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc index 7a696a7..25b2b6e 100644 Binary files a/backend/__pycache__/main.cpython-312.pyc and b/backend/__pycache__/main.cpython-312.pyc differ diff --git a/backend/__pycache__/oss_uploader.cpython-312.pyc b/backend/__pycache__/oss_uploader.cpython-312.pyc new file mode 100644 index 0000000..9e89360 Binary files /dev/null and b/backend/__pycache__/oss_uploader.cpython-312.pyc differ diff --git a/backend/__pycache__/tingwu_client.cpython-312.pyc b/backend/__pycache__/tingwu_client.cpython-312.pyc new file mode 100644 index 0000000..58f81e4 Binary files /dev/null and b/backend/__pycache__/tingwu_client.cpython-312.pyc differ diff --git a/backend/knowledge_reasoner.py b/backend/knowledge_reasoner.py new file mode 100644 index 0000000..24c62fe --- /dev/null +++ b/backend/knowledge_reasoner.py @@ -0,0 +1,533 @@ +#!/usr/bin/env python3 +""" +InsightFlow Knowledge Reasoning - Phase 5 +知识推理与问答增强模块 +""" + +import os +import json +import httpx +from typing import List, Dict, Optional, Any +from dataclasses import dataclass +from enum import Enum + +KIMI_API_KEY = os.getenv("KIMI_API_KEY", "") +KIMI_BASE_URL = os.getenv("KIMI_BASE_URL", "https://api.kimi.com/coding") + + +class ReasoningType(Enum): + """推理类型""" + CAUSAL = "causal" # 因果推理 + ASSOCIATIVE = "associative" # 关联推理 + TEMPORAL = "temporal" # 时序推理 + COMPARATIVE = "comparative" # 对比推理 + SUMMARY = "summary" # 总结推理 + + +@dataclass +class ReasoningResult: + """推理结果""" + answer: str + reasoning_type: ReasoningType + confidence: float + evidence: List[Dict] # 支撑证据 + related_entities: List[str] # 相关实体 + gaps: List[str] # 知识缺口 + + +@dataclass +class InferencePath: + """推理路径""" + start_entity: str + end_entity: str + path: List[Dict] # 路径上的节点和关系 + strength: float # 路径强度 + + +class KnowledgeReasoner: + """知识推理引擎""" + + def __init__(self, api_key: str = None, base_url: str = None): + self.api_key = api_key or KIMI_API_KEY + self.base_url = base_url or KIMI_BASE_URL + self.headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json" + } + + async def _call_llm(self, prompt: str, temperature: float = 0.3) -> str: + """调用 LLM""" + if not self.api_key: + raise ValueError("KIMI_API_KEY not set") + + payload = { + "model": "k2p5", + "messages": [{"role": "user", "content": prompt}], + "temperature": temperature + } + + async with httpx.AsyncClient() as client: + response = await client.post( + f"{self.base_url}/v1/chat/completions", + headers=self.headers, + json=payload, + timeout=120.0 + ) + response.raise_for_status() + result = response.json() + return result["choices"][0]["message"]["content"] + + async def enhanced_qa( + self, + query: str, + project_context: Dict, + graph_data: Dict, + reasoning_depth: str = "medium" + ) -> ReasoningResult: + """ + 增强问答 - 结合图谱推理的问答 + + Args: + query: 用户问题 + project_context: 项目上下文 + graph_data: 知识图谱数据 + reasoning_depth: 推理深度 (shallow/medium/deep) + """ + # 1. 分析问题类型 + analysis = await self._analyze_question(query) + + # 2. 根据问题类型选择推理策略 + if analysis["type"] == "causal": + return await self._causal_reasoning(query, project_context, graph_data) + elif analysis["type"] == "comparative": + return await self._comparative_reasoning(query, project_context, graph_data) + elif analysis["type"] == "temporal": + return await self._temporal_reasoning(query, project_context, graph_data) + else: + return await self._associative_reasoning(query, project_context, graph_data) + + async def _analyze_question(self, query: str) -> Dict: + """分析问题类型和意图""" + prompt = f"""分析以下问题的类型和意图: + +问题:{query} + +请返回 JSON 格式: +{{ + "type": "causal|comparative|temporal|factual|opinion", + "entities": ["提到的实体"], + "intent": "问题意图描述", + "complexity": "simple|medium|complex" +}} + +类型说明: +- causal: 因果类问题(为什么、导致、影响) +- comparative: 对比类问题(区别、比较、优劣) +- temporal: 时序类问题(什么时候、进度、变化) +- factual: 事实类问题(是什么、有哪些) +- opinion: 观点类问题(怎么看、态度、评价)""" + + content = await self._call_llm(prompt, temperature=0.1) + + import re + json_match = re.search(r'\{{.*?\}}', content, re.DOTALL) + if json_match: + try: + return json.loads(json_match.group()) + except: + pass + + return {"type": "factual", "entities": [], "intent": "general", "complexity": "simple"} + + async def _causal_reasoning( + self, + query: str, + project_context: Dict, + graph_data: Dict + ) -> ReasoningResult: + """因果推理 - 分析原因和影响""" + + # 构建因果分析提示 + entities_str = json.dumps(graph_data.get("entities", []), ensure_ascii=False, indent=2) + relations_str = json.dumps(graph_data.get("relations", []), ensure_ascii=False, indent=2) + + prompt = f"""基于以下知识图谱进行因果推理分析: + +## 问题 +{query} + +## 实体 +{entities_str[:2000]} + +## 关系 +{relations_str[:2000]} + +## 项目上下文 +{json.dumps(project_context, ensure_ascii=False, indent=2)[:1500]} + +请进行因果分析,返回 JSON 格式: +{{ + "answer": "详细回答", + "reasoning_chain": ["推理步骤1", "推理步骤2"], + "root_causes": ["根本原因1", "根本原因2"], + "effects": ["影响1", "影响2"], + "confidence": 0.85, + "evidence": ["证据1", "证据2"], + "knowledge_gaps": ["缺失信息1"] +}}""" + + content = await self._call_llm(prompt, temperature=0.3) + + import re + json_match = re.search(r'\{{.*?\}}', content, re.DOTALL) + + if json_match: + try: + data = json.loads(json_match.group()) + return ReasoningResult( + answer=data.get("answer", ""), + reasoning_type=ReasoningType.CAUSAL, + confidence=data.get("confidence", 0.7), + evidence=[{"text": e} for e in data.get("evidence", [])], + related_entities=[], + gaps=data.get("knowledge_gaps", []) + ) + except: + pass + + return ReasoningResult( + answer=content, + reasoning_type=ReasoningType.CAUSAL, + confidence=0.5, + evidence=[], + related_entities=[], + gaps=["无法完成因果推理"] + ) + + async def _comparative_reasoning( + self, + query: str, + project_context: Dict, + graph_data: Dict + ) -> ReasoningResult: + """对比推理 - 比较实体间的异同""" + + prompt = f"""基于以下知识图谱进行对比分析: + +## 问题 +{query} + +## 实体 +{json.dumps(graph_data.get("entities", []), ensure_ascii=False, indent=2)[:2000]} + +## 关系 +{json.dumps(graph_data.get("relations", []), ensure_ascii=False, indent=2)[:1500]} + +请进行对比分析,返回 JSON 格式: +{{ + "answer": "详细对比分析", + "similarities": ["相似点1", "相似点2"], + "differences": ["差异点1", "差异点2"], + "comparison_table": {{"维度": ["实体A值", "实体B值"]}}, + "confidence": 0.85, + "evidence": ["证据1"], + "knowledge_gaps": [] +}}""" + + content = await self._call_llm(prompt, temperature=0.3) + + import re + json_match = re.search(r'\{{.*?\}}', content, re.DOTALL) + + if json_match: + try: + data = json.loads(json_match.group()) + return ReasoningResult( + answer=data.get("answer", ""), + reasoning_type=ReasoningType.COMPARATIVE, + confidence=data.get("confidence", 0.7), + evidence=[{"text": e} for e in data.get("evidence", [])], + related_entities=[], + gaps=data.get("knowledge_gaps", []) + ) + except: + pass + + return ReasoningResult( + answer=content, + reasoning_type=ReasoningType.COMPARATIVE, + confidence=0.5, + evidence=[], + related_entities=[], + gaps=[] + ) + + async def _temporal_reasoning( + self, + query: str, + project_context: Dict, + graph_data: Dict + ) -> ReasoningResult: + """时序推理 - 分析时间线和演变""" + + prompt = f"""基于以下知识图谱进行时序分析: + +## 问题 +{query} + +## 项目时间线 +{json.dumps(project_context.get("timeline", []), ensure_ascii=False, indent=2)[:2000]} + +## 实体提及历史 +{json.dumps(graph_data.get("entities", []), ensure_ascii=False, indent=2)[:1500]} + +请进行时序分析,返回 JSON 格式: +{{ + "answer": "时序分析结果", + "timeline": [{{"date": "时间", "event": "事件", "significance": "重要性"}}], + "trends": ["趋势1", "趋势2"], + "milestones": ["里程碑1"], + "confidence": 0.85, + "evidence": ["证据1"], + "knowledge_gaps": [] +}}""" + + content = await self._call_llm(prompt, temperature=0.3) + + import re + json_match = re.search(r'\{{.*?\}}', content, re.DOTALL) + + if json_match: + try: + data = json.loads(json_match.group()) + return ReasoningResult( + answer=data.get("answer", ""), + reasoning_type=ReasoningType.TEMPORAL, + confidence=data.get("confidence", 0.7), + evidence=[{"text": e} for e in data.get("evidence", [])], + related_entities=[], + gaps=data.get("knowledge_gaps", []) + ) + except: + pass + + return ReasoningResult( + answer=content, + reasoning_type=ReasoningType.TEMPORAL, + confidence=0.5, + evidence=[], + related_entities=[], + gaps=[] + ) + + async def _associative_reasoning( + self, + query: str, + project_context: Dict, + graph_data: Dict + ) -> ReasoningResult: + """关联推理 - 发现实体间的隐含关联""" + + prompt = f"""基于以下知识图谱进行关联分析: + +## 问题 +{query} + +## 实体 +{json.dumps(graph_data.get("entities", [])[:20], ensure_ascii=False, indent=2)} + +## 关系 +{json.dumps(graph_data.get("relations", [])[:30], ensure_ascii=False, indent=2)} + +请进行关联推理,发现隐含联系,返回 JSON 格式: +{{ + "answer": "关联分析结果", + "direct_connections": ["直接关联1"], + "indirect_connections": ["间接关联1"], + "inferred_relations": [{{"source": "A", "target": "B", "relation": "可能关系", "confidence": 0.7}}], + "confidence": 0.85, + "evidence": ["证据1"], + "knowledge_gaps": [] +}}""" + + content = await self._call_llm(prompt, temperature=0.4) + + import re + json_match = re.search(r'\{{.*?\}}', content, re.DOTALL) + + if json_match: + try: + data = json.loads(json_match.group()) + return ReasoningResult( + answer=data.get("answer", ""), + reasoning_type=ReasoningType.ASSOCIATIVE, + confidence=data.get("confidence", 0.7), + evidence=[{"text": e} for e in data.get("evidence", [])], + related_entities=[], + gaps=data.get("knowledge_gaps", []) + ) + except: + pass + + return ReasoningResult( + answer=content, + reasoning_type=ReasoningType.ASSOCIATIVE, + confidence=0.5, + evidence=[], + related_entities=[], + gaps=[] + ) + + def find_inference_paths( + self, + start_entity: str, + end_entity: str, + graph_data: Dict, + max_depth: int = 3 + ) -> List[InferencePath]: + """ + 发现两个实体之间的推理路径 + + 使用 BFS 在关系图中搜索路径 + """ + entities = {e["id"]: e for e in graph_data.get("entities", [])} + relations = graph_data.get("relations", []) + + # 构建邻接表 + adj = {} + for r in relations: + src = r.get("source_id") or r.get("source") + tgt = r.get("target_id") or r.get("target") + if src not in adj: + adj[src] = [] + if tgt not in adj: + adj[tgt] = [] + adj[src].append({"target": tgt, "relation": r.get("type", "related"), "data": r}) + # 无向图也添加反向 + adj[tgt].append({"target": src, "relation": r.get("type", "related"), "data": r, "reverse": True}) + + # BFS 搜索路径 + from collections import deque + paths = [] + queue = deque([(start_entity, [{"entity": start_entity, "relation": None}])]) + visited = {start_entity} + + while queue and len(paths) < 5: + current, path = queue.popleft() + + if current == end_entity and len(path) > 1: + # 找到一条路径 + paths.append(InferencePath( + start_entity=start_entity, + end_entity=end_entity, + path=path, + strength=self._calculate_path_strength(path) + )) + continue + + if len(path) >= max_depth: + continue + + for neighbor in adj.get(current, []): + next_entity = neighbor["target"] + if next_entity not in [p["entity"] for p in path]: # 避免循环 + new_path = path + [{ + "entity": next_entity, + "relation": neighbor["relation"], + "relation_data": neighbor.get("data", {}) + }] + queue.append((next_entity, new_path)) + + # 按强度排序 + paths.sort(key=lambda p: p.strength, reverse=True) + return paths + + def _calculate_path_strength(self, path: List[Dict]) -> float: + """计算路径强度""" + if len(path) < 2: + return 0.0 + + # 路径越短越强 + length_factor = 1.0 / len(path) + + # 关系置信度 + confidence_sum = 0 + confidence_count = 0 + for node in path[1:]: # 跳过第一个节点 + rel_data = node.get("relation_data", {}) + if "confidence" in rel_data: + confidence_sum += rel_data["confidence"] + confidence_count += 1 + + confidence_factor = (confidence_sum / confidence_count) if confidence_count > 0 else 0.5 + + return length_factor * confidence_factor + + async def summarize_project( + self, + project_context: Dict, + graph_data: Dict, + summary_type: str = "comprehensive" + ) -> Dict: + """ + 项目智能总结 + + Args: + summary_type: comprehensive/executive/technical/risk + """ + type_prompts = { + "comprehensive": "全面总结项目的所有方面", + "executive": "高管摘要,关注关键决策和风险", + "technical": "技术总结,关注架构和技术栈", + "risk": "风险分析,关注潜在问题和依赖" + } + + prompt = f"""请对以下项目进行{type_prompts.get(summary_type, "全面总结")}: + +## 项目信息 +{json.dumps(project_context, ensure_ascii=False, indent=2)[:3000]} + +## 知识图谱 +实体数: {len(graph_data.get("entities", []))} +关系数: {len(graph_data.get("relations", []))} + +请返回 JSON 格式: +{{ + "overview": "项目概述", + "key_points": ["要点1", "要点2"], + "key_entities": ["关键实体1"], + "risks": ["风险1"], + "recommendations": ["建议1"], + "confidence": 0.85 +}}""" + + content = await self._call_llm(prompt, temperature=0.3) + + import re + json_match = re.search(r'\{{.*?\}}', content, re.DOTALL) + + if json_match: + try: + return json.loads(json_match.group()) + except: + pass + + return { + "overview": content, + "key_points": [], + "key_entities": [], + "risks": [], + "recommendations": [], + "confidence": 0.5 + } + + +# Singleton instance +_reasoner = None + + +def get_knowledge_reasoner() -> KnowledgeReasoner: + global _reasoner + if _reasoner is None: + _reasoner = KnowledgeReasoner() + return _reasoner \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 18ad1ca..efa664c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -61,6 +61,12 @@ try: except ImportError: LLM_CLIENT_AVAILABLE = False +try: + from knowledge_reasoner import get_knowledge_reasoner, KnowledgeReasoner, ReasoningType + REASONER_AVAILABLE = True +except ImportError: + REASONER_AVAILABLE = False + app = FastAPI(title="InsightFlow", version="0.3.0") app.add_middleware( @@ -983,14 +989,15 @@ async def get_entity_mentions(entity_id: str): async def health_check(): return { "status": "ok", - "version": "0.5.0", - "phase": "Phase 5 - Timeline View", + "version": "0.6.0", + "phase": "Phase 5 - Knowledge Reasoning", "oss_available": OSS_AVAILABLE, "tingwu_available": TINGWU_AVAILABLE, "db_available": DB_AVAILABLE, "doc_processor_available": DOC_PROCESSOR_AVAILABLE, "aligner_available": ALIGNER_AVAILABLE, - "llm_client_available": LLM_CLIENT_AVAILABLE + "llm_client_available": LLM_CLIENT_AVAILABLE, + "reasoner_available": REASONER_AVAILABLE } @@ -1336,6 +1343,164 @@ async def get_entity_timeline(entity_id: str): } +# ==================== Phase 5: 知识推理与问答增强 API ==================== + +class ReasoningQuery(BaseModel): + query: str + reasoning_depth: str = "medium" # shallow/medium/deep + stream: bool = False + + +@app.post("/api/v1/projects/{project_id}/reasoning/query") +async def reasoning_query(project_id: str, query: ReasoningQuery): + """ + 增强问答 - 基于知识推理的智能问答 + + 支持多种推理类型: + - 因果推理:分析原因和影响 + - 对比推理:比较实体间的异同 + - 时序推理:分析时间线和演变 + - 关联推理:发现隐含关联 + """ + if not DB_AVAILABLE or not REASONER_AVAILABLE: + raise HTTPException(status_code=500, detail="Knowledge reasoner not available") + + db = get_db_manager() + reasoner = get_knowledge_reasoner() + + project = db.get_project(project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # 获取项目上下文 + project_context = db.get_project_summary(project_id) + + # 获取知识图谱数据 + entities = db.list_project_entities(project_id) + relations = db.list_project_relations(project_id) + + graph_data = { + "entities": [{"id": e.id, "name": e.name, "type": e.type, "definition": e.definition} for e in entities], + "relations": relations + } + + # 执行增强问答 + result = await reasoner.enhanced_qa( + query=query.query, + project_context=project_context, + graph_data=graph_data, + reasoning_depth=query.reasoning_depth + ) + + return { + "answer": result.answer, + "reasoning_type": result.reasoning_type.value, + "confidence": result.confidence, + "evidence": result.evidence, + "knowledge_gaps": result.gaps, + "project_id": project_id + } + + +@app.post("/api/v1/projects/{project_id}/reasoning/inference-path") +async def find_inference_path( + project_id: str, + start_entity: str, + end_entity: str +): + """ + 发现两个实体之间的推理路径 + + 在知识图谱中搜索从 start_entity 到 end_entity 的路径 + """ + if not DB_AVAILABLE or not REASONER_AVAILABLE: + raise HTTPException(status_code=500, detail="Knowledge reasoner not available") + + db = get_db_manager() + reasoner = get_knowledge_reasoner() + + project = db.get_project(project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # 获取知识图谱数据 + entities = db.list_project_entities(project_id) + relations = db.list_project_relations(project_id) + + graph_data = { + "entities": [{"id": e.id, "name": e.name, "type": e.type} for e in entities], + "relations": relations + } + + # 查找推理路径 + paths = reasoner.find_inference_paths(start_entity, end_entity, graph_data) + + return { + "start_entity": start_entity, + "end_entity": end_entity, + "paths": [ + { + "path": path.path, + "strength": path.strength, + "path_description": " -> ".join([p["entity"] for p in path.path]) + } + for path in paths[:5] # 最多返回5条路径 + ], + "total_paths": len(paths) + } + + +class SummaryRequest(BaseModel): + summary_type: str = "comprehensive" # comprehensive/executive/technical/risk + + +@app.post("/api/v1/projects/{project_id}/reasoning/summary") +async def project_summary(project_id: str, req: SummaryRequest): + """ + 项目智能总结 + + 根据类型生成不同侧重点的总结: + - comprehensive: 全面总结 + - executive: 高管摘要 + - technical: 技术总结 + - risk: 风险分析 + """ + if not DB_AVAILABLE or not REASONER_AVAILABLE: + raise HTTPException(status_code=500, detail="Knowledge reasoner not available") + + db = get_db_manager() + reasoner = get_knowledge_reasoner() + + project = db.get_project(project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # 获取项目上下文 + project_context = db.get_project_summary(project_id) + + # 获取知识图谱数据 + entities = db.list_project_entities(project_id) + relations = db.list_project_relations(project_id) + + graph_data = { + "entities": [{"id": e.id, "name": e.name, "type": e.type} for e in entities], + "relations": relations + } + + # 生成总结 + summary = await reasoner.summarize_project( + project_context=project_context, + graph_data=graph_data, + summary_type=req.summary_type + ) + + return { + "project_id": project_id, + "summary_type": req.summary_type, + **summary + } + + # Serve frontend - MUST be last to not override API routes app.mount("/", StaticFiles(directory="frontend", html=True), name="frontend") diff --git a/frontend/app.js b/frontend/app.js index ae71ff3..a88b16a 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -924,6 +924,7 @@ window.switchView = function(viewName) { document.getElementById('workbenchView').style.display = 'none'; document.getElementById('knowledgeBaseView').classList.remove('show'); document.getElementById('timelineView').classList.remove('show'); + document.getElementById('reasoningView').classList.remove('active'); // 显示选中的视图 if (viewName === 'workbench') { @@ -937,6 +938,9 @@ window.switchView = function(viewName) { document.getElementById('timelineView').classList.add('show'); document.querySelector('.sidebar-btn:nth-child(3)').classList.add('active'); loadTimeline(); + } else if (viewName === 'reasoning') { + document.getElementById('reasoningView').classList.add('active'); + document.querySelector('.sidebar-btn:nth-child(4)').classList.add('active'); } }; @@ -1109,3 +1113,265 @@ window.deleteGlossaryTerm = async function(termId) { console.error('Delete glossary term failed:', err); } }; + +// ==================== Phase 5: Knowledge Reasoning ==================== + +window.submitReasoningQuery = async function() { + const input = document.getElementById('reasoningInput'); + const depth = document.getElementById('reasoningDepth').value; + const query = input.value.trim(); + + if (!query) return; + + const resultsDiv = document.getElementById('reasoningResults'); + + // 显示加载状态 + resultsDiv.innerHTML = ` +
+
+ +
+

正在进行知识推理...

+
+ `; + + try { + const res = await fetch(`${API_BASE}/projects/${currentProject.id}/reasoning/query`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, reasoning_depth: depth }) + }); + + if (!res.ok) throw new Error('Reasoning failed'); + + const data = await res.json(); + renderReasoningResult(data); + + } catch (err) { + console.error('Reasoning query failed:', err); + resultsDiv.innerHTML = ` +
+

推理失败,请稍后重试

+
+ `; + } +}; + +function renderReasoningResult(data) { + const resultsDiv = document.getElementById('reasoningResults'); + + const typeLabels = { + 'causal': '🔍 因果推理', + 'comparative': '⚖️ 对比推理', + 'temporal': '⏱️ 时序推理', + 'associative': '🔗 关联推理', + 'summary': '📝 总结推理' + }; + + const typeLabel = typeLabels[data.reasoning_type] || '🤔 智能分析'; + const confidencePercent = Math.round(data.confidence * 100); + + let evidenceHtml = ''; + if (data.evidence && data.evidence.length > 0) { + evidenceHtml = ` +
+

📋 支撑证据

+ ${data.evidence.map(e => `
${e.text || e}
`).join('')} +
+ `; + } + + let gapsHtml = ''; + if (data.knowledge_gaps && data.knowledge_gaps.length > 0) { + gapsHtml = ` +
+

⚠️ 知识缺口

+ +
+ `; + } + + resultsDiv.innerHTML = ` +
+
+
+ ${typeLabel} +
+
+ 置信度: ${confidencePercent}% +
+
+
+ ${data.answer.replace(/\n/g, '
')} +
+ ${evidenceHtml} + ${gapsHtml} +
+ `; +} + +window.clearReasoningResult = function() { + document.getElementById('reasoningResults').innerHTML = ''; + document.getElementById('reasoningInput').value = ''; + document.getElementById('inferencePathsSection').style.display = 'none'; +}; + +window.generateSummary = async function(summaryType) { + const resultsDiv = document.getElementById('reasoningResults'); + + // 显示加载状态 + resultsDiv.innerHTML = ` +
+
+ +
+

正在生成项目总结...

+
+ `; + + try { + const res = await fetch(`${API_BASE}/projects/${currentProject.id}/reasoning/summary`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ summary_type: summaryType }) + }); + + if (!res.ok) throw new Error('Summary failed'); + + const data = await res.json(); + renderSummaryResult(data); + + } catch (err) { + console.error('Summary generation failed:', err); + resultsDiv.innerHTML = ` +
+

总结生成失败,请稍后重试

+
+ `; + } +}; + +function renderSummaryResult(data) { + const resultsDiv = document.getElementById('reasoningResults'); + + const typeLabels = { + 'comprehensive': '📋 全面总结', + 'executive': '💼 高管摘要', + 'technical': '⚙️ 技术总结', + 'risk': '⚠️ 风险分析' + }; + + const typeLabel = typeLabels[data.summary_type] || '📝 项目总结'; + + let keyPointsHtml = ''; + if (data.key_points && data.key_points.length > 0) { + keyPointsHtml = ` +
+

📌 关键要点

+ +
+ `; + } + + let risksHtml = ''; + if (data.risks && data.risks.length > 0) { + risksHtml = ` +
+

⚠️ 风险与问题

+ +
+ `; + } + + let recommendationsHtml = ''; + if (data.recommendations && data.recommendations.length > 0) { + recommendationsHtml = ` +
+

💡 建议

+ ${data.recommendations.map(r => `
${r}
`).join('')} +
+ `; + } + + resultsDiv.innerHTML = ` +
+
+
+ ${typeLabel} +
+
+ 置信度: ${Math.round(data.confidence * 100)}% +
+
+
+ ${data.overview ? data.overview.replace(/\n/g, '
') : ''} +
+ ${keyPointsHtml} + ${risksHtml} + ${recommendationsHtml} +
+ `; +} + +window.findInferencePath = async function(startEntity, endEntity) { + const pathsSection = document.getElementById('inferencePathsSection'); + const pathsList = document.getElementById('inferencePathsList'); + + pathsSection.style.display = 'block'; + pathsList.innerHTML = '

正在搜索关联路径...

'; + + try { + const res = await fetch( + `${API_BASE}/projects/${currentProject.id}/reasoning/inference-path?start_entity=${encodeURIComponent(startEntity)}&end_entity=${encodeURIComponent(endEntity)}` + ); + + if (!res.ok) throw new Error('Path finding failed'); + + const data = await res.json(); + renderInferencePaths(data); + + } catch (err) { + console.error('Path finding failed:', err); + pathsList.innerHTML = '

路径搜索失败

'; + } +}; + +function renderInferencePaths(data) { + const pathsList = document.getElementById('inferencePathsList'); + + if (!data.paths || data.paths.length === 0) { + pathsList.innerHTML = '

未找到关联路径

'; + return; + } + + pathsList.innerHTML = data.paths.map((path, idx) => { + const strengthPercent = Math.round(path.strength * 100); + const pathHtml = path.path.map((node, i) => { + if (i === 0) { + return `${node.entity}`; + } + return ` + → ${node.relation} → + ${node.entity} + `; + }).join(''); + + return ` +
+
+ 路径 ${idx + 1} + 关联强度: ${strengthPercent}% +
+
+ ${pathHtml} +
+
+ `; + }).join(''); +} diff --git a/frontend/workbench.html b/frontend/workbench.html index d8af73a..c93396f 100644 --- a/frontend/workbench.html +++ b/frontend/workbench.html @@ -1020,6 +1020,237 @@ .timeline-legend-dot.mention { background: #7b2cbf; } .timeline-legend-dot.relation { background: #00d4ff; } + /* Phase 5: Reasoning Panel */ + .reasoning-panel { + display: none; + flex-direction: column; + width: 100%; + height: 100%; + padding: 24px; + overflow-y: auto; + } + .reasoning-panel.active { + display: flex; + } + .reasoning-header { + margin-bottom: 24px; + } + .reasoning-header h2 { + font-size: 1.5rem; + margin-bottom: 8px; + } + .reasoning-types { + display: flex; + gap: 12px; + margin-bottom: 24px; + flex-wrap: wrap; + } + .reasoning-type-btn { + padding: 10px 20px; + background: #141414; + border: 1px solid #222; + border-radius: 8px; + color: #888; + cursor: pointer; + transition: all 0.2s; + font-size: 0.9rem; + } + .reasoning-type-btn:hover, .reasoning-type-btn.active { + border-color: #00d4ff; + color: #00d4ff; + background: #00d4ff11; + } + .reasoning-query-box { + background: #141414; + border: 1px solid #222; + border-radius: 12px; + padding: 20px; + margin-bottom: 24px; + } + .reasoning-input { + width: 100%; + background: transparent; + border: none; + color: #e0e0e0; + font-size: 1rem; + resize: none; + outline: none; + min-height: 60px; + font-family: inherit; + } + .reasoning-input::placeholder { + color: #666; + } + .reasoning-actions { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid #222; + } + .reasoning-options { + display: flex; + gap: 16px; + align-items: center; + } + .reasoning-option { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.85rem; + color: #888; + } + .reasoning-option select { + background: #1a1a1a; + border: 1px solid #333; + color: #e0e0e0; + padding: 4px 8px; + border-radius: 4px; + } + .reasoning-result { + background: #141414; + border: 1px solid #222; + border-radius: 12px; + padding: 24px; + margin-bottom: 16px; + } + .reasoning-result-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding-bottom: 16px; + border-bottom: 1px solid #222; + } + .reasoning-result-type { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.9rem; + color: #00d4ff; + } + .reasoning-confidence { + padding: 4px 12px; + background: #00d4ff22; + border-radius: 20px; + font-size: 0.8rem; + color: #00d4ff; + } + .reasoning-answer { + line-height: 1.8; + color: #e0e0e0; + font-size: 1rem; + margin-bottom: 20px; + } + .reasoning-evidence { + background: #0a0a0a; + border-radius: 8px; + padding: 16px; + margin-top: 16px; + } + .reasoning-evidence h4 { + font-size: 0.85rem; + color: #888; + margin-bottom: 12px; + } + .evidence-item { + padding: 10px 12px; + background: #141414; + border-radius: 6px; + margin-bottom: 8px; + font-size: 0.85rem; + color: #aaa; + border-left: 3px solid #00d4ff; + } + .reasoning-gaps { + margin-top: 16px; + padding: 12px; + background: #ff6b6b11; + border: 1px solid #ff6b6b33; + border-radius: 8px; + } + .reasoning-gaps h4 { + font-size: 0.85rem; + color: #ff6b6b; + margin-bottom: 8px; + } + .reasoning-gaps ul { + margin: 0; + padding-left: 20px; + font-size: 0.85rem; + color: #aaa; + } + .reasoning-summary-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 16px; + margin-bottom: 24px; + } + .summary-card { + background: #141414; + border: 1px solid #222; + border-radius: 12px; + padding: 20px; + cursor: pointer; + transition: all 0.2s; + } + .summary-card:hover { + border-color: #00d4ff; + background: #1a1a1a; + } + .summary-card h4 { + color: #00d4ff; + font-size: 0.9rem; + margin-bottom: 8px; + } + .summary-card p { + color: #888; + font-size: 0.85rem; + } + .inference-paths { + background: #141414; + border: 1px solid #222; + border-radius: 12px; + padding: 20px; + margin-bottom: 16px; + } + .inference-path { + padding: 16px; + background: #0a0a0a; + border-radius: 8px; + margin-bottom: 12px; + } + .inference-path-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + } + .path-strength { + padding: 4px 10px; + background: #7b2cbf33; + border-radius: 20px; + font-size: 0.8rem; + color: #7b2cbf; + } + .path-visual { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + font-size: 0.9rem; + } + .path-node { + padding: 6px 12px; + background: #1a1a1a; + border-radius: 6px; + color: #e0e0e0; + } + .path-arrow { + color: #00d4ff; + } + /* Typing indicator */ .typing-indicator { display: flex; @@ -1058,6 +1289,7 @@ + @@ -1175,6 +1407,9 @@ + + + + + +
+
+

🧠 智能推理与问答

+

基于知识图谱的深度推理分析

+
+ +
+
+

📋 全面总结

+

生成项目的完整概览和关键洞察

+
+
+

💼 高管摘要

+

关注关键决策、风险和行动项

+
+
+

⚙️ 技术总结

+

分析技术架构、依赖和实现细节

+
+
+

⚠️ 风险分析

+

识别潜在风险、依赖和缓解建议

+
+
+ +
+ + +
+
+
+ + +
+
+ +
+ + +
+
+
+ +
+ + +