feat: 向量记忆 RAG、工具市场、SSE 流式响应、前端集成与测试覆盖
- 新增 embedding_service(语义检索)、knowledge_service(RAG)、text_chunker、document_parser - 新增 tool_registry(自定义工具注册表)并完善工具市场 API(CRUD + code/http 执行) - 新增 agent_vector_memory / knowledge_base 模型及对应数据库表 - 实现 SSE 流式响应与 Agent 预算控制 - AgentChat.vue 集成 MainLayout 导航布局 - 完善测试体系:7 个新测试文件共 110 个测试覆盖 - 修复 conftest.py SQLite 内存数据库连接隔离问题 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -78,7 +78,9 @@
|
||||
</el-menu>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<slot />
|
||||
<div class="page-content">
|
||||
<slot />
|
||||
</div>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</div>
|
||||
@@ -109,6 +111,7 @@ const activeMenu = computed(() => {
|
||||
if (route.path === '/monitoring') return 'monitoring'
|
||||
if (route.path === '/agent-monitoring') return 'agent-monitoring'
|
||||
if (route.path === '/alert-rules') return 'alert-rules'
|
||||
if (route.path === '/agent-chat' || route.path.startsWith('/agent-chat/')) return 'agent-chat'
|
||||
return 'workflows'
|
||||
})
|
||||
|
||||
@@ -189,4 +192,17 @@ const handleLogout = () => {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
:deep(.el-main) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="agent-chat-page">
|
||||
<div class="chat-header">
|
||||
<div class="header-left">
|
||||
<h2>{{ chatMode === 'single' ? (agent ? agent.name : 'AI Agent 对话') : '多 Agent 编排' }}</h2>
|
||||
<MainLayout>
|
||||
<div class="page-header">
|
||||
<div class="page-header-left">
|
||||
<h3>{{ chatMode === 'single' ? (agent ? agent.name : 'AI Agent 对话') : '多 Agent 编排' }}</h3>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="page-header-actions">
|
||||
<el-switch
|
||||
v-model="chatMode"
|
||||
active-value="orchestrate"
|
||||
@@ -35,19 +35,19 @@
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<el-button @click="clearChat" :disabled="messages.length === 0">清空</el-button>
|
||||
<el-button @click="clearChat" :disabled="displayMessages.length === 0">清空</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-messages" ref="messagesRef">
|
||||
<div v-if="messages.length === 0" class="chat-empty">
|
||||
<div v-if="displayMessages.length === 0" class="chat-empty">
|
||||
<el-icon :size="48"><ChatLineSquare /></el-icon>
|
||||
<p v-if="chatMode === 'single'">选择一个 Agent 开始对话</p>
|
||||
<p v-else>配置多个 Agent 后发送消息进行编排对话</p>
|
||||
<p class="hint">Agent 可以使用内置工具帮你完成任务</p>
|
||||
</div>
|
||||
|
||||
<div v-for="(msg, i) in messages" :key="i" class="message" :class="[msg.role, msg.status === 'error' ? 'error' : '']">
|
||||
<div v-for="(msg, i) in displayMessages" :key="i" class="message" :class="[msg.role, msg.status === 'error' ? 'error' : '']">
|
||||
<div class="message-avatar">
|
||||
<el-avatar :size="36" :icon="msg.role === 'user' ? UserFilled : Promotion" />
|
||||
</div>
|
||||
@@ -137,13 +137,21 @@
|
||||
</template>
|
||||
|
||||
<div class="message-meta">
|
||||
{{ msg.role === 'user' ? '用户' : 'Agent' }} · {{ formatTime(msg.timestamp) }}
|
||||
{{ msg.role === 'user' ? '用户' : 'Agent' }} · {{ relativeTime(msg.timestamp) }}
|
||||
<span v-if="msg.iterations" class="meta-iterations">· {{ msg.iterations }} 步 · {{ msg.tool_calls_made }} 次工具调用</span>
|
||||
<span class="meta-actions">
|
||||
<el-button v-if="msg.role === 'assistant'" link size="small" @click="copyMessage(msg)" title="复制">
|
||||
<el-icon><DocumentCopy /></el-icon>
|
||||
</el-button>
|
||||
<el-button v-if="msg.status === 'error'" link type="danger" size="small" @click="retryMessage(i)" title="重试">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="message assistant">
|
||||
<div v-if="loading && !streamingActive" class="message assistant">
|
||||
<div class="message-avatar"><el-avatar :size="36" icon="Promotion" /></div>
|
||||
<div class="message-bubble">
|
||||
<div class="thinking"><span class="dot"></span><span class="dot"></span><span class="dot"></span></div>
|
||||
@@ -159,7 +167,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 编排 Agent 编辑器 -->
|
||||
<el-dialog v-model="showOrchestrateEditor" title="编排 Agent 配置" width="700px">
|
||||
<el-dialog v-model="showOrchestrateEditor" title="编排 Agent 配置" width="700px" @closed="saveState">
|
||||
<div class="orch-editor">
|
||||
<div v-for="(agt, i) in orchestrateAgents" :key="i" class="orch-agent-card">
|
||||
<div class="orch-agent-header">
|
||||
@@ -188,14 +196,15 @@
|
||||
<el-button @click="showOrchestrateEditor = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</MainLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ChatLineSquare, UserFilled, Promotion, Tools, CaretRight, ChatDotSquare, Select } from '@element-plus/icons-vue'
|
||||
import { ChatLineSquare, UserFilled, Promotion, Tools, CaretRight, ChatDotSquare, Select, DocumentCopy, Refresh } from '@element-plus/icons-vue'
|
||||
import MainLayout from '@/components/MainLayout.vue'
|
||||
import api from '@/api'
|
||||
import type { Agent } from '@/stores/agent'
|
||||
|
||||
@@ -221,16 +230,66 @@ interface OrchestrateAgentForm {
|
||||
temperature: number; max_iterations: number; description: string
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'agent_chat_state'
|
||||
|
||||
interface ChatState {
|
||||
messages: Record<string, ChatMessage[]> // keyed by agentId / '__bare__' / '__orchestrate__'
|
||||
sessionId: Record<string, string>
|
||||
currentAgentId: string
|
||||
chatMode: 'single' | 'orchestrate'
|
||||
orchestrateMode: string
|
||||
orchestrateAgents: OrchestrateAgentForm[]
|
||||
}
|
||||
|
||||
function saveState() {
|
||||
try {
|
||||
const state: ChatState = {
|
||||
messages: messages.value,
|
||||
sessionId: sessionId.value,
|
||||
currentAgentId: currentAgentId.value,
|
||||
chatMode: chatMode.value,
|
||||
orchestrateMode: orchestrateMode.value,
|
||||
orchestrateAgents: orchestrateAgents.value,
|
||||
}
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
|
||||
} catch { /* quota exceeded, ignore */ }
|
||||
}
|
||||
|
||||
function loadState(): ChatState | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) return null
|
||||
const parsed = JSON.parse(raw)
|
||||
// 兼容旧格式:旧版本 messages 是数组,迁移为 Record
|
||||
if (Array.isArray(parsed.messages)) {
|
||||
const oldSessionId = parsed.sessionId || ''
|
||||
parsed.messages = { '__bare__': parsed.messages }
|
||||
parsed.sessionId = { '__bare__': oldSessionId }
|
||||
}
|
||||
return parsed
|
||||
} catch { return null }
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const agents = ref<Agent[]>([])
|
||||
const currentAgentId = ref('')
|
||||
const messages = ref<ChatMessage[]>([])
|
||||
const messages = ref<Record<string, ChatMessage[]>>({})
|
||||
const inputMessage = ref('')
|
||||
const loading = ref(false)
|
||||
const streamingActive = ref(false)
|
||||
const messagesRef = ref<HTMLElement | null>(null)
|
||||
const sessionId = ref('')
|
||||
const sessionId = ref<Record<string, string>>({})
|
||||
const agent = ref<Agent | null>(null)
|
||||
|
||||
const currentAgentKey = computed(() => {
|
||||
if (chatMode.value === 'orchestrate') return '__orchestrate__'
|
||||
return currentAgentId.value || '__bare__'
|
||||
})
|
||||
|
||||
const displayMessages = computed(() => {
|
||||
return messages.value[currentAgentKey.value] || []
|
||||
})
|
||||
|
||||
// 编排模式
|
||||
const chatMode = ref<'single' | 'orchestrate'>('single')
|
||||
const orchestrateMode = ref('debate')
|
||||
@@ -251,14 +310,38 @@ function addOrchestrateAgent() {
|
||||
max_iterations: 10,
|
||||
description: '',
|
||||
})
|
||||
saveState()
|
||||
}
|
||||
|
||||
// 模式/配置变化时自动保存
|
||||
watch(chatMode, saveState)
|
||||
watch(orchestrateMode, saveState)
|
||||
|
||||
onMounted(async () => {
|
||||
await loadAgents()
|
||||
|
||||
const saved = loadState()
|
||||
if (saved) {
|
||||
messages.value = saved.messages
|
||||
sessionId.value = saved.sessionId
|
||||
chatMode.value = saved.chatMode
|
||||
orchestrateMode.value = saved.orchestrateMode
|
||||
orchestrateAgents.value = saved.orchestrateAgents
|
||||
// 恢复展开状态
|
||||
for (const arr of Object.values(messages.value)) {
|
||||
arr.forEach(m => { if (m.steps?.length) m._traceOpen = false })
|
||||
}
|
||||
}
|
||||
|
||||
if (route.params.id) {
|
||||
currentAgentId.value = route.params.id as string
|
||||
await switchAgent()
|
||||
} else if (saved?.currentAgentId) {
|
||||
currentAgentId.value = saved.currentAgentId
|
||||
await switchAgent()
|
||||
}
|
||||
|
||||
nextTick(scrollToBottom)
|
||||
})
|
||||
|
||||
async function loadAgents() {
|
||||
@@ -267,16 +350,25 @@ async function loadAgents() {
|
||||
}
|
||||
|
||||
async function switchAgent() {
|
||||
if (!currentAgentId.value) { agent.value = null; return }
|
||||
try { const resp = await api.get(`/api/v1/agents/${currentAgentId.value}`); agent.value = resp.data }
|
||||
catch { ElMessage.error('加载 Agent 失败'); agent.value = null }
|
||||
if (!currentAgentId.value) { agent.value = null; saveState(); nextTick(scrollToBottom); return }
|
||||
try {
|
||||
const resp = await api.get(`/api/v1/agents/${currentAgentId.value}`)
|
||||
agent.value = resp.data
|
||||
saveState()
|
||||
nextTick(scrollToBottom)
|
||||
} catch {
|
||||
ElMessage.error('加载 Agent 失败')
|
||||
agent.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
const text = inputMessage.value.trim()
|
||||
if (!text || loading.value) return
|
||||
|
||||
messages.value.push({ role: 'user', content: text, timestamp: Date.now() })
|
||||
const key = currentAgentKey.value
|
||||
if (!messages.value[key]) messages.value[key] = []
|
||||
messages.value[key].push({ role: 'user', content: text, timestamp: Date.now() })
|
||||
inputMessage.value = ''
|
||||
loading.value = true
|
||||
scrollToBottom()
|
||||
@@ -294,36 +386,212 @@ async function sendMessage() {
|
||||
})
|
||||
const data = resp.data as OrchestrateResult
|
||||
data.steps.forEach(s => { s._open = false })
|
||||
messages.value.push({
|
||||
messages.value[key].push({
|
||||
role: 'assistant', content: data.final_answer, timestamp: Date.now(),
|
||||
orchestrateResult: data, _traceOpen: true,
|
||||
})
|
||||
} else {
|
||||
const endpoint = currentAgentId.value ? `/api/v1/agent-chat/${currentAgentId.value}` : '/api/v1/agent-chat/bare'
|
||||
const resp = await api.post(endpoint, { message: text, session_id: sessionId.value || undefined })
|
||||
const data = resp.data
|
||||
sessionId.value = data.session_id
|
||||
messages.value.push({
|
||||
role: 'assistant', content: data.content, timestamp: Date.now(),
|
||||
iterations: data.iterations_used, tool_calls_made: data.tool_calls_made,
|
||||
status: data.truncated ? 'error' : 'success', steps: data.steps || [],
|
||||
_traceOpen: data.steps && data.steps.length > 0,
|
||||
})
|
||||
const sessId = sessionId.value[key] || ''
|
||||
const streamEndpoint = currentAgentId.value
|
||||
? `/api/v1/agent-chat/${currentAgentId.value}/stream`
|
||||
: '/api/v1/agent-chat/bare/stream'
|
||||
|
||||
// 尝试 SSE 流式
|
||||
let usedStreaming = false
|
||||
streamingActive.value = false
|
||||
try {
|
||||
const token = localStorage.getItem('token') || ''
|
||||
const resp = await fetch(streamEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify({ message: text, session_id: sessId || undefined }),
|
||||
})
|
||||
|
||||
if (resp.ok && resp.body) {
|
||||
usedStreaming = true
|
||||
|
||||
// 创建占位消息,流式更新
|
||||
const msg: ChatMessage = {
|
||||
role: 'assistant', content: '', timestamp: Date.now(),
|
||||
steps: [], _traceOpen: true, iterations: 0, tool_calls_made: 0,
|
||||
}
|
||||
const idx = messages.value[key].push(msg) - 1
|
||||
const currentMsg = messages.value[key][idx]
|
||||
|
||||
const reader = resp.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
let receivedFirstEvent = false
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const parts = buffer.split('\n\n')
|
||||
buffer = parts.pop() || ''
|
||||
|
||||
for (const part of parts) {
|
||||
const lines = part.split('\n')
|
||||
let eventType = ''
|
||||
let dataStr = ''
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event: ')) eventType = line.slice(7)
|
||||
else if (line.startsWith('data: ')) dataStr = line.slice(6)
|
||||
}
|
||||
if (!dataStr) continue
|
||||
|
||||
try {
|
||||
const data = JSON.parse(dataStr)
|
||||
|
||||
// 首个事件到达 → 隐藏 loading dots
|
||||
if (!receivedFirstEvent && (eventType === 'think' || eventType === 'tool_call' || eventType === 'tool_result')) {
|
||||
receivedFirstEvent = true
|
||||
streamingActive.value = true
|
||||
}
|
||||
|
||||
if (eventType === 'think') {
|
||||
currentMsg.steps!.push({
|
||||
iteration: data.iteration, type: 'think',
|
||||
content: data.content || '',
|
||||
reasoning: data.reasoning,
|
||||
tool_name: data.tool_names?.[0],
|
||||
})
|
||||
} else if (eventType === 'tool_call') {
|
||||
currentMsg.steps!.push({
|
||||
iteration: data.iteration, type: 'tool_call',
|
||||
content: `调用工具: ${data.name}`,
|
||||
tool_name: data.name,
|
||||
tool_input: data.input,
|
||||
})
|
||||
} else if (eventType === 'tool_result') {
|
||||
currentMsg.steps!.push({
|
||||
iteration: data.iteration, type: 'tool_result',
|
||||
content: `工具 ${data.name} 返回结果`,
|
||||
tool_name: data.name,
|
||||
tool_result: data.result,
|
||||
})
|
||||
} else if (eventType === 'final') {
|
||||
currentMsg.content = data.content || ''
|
||||
currentMsg.iterations = data.iterations_used || 0
|
||||
currentMsg.tool_calls_made = data.tool_calls_made || 0
|
||||
if (data.session_id) {
|
||||
sessionId.value[key] = data.session_id
|
||||
}
|
||||
streamingActive.value = false
|
||||
} else if (eventType === 'error') {
|
||||
currentMsg.content = data.content || ''
|
||||
currentMsg.status = 'error'
|
||||
streamingActive.value = false
|
||||
}
|
||||
} catch { /* 跳过畸形事件 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { /* 流式不可用,降级到普通 POST */
|
||||
streamingActive.value = false
|
||||
}
|
||||
|
||||
if (!usedStreaming) {
|
||||
// 降级:标准 POST 请求
|
||||
const fallbackEndpoint = currentAgentId.value
|
||||
? `/api/v1/agent-chat/${currentAgentId.value}`
|
||||
: '/api/v1/agent-chat/bare'
|
||||
const resp = await api.post(fallbackEndpoint, { message: text, session_id: sessId || undefined })
|
||||
const data = resp.data
|
||||
sessionId.value[key] = data.session_id
|
||||
messages.value[key].push({
|
||||
role: 'assistant', content: data.content, timestamp: Date.now(),
|
||||
iterations: data.iterations_used, tool_calls_made: data.tool_calls_made,
|
||||
status: data.truncated ? 'error' : 'success', steps: data.steps || [],
|
||||
_traceOpen: data.steps && data.steps.length > 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
saveState()
|
||||
} catch (e: any) {
|
||||
messages.value.push({
|
||||
messages.value[key].push({
|
||||
role: 'assistant', content: `错误:${e.response?.data?.detail || e.message || '请求失败'}`,
|
||||
timestamp: Date.now(), status: 'error',
|
||||
})
|
||||
saveState()
|
||||
} finally {
|
||||
loading.value = false; scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTrace(msg: ChatMessage) { msg._traceOpen = !msg._traceOpen }
|
||||
function clearChat() { messages.value = []; sessionId.value = '' }
|
||||
function clearChat() {
|
||||
const key = currentAgentKey.value
|
||||
messages.value[key] = []
|
||||
if (chatMode.value === 'single') {
|
||||
sessionId.value[key] = ''
|
||||
}
|
||||
saveState()
|
||||
}
|
||||
function scrollToBottom() { nextTick(() => { if (messagesRef.value) messagesRef.value.scrollTop = messagesRef.value.scrollHeight }) }
|
||||
function formatTime(ts: number) { return new Date(ts).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) }
|
||||
|
||||
function relativeTime(ts: number): string {
|
||||
const diff = Date.now() - ts
|
||||
if (diff < 60000) return '刚刚'
|
||||
if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`
|
||||
if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`
|
||||
const d = new Date(ts)
|
||||
return `${d.getMonth() + 1}月${d.getDate()}日 ${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function copyMessage(msg: ChatMessage) {
|
||||
const text = msg.orchestrateResult?.final_answer || msg.content
|
||||
if (!text) return
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
ElMessage.success('已复制')
|
||||
}).catch(() => {
|
||||
ElMessage.warning('复制失败')
|
||||
})
|
||||
}
|
||||
|
||||
function retryMessage(idx: number) {
|
||||
const key = currentAgentKey.value
|
||||
const msgs = messages.value[key]
|
||||
if (!msgs) return
|
||||
|
||||
// 查找错误消息之前的最后一条用户消息
|
||||
let userMsg = ''
|
||||
for (let i = idx - 1; i >= 0; i--) {
|
||||
if (msgs[i].role === 'user') {
|
||||
userMsg = msgs[i].content
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!userMsg) {
|
||||
ElMessage.warning('未找到可重试的消息')
|
||||
return
|
||||
}
|
||||
|
||||
// 移除该错误消息及关联的用户消息
|
||||
const removeIndices: number[] = []
|
||||
for (let i = idx - 1; i >= 0; i--) {
|
||||
if (msgs[i].role === 'user' && msgs[i].content === userMsg) {
|
||||
removeIndices.push(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
removeIndices.push(idx)
|
||||
|
||||
// 从后往前删除,避免 index 错乱
|
||||
removeIndices.sort((a, b) => b - a)
|
||||
for (const ri of removeIndices) {
|
||||
msgs.splice(ri, 1)
|
||||
}
|
||||
|
||||
saveState()
|
||||
// 填入输入框并发送
|
||||
inputMessage.value = userMsg
|
||||
nextTick(() => sendMessage())
|
||||
}
|
||||
|
||||
function renderMarkdown(text: string): string {
|
||||
if (!text) return ''
|
||||
@@ -336,12 +604,12 @@ function renderMarkdown(text: string): string {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.agent-chat-page { display: flex; flex-direction: column; height: calc(100vh - 120px); max-width: 960px; margin: 0 auto; padding: 16px; }
|
||||
.chat-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--el-border-color-light); }
|
||||
.header-left { display: flex; align-items: center; gap: 8px; }
|
||||
.header-left h2 { margin: 0; font-size: 18px; }
|
||||
.header-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||||
.chat-messages { flex: 1; overflow-y: auto; padding: 12px 0; display: flex; flex-direction: column; gap: 16px; }
|
||||
.agent-chat-page { display: flex; flex-direction: column; height: 100%; max-width: 960px; margin: 0 auto; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid var(--el-border-color-light); }
|
||||
.page-header-left { display: flex; align-items: center; gap: 8px; }
|
||||
.page-header-left h3 { margin: 0; font-size: 16px; font-weight: 600; }
|
||||
.page-header-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||||
.chat-messages { flex: 1; overflow-y: auto; padding: 8px 0; display: flex; flex-direction: column; gap: 12px; min-height: 0; }
|
||||
.chat-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--el-text-color-secondary); gap: 12px; }
|
||||
.chat-empty .hint { font-size: 13px; color: var(--el-text-color-placeholder); }
|
||||
.message { display: flex; gap: 12px; max-width: 88%; }
|
||||
@@ -358,8 +626,10 @@ function renderMarkdown(text: string): string {
|
||||
.tool-calls-header { display: flex; align-items: center; gap: 4px; font-size: 12px; color: var(--el-text-color-secondary); margin-bottom: 4px; }
|
||||
.tool-call-item { display: flex; align-items: center; gap: 8px; padding: 4px 8px; font-size: 12px; }
|
||||
.tool-name { font-weight: 500; color: var(--el-color-primary); }
|
||||
.message-meta { font-size: 11px; color: var(--el-text-color-placeholder); margin-top: 4px; }
|
||||
.message-meta { font-size: 11px; color: var(--el-text-color-placeholder); margin-top: 4px; display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
|
||||
.meta-iterations { color: var(--el-color-info); }
|
||||
.meta-actions { margin-left: auto; display: flex; gap: 2px; opacity: 0; transition: opacity 0.15s; }
|
||||
.message-bubble:hover .meta-actions { opacity: 1; }
|
||||
|
||||
/* Thinking trace */
|
||||
.thinking-trace { margin-top: 10px; border-top: 1px solid var(--el-border-color-light); padding-top: 8px; }
|
||||
|
||||
@@ -99,6 +99,33 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 执行预算 -->
|
||||
<el-divider content-position="left">执行预算</el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="LLM 调用次数上限">
|
||||
<el-input-number
|
||||
v-model="form.max_llm_invocations"
|
||||
:min="1"
|
||||
:max="10000"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="form-tip">单次会话最多可调用 LLM 的次数,超限将停止执行</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="工具调用次数上限">
|
||||
<el-input-number
|
||||
v-model="form.max_tool_calls"
|
||||
:min="1"
|
||||
:max="50000"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="form-tip">单次会话最多可调用工具的次数,超限将停止执行</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 工具选择 -->
|
||||
<el-form-item label="可用工具">
|
||||
<el-checkbox-group v-model="form.tools" class="tool-checkbox-group">
|
||||
@@ -149,6 +176,9 @@ const form = ref({
|
||||
max_iterations: 10,
|
||||
tools: [] as string[],
|
||||
memory_enabled: true,
|
||||
// 预算配置
|
||||
max_llm_invocations: 200,
|
||||
max_tool_calls: 500,
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -173,6 +203,11 @@ onMounted(async () => {
|
||||
form.value.max_iterations = data.max_iterations ?? form.value.max_iterations
|
||||
form.value.tools = Array.isArray(data.tools) ? [...data.tools] : []
|
||||
form.value.memory_enabled = data.memory !== false
|
||||
|
||||
// 从 Agent budget_config 加载预算配置
|
||||
const bc = a.budget_config || {}
|
||||
form.value.max_llm_invocations = bc.max_llm_invocations ?? form.value.max_llm_invocations
|
||||
form.value.max_tool_calls = bc.max_tool_calls ?? form.value.max_tool_calls
|
||||
} catch (e: any) {
|
||||
ElMessage.error('加载 Agent 失败')
|
||||
router.push('/agents')
|
||||
@@ -241,7 +276,13 @@ async function handleSave() {
|
||||
targetNode.data.memory = form.value.memory_enabled
|
||||
|
||||
wf.nodes = nodes
|
||||
await agentStore.updateAgent(agent.value.id, { workflow_config: wf })
|
||||
await agentStore.updateAgent(agent.value.id, {
|
||||
workflow_config: wf,
|
||||
budget_config: {
|
||||
max_llm_invocations: form.value.max_llm_invocations,
|
||||
max_tool_calls: form.value.max_tool_calls,
|
||||
},
|
||||
})
|
||||
ElMessage.success('配置已保存')
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.response?.data?.detail || '保存失败')
|
||||
|
||||
Reference in New Issue
Block a user