debug
This commit is contained in:
parent
8089d94e78
commit
69b11ea7d4
|
|
@ -32,6 +32,10 @@ const navItems = [
|
|||
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>`
|
||||
},
|
||||
{
|
||||
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',
|
||||
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'),
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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`)
|
||||
}
|
||||
|
||||
// ============ 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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(
|
||||
model=self.agent.config.model,
|
||||
messages=messages,
|
||||
temperature=self.agent.config.temperature,
|
||||
max_tokens=self.agent.config.max_tokens
|
||||
)
|
||||
logger.info(f"LLM response received: {response}")
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(0.5, "Processing decomposition...")
|
||||
|
||||
# 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
|
||||
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.agents_ws import router as agents_ws_router
|
||||
from luxx.routes.agents import router as agents_router
|
||||
|
||||
|
||||
api_router = APIRouter()
|
||||
|
|
@ -14,3 +15,4 @@ api_router.include_router(messages.router)
|
|||
api_router.include_router(tools.router)
|
||||
api_router.include_router(providers.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
|
||||
usage = None
|
||||
|
||||
if "choices" in data:
|
||||
logger.info(f"Parsing LLM response: {data}")
|
||||
|
||||
if "choices" in data and data["choices"]:
|
||||
choice = data["choices"][0]
|
||||
content = choice.get("message", {}).get("content", "")
|
||||
tool_calls = choice.get("message", {}).get("tool_calls")
|
||||
message = choice.get("message") if choice else {}
|
||||
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:
|
||||
usage = data["usage"]
|
||||
|
|
|
|||
Loading…
Reference in New Issue