450 lines
11 KiB
HTML
Executable File
450 lines
11 KiB
HTML
Executable File
<!--
|
|
bbav5前端Agent状态组件
|
|
用于显示RAG查询状态和知识图谱可视化
|
|
-->
|
|
<template id="agent-status-template">
|
|
<div class="agent-status-panel">
|
|
<!-- Agent状态显示 -->
|
|
<div class="status-header">
|
|
<h4>🤖 Agent状态</h4>
|
|
<span :class="['status-badge', status]">{{ statusText }}</span>
|
|
</div>
|
|
|
|
<!-- RAG查询状态 -->
|
|
<div class="rag-status" v-if="ragQuery">
|
|
<div class="query-info">
|
|
<div class="query-text">{{ ragQuery }}</div>
|
|
<div class="query-progress">
|
|
<el-progress :percentage="progress" :status="progressStatus"></el-progress>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 检索步骤 -->
|
|
<div class="retrieval-steps">
|
|
<div v-for="(step, idx) in steps" :key="idx"
|
|
:class="['step', step.status]">
|
|
<span class="step-icon">{{ step.icon }}</span>
|
|
<span class="step-name">{{ step.name }}</span>
|
|
<span class="step-time" v-if="step.time">{{ step.time }}ms</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Self-RAG反思状态 -->
|
|
<div class="reflection-panel" v-if="reflection">
|
|
<div class="reflection-title">📝 Self-RAG反思</div>
|
|
<div class="reflection-content">
|
|
<div>迭代: {{ reflection.iteration }}/{{ reflection.maxIterations }}</div>
|
|
<div>原因: {{ reflection.reason }}</div>
|
|
<div v-if="reflection.rewrittenQuery">
|
|
重写查询: {{ reflection.rewrittenQuery }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MultiHopRAG状态 -->
|
|
<div class="multihop-panel" v-if="multiHop">
|
|
<div class="multihop-title">🔀 MultiHopRAG</div>
|
|
<div class="multihop-content">
|
|
<div>跳数: {{ multiHop.hops }}</div>
|
|
<div>来源: {{ multiHop.sources.join(', ') }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 知识图谱可视化 -->
|
|
<div class="kg-visualization" v-if="showKG">
|
|
<div class="kg-header">
|
|
<h5>🧬 知识图谱</h5>
|
|
<el-button size="small" @click="refreshKG">刷新</el-button>
|
|
</div>
|
|
<div id="kg-chart" class="kg-chart"></div>
|
|
<div class="kg-stats">
|
|
节点: {{ kgStats.nodes }} | 关系: {{ kgStats.edges }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 结果展示 -->
|
|
<div class="results-panel" v-if="results.length > 0">
|
|
<h5>📄 检索结果</h5>
|
|
<div v-for="(result, idx) in results" :key="idx" class="result-item">
|
|
<div class="result-header">
|
|
<span class="result-doc">{{ result.doc_id }}</span>
|
|
<span class="result-score">Score: {{ result.score.toFixed(3) }}</span>
|
|
</div>
|
|
<div class="result-content">{{ result.content }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
// Agent状态组件
|
|
const AgentStatusComponent = {
|
|
template: '#agent-status-template',
|
|
props: {
|
|
initialQuery: String,
|
|
autoStart: Boolean
|
|
},
|
|
data() {
|
|
return {
|
|
status: 'idle', // idle, analyzing, retrieving, reflecting, generating, completed, error
|
|
ragQuery: '',
|
|
progress: 0,
|
|
steps: [
|
|
{ name: '查询分析', icon: '🔍', status: 'pending', time: null },
|
|
{ name: '文档检索', icon: '📚', status: 'pending', time: null },
|
|
{ name: '结果评估', icon: '✓', status: 'pending', time: null },
|
|
{ name: '生成回答', icon: '✍', status: 'pending', time: null }
|
|
],
|
|
reflection: null,
|
|
multiHop: null,
|
|
results: [],
|
|
showKG: true,
|
|
kgStats: { nodes: 0, edges: 0 },
|
|
kgChart: null
|
|
}
|
|
},
|
|
computed: {
|
|
statusText() {
|
|
const statusMap = {
|
|
'idle': '待机',
|
|
'analyzing': '分析中',
|
|
'retrieving': '检索中',
|
|
'reflecting': '反思中',
|
|
'generating': '生成中',
|
|
'completed': '完成',
|
|
'error': '错误'
|
|
}
|
|
return statusMap[this.status] || this.status
|
|
},
|
|
progressStatus() {
|
|
if (this.status === 'error') return 'exception'
|
|
if (this.status === 'completed') return 'success'
|
|
return ''
|
|
}
|
|
},
|
|
mounted() {
|
|
if (this.initialQuery) {
|
|
this.ragQuery = this.initialQuery
|
|
}
|
|
if (this.autoStart && this.ragQuery) {
|
|
this.startRAG()
|
|
}
|
|
this.initKGChart()
|
|
},
|
|
methods: {
|
|
async startRAG() {
|
|
this.status = 'analyzing'
|
|
this.progress = 10
|
|
this.updateStep(0, 'running')
|
|
|
|
try {
|
|
// 步骤1: 查询分析
|
|
await this.simulateStep(0, 500)
|
|
this.status = 'retrieving'
|
|
this.progress = 30
|
|
|
|
// 步骤2: 文档检索
|
|
this.updateStep(1, 'running')
|
|
const searchResults = await this.searchDocuments()
|
|
this.updateStep(1, 'completed', searchResults.time)
|
|
this.results = searchResults.results
|
|
this.progress = 60
|
|
|
|
// 步骤3: 结果评估 (Self-RAG)
|
|
this.status = 'reflecting'
|
|
this.updateStep(2, 'running')
|
|
const reflection = await this.evaluateResults()
|
|
this.reflection = reflection
|
|
this.updateStep(2, 'completed', reflection.time)
|
|
this.progress = 80
|
|
|
|
// 如果需要重写,执行MultiHopRAG
|
|
if (reflection.needsRewrite) {
|
|
this.status = 'retrieving'
|
|
const multiHopResults = await this.multiHopSearch(reflection.rewrittenQuery)
|
|
this.multiHop = multiHopResults
|
|
this.results = [...this.results, ...multiHopResults.results]
|
|
}
|
|
|
|
// 步骤4: 生成回答
|
|
this.status = 'generating'
|
|
this.updateStep(3, 'running')
|
|
await this.simulateStep(3, 800)
|
|
this.updateStep(3, 'completed', 800)
|
|
this.progress = 100
|
|
|
|
this.status = 'completed'
|
|
|
|
// 更新知识图谱
|
|
this.updateKG()
|
|
|
|
} catch (error) {
|
|
this.status = 'error'
|
|
console.error('RAG查询失败:', error)
|
|
}
|
|
},
|
|
|
|
async searchDocuments() {
|
|
// 调用API进行文档检索
|
|
const startTime = Date.now()
|
|
|
|
try {
|
|
const response = await fetch('/api/v1/rag/advanced/search', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
query: this.ragQuery,
|
|
use_agentic: true,
|
|
enable_self_rag: true,
|
|
enable_multi_hop: true
|
|
})
|
|
})
|
|
|
|
const data = await response.json()
|
|
|
|
return {
|
|
results: data.sources || [],
|
|
time: Date.now() - startTime
|
|
}
|
|
} catch (error) {
|
|
console.error('检索失败:', error)
|
|
return { results: [], time: Date.now() - startTime }
|
|
}
|
|
},
|
|
|
|
async evaluateResults() {
|
|
// 模拟结果评估
|
|
const startTime = Date.now()
|
|
|
|
// 模拟Self-RAG反思
|
|
await new Promise(resolve => setTimeout(resolve, 300))
|
|
|
|
return {
|
|
iteration: 1,
|
|
maxIterations: 3,
|
|
reason: '检索结果充分',
|
|
needsRewrite: false,
|
|
rewrittenQuery: null,
|
|
time: Date.now() - startTime
|
|
}
|
|
},
|
|
|
|
async multiHopSearch(query) {
|
|
// 模拟多跳检索
|
|
const startTime = Date.now()
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 600))
|
|
|
|
return {
|
|
hops: 2,
|
|
sources: ['Doc1', 'Doc2', 'Doc3'],
|
|
results: [],
|
|
time: Date.now() - startTime
|
|
}
|
|
},
|
|
|
|
async simulateStep(stepIndex, duration) {
|
|
await new Promise(resolve => setTimeout(resolve, duration))
|
|
},
|
|
|
|
updateStep(index, status, time = null) {
|
|
this.steps[index].status = status
|
|
if (time !== null) {
|
|
this.steps[index].time = time
|
|
}
|
|
},
|
|
|
|
initKGChart() {
|
|
// 初始化ECharts知识图谱
|
|
if (typeof echarts !== 'undefined') {
|
|
this.kgChart = echarts.init(document.getElementById('kg-chart'))
|
|
this.loadKGData()
|
|
}
|
|
},
|
|
|
|
async loadKGData() {
|
|
try {
|
|
const response = await fetch('/api/v1/kg/graph')
|
|
const data = await response.json()
|
|
|
|
this.kgStats = {
|
|
nodes: data.nodes?.length || 0,
|
|
edges: data.edges?.length || 0
|
|
}
|
|
|
|
this.renderKG(data.nodes || [], data.edges || [])
|
|
} catch (error) {
|
|
console.error('加载知识图谱失败:', error)
|
|
}
|
|
},
|
|
|
|
renderKG(nodes, edges) {
|
|
if (!this.kgChart) return
|
|
|
|
const option = {
|
|
tooltip: {},
|
|
series: [{
|
|
type: 'graph',
|
|
layout: 'force',
|
|
data: nodes.map(n => ({
|
|
id: n.id,
|
|
name: n.name || n.id,
|
|
symbolSize: n.degree || 20,
|
|
category: n.category || 'default'
|
|
})),
|
|
links: edges.map(e => ({
|
|
source: e.source,
|
|
target: e.target,
|
|
label: { show: true, formatter: e.relation }
|
|
})),
|
|
roam: true,
|
|
label: { show: true },
|
|
force: { repulsion: 100 }
|
|
}]
|
|
}
|
|
|
|
this.kgChart.setOption(option)
|
|
},
|
|
|
|
refreshKG() {
|
|
this.loadKGData()
|
|
},
|
|
|
|
updateKG() {
|
|
// 根据检索结果更新知识图谱
|
|
this.loadKGData()
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.agent-status-panel {
|
|
padding: 15px;
|
|
background: #f5f7fa;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.status-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.status-badge {
|
|
padding: 4px 12px;
|
|
border-radius: 12px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.status-badge.idle { background: #909399; color: white; }
|
|
.status-badge.analyzing { background: #409EFF; color: white; }
|
|
.status-badge.retrieving { background: #E6A23C; color: white; }
|
|
.status-badge.reflecting { background: #67C23A; color: white; }
|
|
.status-badge.generating { background: #8E44AD; color: white; }
|
|
.status-badge.completed { background: #67C23A; color: white; }
|
|
.status-badge.error { background: #F56C6C; color: white; }
|
|
|
|
.rag-status {
|
|
background: white;
|
|
padding: 15px;
|
|
border-radius: 6px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.query-text {
|
|
font-weight: bold;
|
|
margin-bottom: 10px;
|
|
color: #303133;
|
|
}
|
|
|
|
.retrieval-steps {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin: 15px 0;
|
|
}
|
|
|
|
.step {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 8px 12px;
|
|
border-radius: 4px;
|
|
background: #f0f0f0;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.step.pending { opacity: 0.5; }
|
|
.step.running { background: #ecf5ff; color: #409EFF; }
|
|
.step.completed { background: #f0f9eb; color: #67C23A; }
|
|
|
|
.reflection-panel, .multihop-panel {
|
|
margin-top: 15px;
|
|
padding: 10px;
|
|
background: #fdf6ec;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.reflection-title, .multihop-title {
|
|
font-weight: bold;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.kg-visualization {
|
|
background: white;
|
|
padding: 15px;
|
|
border-radius: 6px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.kg-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.kg-chart {
|
|
height: 300px;
|
|
background: #f5f7fa;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.kg-stats {
|
|
margin-top: 10px;
|
|
font-size: 12px;
|
|
color: #666;
|
|
}
|
|
|
|
.results-panel {
|
|
background: white;
|
|
padding: 15px;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.result-item {
|
|
margin-bottom: 10px;
|
|
padding: 10px;
|
|
background: #f5f7fa;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.result-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 5px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.result-doc { color: #409EFF; }
|
|
.result-score { color: #67C23A; }
|
|
|
|
.result-content {
|
|
font-size: 12px;
|
|
color: #666;
|
|
line-height: 1.5;
|
|
}
|
|
</style> |