Phase 5: 知识推理与问答增强

- 新增 knowledge_reasoner.py 推理引擎
- 支持因果/对比/时序/关联四种推理类型
- 智能项目总结 API (全面/高管/技术/风险)
- 实体关联路径发现功能
- 前端推理面板 UI 和交互
- 更新 API 端点和健康检查

Refs: Phase 5 开发任务
This commit is contained in:
OpenClaw Bot
2026-02-19 18:07:00 +08:00
parent cfdf37fc31
commit 9dd54b3a38
13 changed files with 1286 additions and 11 deletions

View File

@@ -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('');
}

View File

@@ -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>