Phase 5: 知识推理与问答增强
- 新增 knowledge_reasoner.py 推理引擎 - 支持因果/对比/时序/关联四种推理类型 - 智能项目总结 API (全面/高管/技术/风险) - 实体关联路径发现功能 - 前端推理面板 UI 和交互 - 更新 API 端点和健康检查 Refs: Phase 5 开发任务
This commit is contained in:
21
README.md
21
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. **协作功能** - 待开发
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 完成)
|
||||
- [ ] 导出功能
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/__pycache__/knowledge_reasoner.cpython-312.pyc
Normal file
BIN
backend/__pycache__/knowledge_reasoner.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
backend/__pycache__/oss_uploader.cpython-312.pyc
Normal file
BIN
backend/__pycache__/oss_uploader.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/__pycache__/tingwu_client.cpython-312.pyc
Normal file
BIN
backend/__pycache__/tingwu_client.cpython-312.pyc
Normal file
Binary file not shown.
533
backend/knowledge_reasoner.py
Normal file
533
backend/knowledge_reasoner.py
Normal file
@@ -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
|
||||
171
backend/main.py
171
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")
|
||||
|
||||
|
||||
266
frontend/app.js
266
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 = `
|
||||
<div class="reasoning-result">
|
||||
<div class="typing-indicator">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<p style="color:#666;text-align:center;">正在进行知识推理...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div class="reasoning-result">
|
||||
<p style="color:#ff6b6b;">推理失败,请稍后重试</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
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 = `
|
||||
<div class="reasoning-evidence">
|
||||
<h4>📋 支撑证据</h4>
|
||||
${data.evidence.map(e => `<div class="evidence-item">${e.text || e}</div>`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
let gapsHtml = '';
|
||||
if (data.knowledge_gaps && data.knowledge_gaps.length > 0) {
|
||||
gapsHtml = `
|
||||
<div class="reasoning-gaps">
|
||||
<h4>⚠️ 知识缺口</h4>
|
||||
<ul>
|
||||
${data.knowledge_gaps.map(g => `<li>${g}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="reasoning-result">
|
||||
<div class="reasoning-result-header">
|
||||
<div class="reasoning-result-type">
|
||||
<span>${typeLabel}</span>
|
||||
</div>
|
||||
<div class="reasoning-confidence">
|
||||
置信度: ${confidencePercent}%
|
||||
</div>
|
||||
</div>
|
||||
<div class="reasoning-answer">
|
||||
${data.answer.replace(/\n/g, '<br>')}
|
||||
</div>
|
||||
${evidenceHtml}
|
||||
${gapsHtml}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div class="reasoning-result">
|
||||
<div class="typing-indicator">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<p style="color:#666;text-align:center;">正在生成项目总结...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div class="reasoning-result">
|
||||
<p style="color:#ff6b6b;">总结生成失败,请稍后重试</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
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 = `
|
||||
<div style="margin: 16px 0;">
|
||||
<h4 style="color:#888;font-size:0.9rem;margin-bottom:12px;">📌 关键要点</h4>
|
||||
<ul style="margin:0;padding-left:20px;color:#aaa;line-height:1.8;">
|
||||
${data.key_points.map(p => `<li>${p}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
let risksHtml = '';
|
||||
if (data.risks && data.risks.length > 0) {
|
||||
risksHtml = `
|
||||
<div class="reasoning-gaps">
|
||||
<h4>⚠️ 风险与问题</h4>
|
||||
<ul>
|
||||
${data.risks.map(r => `<li>${r}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
let recommendationsHtml = '';
|
||||
if (data.recommendations && data.recommendations.length > 0) {
|
||||
recommendationsHtml = `
|
||||
<div class="reasoning-evidence">
|
||||
<h4>💡 建议</h4>
|
||||
${data.recommendations.map(r => `<div class="evidence-item">${r}</div>`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="reasoning-result">
|
||||
<div class="reasoning-result-header">
|
||||
<div class="reasoning-result-type">
|
||||
<span>${typeLabel}</span>
|
||||
</div>
|
||||
<div class="reasoning-confidence">
|
||||
置信度: ${Math.round(data.confidence * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
<div class="reasoning-answer">
|
||||
${data.overview ? data.overview.replace(/\n/g, '<br>') : ''}
|
||||
</div>
|
||||
${keyPointsHtml}
|
||||
${risksHtml}
|
||||
${recommendationsHtml}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
window.findInferencePath = async function(startEntity, endEntity) {
|
||||
const pathsSection = document.getElementById('inferencePathsSection');
|
||||
const pathsList = document.getElementById('inferencePathsList');
|
||||
|
||||
pathsSection.style.display = 'block';
|
||||
pathsList.innerHTML = '<p style="color:#666;">正在搜索关联路径...</p>';
|
||||
|
||||
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 = '<p style="color:#ff6b6b;">路径搜索失败</p>';
|
||||
}
|
||||
};
|
||||
|
||||
function renderInferencePaths(data) {
|
||||
const pathsList = document.getElementById('inferencePathsList');
|
||||
|
||||
if (!data.paths || data.paths.length === 0) {
|
||||
pathsList.innerHTML = '<p style="color:#666;">未找到关联路径</p>';
|
||||
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 `<span class="path-node">${node.entity}</span>`;
|
||||
}
|
||||
return `
|
||||
<span class="path-arrow">→ ${node.relation} →</span>
|
||||
<span class="path-node">${node.entity}</span>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="inference-path">
|
||||
<div class="inference-path-header">
|
||||
<span style="color:#888;font-size:0.85rem;">路径 ${idx + 1}</span>
|
||||
<span class="path-strength">关联强度: ${strengthPercent}%</span>
|
||||
</div>
|
||||
<div class="path-visual">
|
||||
${pathHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
<button class="sidebar-btn active" onclick="switchView('workbench')" title="工作台">📝</button>
|
||||
<button class="sidebar-btn" onclick="switchView('knowledge-base')" title="知识库">📚</button>
|
||||
<button class="sidebar-btn" onclick="switchView('timeline')" title="时间线">📅</button>
|
||||
<button class="sidebar-btn" onclick="switchView('reasoning')" title="智能推理">🧠</button>
|
||||
</div>
|
||||
|
||||
<!-- Content Area -->
|
||||
@@ -1175,6 +1407,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Knowledge Base View -->
|
||||
<div id="knowledgeBaseView" class="kb-panel" style="display: none;">
|
||||
<div class="kb-header">
|
||||
<h2>📚 项目知识库</h2>
|
||||
<div class="kb-stats">
|
||||
@@ -1230,6 +1465,70 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reasoning View -->
|
||||
<div id="reasoningView" class="reasoning-panel">
|
||||
<div class="reasoning-header">
|
||||
<h2>🧠 智能推理与问答</h2>
|
||||
<p style="color:#888;">基于知识图谱的深度推理分析</p>
|
||||
</div>
|
||||
|
||||
<div class="reasoning-summary-cards">
|
||||
<div class="summary-card" onclick="generateSummary('comprehensive')">
|
||||
<h4>📋 全面总结</h4>
|
||||
<p>生成项目的完整概览和关键洞察</p>
|
||||
</div>
|
||||
<div class="summary-card" onclick="generateSummary('executive')">
|
||||
<h4>💼 高管摘要</h4>
|
||||
<p>关注关键决策、风险和行动项</p>
|
||||
</div>
|
||||
<div class="summary-card" onclick="generateSummary('technical')">
|
||||
<h4>⚙️ 技术总结</h4>
|
||||
<p>分析技术架构、依赖和实现细节</p>
|
||||
</div>
|
||||
<div class="summary-card" onclick="generateSummary('risk')">
|
||||
<h4>⚠️ 风险分析</h4>
|
||||
<p>识别潜在风险、依赖和缓解建议</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="reasoning-query-box">
|
||||
<textarea
|
||||
class="reasoning-input"
|
||||
id="reasoningInput"
|
||||
placeholder="输入你的问题,例如:
|
||||
• 为什么项目延期了?(因果推理)
|
||||
• 前端和后端技术栈有什么区别?(对比推理)
|
||||
• 项目进度如何随时间变化?(时序推理)
|
||||
• 哪些实体之间存在隐含关联?(关联推理)"
|
||||
></textarea>
|
||||
|
||||
<div class="reasoning-actions">
|
||||
<div class="reasoning-options">
|
||||
<div class="reasoning-option">
|
||||
<label>推理深度:</label>
|
||||
<select id="reasoningDepth">
|
||||
<option value="shallow">浅层</option>
|
||||
<option value="medium" selected>中等</option>
|
||||
<option value="deep">深度</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:12px;">
|
||||
<button class="btn btn-secondary" onclick="clearReasoningResult()">清除</button>
|
||||
<button class="btn" onclick="submitReasoningQuery()">开始推理</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="reasoningResults"></div>
|
||||
|
||||
<div class="inference-paths" id="inferencePathsSection" style="display:none;">
|
||||
<h3 style="margin-bottom:16px;">🔗 实体关联路径</h3>
|
||||
<div id="inferencePathsList"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user