debug
This commit is contained in:
parent
8089d94e78
commit
69b11ea7d4
|
|
@ -32,6 +32,10 @@ const navItems = [
|
||||||
path: '/conversations',
|
path: '/conversations',
|
||||||
icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>`
|
icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>`
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/agents',
|
||||||
|
icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="9"></line><line x1="9" y1="13" x2="15" y2="13"></line><line x1="9" y1="17" x2="12" y2="17"></line></svg>`
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/tools',
|
path: '/tools',
|
||||||
icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>`
|
icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>`
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,18 @@ const routes = [
|
||||||
component: () => import('../views/ToolsView.vue'),
|
component: () => import('../views/ToolsView.vue'),
|
||||||
meta: { requiresAuth: true }
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/agents',
|
||||||
|
name: 'Agents',
|
||||||
|
component: () => import('../views/AgentView.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/agents/:taskId',
|
||||||
|
name: 'AgentTask',
|
||||||
|
component: () => import('../views/AgentView.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
// 首页重定向
|
// 首页重定向
|
||||||
{
|
{
|
||||||
path: '/home',
|
path: '/home',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,381 @@
|
||||||
|
/**
|
||||||
|
* AgentWs - WebSocket client for agent real-time communication
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* 1. 连接 WebSocket 订阅 DAG 进度
|
||||||
|
* 2. 处理服务端推送的事件
|
||||||
|
* 3. 支持心跳检测和自动重连
|
||||||
|
*/
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
|
||||||
|
class AgentWsClient {
|
||||||
|
constructor() {
|
||||||
|
// WebSocket 连接
|
||||||
|
this.ws = null
|
||||||
|
// 当前订阅的任务 ID
|
||||||
|
this.taskId = null
|
||||||
|
// 连接状态
|
||||||
|
this.connected = ref(false)
|
||||||
|
// 状态
|
||||||
|
this.status = ref('disconnected')
|
||||||
|
// DAG 数据
|
||||||
|
this.dagData = reactive({
|
||||||
|
id: null,
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
progress: 0,
|
||||||
|
completed_count: 0,
|
||||||
|
total_count: 0
|
||||||
|
})
|
||||||
|
// 节点状态映射
|
||||||
|
this.nodeStatus = reactive({})
|
||||||
|
// 事件回调
|
||||||
|
this.callbacks = {}
|
||||||
|
// 重连配置
|
||||||
|
this.reconnectAttempts = 0
|
||||||
|
this.maxReconnectAttempts = 5
|
||||||
|
this.reconnectDelay = 3000
|
||||||
|
// 心跳
|
||||||
|
this.pingInterval = null
|
||||||
|
this.pingIntervalTime = 25000
|
||||||
|
// 订阅确认
|
||||||
|
this.subscribed = ref(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接到 WebSocket 并订阅任务
|
||||||
|
* @param {string} taskId - 任务 ID
|
||||||
|
*/
|
||||||
|
connect(taskId) {
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
if (this.taskId === taskId) {
|
||||||
|
return // 已连接到此任务
|
||||||
|
}
|
||||||
|
this.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.taskId = taskId
|
||||||
|
this.status.value = 'connecting'
|
||||||
|
|
||||||
|
// 构建 WebSocket URL
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
|
const host = window.location.host
|
||||||
|
const wsUrl = `${protocol}//${host}/api/ws/dag/${taskId}`
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.ws = new WebSocket(wsUrl)
|
||||||
|
this._setupEventHandlers()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('WebSocket connection error:', e)
|
||||||
|
this.status.value = 'error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 WebSocket 事件处理器
|
||||||
|
*/
|
||||||
|
_setupEventHandlers() {
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
console.log('WebSocket connected')
|
||||||
|
this.connected.value = true
|
||||||
|
this.status.value = 'connected'
|
||||||
|
this.reconnectAttempts = 0
|
||||||
|
|
||||||
|
// 发送订阅消息
|
||||||
|
this.send({ type: 'subscribe' })
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(event.data)
|
||||||
|
this._handleMessage(message)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse WebSocket message:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket error:', error)
|
||||||
|
this.status.value = 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onclose = () => {
|
||||||
|
console.log('WebSocket closed')
|
||||||
|
this.connected.value = false
|
||||||
|
this.status.value = 'disconnected'
|
||||||
|
this.subscribed.value = false
|
||||||
|
this._clearPingInterval()
|
||||||
|
|
||||||
|
// 尝试重连
|
||||||
|
if (this.reconnectAttempts < this.maxReconnectAttempts && this.taskId) {
|
||||||
|
this.reconnectAttempts++
|
||||||
|
console.log(`Reconnecting... attempt ${this.reconnectAttempts}`)
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.taskId) {
|
||||||
|
this.connect(this.taskId)
|
||||||
|
}
|
||||||
|
}, this.reconnectDelay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理收到的消息
|
||||||
|
*/
|
||||||
|
_handleMessage(message) {
|
||||||
|
const type = message.type
|
||||||
|
const data = message.data || {}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'subscribed':
|
||||||
|
console.log('Subscribed to task:', message.task_id)
|
||||||
|
this.subscribed.value = true
|
||||||
|
this.status.value = 'subscribed'
|
||||||
|
this._triggerCallback('subscribed', message)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'heartbeat':
|
||||||
|
// 心跳响应
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'dag_start':
|
||||||
|
this._updateDagData(data.graph)
|
||||||
|
this._triggerCallback('dag_start', data)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'dag_progress':
|
||||||
|
this.dagData.progress = data.progress
|
||||||
|
this._triggerCallback('dag_progress', data)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'dag_status':
|
||||||
|
if (data) {
|
||||||
|
this._updateDagData(data)
|
||||||
|
}
|
||||||
|
this._triggerCallback('dag_status', data)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'node_start':
|
||||||
|
this._updateNodeStatus(data.node_id, 'running', data)
|
||||||
|
this._triggerCallback('node_start', data)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'node_progress':
|
||||||
|
this._updateNodeProgress(data.node_id, data.progress, data.message)
|
||||||
|
this._triggerCallback('node_progress', data)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'node_complete':
|
||||||
|
this._updateNodeStatus(data.node_id, 'completed', data)
|
||||||
|
this._updateNodeResult(data.node_id, data)
|
||||||
|
this._triggerCallback('node_complete', data)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'node_error':
|
||||||
|
this._updateNodeStatus(data.node_id, 'failed', { error: data.error })
|
||||||
|
this._triggerCallback('node_error', data)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'dag_complete':
|
||||||
|
this._triggerCallback('dag_complete', data)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'task_cancelled':
|
||||||
|
this._triggerCallback('task_cancelled', data)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'pong':
|
||||||
|
// 心跳响应
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'error':
|
||||||
|
console.error('Server error:', message.message)
|
||||||
|
this._triggerCallback('error', message)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 DAG 数据
|
||||||
|
*/
|
||||||
|
_updateDagData(graph) {
|
||||||
|
if (!graph) return
|
||||||
|
|
||||||
|
this.dagData.id = graph.id
|
||||||
|
this.dagData.name = graph.name
|
||||||
|
this.dagData.description = graph.description
|
||||||
|
this.dagData.nodes = graph.nodes || []
|
||||||
|
this.dagData.edges = graph.edges || []
|
||||||
|
this.dagData.progress = graph.progress || 0
|
||||||
|
this.dagData.completed_count = graph.completed_count || 0
|
||||||
|
this.dagData.total_count = graph.total_count || 0
|
||||||
|
|
||||||
|
// 初始化节点状态
|
||||||
|
for (const node of this.dagData.nodes) {
|
||||||
|
if (!this.nodeStatus[node.id]) {
|
||||||
|
this.nodeStatus[node.id] = reactive({
|
||||||
|
status: node.status,
|
||||||
|
progress: node.progress || 0,
|
||||||
|
message: node.progress_message || '',
|
||||||
|
result: null,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新节点状态
|
||||||
|
*/
|
||||||
|
_updateNodeStatus(nodeId, status, data) {
|
||||||
|
if (!this.nodeStatus[nodeId]) {
|
||||||
|
this.nodeStatus[nodeId] = reactive({})
|
||||||
|
}
|
||||||
|
this.nodeStatus[nodeId].status = status
|
||||||
|
this.nodeStatus[nodeId].data = data
|
||||||
|
|
||||||
|
// 更新 DAG 中的节点
|
||||||
|
const node = this.dagData.nodes.find(n => n.id === nodeId)
|
||||||
|
if (node) {
|
||||||
|
node.status = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新节点进度
|
||||||
|
*/
|
||||||
|
_updateNodeProgress(nodeId, progress, message) {
|
||||||
|
if (this.nodeStatus[nodeId]) {
|
||||||
|
this.nodeStatus[nodeId].progress = progress
|
||||||
|
if (message) {
|
||||||
|
this.nodeStatus[nodeId].message = message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新节点结果
|
||||||
|
*/
|
||||||
|
_updateNodeResult(nodeId, data) {
|
||||||
|
if (this.nodeStatus[nodeId]) {
|
||||||
|
this.nodeStatus[nodeId].result = data.result
|
||||||
|
this.nodeStatus[nodeId].output_data = data.output_data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息到服务器
|
||||||
|
*/
|
||||||
|
send(message) {
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.send(JSON.stringify(message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态
|
||||||
|
*/
|
||||||
|
getStatus() {
|
||||||
|
this.send({ type: 'get_status' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送 ping
|
||||||
|
*/
|
||||||
|
ping() {
|
||||||
|
this.send({ type: 'ping' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消任务
|
||||||
|
*/
|
||||||
|
cancelTask() {
|
||||||
|
this.send({ type: 'cancel_task' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断开连接
|
||||||
|
*/
|
||||||
|
disconnect() {
|
||||||
|
this._clearPingInterval()
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close()
|
||||||
|
this.ws = null
|
||||||
|
}
|
||||||
|
this.taskId = null
|
||||||
|
this.connected.value = false
|
||||||
|
this.subscribed.value = false
|
||||||
|
this.status.value = 'disconnected'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置心跳间隔
|
||||||
|
*/
|
||||||
|
_startPingInterval() {
|
||||||
|
this._clearPingInterval()
|
||||||
|
this.pingInterval = setInterval(() => {
|
||||||
|
this.ping()
|
||||||
|
}, this.pingIntervalTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除心跳间隔
|
||||||
|
*/
|
||||||
|
_clearPingInterval() {
|
||||||
|
if (this.pingInterval) {
|
||||||
|
clearInterval(this.pingInterval)
|
||||||
|
this.pingInterval = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册回调
|
||||||
|
*/
|
||||||
|
on(event, callback) {
|
||||||
|
if (!this.callbacks[event]) {
|
||||||
|
this.callbacks[event] = []
|
||||||
|
}
|
||||||
|
this.callbacks[event].push(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除回调
|
||||||
|
*/
|
||||||
|
off(event, callback) {
|
||||||
|
if (this.callbacks[event]) {
|
||||||
|
const index = this.callbacks[event].indexOf(callback)
|
||||||
|
if (index > -1) {
|
||||||
|
this.callbacks[event].splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发回调
|
||||||
|
*/
|
||||||
|
_triggerCallback(event, data) {
|
||||||
|
if (this.callbacks[event]) {
|
||||||
|
this.callbacks[event].forEach(cb => cb(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取节点状态
|
||||||
|
*/
|
||||||
|
getNodeStatus(nodeId) {
|
||||||
|
return this.nodeStatus[nodeId]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有节点状态
|
||||||
|
*/
|
||||||
|
getAllNodeStatus() {
|
||||||
|
return this.nodeStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例
|
||||||
|
export const agentWs = new AgentWsClient()
|
||||||
|
export default agentWs
|
||||||
|
|
@ -210,4 +210,19 @@ export const providersAPI = {
|
||||||
test: (id) => api.post(`/providers/${id}/test`)
|
test: (id) => api.post(`/providers/${id}/test`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ Agent 接口 ============
|
||||||
|
|
||||||
|
export const agentsAPI = {
|
||||||
|
// 创建 Agent 任务
|
||||||
|
create: (data) => api.post('/agents/request', data),
|
||||||
|
// 获取任务状态
|
||||||
|
getTask: (taskId) => api.get(`/agents/task/${taskId}`),
|
||||||
|
// 取消任务
|
||||||
|
cancelTask: (taskId) => api.post(`/agents/task/${taskId}/cancel`),
|
||||||
|
// 列出用户的任务
|
||||||
|
listTasks: (params) => api.get('/agents/tasks', { params }),
|
||||||
|
// 删除任务
|
||||||
|
deleteTask: (taskId) => api.delete(`/agents/task/${taskId}`)
|
||||||
|
}
|
||||||
|
|
||||||
export default api
|
export default api
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,910 @@
|
||||||
|
<template>
|
||||||
|
<div class="page-container agent-view">
|
||||||
|
<!-- 任务创建界面(无 taskId 时显示) -->
|
||||||
|
<div v-if="!activeTaskId" class="task-create-section">
|
||||||
|
<h1>创建 Agent 任务</h1>
|
||||||
|
|
||||||
|
<!-- 对话选择 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label>选择对话</label>
|
||||||
|
<div class="conversation-selector">
|
||||||
|
<select v-model="selectedConversationId" class="select-input">
|
||||||
|
<option value="">-- 选择现有对话 --</option>
|
||||||
|
<option v-for="conv in conversations" :key="conv.id" :value="conv.id">
|
||||||
|
{{ conv.title || conv.first_message || '无标题对话' }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<button @click="showNewConversation = true" class="btn-secondary">
|
||||||
|
新建对话
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新建对话表单 -->
|
||||||
|
<div v-if="showNewConversation" class="new-conversation-form">
|
||||||
|
<input
|
||||||
|
v-model="newConversationTitle"
|
||||||
|
placeholder="对话标题(可选)"
|
||||||
|
class="text-input"
|
||||||
|
/>
|
||||||
|
<button @click="createConversation" class="btn-primary">创建</button>
|
||||||
|
<button @click="showNewConversation = false" class="btn-secondary">取消</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 任务输入 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label>任务描述</label>
|
||||||
|
<textarea
|
||||||
|
v-model="taskInput"
|
||||||
|
placeholder="描述你要完成的任务..."
|
||||||
|
class="textarea-input"
|
||||||
|
rows="4"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 执行选项 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label>执行选项</label>
|
||||||
|
<div class="options-row">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" v-model="options.thinking_enabled" />
|
||||||
|
启用思考模式
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" v-model="options.auto_execute" />
|
||||||
|
自动执行
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 开始按钮 -->
|
||||||
|
<button
|
||||||
|
@click="startTask"
|
||||||
|
class="btn-start"
|
||||||
|
:disabled="!canStartTask || isCreating"
|
||||||
|
>
|
||||||
|
{{ isCreating ? '创建中...' : '开始执行' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 任务执行界面(有 taskId 时显示) -->
|
||||||
|
<div v-else class="task-progress-section">
|
||||||
|
<div class="header">
|
||||||
|
<h1>Agent 执行进度</h1>
|
||||||
|
<div class="controls">
|
||||||
|
<button v-if="!agentWs.connected.value" @click="reconnect" class="btn-primary">
|
||||||
|
连接
|
||||||
|
</button>
|
||||||
|
<button v-else @click="cancelTask" class="btn-danger" :disabled="!canCancel">
|
||||||
|
取消任务
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 连接状态 -->
|
||||||
|
<div class="status-bar" :class="statusClass">
|
||||||
|
<span class="status-indicator"></span>
|
||||||
|
<span class="status-text">{{ statusText }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DAG 概览 -->
|
||||||
|
<div class="dag-overview" v-if="agentWs.dagData.id">
|
||||||
|
<div class="dag-info">
|
||||||
|
<h2>{{ agentWs.dagData.name || '任务执行中' }}</h2>
|
||||||
|
<p class="dag-description">{{ agentWs.dagData.description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-container">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" :style="{ width: `${agentWs.dagData.progress * 100}%` }"></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-text">
|
||||||
|
{{ Math.round(agentWs.dagData.progress * 100) }}%
|
||||||
|
({{ agentWs.dagData.completed_count }} / {{ agentWs.dagData.total_count }})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 节点列表 -->
|
||||||
|
<div class="nodes-section">
|
||||||
|
<h3>任务节点</h3>
|
||||||
|
<div class="nodes-list">
|
||||||
|
<div
|
||||||
|
v-for="node in agentWs.dagData.nodes"
|
||||||
|
:key="node.id"
|
||||||
|
class="node-card"
|
||||||
|
:class="getNodeStatusClass(node.id)"
|
||||||
|
>
|
||||||
|
<div class="node-header">
|
||||||
|
<span class="node-id">{{ node.id }}</span>
|
||||||
|
<span class="node-status-badge" :class="getNodeStatusClass(node.id)">
|
||||||
|
{{ getNodeStatusText(node.id) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="node-name">{{ node.name }}</div>
|
||||||
|
<div class="node-description">{{ node.description }}</div>
|
||||||
|
|
||||||
|
<div class="node-progress" v-if="getNodeProgress(node.id) > 0">
|
||||||
|
<div class="mini-progress-bar">
|
||||||
|
<div class="mini-progress-fill"
|
||||||
|
:style="{ width: `${getNodeProgress(node.id) * 100}%` }">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="node-progress-text">
|
||||||
|
{{ Math.round(getNodeProgress(node.id) * 100) }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="node-message" v-if="getNodeMessage(node.id)">
|
||||||
|
{{ getNodeMessage(node.id) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="node-error" v-if="getNodeError(node.id)">
|
||||||
|
错误: {{ getNodeError(node.id) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="node-result" v-if="getNodeResult(node.id)">
|
||||||
|
<details>
|
||||||
|
<summary>查看结果</summary>
|
||||||
|
<pre>{{ formatResult(getNodeResult(node.id)) }}</pre>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 执行日志 -->
|
||||||
|
<div class="execution-log">
|
||||||
|
<h3>执行日志</h3>
|
||||||
|
<div class="log-entries">
|
||||||
|
<div
|
||||||
|
v-for="(log, index) in logs"
|
||||||
|
:key="index"
|
||||||
|
class="log-entry"
|
||||||
|
:class="log.type"
|
||||||
|
>
|
||||||
|
<span class="log-time">{{ log.time }}</span>
|
||||||
|
<span class="log-message">{{ log.message }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { agentWs } from '../utils/agentWs.js'
|
||||||
|
import { conversationsAPI, agentsAPI } from '../utils/api.js'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
const activeTaskId = ref(null) // 当前活动任务 ID
|
||||||
|
const conversations = ref([])
|
||||||
|
const selectedConversationId = ref('')
|
||||||
|
const showNewConversation = ref(false)
|
||||||
|
const newConversationTitle = ref('')
|
||||||
|
const taskInput = ref('')
|
||||||
|
const isCreating = ref(false)
|
||||||
|
const options = ref({
|
||||||
|
thinking_enabled: false,
|
||||||
|
auto_execute: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 日志
|
||||||
|
const logs = ref([])
|
||||||
|
|
||||||
|
// 从路由获取 taskId
|
||||||
|
const taskIdFromRoute = computed(() => route.params.taskId || null)
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const canStartTask = computed(() => {
|
||||||
|
// 必须有对话ID(选择现有或新建后)且任务描述不为空
|
||||||
|
return !!selectedConversationId.value && !!taskInput.value.trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
const status = agentWs.status.value
|
||||||
|
return {
|
||||||
|
'disconnected': status === 'disconnected',
|
||||||
|
'connecting': status === 'connecting',
|
||||||
|
'connected': status === 'connected' || status === 'subscribed',
|
||||||
|
'error': status === 'error'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
switch (agentWs.status.value) {
|
||||||
|
case 'disconnected': return '未连接'
|
||||||
|
case 'connecting': return '连接中...'
|
||||||
|
case 'connected': return '已连接'
|
||||||
|
case 'subscribed': return '已订阅'
|
||||||
|
case 'error': return '错误'
|
||||||
|
default: return '未知状态'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const canCancel = computed(() => {
|
||||||
|
return agentWs.subscribed.value && agentWs.dagData.progress < 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
async function loadConversations() {
|
||||||
|
try {
|
||||||
|
const res = await conversationsAPI.list({ page: 1, page_size: 100 })
|
||||||
|
if (res.success) {
|
||||||
|
conversations.value = res.data?.items || []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load conversations:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createConversation() {
|
||||||
|
try {
|
||||||
|
const res = await conversationsAPI.create({
|
||||||
|
title: newConversationTitle.value || undefined
|
||||||
|
})
|
||||||
|
if (res.success) {
|
||||||
|
selectedConversationId.value = res.data.id
|
||||||
|
conversations.value.unshift(res.data)
|
||||||
|
showNewConversation.value = false
|
||||||
|
newConversationTitle.value = ''
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to create conversation:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startTask() {
|
||||||
|
console.log('startTask called', {
|
||||||
|
canStartTask: canStartTask.value,
|
||||||
|
selectedConversationId: selectedConversationId.value,
|
||||||
|
taskInput: taskInput.value
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!canStartTask.value) {
|
||||||
|
console.log('canStartTask is false, not proceeding')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isCreating.value = true
|
||||||
|
try {
|
||||||
|
console.log('Calling agentsAPI.create...')
|
||||||
|
const res = await agentsAPI.create({
|
||||||
|
conversation_id: selectedConversationId.value,
|
||||||
|
task: taskInput.value,
|
||||||
|
options: options.value
|
||||||
|
})
|
||||||
|
console.log('agentsAPI.create response:', res)
|
||||||
|
|
||||||
|
if (res.success && res.data?.task_id) {
|
||||||
|
activeTaskId.value = res.data.task_id
|
||||||
|
// 使用 router 导航到任务页面
|
||||||
|
router.push(`/agents/${res.data.task_id}`)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to create task:', e)
|
||||||
|
addLog('error', `创建任务失败: ${e.message || e}`)
|
||||||
|
} finally {
|
||||||
|
isCreating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听路由变化
|
||||||
|
watch(taskIdFromRoute, (newId) => {
|
||||||
|
if (newId) {
|
||||||
|
activeTaskId.value = newId
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// 节点状态方法
|
||||||
|
function getNodeStatusClass(nodeId) {
|
||||||
|
const status = agentWs.nodeStatus[nodeId]
|
||||||
|
if (!status) return ''
|
||||||
|
return `status-${status.status}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeStatusText(nodeId) {
|
||||||
|
const status = agentWs.nodeStatus[nodeId]
|
||||||
|
if (!status) return 'pending'
|
||||||
|
switch (status.status) {
|
||||||
|
case 'pending': return '等待中'
|
||||||
|
case 'ready': return '就绪'
|
||||||
|
case 'running': return '执行中'
|
||||||
|
case 'completed': return '已完成'
|
||||||
|
case 'failed': return '失败'
|
||||||
|
case 'cancelled': return '已取消'
|
||||||
|
case 'blocked': return '阻塞'
|
||||||
|
default: return status.status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeProgress(nodeId) {
|
||||||
|
return agentWs.nodeStatus[nodeId]?.progress || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeMessage(nodeId) {
|
||||||
|
return agentWs.nodeStatus[nodeId]?.message || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeError(nodeId) {
|
||||||
|
const status = agentWs.nodeStatus[nodeId]
|
||||||
|
if (status?.status === 'failed') {
|
||||||
|
return status.data?.error || '未知错误'
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeResult(nodeId) {
|
||||||
|
return agentWs.nodeStatus[nodeId]?.result
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatResult(result) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(result, null, 2)
|
||||||
|
} catch {
|
||||||
|
return String(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日志方法
|
||||||
|
function addLog(type, message) {
|
||||||
|
const now = new Date()
|
||||||
|
const time = now.toLocaleTimeString()
|
||||||
|
logs.value.push({ type, message, time })
|
||||||
|
|
||||||
|
// 限制日志数量
|
||||||
|
if (logs.value.length > 100) {
|
||||||
|
logs.value = logs.value.slice(-100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作方法
|
||||||
|
function reconnect() {
|
||||||
|
if (activeTaskId.value) {
|
||||||
|
agentWs.connect(activeTaskId.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelTask() {
|
||||||
|
agentWs.cancelTask()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 事件处理
|
||||||
|
function handleDagStart(data) {
|
||||||
|
addLog('info', `DAG 开始执行: ${data.graph?.name || '未知任务'}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNodeStart(data) {
|
||||||
|
addLog('info', `节点开始: ${data.node_id} - ${data.name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNodeProgress(data) {
|
||||||
|
// 进度更新太频繁,不记录日志
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNodeComplete(data) {
|
||||||
|
addLog('success', `节点完成: ${data.node_id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNodeError(data) {
|
||||||
|
addLog('error', `节点错误: ${data.node_id} - ${data.error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDagComplete(data) {
|
||||||
|
if (data.success) {
|
||||||
|
addLog('success', '所有任务执行完成')
|
||||||
|
} else {
|
||||||
|
addLog('error', '任务执行失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubscribed(data) {
|
||||||
|
addLog('info', `已订阅任务: ${data.task_id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleError(data) {
|
||||||
|
addLog('error', `错误: ${data.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(async () => {
|
||||||
|
// 加载对话列表
|
||||||
|
await loadConversations()
|
||||||
|
|
||||||
|
// 注册事件回调
|
||||||
|
agentWs.on('subscribed', handleSubscribed)
|
||||||
|
agentWs.on('dag_start', handleDagStart)
|
||||||
|
agentWs.on('node_start', handleNodeStart)
|
||||||
|
agentWs.on('node_progress', handleNodeProgress)
|
||||||
|
agentWs.on('node_complete', handleNodeComplete)
|
||||||
|
agentWs.on('node_error', handleNodeError)
|
||||||
|
agentWs.on('dag_complete', handleDagComplete)
|
||||||
|
agentWs.on('error', handleError)
|
||||||
|
|
||||||
|
// 连接
|
||||||
|
if (activeTaskId.value) {
|
||||||
|
agentWs.connect(activeTaskId.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 移除事件回调
|
||||||
|
agentWs.off('subscribed', handleSubscribed)
|
||||||
|
agentWs.off('dag_start', handleDagStart)
|
||||||
|
agentWs.off('node_start', handleNodeStart)
|
||||||
|
agentWs.off('node_progress', handleNodeProgress)
|
||||||
|
agentWs.off('node_complete', handleNodeComplete)
|
||||||
|
agentWs.off('node_error', handleNodeError)
|
||||||
|
agentWs.off('dag_complete', handleDagComplete)
|
||||||
|
agentWs.off('error', handleError)
|
||||||
|
|
||||||
|
// 断开连接
|
||||||
|
agentWs.disconnect()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.agent-view {
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 任务创建界面样式 */
|
||||||
|
.task-create-section {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-create-section h1 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-selector {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
resize: vertical;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-weight: normal;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label input {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-start {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-start:hover:not(:disabled) {
|
||||||
|
background: var(--accent-primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-start:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-conversation-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
padding: 0.5rem 1.25rem;
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-color: var(--border-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
padding: 0.5rem 1.25rem;
|
||||||
|
background: #dc2626;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar.disconnected {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar.disconnected .status-indicator {
|
||||||
|
background: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar.connecting {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar.connecting .status-indicator {
|
||||||
|
background: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar.connected {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar.connected .status-indicator {
|
||||||
|
background: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar.error {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar.error .status-indicator {
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dag-overview {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dag-info h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dag-description {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 8px;
|
||||||
|
background: var(--border-light);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--accent-primary);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodes-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodes-section h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodes-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-card {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-card.status-running {
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
box-shadow: 0 0 0 1px var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-card.status-completed {
|
||||||
|
border-color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-card.status-failed {
|
||||||
|
border-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-id {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-status-badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #e5e7eb;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-status-badge.status-running {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-status-badge.status-completed {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-status-badge.status-failed {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-name {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-description {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-progress {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-progress-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--border-light);
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-progress-text {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-message {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-error {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-result {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-result details {
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-result summary {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-result pre {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.execution-log h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entries {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry.info {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry.success {
|
||||||
|
color: #065f46;
|
||||||
|
background: #d1fae5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry.error {
|
||||||
|
color: #991b1b;
|
||||||
|
background: #fee2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-time {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -111,18 +111,21 @@ Guidelines:
|
||||||
|
|
||||||
# Call LLM
|
# Call LLM
|
||||||
try:
|
try:
|
||||||
|
logger.info(f"Calling LLM with model: {self.agent.config.model}, messages count: {len(messages)}")
|
||||||
|
logger.info(f"LLM client - api_url: {self.llm_client.api_url}, api_key set: {bool(self.llm_client.api_key)}")
|
||||||
response = await self.llm_client.sync_call(
|
response = await self.llm_client.sync_call(
|
||||||
model=self.agent.config.model,
|
model=self.agent.config.model,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
temperature=self.agent.config.temperature,
|
temperature=self.agent.config.temperature,
|
||||||
max_tokens=self.agent.config.max_tokens
|
max_tokens=self.agent.config.max_tokens
|
||||||
)
|
)
|
||||||
|
logger.info(f"LLM response received: {response}")
|
||||||
|
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
progress_callback(0.5, "Processing decomposition...")
|
progress_callback(0.5, "Processing decomposition...")
|
||||||
|
|
||||||
# Parse LLM response to extract DAG
|
# Parse LLM response to extract DAG
|
||||||
dag = self._parse_dag_from_response(response.content, task)
|
dag = self._parse_dag_from_response(response.content if response else "", task)
|
||||||
|
|
||||||
# Add assistant response to context
|
# Add assistant response to context
|
||||||
self.agent.add_message("assistant", response.content)
|
self.agent.add_message("assistant", response.content)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from fastapi import APIRouter
|
||||||
|
|
||||||
from luxx.routes import auth, conversations, messages, tools, providers
|
from luxx.routes import auth, conversations, messages, tools, providers
|
||||||
from luxx.routes.agents_ws import router as agents_ws_router
|
from luxx.routes.agents_ws import router as agents_ws_router
|
||||||
|
from luxx.routes.agents import router as agents_router
|
||||||
|
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
|
|
@ -14,3 +15,4 @@ api_router.include_router(messages.router)
|
||||||
api_router.include_router(tools.router)
|
api_router.include_router(tools.router)
|
||||||
api_router.include_router(providers.router)
|
api_router.include_router(providers.router)
|
||||||
api_router.include_router(agents_ws_router)
|
api_router.include_router(agents_ws_router)
|
||||||
|
api_router.include_router(agents_router)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,295 @@
|
||||||
|
"""Agent routes - REST API for agent task management"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
from fastapi import APIRouter, Depends, WebSocket
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from luxx.database import get_db
|
||||||
|
from luxx.models import User
|
||||||
|
from luxx.routes.auth import get_current_user
|
||||||
|
from luxx.utils.helpers import success_response, error_response, generate_id
|
||||||
|
from luxx.agents.core import AgentConfig, AgentType, AgentStatus
|
||||||
|
from luxx.agents.registry import AgentRegistry
|
||||||
|
from luxx.agents.supervisor import SupervisorAgent
|
||||||
|
from luxx.agents.worker import WorkerAgent
|
||||||
|
from luxx.agents.dag_scheduler import SchedulerPool, DAGScheduler
|
||||||
|
from luxx.services.llm_client import LLMClient
|
||||||
|
from luxx.tools.executor import ToolExecutor
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/agents", tags=["Agents"])
|
||||||
|
|
||||||
|
|
||||||
|
# ============ Request/Response Models ============
|
||||||
|
|
||||||
|
class AgentCreateRequest(BaseModel):
|
||||||
|
"""Create agent task request"""
|
||||||
|
conversation_id: str
|
||||||
|
task: str
|
||||||
|
options: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AgentTaskResponse(BaseModel):
|
||||||
|
"""Agent task response"""
|
||||||
|
task_id: str
|
||||||
|
status: str
|
||||||
|
conversation_id: str
|
||||||
|
|
||||||
|
|
||||||
|
# ============ 全局实例 ============
|
||||||
|
|
||||||
|
# Scheduler pool for managing concurrent DAG executions
|
||||||
|
scheduler_pool = SchedulerPool(max_concurrent=10)
|
||||||
|
|
||||||
|
# Tool executor
|
||||||
|
tool_executor = ToolExecutor()
|
||||||
|
|
||||||
|
|
||||||
|
# ============ Routes ============
|
||||||
|
|
||||||
|
@router.post("/request", response_model=dict)
|
||||||
|
async def create_agent_task(
|
||||||
|
data: AgentCreateRequest,
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create a new agent task
|
||||||
|
|
||||||
|
This will:
|
||||||
|
1. Create a Supervisor agent
|
||||||
|
2. Decompose the task into a DAG
|
||||||
|
3. Start DAG execution
|
||||||
|
4. Return task_id for WebSocket subscription
|
||||||
|
"""
|
||||||
|
from luxx.config import config
|
||||||
|
from luxx.models import Conversation, LLMProvider
|
||||||
|
|
||||||
|
# Get conversation to find provider
|
||||||
|
conversation = db.query(Conversation).filter(
|
||||||
|
Conversation.id == data.conversation_id,
|
||||||
|
Conversation.user_id == current_user.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not conversation:
|
||||||
|
return error_response("Conversation not found", 404)
|
||||||
|
|
||||||
|
# Determine LLM client configuration
|
||||||
|
llm_api_key = None
|
||||||
|
llm_api_url = None
|
||||||
|
llm_model = None
|
||||||
|
|
||||||
|
logger.info(f"Conversation provider_id: {conversation.provider_id}")
|
||||||
|
|
||||||
|
if conversation.provider_id:
|
||||||
|
provider = db.query(LLMProvider).filter(LLMProvider.id == conversation.provider_id).first()
|
||||||
|
logger.info(f"Provider found: {provider}")
|
||||||
|
if provider:
|
||||||
|
llm_api_key = provider.api_key
|
||||||
|
llm_api_url = provider.base_url
|
||||||
|
llm_model = provider.default_model
|
||||||
|
logger.info(f"Provider config - api_key: {'set' if llm_api_key else 'None'}, url: {llm_api_url}, model: {llm_model}")
|
||||||
|
|
||||||
|
# Fallback to config if no provider
|
||||||
|
if not llm_api_key:
|
||||||
|
llm_api_key = config.llm_api_key
|
||||||
|
llm_api_url = config.llm_api_url
|
||||||
|
llm_model = "deepseek-chat"
|
||||||
|
|
||||||
|
# Check if LLM API key is configured
|
||||||
|
if not llm_api_key:
|
||||||
|
return error_response(
|
||||||
|
"LLM API key not configured. Please set up a provider in settings or set DEEPSEEK_API_KEY environment variable.",
|
||||||
|
500
|
||||||
|
)
|
||||||
|
|
||||||
|
task_id = generate_id("task")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create LLM client with proper configuration
|
||||||
|
llm_client = LLMClient(
|
||||||
|
api_key=llm_api_key,
|
||||||
|
api_url=llm_api_url,
|
||||||
|
model=llm_model
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create supervisor agent
|
||||||
|
agent_registry = AgentRegistry()
|
||||||
|
|
||||||
|
supervisor_config = AgentConfig(
|
||||||
|
name=f"supervisor_{task_id}",
|
||||||
|
agent_type=AgentType.SUPERVISOR,
|
||||||
|
description=f"Supervisor for task {task_id}",
|
||||||
|
model=llm_model, # Use the model's default model
|
||||||
|
max_turns=10
|
||||||
|
)
|
||||||
|
|
||||||
|
supervisor_agent = agent_registry.create_agent(
|
||||||
|
config=supervisor_config,
|
||||||
|
user_id=current_user.id,
|
||||||
|
conversation_id=data.conversation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create supervisor instance
|
||||||
|
supervisor = SupervisorAgent(
|
||||||
|
agent=supervisor_agent,
|
||||||
|
llm_client=llm_client
|
||||||
|
)
|
||||||
|
|
||||||
|
# Decompose task into DAG
|
||||||
|
context = {
|
||||||
|
"user_id": current_user.id,
|
||||||
|
"username": current_user.username,
|
||||||
|
"conversation_id": data.conversation_id
|
||||||
|
}
|
||||||
|
|
||||||
|
dag = await supervisor.decompose_task(data.task, context)
|
||||||
|
|
||||||
|
# Create worker factory
|
||||||
|
def worker_factory():
|
||||||
|
# Create new LLM client for each worker with proper config
|
||||||
|
worker_llm_client = LLMClient(
|
||||||
|
api_key=llm_api_key,
|
||||||
|
api_url=llm_api_url,
|
||||||
|
model=llm_model
|
||||||
|
)
|
||||||
|
worker_config = AgentConfig(
|
||||||
|
name=f"worker_{task_id}",
|
||||||
|
agent_type=AgentType.WORKER,
|
||||||
|
description=f"Worker for task {task_id}",
|
||||||
|
model=llm_model,
|
||||||
|
max_turns=5
|
||||||
|
)
|
||||||
|
worker_agent = agent_registry.create_agent(
|
||||||
|
config=worker_config,
|
||||||
|
user_id=current_user.id,
|
||||||
|
conversation_id=data.conversation_id
|
||||||
|
)
|
||||||
|
return WorkerAgent(
|
||||||
|
agent=worker_agent,
|
||||||
|
llm_client=worker_llm_client,
|
||||||
|
tool_executor=tool_executor
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create scheduler
|
||||||
|
scheduler = scheduler_pool.create_scheduler(
|
||||||
|
task_id=task_id,
|
||||||
|
dag=dag,
|
||||||
|
supervisor=supervisor,
|
||||||
|
worker_factory=worker_factory,
|
||||||
|
max_workers=3
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start execution in background
|
||||||
|
asyncio.create_task(_execute_dag_background(
|
||||||
|
task_id=task_id,
|
||||||
|
scheduler=scheduler,
|
||||||
|
context=context,
|
||||||
|
task=data.task,
|
||||||
|
supervisor_agent=supervisor_agent
|
||||||
|
))
|
||||||
|
|
||||||
|
return success_response(data={
|
||||||
|
"task_id": task_id,
|
||||||
|
"status": "planning",
|
||||||
|
"conversation_id": data.conversation_id
|
||||||
|
}, message="Agent task created successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create agent task: {e}")
|
||||||
|
return error_response(f"Failed to create task: {str(e)}", 500)
|
||||||
|
|
||||||
|
|
||||||
|
async def _execute_dag_background(
|
||||||
|
task_id: str,
|
||||||
|
scheduler: DAGScheduler,
|
||||||
|
context: dict,
|
||||||
|
task: str,
|
||||||
|
supervisor_agent
|
||||||
|
):
|
||||||
|
"""Execute DAG in background and handle completion"""
|
||||||
|
try:
|
||||||
|
result = await scheduler.execute(context, task)
|
||||||
|
|
||||||
|
# Update supervisor status
|
||||||
|
supervisor_agent.status = AgentStatus.COMPLETED if result["success"] else AgentStatus.FAILED
|
||||||
|
|
||||||
|
# Emit completion event via WebSocket
|
||||||
|
from luxx.routes.agents_ws import emit_dag_complete, emit_node_complete
|
||||||
|
|
||||||
|
# Emit node complete events for each completed node
|
||||||
|
for node_id, node_result in result.get("results", {}).items():
|
||||||
|
if node_result.get("success"):
|
||||||
|
# Find the node in the DAG
|
||||||
|
node = scheduler.dag.nodes.get(node_id)
|
||||||
|
if node:
|
||||||
|
await emit_node_complete(task_id, node)
|
||||||
|
|
||||||
|
# Emit DAG complete event
|
||||||
|
await emit_dag_complete(task_id, result["success"], result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"DAG execution failed for task {task_id}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/task/{task_id}", response_model=dict)
|
||||||
|
async def get_task_status(
|
||||||
|
task_id: str,
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Get task status and DAG info"""
|
||||||
|
scheduler = scheduler_pool.get(task_id)
|
||||||
|
|
||||||
|
if not scheduler:
|
||||||
|
return error_response("Task not found", 404)
|
||||||
|
|
||||||
|
return success_response(data={
|
||||||
|
"task_id": task_id,
|
||||||
|
"status": "executing",
|
||||||
|
"dag": scheduler.dag.to_dict(),
|
||||||
|
"progress": scheduler.dag.progress
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/task/{task_id}/cancel", response_model=dict)
|
||||||
|
async def cancel_task(
|
||||||
|
task_id: str,
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Cancel a running task"""
|
||||||
|
if scheduler_pool.cancel(task_id):
|
||||||
|
return success_response(message="Task cancelled")
|
||||||
|
return error_response("Task not found or already completed", 404)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/task/{task_id}", response_model=dict)
|
||||||
|
async def delete_task(
|
||||||
|
task_id: str,
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""Delete a task"""
|
||||||
|
if scheduler_pool.remove(task_id):
|
||||||
|
return success_response(message="Task deleted")
|
||||||
|
return error_response("Task not found", 404)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tasks", response_model=dict)
|
||||||
|
async def list_tasks(
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 20,
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""List user's agent tasks"""
|
||||||
|
# Get active schedulers for this user
|
||||||
|
# Note: In a real implementation, you'd store task metadata in database
|
||||||
|
tasks = []
|
||||||
|
|
||||||
|
return success_response(data={
|
||||||
|
"items": tasks,
|
||||||
|
"total": len(tasks),
|
||||||
|
"page": page,
|
||||||
|
"page_size": page_size
|
||||||
|
})
|
||||||
|
|
@ -95,10 +95,15 @@ class LLMClient:
|
||||||
tool_calls = None
|
tool_calls = None
|
||||||
usage = None
|
usage = None
|
||||||
|
|
||||||
if "choices" in data:
|
logger.info(f"Parsing LLM response: {data}")
|
||||||
|
|
||||||
|
if "choices" in data and data["choices"]:
|
||||||
choice = data["choices"][0]
|
choice = data["choices"][0]
|
||||||
content = choice.get("message", {}).get("content", "")
|
message = choice.get("message") if choice else {}
|
||||||
tool_calls = choice.get("message", {}).get("tool_calls")
|
content = message.get("content", "") if message else ""
|
||||||
|
tool_calls = message.get("tool_calls") if message else None
|
||||||
|
else:
|
||||||
|
logger.warning(f"No choices in LLM response: {data}")
|
||||||
|
|
||||||
if "usage" in data:
|
if "usage" in data:
|
||||||
usage = data["usage"]
|
usage = data["usage"]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue