refactor: 优化项目架构
This commit is contained in:
parent
08d2a2be98
commit
e5c0720650
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { useAuth } from './utils/useAuth.js'
|
||||
import { useAuth } from './composables/useAuth.js'
|
||||
import AppHeader from './components/AppHeader.vue'
|
||||
|
||||
const { isLoggedIn } = useAuth()
|
||||
|
|
|
|||
|
|
@ -72,14 +72,11 @@ export function createSSEStream(url, body, { onProcessStep, onDone, onError }) {
|
|||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
|
||||
// 处理数据
|
||||
if (value) {
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
}
|
||||
|
||||
// 流结束时,先处理 buffer 中的剩余数据,再 break
|
||||
if (done) {
|
||||
// 处理 buffer 中剩余的数据
|
||||
const lines = buffer.split('\n')
|
||||
buffer = ''
|
||||
|
||||
|
|
@ -99,12 +96,11 @@ export function createSSEStream(url, body, { onProcessStep, onDone, onError }) {
|
|||
onError(data.content)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('SSE parse error:', e, 'line:', line)
|
||||
console.error('SSE parse error:', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有收到 done 事件,触发错误
|
||||
if (!completed && onError) {
|
||||
onError('stream ended without done event')
|
||||
}
|
||||
|
|
@ -129,14 +125,11 @@ export function createSSEStream(url, body, { onProcessStep, onDone, onError }) {
|
|||
} else if (currentEvent === 'error' && onError) {
|
||||
onError(data.content)
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略解析错误
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 流结束但没有收到 done 事件,才报错
|
||||
if (!completed && onError) {
|
||||
onError('stream ended unexpectedly')
|
||||
}
|
||||
|
|
@ -152,7 +145,6 @@ export function createSSEStream(url, body, { onProcessStep, onDone, onError }) {
|
|||
}
|
||||
|
||||
// ============ 认证接口 ============
|
||||
|
||||
export const authAPI = {
|
||||
login: (data) => api.post('/auth/login', data),
|
||||
register: (data) => api.post('/auth/register', data),
|
||||
|
|
@ -163,7 +155,6 @@ export const authAPI = {
|
|||
}
|
||||
|
||||
// ============ 会话接口 ============
|
||||
|
||||
export const conversationsAPI = {
|
||||
list: (params) => api.get('/conversations/', { params }),
|
||||
create: (data) => api.post('/conversations/', data),
|
||||
|
|
@ -173,12 +164,9 @@ export const conversationsAPI = {
|
|||
}
|
||||
|
||||
// ============ 消息接口 ============
|
||||
|
||||
export const messagesAPI = {
|
||||
list: (conversationId, params) => api.get('/messages/', { params: { conversation_id: conversationId, ...params } }),
|
||||
send: (data) => api.post('/messages/', data),
|
||||
|
||||
// 发送消息(流式)
|
||||
sendStream: (data, callbacks) => {
|
||||
return createSSEStream('/messages/stream', {
|
||||
conversation_id: data.conversation_id,
|
||||
|
|
@ -187,12 +175,10 @@ export const messagesAPI = {
|
|||
enabled_tools: data.enabled_tools || []
|
||||
}, callbacks)
|
||||
},
|
||||
|
||||
delete: (id) => api.delete(`/messages/${id}`)
|
||||
}
|
||||
|
||||
// ============ 工具接口 ============
|
||||
|
||||
export const toolsAPI = {
|
||||
list: (params) => api.get('/tools/', { params }),
|
||||
get: (name) => api.get(`/tools/${name}`),
|
||||
|
|
@ -200,7 +186,6 @@ export const toolsAPI = {
|
|||
}
|
||||
|
||||
// ============ LLM Provider 接口 ============
|
||||
|
||||
export const providersAPI = {
|
||||
list: () => api.get('/providers/'),
|
||||
create: (data) => api.post('/providers/', data),
|
||||
|
|
@ -210,4 +195,101 @@ export const providersAPI = {
|
|||
test: (id) => api.post(`/providers/${id}/test`)
|
||||
}
|
||||
|
||||
// ============ Agent 接口 ============
|
||||
export const agentsAPI = {
|
||||
list: (params = {}) => api.get('/agents/', { params }),
|
||||
create: (data) => api.post('/agents/', data),
|
||||
get: (id) => api.get(`/agents/${id}`),
|
||||
update: (id, data) => api.put(`/agents/${id}`, data),
|
||||
delete: (id) => api.delete(`/agents/${id}`)
|
||||
}
|
||||
|
||||
// ============ Chat Room 接口 ============
|
||||
export const roomsAPI = {
|
||||
list: () => api.get('/chat-rooms/'),
|
||||
create: (data) => api.post('/chat-rooms/', data),
|
||||
get: (id) => api.get(`/chat-rooms/${id}`),
|
||||
update: (id, data) => api.put(`/chat-rooms/${id}`, data),
|
||||
delete: (id) => api.delete(`/chat-rooms/${id}`),
|
||||
listAgents: (roomId) => api.get(`/chat-rooms/${roomId}/agents`),
|
||||
addAgent: (roomId, agentId) => api.post(`/chat-rooms/${roomId}/agents`, { agent_id: agentId }),
|
||||
removeAgent: (roomId, agentId) => api.delete(`/chat-rooms/${roomId}/agents/${agentId}`),
|
||||
listMessages: (roomId, params = {}) => api.get(`/chat-rooms/${roomId}/messages`, { params })
|
||||
}
|
||||
|
||||
// ============ WebSocket ============
|
||||
export function createRoomWS(roomId, callbacks = {}) {
|
||||
const token = localStorage.getItem('access_token')
|
||||
const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws/chat-room/${roomId}${token ? '?token=' + token : ''}`
|
||||
|
||||
const ws = new WebSocket(wsUrl)
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connected to room:', roomId)
|
||||
callbacks.onConnect?.()
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data)
|
||||
const eventName = msg.event
|
||||
|
||||
switch (eventName) {
|
||||
case 'connected':
|
||||
callbacks.onConnected?.(msg.data)
|
||||
break
|
||||
case 'history':
|
||||
callbacks.onHistory?.(msg.data.messages)
|
||||
break
|
||||
case 'agents':
|
||||
callbacks.onAgentsUpdate?.(msg.data.agents)
|
||||
break
|
||||
case 'message':
|
||||
callbacks.onMessage?.(msg.data)
|
||||
break
|
||||
case 'typing':
|
||||
callbacks.onTyping?.(msg.data)
|
||||
break
|
||||
case 'process_step':
|
||||
case 'done':
|
||||
case 'error':
|
||||
callbacks.onStream?.(eventName, msg.data, msg.agent_id, msg.agent_name)
|
||||
break
|
||||
case 'system':
|
||||
callbacks.onSystem?.(msg.data)
|
||||
break
|
||||
default:
|
||||
console.log('Unknown event:', eventName, msg)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse WebSocket message:', e)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error)
|
||||
callbacks.onError?.(error)
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket disconnected from room:', roomId)
|
||||
callbacks.onDisconnect?.()
|
||||
}
|
||||
|
||||
return {
|
||||
send: (action, data = {}) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ action, ...data }))
|
||||
}
|
||||
},
|
||||
sendMessage: (content, userId = 'user', userName = 'User') => {
|
||||
ws.send(JSON.stringify({ action: 'send_message', content, user_id: userId, user_name: userName }))
|
||||
},
|
||||
ping: () => {
|
||||
ws.send(JSON.stringify({ action: 'ping' }))
|
||||
},
|
||||
close: () => ws.close()
|
||||
}
|
||||
}
|
||||
|
||||
export default api
|
||||
|
|
@ -32,10 +32,18 @@ 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: '/rooms',
|
||||
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="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></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>`
|
||||
},
|
||||
{
|
||||
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"><path d="M12 2a10 10 0 1 0 10 10 4 4 0 0 1-5-5 4 4 0 0 1-5-5"></path><path d="M8.5 8.5v.01"></path><path d="M16 15.5v.01"></path><path d="M12 12v.01"></path><path d="M11 17v.01"></path><path d="M7 14v.01"></path></svg>`
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
icon: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>`
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@
|
|||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { renderMarkdown } from '../utils/markdown.js'
|
||||
import { formatNumber } from '../utils/useFormatters.js'
|
||||
import { renderMarkdown } from '@/utils/markdown.js'
|
||||
import { formatNumber } from '@/composables/useFormatters.js'
|
||||
import ProcessBlock from './ProcessBlock.vue'
|
||||
|
||||
const props = defineProps({
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { renderMarkdown } from '../utils/markdown.js'
|
||||
import { renderMarkdown } from '@/utils/markdown.js'
|
||||
|
||||
const props = defineProps({
|
||||
processSteps: { type: Array, default: () => [] },
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { ref, computed, watch } from 'vue'
|
||||
import { conversationsAPI, messagesAPI, toolsAPI, providersAPI } from './api.js'
|
||||
import { streamManager } from './streamManager.js'
|
||||
import { useStreamStore } from './streamStore.js'
|
||||
import { conversationsAPI, messagesAPI, toolsAPI, providersAPI } from '@/api'
|
||||
import { streamManager } from '@/utils/streamManager.js'
|
||||
import { useStreamStore } from '@/utils/streamStore.js'
|
||||
|
||||
// 对话管理 Composable
|
||||
export function useConversations() {
|
||||
|
|
@ -2,7 +2,7 @@ import { createApp } from 'vue'
|
|||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { pinia } from './utils'
|
||||
import pinia from './utils/store.js'
|
||||
|
||||
// 初始化夜间模式
|
||||
if (localStorage.getItem('theme') === 'dark') {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,20 @@ const routes = [
|
|||
component: () => import('../views/ToolsView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
// Agent 管理
|
||||
{
|
||||
path: '/agents',
|
||||
name: 'Agents',
|
||||
component: () => import('../views/AgentsView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
// 聊天室
|
||||
{
|
||||
path: '/rooms',
|
||||
name: 'Rooms',
|
||||
component: () => import('../views/RoomView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
// 首页重定向
|
||||
{
|
||||
path: '/home',
|
||||
|
|
@ -63,13 +77,11 @@ router.beforeEach((to, from, next) => {
|
|||
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
|
||||
|
||||
if (requiresAuth && !token) {
|
||||
// 需要认证但未登录,重定向到登录页
|
||||
next({
|
||||
name: 'Auth',
|
||||
query: { redirect: to.fullPath }
|
||||
})
|
||||
} else if (to.name === 'Auth' && token) {
|
||||
// 已登录访问登录页,重定向到首页
|
||||
next({ name: 'Home' })
|
||||
} else {
|
||||
next()
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
/**
|
||||
* Luxx 前端工具库
|
||||
* 合并了 composables、services 和 stores 的统一导出
|
||||
*/
|
||||
|
||||
// ============ API 服务 ============
|
||||
export { default as api, authAPI, conversationsAPI, messagesAPI, toolsAPI, providersAPI, createSSEStream } from './api.js'
|
||||
|
||||
// ============ Pinia 状态管理 ============
|
||||
export { default as pinia } from './store.js'
|
||||
|
||||
// ============ 认证相关 ============
|
||||
export { useAuth } from './useAuth.js'
|
||||
|
||||
// ============ API 请求组合式函数 ============
|
||||
export { useApi, usePagination, useForm } from './useApi.js'
|
||||
|
||||
// ============ 格式化工具 ============
|
||||
export { formatDate, formatNumber, truncate, formatFileSize, capitalize, formatTokens } from './useFormatters.js'
|
||||
|
||||
// ============ 通用工具函数 ============
|
||||
export { debounce, throttle, deepClone, generateId, storage, getDeviceType, copyToClipboard } from './useUtils.js'
|
||||
|
||||
// ============ Markdown 渲染 ============
|
||||
export { renderMarkdown } from './markdown.js'
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
/**
|
||||
* API 请求组合式函数
|
||||
* 提供统一的错误处理和加载状态管理
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
|
||||
export function useApi() {
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
/**
|
||||
* 执行 API 请求
|
||||
* @param {Function} apiFn - API 函数
|
||||
* @param {Object} options - 配置选项
|
||||
* @returns {Promise<Object>} 响应数据
|
||||
*/
|
||||
const request = async (apiFn, options = {}) => {
|
||||
const { showLoading = true, errorMessage = '请求失败' } = options
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await apiFn()
|
||||
|
||||
if (response.success) {
|
||||
return response.data
|
||||
} else {
|
||||
throw new Error(response.message || errorMessage)
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err.message || errorMessage
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
const reset = () => {
|
||||
loading.value = false
|
||||
error.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
request,
|
||||
reset
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页组合式函数
|
||||
*/
|
||||
export function usePagination(initialPage = 1, initialPageSize = 20) {
|
||||
const page = ref(initialPage)
|
||||
const pageSize = ref(initialPageSize)
|
||||
const total = ref(0)
|
||||
|
||||
const totalPages = () => Math.ceil(total.value / pageSize.value)
|
||||
|
||||
const nextPage = () => {
|
||||
if (page.value < totalPages()) {
|
||||
page.value++
|
||||
}
|
||||
}
|
||||
|
||||
const prevPage = () => {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
}
|
||||
|
||||
const goToPage = (p) => {
|
||||
if (p >= 1 && p <= totalPages()) {
|
||||
page.value = p
|
||||
}
|
||||
}
|
||||
|
||||
const resetPagination = () => {
|
||||
page.value = initialPage
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
return {
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
totalPages,
|
||||
nextPage,
|
||||
prevPage,
|
||||
goToPage,
|
||||
resetPagination
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单组合式函数
|
||||
*/
|
||||
export function useForm(initialData = {}) {
|
||||
const formData = ref({ ...initialData })
|
||||
const errors = ref({})
|
||||
const submitting = ref(false)
|
||||
|
||||
const updateField = (field, value) => {
|
||||
formData.value[field] = value
|
||||
// 清除字段错误
|
||||
if (errors.value[field]) {
|
||||
delete errors.value[field]
|
||||
}
|
||||
}
|
||||
|
||||
const setFieldError = (field, message) => {
|
||||
errors.value[field] = message
|
||||
}
|
||||
|
||||
const setErrors = (errorObj) => {
|
||||
errors.value = errorObj
|
||||
}
|
||||
|
||||
const clearErrors = () => {
|
||||
errors.value = {}
|
||||
}
|
||||
|
||||
const resetForm = (newData = {}) => {
|
||||
formData.value = { ...initialData, ...newData }
|
||||
errors.value = {}
|
||||
}
|
||||
|
||||
const validate = (rules) => {
|
||||
const newErrors = {}
|
||||
|
||||
for (const [field, rule] of Object.entries(rules)) {
|
||||
const value = formData.value[field]
|
||||
|
||||
if (rule.required && !value) {
|
||||
newErrors[field] = rule.message || `${field}不能为空`
|
||||
}
|
||||
|
||||
if (rule.minLength && value && value.length < rule.minLength) {
|
||||
newErrors[field] = rule.message || `${field}长度不能少于${rule.minLength}位`
|
||||
}
|
||||
|
||||
if (rule.maxLength && value && value.length > rule.maxLength) {
|
||||
newErrors[field] = rule.message || `${field}长度不能超过${rule.maxLength}位`
|
||||
}
|
||||
|
||||
if (rule.pattern && value && !rule.pattern.test(value)) {
|
||||
newErrors[field] = rule.message || `${field}格式不正确`
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
return {
|
||||
formData,
|
||||
errors,
|
||||
submitting,
|
||||
updateField,
|
||||
setFieldError,
|
||||
setErrors,
|
||||
clearErrors,
|
||||
resetForm,
|
||||
validate
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,387 @@
|
|||
<template>
|
||||
<div class="agents-view">
|
||||
<div class="header">
|
||||
<h1>Agent 管理</h1>
|
||||
<button class="btn-primary" @click="showCreateModal = true">
|
||||
<span class="icon">+</span> 创建 Agent
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Agent 列表 -->
|
||||
<div class="agents-grid">
|
||||
<div v-for="agent in agents" :key="agent.id" class="agent-card">
|
||||
<div class="agent-header">
|
||||
<div class="agent-avatar">
|
||||
{{ agent.name?.charAt(0).toUpperCase() || 'A' }}
|
||||
</div>
|
||||
<div class="agent-info">
|
||||
<h3>{{ agent.name }}</h3>
|
||||
<span class="role-badge" :class="agent.role">{{ agent.role }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="agent-meta">
|
||||
<div class="meta-item">
|
||||
<span class="label">优先级</span>
|
||||
<span class="value">{{ agent.priority }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">响应方式</span>
|
||||
<span class="value">
|
||||
{{ agent.auto_response ? '自动' : '@触发' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="agent-prompt">{{ agent.system_prompt?.slice(0, 100) }}...</p>
|
||||
|
||||
<div class="agent-actions">
|
||||
<button class="btn-small" @click="editAgent(agent)">编辑</button>
|
||||
<button class="btn-small btn-danger" @click="deleteAgent(agent.id)">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建/编辑 Modal -->
|
||||
<div v-if="showCreateModal || editingAgent" class="modal-overlay" @click.self="closeModal">
|
||||
<div class="modal">
|
||||
<h2>{{ editingAgent ? '编辑 Agent' : '创建 Agent' }}</h2>
|
||||
|
||||
<form @submit.prevent="saveAgent">
|
||||
<div class="form-group">
|
||||
<label>名称 *</label>
|
||||
<input v-model="form.name" type="text" required placeholder="Agent 名称" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>角色 *</label>
|
||||
<input v-model="form.role" type="text" required placeholder="如: coder, thinker" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>系统提示词 *</label>
|
||||
<textarea v-model="form.system_prompt" rows="6" required
|
||||
placeholder="定义 Agent 的行为和职责..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>优先级</label>
|
||||
<input v-model.number="form.priority" type="number" min="1" max="10" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>温度</label>
|
||||
<input v-model.number="form.temperature" type="number" step="0.1" min="0" max="2" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>最大 Token</label>
|
||||
<input v-model.number="form.max_tokens" type="number" min="100" max="32000" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label class="checkbox-label">
|
||||
<input v-model="form.auto_response" type="checkbox" />
|
||||
自动响应
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input v-model="form.mention_trigger" type="checkbox" />
|
||||
@触发
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn-secondary" @click="closeModal">取消</button>
|
||||
<button type="submit" class="btn-primary">
|
||||
{{ editingAgent ? '保存' : '创建' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { agentsAPI } from '@/api'
|
||||
|
||||
const agents = ref([])
|
||||
const showCreateModal = ref(false)
|
||||
const editingAgent = ref(null)
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
role: 'helper',
|
||||
system_prompt: '',
|
||||
priority: 5,
|
||||
temperature: 0.7,
|
||||
max_tokens: 2048,
|
||||
auto_response: true,
|
||||
mention_trigger: false
|
||||
})
|
||||
|
||||
async function loadAgents() {
|
||||
try {
|
||||
const res = await agentsAPI.list()
|
||||
agents.value = res.agents || []
|
||||
} catch (e) {
|
||||
console.error('Failed to load agents:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function editAgent(agent) {
|
||||
editingAgent.value = agent
|
||||
Object.assign(form, {
|
||||
name: agent.name,
|
||||
role: agent.role,
|
||||
system_prompt: agent.system_prompt,
|
||||
priority: agent.priority,
|
||||
temperature: agent.temperature,
|
||||
max_tokens: agent.max_tokens,
|
||||
auto_response: agent.auto_response,
|
||||
mention_trigger: agent.mention_trigger
|
||||
})
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showCreateModal.value = false
|
||||
editingAgent.value = null
|
||||
Object.assign(form, {
|
||||
name: '', role: 'helper', system_prompt: '',
|
||||
priority: 5, temperature: 0.7, max_tokens: 2048,
|
||||
auto_response: true, mention_trigger: false
|
||||
})
|
||||
}
|
||||
|
||||
async function saveAgent() {
|
||||
try {
|
||||
if (editingAgent.value) {
|
||||
await agentsAPI.update(editingAgent.value.id, { ...form })
|
||||
} else {
|
||||
await agentsAPI.create({ ...form })
|
||||
}
|
||||
closeModal()
|
||||
loadAgents()
|
||||
} catch (e) {
|
||||
console.error('Failed to save agent:', e)
|
||||
alert('保存失败: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAgent(id) {
|
||||
if (!confirm('确定删除此 Agent?')) return
|
||||
try {
|
||||
await agentsAPI.delete(id)
|
||||
loadAgents()
|
||||
} catch (e) {
|
||||
console.error('Failed to delete agent:', e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadAgents)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.agents-view {
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.agents-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.agent-card {
|
||||
background: var(--card-bg, #fff);
|
||||
border: 1px solid var(--border-color, #e5e7eb);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.agent-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.agent-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.agent-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.agent-info h3 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
background: var(--badge-bg, #e5e7eb);
|
||||
color: var(--badge-color, #666);
|
||||
}
|
||||
|
||||
.agent-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.meta-item .label {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.agent-prompt {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.agent-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #e5e7eb;
|
||||
color: #333;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
color: #dc2626;
|
||||
border-color: #dc2626;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--card-bg, #fff);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal h2 {
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-row .form-group {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -48,8 +48,8 @@
|
|||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { authAPI } from '../utils/api.js'
|
||||
import { useAuth } from '../utils/useAuth.js'
|
||||
import { authAPI } from '@/api'
|
||||
import { useAuth } from '@/composables/useAuth.js'
|
||||
|
||||
const router = useRouter()
|
||||
const { login } = useAuth()
|
||||
|
|
|
|||
|
|
@ -163,8 +163,8 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { useConversations } from '../utils/useConversations.js'
|
||||
import { formatDate } from '../utils/useFormatters.js'
|
||||
import { useConversations } from '@/composables/useConversations.js'
|
||||
import { formatDate } from '@/composables/useFormatters.js'
|
||||
import ProcessBlock from '../components/ProcessBlock.vue'
|
||||
import MessageNav from '../components/MessageNav.vue'
|
||||
import MessageBubble from '../components/MessageBubble.vue'
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { conversationsAPI, toolsAPI } from '../utils/api.js'
|
||||
import { formatTokens } from '../utils/useFormatters.js'
|
||||
import { conversationsAPI, toolsAPI } from '@/api'
|
||||
import { formatTokens } from '@/composables/useFormatters.js'
|
||||
|
||||
const stats = ref({ conversations: 0, tools: 0, totalTokens: 0 })
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,785 @@
|
|||
<template>
|
||||
<div class="room-view">
|
||||
<!-- 左侧:房间列表 -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2>聊天室</h2>
|
||||
<button class="btn-icon" @click="showCreateRoom = true" title="创建房间">+</button>
|
||||
</div>
|
||||
|
||||
<div class="room-list">
|
||||
<div v-for="room in rooms" :key="room.id"
|
||||
class="room-item" :class="{ active: currentRoom?.id === room.id }"
|
||||
@click="joinRoom(room)">
|
||||
<div class="room-icon">💬</div>
|
||||
<div class="room-info">
|
||||
<span class="room-name">{{ room.name }}</span>
|
||||
<span class="room-desc">{{ room.description || '无描述' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<router-link to="/agents" class="nav-link">
|
||||
<span>⚙️</span> 管理 Agent
|
||||
</router-link>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 主聊天区 -->
|
||||
<main class="chat-main">
|
||||
<template v-if="currentRoom">
|
||||
<!-- 房间头部 -->
|
||||
<header class="chat-header">
|
||||
<div class="header-info">
|
||||
<h1>{{ currentRoom.name }}</h1>
|
||||
<span class="agents-count">{{ roomAgents.length }} 个 Agent</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="btn-small" @click="showManageAgents = true">管理成员</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Agent 在线状态 -->
|
||||
<div class="agents-bar">
|
||||
<div v-for="agent in roomAgents" :key="agent.id"
|
||||
class="agent-pill" :class="{ typing: typingAgents.has(agent.id) }">
|
||||
<span class="agent-avatar-sm">{{ agent.name?.charAt(0) }}</span>
|
||||
<span class="agent-name">{{ agent.name }}</span>
|
||||
<span v-if="typingAgents.has(agent.id)" class="typing-indicator">...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<div class="messages-container" ref="messagesContainer">
|
||||
<div v-for="msg in messages" :key="msg.id" class="message-wrapper"
|
||||
:class="msg.sender_type">
|
||||
<div class="message-avatar">
|
||||
{{ msg.sender_name?.charAt(0).toUpperCase() }}
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message-meta">
|
||||
<span class="sender-name">{{ msg.sender_name }}</span>
|
||||
<span class="message-time">{{ formatTime(msg.created_at) }}</span>
|
||||
</div>
|
||||
<div class="message-text" v-html="renderMarkdown(msg.content)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 正在输入的流式消息 -->
|
||||
<div v-for="(stream, agentId) in streamingMessages" :key="'stream-' + agentId"
|
||||
class="message-wrapper agent streaming">
|
||||
<div class="message-avatar">
|
||||
{{ stream.agentName?.charAt(0).toUpperCase() }}
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message-meta">
|
||||
<span class="sender-name">{{ stream.agentName }}</span>
|
||||
<span class="typing-indicator">正在输入...</span>
|
||||
</div>
|
||||
<div class="message-text" v-html="renderMarkdown(stream.content)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入区 -->
|
||||
<div class="input-area">
|
||||
<div class="input-hint">
|
||||
<span v-if="autoAgents.length">@ 触发特定 Agent,或等待自动响应</span>
|
||||
<span v-else>所有 Agent 需要 @ 触发</span>
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<textarea v-model="inputMessage" rows="1" placeholder="输入消息... (@AgentName 触发特定Agent)"
|
||||
@keydown.enter.exact.prevent="sendMessage" @input="autoResize"></textarea>
|
||||
<button class="btn-send" @click="sendMessage" :disabled="!inputMessage.trim()">
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="empty-state">
|
||||
<div class="empty-icon">💬</div>
|
||||
<h2>选择一个聊天室</h2>
|
||||
<p>或创建新房间开始与多个 Agent 互动</p>
|
||||
<button class="btn-primary" @click="showCreateRoom = true">创建聊天室</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 创建房间 Modal -->
|
||||
<div v-if="showCreateRoom" class="modal-overlay" @click.self="showCreateRoom = false">
|
||||
<div class="modal">
|
||||
<h2>创建聊天室</h2>
|
||||
<form @submit.prevent="createRoom">
|
||||
<div class="form-group">
|
||||
<label>房间名称 *</label>
|
||||
<input v-model="newRoom.name" type="text" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>描述</label>
|
||||
<input v-model="newRoom.description" type="text" />
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn-secondary" @click="showCreateRoom = false">取消</button>
|
||||
<button type="submit" class="btn-primary">创建</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 管理 Agent Modal -->
|
||||
<div v-if="showManageAgents" class="modal-overlay" @click.self="showManageAgents = false">
|
||||
<div class="modal">
|
||||
<h2>管理聊天室成员</h2>
|
||||
<div class="agents-management">
|
||||
<div class="section">
|
||||
<h3>当前成员</h3>
|
||||
<div class="agent-tags">
|
||||
<span v-for="agent in roomAgents" :key="agent.id" class="agent-tag">
|
||||
{{ agent.name }}
|
||||
<button @click="removeAgentFromRoom(agent.id)">×</button>
|
||||
</span>
|
||||
<div v-if="!roomAgents.length" class="empty-hint">暂无成员</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h3>添加成员</h3>
|
||||
<div class="agent-list-select">
|
||||
<div v-for="agent in availableAgents" :key="agent.id"
|
||||
class="agent-option" @click="addAgentToRoom(agent.id)">
|
||||
<span class="agent-avatar-sm">{{ agent.name?.charAt(0) }}</span>
|
||||
<span>{{ agent.name }}</span>
|
||||
<span class="role">{{ agent.role }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn-secondary" @click="showManageAgents = false">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { roomsAPI, createRoomWS, agentsAPI } from '@/api'
|
||||
import { marked } from 'marked'
|
||||
|
||||
const rooms = ref([])
|
||||
const currentRoom = ref(null)
|
||||
const messages = ref([])
|
||||
const roomAgents = ref([])
|
||||
const allAgents = ref([])
|
||||
const typingAgents = ref(new Set())
|
||||
const streamingMessages = ref({})
|
||||
|
||||
const showCreateRoom = ref(false)
|
||||
const showManageAgents = ref(false)
|
||||
const inputMessage = ref('')
|
||||
const messagesContainer = ref(null)
|
||||
let ws = null
|
||||
|
||||
const newRoom = reactive({ name: '', description: '' })
|
||||
|
||||
const availableAgents = computed(() => {
|
||||
const roomAgentIds = new Set(roomAgents.value.map(a => a.id))
|
||||
return allAgents.value.filter(a => !roomAgentIds.has(a.id))
|
||||
})
|
||||
|
||||
const autoAgents = computed(() => roomAgents.value.filter(a => a.auto_response && !a.mention_trigger))
|
||||
|
||||
function formatTime(iso) {
|
||||
if (!iso) return ''
|
||||
return new Date(iso).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
function renderMarkdown(text) {
|
||||
if (!text) return ''
|
||||
return marked.parse(text)
|
||||
}
|
||||
|
||||
function autoResize(e) {
|
||||
e.target.style.height = 'auto'
|
||||
e.target.style.height = Math.min(e.target.scrollHeight, 150) + 'px'
|
||||
}
|
||||
|
||||
async function loadRooms() {
|
||||
try {
|
||||
const res = await roomsAPI.list()
|
||||
rooms.value = res.rooms || []
|
||||
} catch (e) {
|
||||
console.error('Failed to load rooms:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAllAgents() {
|
||||
try {
|
||||
const res = await agentsAPI.list()
|
||||
allAgents.value = res.agents || []
|
||||
} catch (e) {
|
||||
console.error('Failed to load agents:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function createRoom() {
|
||||
try {
|
||||
const res = await roomsAPI.create(newRoom)
|
||||
rooms.value.push(res.room)
|
||||
showCreateRoom.value = false
|
||||
Object.assign(newRoom, { name: '', description: '' })
|
||||
joinRoom(res.room)
|
||||
} catch (e) {
|
||||
console.error('Failed to create room:', e)
|
||||
alert('创建失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function joinRoom(room) {
|
||||
if (ws) {
|
||||
ws.close()
|
||||
ws = null
|
||||
}
|
||||
|
||||
currentRoom.value = room
|
||||
messages.value = []
|
||||
streamingMessages.value = {}
|
||||
typingAgents.value = new Set()
|
||||
|
||||
// 加载房间成员
|
||||
try {
|
||||
const res = await roomsAPI.listAgents(room.id)
|
||||
roomAgents.value = res.agents || []
|
||||
} catch (e) {
|
||||
console.error('Failed to load room agents:', e)
|
||||
roomAgents.value = []
|
||||
}
|
||||
|
||||
// WebSocket 连接
|
||||
ws = createRoomWS(room.id, {
|
||||
onConnected: () => console.log('Connected to room'),
|
||||
onHistory: (msgs) => {
|
||||
messages.value = msgs || []
|
||||
scrollToBottom()
|
||||
},
|
||||
onAgentsUpdate: (agents) => {
|
||||
roomAgents.value = agents || []
|
||||
},
|
||||
onMessage: (msg) => {
|
||||
messages.value.push(msg)
|
||||
scrollToBottom()
|
||||
},
|
||||
onTyping: (data) => {
|
||||
if (data.is_typing) {
|
||||
typingAgents.value.add(data.agent_id)
|
||||
} else {
|
||||
typingAgents.value.delete(data.agent_id)
|
||||
}
|
||||
},
|
||||
onStream: (event, data, agentId, agentName) => {
|
||||
if (event === 'process_step') {
|
||||
if (!streamingMessages.value[agentId]) {
|
||||
streamingMessages.value[agentId] = { agentName, content: '' }
|
||||
}
|
||||
if (data.type === 'text') {
|
||||
streamingMessages.value[agentId].content = data.content
|
||||
}
|
||||
} else if (event === 'done') {
|
||||
delete streamingMessages.value[agentId]
|
||||
typingAgents.value.delete(agentId)
|
||||
}
|
||||
},
|
||||
onSystem: (data) => {
|
||||
console.log('System:', data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
if (!inputMessage.value.trim() || !ws) return
|
||||
const user = JSON.parse(localStorage.getItem('user') || '{}')
|
||||
ws.sendMessage(inputMessage.value, user.id || 'user', user.username || 'User')
|
||||
inputMessage.value = ''
|
||||
}
|
||||
|
||||
async function addAgentToRoom(agentId) {
|
||||
try {
|
||||
await roomsAPI.addAgent(currentRoom.value.id, agentId)
|
||||
const res = await roomsAPI.listAgents(currentRoom.value.id)
|
||||
roomAgents.value = res.agents || []
|
||||
} catch (e) {
|
||||
console.error('Failed to add agent:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function removeAgentFromRoom(agentId) {
|
||||
try {
|
||||
await roomsAPI.removeAgent(currentRoom.value.id, agentId)
|
||||
roomAgents.value = roomAgents.value.filter(a => a.id !== agentId)
|
||||
} catch (e) {
|
||||
console.error('Failed to remove agent:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
nextTick(() => {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRooms()
|
||||
loadAllAgents()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (ws) ws.close()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.room-view {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
background: var(--bg-color, #f5f7fa);
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background: var(--card-bg, #fff);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.sidebar-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.room-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.room-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.room-item:hover {
|
||||
background: var(--hover-bg, #f0f0f0);
|
||||
}
|
||||
|
||||
.room-item.active {
|
||||
background: #667eea20;
|
||||
border-left: 3px solid #667eea;
|
||||
}
|
||||
|
||||
.room-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.room-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.room-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.room-desc {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
|
||||
/* Main */
|
||||
.chat-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 16px 24px;
|
||||
background: var(--card-bg, #fff);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-header h1 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.agents-count {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.agents-bar {
|
||||
padding: 8px 24px;
|
||||
background: var(--card-bg, #fff);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.agent-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 12px;
|
||||
background: #e5e7eb;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.agent-pill.typing {
|
||||
background: #fef3c7;
|
||||
}
|
||||
|
||||
.agent-avatar-sm {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.message-wrapper {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.message-wrapper.user {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message-wrapper.user .message-avatar {
|
||||
background: linear-gradient(135deg, #11998e, #38ef7d);
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.message-meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.message-wrapper.user .message-meta {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.sender-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
padding: 12px 16px;
|
||||
background: var(--card-bg, #fff);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.message-wrapper.user .message-text {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Input */
|
||||
.input-area {
|
||||
padding: 16px 24px;
|
||||
background: var(--card-bg, #fff);
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.input-hint {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.input-row textarea {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-send {
|
||||
padding: 12px 24px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-send:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--card-bg, #fff);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal h2 {
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #e5e7eb;
|
||||
color: #333;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Agent management */
|
||||
.agents-management .section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.agents-management h3 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.agent-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.agent-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background: #667eea20;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.agent-tag button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.agent-list-select {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.agent-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.agent-option:hover {
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
|
||||
.agent-option .role {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -295,9 +295,8 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { providersAPI } from '../utils/api.js'
|
||||
import { useAuth } from '../utils/useAuth.js'
|
||||
import { authAPI } from '../utils/api.js'
|
||||
import { providersAPI, authAPI } from '@/api'
|
||||
import { useAuth } from '@/composables/useAuth.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { toolsAPI } from '../utils/api.js'
|
||||
import { toolsAPI } from '@/api'
|
||||
|
||||
const list = ref([])
|
||||
const loading = ref(true)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ import logging
|
|||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.websockets import WebSocket
|
||||
|
||||
from luxx.config import config
|
||||
from luxx.database import init_db
|
||||
from luxx.routes import api_router
|
||||
from luxx.core.config import config
|
||||
from luxx.core.database import init_db
|
||||
from luxx.api import api_router
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -15,12 +16,13 @@ logger = logging.getLogger(__name__)
|
|||
async def lifespan(app: FastAPI):
|
||||
"""Application lifespan manager"""
|
||||
# Import all models to ensure they are registered with Base
|
||||
from luxx.models import User, Conversation, Message, Project, LLMProvider # noqa
|
||||
from luxx.models.user import User, LLMProvider, Project
|
||||
from luxx.models.chat import Conversation, Message
|
||||
from luxx.models.room import ChatRoom, Agent, ChatRoomAgent, ChatRoomMessage
|
||||
init_db()
|
||||
|
||||
# Create default test user if not exists
|
||||
from luxx.database import SessionLocal
|
||||
from luxx.models import User
|
||||
from luxx.core.database import SessionLocal
|
||||
from luxx.utils.helpers import hash_password
|
||||
|
||||
db = SessionLocal()
|
||||
|
|
@ -40,6 +42,7 @@ async def lifespan(app: FastAPI):
|
|||
|
||||
# Import and register tools
|
||||
from luxx.tools.builtin import crawler, code, data
|
||||
|
||||
yield
|
||||
|
||||
|
||||
|
|
@ -55,7 +58,7 @@ def create_app() -> FastAPI:
|
|||
# Configure CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # Should be restricted in production
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
|
|
@ -64,6 +67,13 @@ def create_app() -> FastAPI:
|
|||
# Register routes
|
||||
app.include_router(api_router, prefix="/api")
|
||||
|
||||
# WebSocket endpoint for chat rooms
|
||||
from luxx.services.room_ws import websocket_handler
|
||||
|
||||
@app.websocket("/ws/chat-room/{room_id}")
|
||||
async def chat_room_websocket(websocket: WebSocket, room_id: str):
|
||||
await websocket_handler(websocket, room_id)
|
||||
|
||||
# Health check
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
"""Agents package"""
|
||||
from luxx.agents.base import BaseAgent
|
||||
from luxx.agents.registry import agent_registry
|
||||
|
||||
__all__ = ["BaseAgent", "agent_registry"]
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
"""Base Agent class"""
|
||||
import json
|
||||
import uuid
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional, AsyncGenerator
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from luxx.services.llm_client import LLMClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseAgent(ABC):
|
||||
"""Base class for all agents"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
agent_id: str,
|
||||
name: str,
|
||||
role: str,
|
||||
system_prompt: str,
|
||||
provider_id: int = None,
|
||||
model: str = None,
|
||||
tools: List[str] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 2048,
|
||||
priority: int = 5,
|
||||
auto_response: bool = True,
|
||||
mention_trigger: bool = False,
|
||||
avatar: str = None
|
||||
):
|
||||
self.agent_id = agent_id
|
||||
self.name = name
|
||||
self.role = role
|
||||
self.system_prompt = system_prompt
|
||||
self.provider_id = provider_id
|
||||
self.model = model
|
||||
self.tools = tools or []
|
||||
self.temperature = temperature
|
||||
self.max_tokens = max_tokens
|
||||
self.priority = priority
|
||||
self.auto_response = auto_response
|
||||
self.mention_trigger = mention_trigger
|
||||
self.avatar = avatar
|
||||
self.llm_client = None
|
||||
|
||||
def _get_llm_client(self, room_id: str = None):
|
||||
"""Get LLM client, optionally using agent's provider"""
|
||||
if self.llm_client:
|
||||
return self.llm_client
|
||||
|
||||
if self.provider_id:
|
||||
from luxx.core.database import SessionLocal
|
||||
from luxx.models import LLMProvider
|
||||
db = SessionLocal()
|
||||
try:
|
||||
provider = db.query(LLMProvider).filter(LLMProvider.id == self.provider_id).first()
|
||||
if provider:
|
||||
self.llm_client = LLMClient(
|
||||
api_key=provider.api_key,
|
||||
api_url=provider.base_url,
|
||||
model=provider.default_model
|
||||
)
|
||||
return self.llm_client
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# Fallback to global config
|
||||
self.llm_client = LLMClient()
|
||||
return self.llm_client
|
||||
|
||||
async def stream_response(
|
||||
self,
|
||||
user_message: str,
|
||||
conversation_history: List[Dict] = None,
|
||||
context: Dict = None,
|
||||
thinking_enabled: bool = False
|
||||
) -> AsyncGenerator[Dict[str, Any], None]:
|
||||
"""
|
||||
Generate streaming response for the agent.
|
||||
|
||||
Args:
|
||||
user_message: The user's message
|
||||
conversation_history: Previous messages in the room
|
||||
context: Additional context (workspace, user info, etc.)
|
||||
thinking_enabled: Enable reasoning chain
|
||||
|
||||
Yields:
|
||||
SSE-formatted event dictionaries
|
||||
"""
|
||||
messages = []
|
||||
|
||||
# Add system prompt
|
||||
final_system_prompt = self._build_system_prompt(context)
|
||||
messages.append({"role": "system", "content": final_system_prompt})
|
||||
|
||||
# Add conversation history (last 10 messages)
|
||||
if conversation_history:
|
||||
for msg in conversation_history[-10:]:
|
||||
role = "assistant" if msg["sender_type"] == "agent" else "user"
|
||||
messages.append({
|
||||
"role": role,
|
||||
"content": msg["content"]
|
||||
})
|
||||
|
||||
# Add current user message
|
||||
messages.append({"role": "user", "content": user_message})
|
||||
|
||||
# Get LLM client
|
||||
llm = self._get_llm_client()
|
||||
|
||||
# Get tools if enabled
|
||||
enabled_tools = []
|
||||
if self.tools:
|
||||
from luxx.tools.core import registry
|
||||
for tool_name in self.tools:
|
||||
tool = registry.get(tool_name)
|
||||
if tool:
|
||||
enabled_tools.append(tool)
|
||||
|
||||
# Stream response
|
||||
step_index = 0
|
||||
full_content = ""
|
||||
|
||||
try:
|
||||
async for sse_line in llm.stream_call(
|
||||
model=self.model or llm.default_model,
|
||||
messages=messages,
|
||||
tools=enabled_tools if enabled_tools else None,
|
||||
temperature=self.temperature,
|
||||
max_tokens=self.max_tokens,
|
||||
thinking_enabled=thinking_enabled
|
||||
):
|
||||
# Parse SSE line
|
||||
event_type = None
|
||||
data_str = None
|
||||
|
||||
for line in sse_line.strip().split('\n'):
|
||||
if line.startswith('event: '):
|
||||
event_type = line[7:].strip()
|
||||
elif line.startswith('data: '):
|
||||
data_str = line[6:].strip()
|
||||
|
||||
if data_str is None:
|
||||
continue
|
||||
|
||||
# Handle error events
|
||||
if event_type == 'error':
|
||||
try:
|
||||
error_data = json.loads(data_str)
|
||||
yield {
|
||||
"event": "error",
|
||||
"data": {"content": error_data.get("content", "Unknown error")}
|
||||
}
|
||||
except json.JSONDecodeError:
|
||||
yield {
|
||||
"event": "error",
|
||||
"data": {"content": data_str}
|
||||
}
|
||||
return
|
||||
|
||||
# Parse the data
|
||||
try:
|
||||
chunk = json.loads(data_str)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
# Check for error in response
|
||||
if "error" in chunk:
|
||||
error_msg = chunk["error"].get("message", str(chunk["error"]))
|
||||
yield {
|
||||
"event": "error",
|
||||
"data": {"content": f"API Error: {error_msg}"}
|
||||
}
|
||||
return
|
||||
|
||||
# Get delta
|
||||
choices = chunk.get("choices", [])
|
||||
if not choices:
|
||||
continue
|
||||
|
||||
delta = choices[0].get("delta", {})
|
||||
|
||||
# Handle reasoning (thinking)
|
||||
reasoning = delta.get("reasoning_content", "")
|
||||
if reasoning:
|
||||
step_index += 1
|
||||
yield {
|
||||
"event": "process_step",
|
||||
"data": {
|
||||
"step": {
|
||||
"id": f"{self.agent_id}-step-{step_index}",
|
||||
"type": "thinking",
|
||||
"content": reasoning
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Handle content
|
||||
content = delta.get("content", "")
|
||||
if content:
|
||||
step_index += 1
|
||||
full_content += content
|
||||
yield {
|
||||
"event": "process_step",
|
||||
"data": {
|
||||
"step": {
|
||||
"id": f"{self.agent_id}-step-{step_index}",
|
||||
"type": "text",
|
||||
"content": full_content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Final message
|
||||
yield {
|
||||
"event": "done",
|
||||
"data": {
|
||||
"message_id": str(uuid.uuid4()),
|
||||
"agent_id": self.agent_id,
|
||||
"agent_name": self.name,
|
||||
"content": full_content,
|
||||
"token_count": len(full_content) // 4
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Agent {self.name} stream error: {e}")
|
||||
yield {
|
||||
"event": "error",
|
||||
"data": {"content": str(e)}
|
||||
}
|
||||
|
||||
def _build_system_prompt(self, context: Dict = None) -> str:
|
||||
"""Build the final system prompt with context"""
|
||||
prompt = self.system_prompt
|
||||
if context:
|
||||
workspace = context.get("workspace", "")
|
||||
if workspace:
|
||||
prompt += f"\n\nCurrent workspace: {workspace}"
|
||||
user_name = context.get("username", "")
|
||||
if user_name:
|
||||
prompt += f"\nCurrent user: {user_name}"
|
||||
return prompt
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert agent to dictionary"""
|
||||
return {
|
||||
"id": self.agent_id,
|
||||
"name": self.name,
|
||||
"role": self.role,
|
||||
"avatar": self.avatar,
|
||||
"system_prompt": self.system_prompt,
|
||||
"model": self.model,
|
||||
"tools": self.tools,
|
||||
"priority": self.priority,
|
||||
"auto_response": self.auto_response,
|
||||
"mention_trigger": self.mention_trigger,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, agent_db_model) -> "BaseAgent":
|
||||
"""Create agent instance from database model"""
|
||||
import json
|
||||
tools = []
|
||||
if agent_db_model.tools:
|
||||
try:
|
||||
tools = json.loads(agent_db_model.tools)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
return cls(
|
||||
agent_id=agent_db_model.id,
|
||||
name=agent_db_model.name,
|
||||
role=agent_db_model.role,
|
||||
system_prompt=agent_db_model.system_prompt,
|
||||
provider_id=agent_db_model.provider_id,
|
||||
model=agent_db_model.model,
|
||||
tools=tools,
|
||||
temperature=float(agent_db_model.temperature) if agent_db_model.temperature else 0.7,
|
||||
max_tokens=agent_db_model.max_tokens or 2048,
|
||||
priority=agent_db_model.priority or 5,
|
||||
auto_response=agent_db_model.auto_response,
|
||||
mention_trigger=agent_db_model.mention_trigger,
|
||||
avatar=agent_db_model.avatar
|
||||
)
|
||||
|
|
@ -0,0 +1 @@
|
|||
# Builtins package - user-defined agent templates can be placed here
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
"""Agent Registry - manages agent instances in memory"""
|
||||
from typing import Dict, List, Optional
|
||||
import logging
|
||||
|
||||
from luxx.agents.base import BaseAgent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AgentRegistry:
|
||||
"""Registry for managing agent instances"""
|
||||
|
||||
def __init__(self):
|
||||
self._agents: Dict[str, BaseAgent] = {}
|
||||
|
||||
def register(self, agent: BaseAgent) -> None:
|
||||
"""Register an agent instance"""
|
||||
self._agents[agent.agent_id] = agent
|
||||
logger.info(f"Registered agent: {agent.name} ({agent.role})")
|
||||
|
||||
def unregister(self, agent_id: str) -> bool:
|
||||
"""Unregister an agent"""
|
||||
if agent_id in self._agents:
|
||||
agent = self._agents.pop(agent_id)
|
||||
logger.info(f"Unregistered agent: {agent.name}")
|
||||
return True
|
||||
return False
|
||||
|
||||
def get(self, agent_id: str) -> Optional[BaseAgent]:
|
||||
"""Get an agent by ID"""
|
||||
return self._agents.get(agent_id)
|
||||
|
||||
def get_by_role(self, role: str) -> Optional[BaseAgent]:
|
||||
"""Get an agent by role (returns first match)"""
|
||||
for agent in self._agents.values():
|
||||
if agent.role == role:
|
||||
return agent
|
||||
return None
|
||||
|
||||
def list_all(self) -> List[BaseAgent]:
|
||||
"""List all registered agents"""
|
||||
return list(self._agents.values())
|
||||
|
||||
def list_by_role(self, role: str) -> List[BaseAgent]:
|
||||
"""List all agents with a specific role"""
|
||||
return [a for a in self._agents.values() if a.role == role]
|
||||
|
||||
def list_auto_response(self) -> List[BaseAgent]:
|
||||
"""List all agents that auto-respond"""
|
||||
return [a for a in self._agents.values() if a.auto_response and not a.mention_trigger]
|
||||
|
||||
def list_mention_trigger(self) -> List[BaseAgent]:
|
||||
"""List all agents that respond on mention"""
|
||||
return [a for a in self._agents.values() if a.mention_trigger]
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all registered agents"""
|
||||
self._agents.clear()
|
||||
logger.info("Cleared all registered agents")
|
||||
|
||||
|
||||
# Global registry instance
|
||||
agent_registry = AgentRegistry()
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
"""API routes module"""
|
||||
from fastapi import APIRouter
|
||||
|
||||
from luxx.routes import auth, conversations, messages, tools, providers
|
||||
from luxx.api import auth, tools, providers, agents, rooms
|
||||
from luxx.api.chat import conversations, messages
|
||||
|
||||
|
||||
api_router = APIRouter()
|
||||
|
|
@ -12,3 +13,5 @@ api_router.include_router(conversations.router)
|
|||
api_router.include_router(messages.router)
|
||||
api_router.include_router(tools.router)
|
||||
api_router.include_router(providers.router)
|
||||
api_router.include_router(agents.router)
|
||||
api_router.include_router(rooms.router)
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
"""Agent Management API Routes"""
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from luxx.services.agent import agent_manager
|
||||
|
||||
router = APIRouter(prefix="/agents", tags=["agents"])
|
||||
|
||||
|
||||
class CreateAgentRequest(BaseModel):
|
||||
name: str
|
||||
role: str
|
||||
system_prompt: str
|
||||
provider_id: Optional[int] = None
|
||||
model: Optional[str] = None
|
||||
tools: Optional[List[str]] = None
|
||||
priority: int = 5
|
||||
auto_response: bool = True
|
||||
mention_trigger: bool = False
|
||||
temperature: float = 0.7
|
||||
max_tokens: int = 2048
|
||||
avatar: Optional[str] = None
|
||||
|
||||
|
||||
class UpdateAgentRequest(BaseModel):
|
||||
name: Optional[str] = None
|
||||
role: Optional[str] = None
|
||||
system_prompt: Optional[str] = None
|
||||
provider_id: Optional[int] = None
|
||||
model: Optional[str] = None
|
||||
tools: Optional[List[str]] = None
|
||||
priority: Optional[int] = None
|
||||
auto_response: Optional[bool] = None
|
||||
mention_trigger: Optional[bool] = None
|
||||
temperature: Optional[float] = None
|
||||
max_tokens: Optional[int] = None
|
||||
is_active: Optional[bool] = None
|
||||
avatar: Optional[str] = None
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_agents(include_inactive: bool = False):
|
||||
"""List all agents"""
|
||||
agents = agent_manager.list_agents(include_inactive=include_inactive)
|
||||
return {"agents": agents}
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_agent(request: CreateAgentRequest):
|
||||
"""Create a new agent"""
|
||||
agent = agent_manager.create_agent(
|
||||
name=request.name,
|
||||
role=request.role,
|
||||
system_prompt=request.system_prompt,
|
||||
provider_id=request.provider_id,
|
||||
model=request.model,
|
||||
tools=request.tools,
|
||||
priority=request.priority,
|
||||
auto_response=request.auto_response,
|
||||
mention_trigger=request.mention_trigger,
|
||||
temperature=request.temperature,
|
||||
max_tokens=request.max_tokens,
|
||||
avatar=request.avatar
|
||||
)
|
||||
return {"agent": agent}
|
||||
|
||||
|
||||
@router.get("/{agent_id}")
|
||||
async def get_agent(agent_id: str):
|
||||
"""Get an agent by ID"""
|
||||
agent = agent_manager.get_agent(agent_id)
|
||||
if not agent:
|
||||
raise HTTPException(status_code=404, detail="Agent not found")
|
||||
return {"agent": agent}
|
||||
|
||||
|
||||
@router.put("/{agent_id}")
|
||||
async def update_agent(agent_id: str, request: UpdateAgentRequest):
|
||||
"""Update an agent"""
|
||||
agent = agent_manager.update_agent(
|
||||
agent_id=agent_id,
|
||||
name=request.name,
|
||||
role=request.role,
|
||||
system_prompt=request.system_prompt,
|
||||
provider_id=request.provider_id,
|
||||
model=request.model,
|
||||
tools=request.tools,
|
||||
priority=request.priority,
|
||||
auto_response=request.auto_response,
|
||||
mention_trigger=request.mention_trigger,
|
||||
temperature=request.temperature,
|
||||
max_tokens=request.max_tokens,
|
||||
is_active=request.is_active,
|
||||
avatar=request.avatar
|
||||
)
|
||||
if not agent:
|
||||
raise HTTPException(status_code=404, detail="Agent not found")
|
||||
return {"agent": agent}
|
||||
|
||||
|
||||
@router.delete("/{agent_id}")
|
||||
async def delete_agent(agent_id: str):
|
||||
"""Delete an agent"""
|
||||
success = agent_manager.delete_agent(agent_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Agent not found")
|
||||
return {"success": True}
|
||||
|
|
@ -5,8 +5,8 @@ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
|||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
|
||||
from luxx.database import get_db
|
||||
from luxx.models import User
|
||||
from luxx.core.database import get_db
|
||||
from luxx.models.user import User
|
||||
from luxx.utils.helpers import (
|
||||
hash_password,
|
||||
verify_password,
|
||||
|
|
@ -23,20 +23,17 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
|
|||
|
||||
|
||||
class UserRegister(BaseModel):
|
||||
"""User registration model"""
|
||||
username: str
|
||||
email: str | None = None
|
||||
password: str
|
||||
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
"""User login model"""
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
"""User response model"""
|
||||
id: int
|
||||
username: str
|
||||
email: str | None
|
||||
|
|
@ -45,12 +42,10 @@ class UserResponse(BaseModel):
|
|||
|
||||
|
||||
class UserPermissionUpdate(BaseModel):
|
||||
"""User permission update model"""
|
||||
permission_level: int
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
"""Token response model"""
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
|
|
@ -97,6 +92,7 @@ def register(user_data: UserRegister, db: Session = Depends(get_db)):
|
|||
email=user_data.email,
|
||||
password_hash=password_hash
|
||||
)
|
||||
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
|
@ -135,7 +131,7 @@ def login(user_data: UserLogin, db: Session = Depends(get_db)):
|
|||
|
||||
@router.post("/logout")
|
||||
def logout(current_user: User = Depends(get_current_user)):
|
||||
"""User logout (client should delete token)"""
|
||||
"""User logout"""
|
||||
return success_response(message="Logout successful")
|
||||
|
||||
|
||||
|
|
@ -159,7 +155,6 @@ def update_user(user_id: int, data: UserPermissionUpdate, admin_user: User = Dep
|
|||
if not user:
|
||||
return error_response("User not found", 404)
|
||||
|
||||
# Validate permission level
|
||||
if data.permission_level < 1 or data.permission_level > 4:
|
||||
return error_response("Invalid permission level (1-4)", 400)
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
"""Chat API package"""
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
"""Conversation routes"""
|
||||
from typing import Optional, List
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from luxx.database import get_db
|
||||
from luxx.models import Conversation, Message, User
|
||||
from luxx.routes.auth import get_current_user
|
||||
from luxx.core.database import get_db
|
||||
from luxx.models.chat import Conversation, Message
|
||||
from luxx.models.user import User
|
||||
from luxx.api.auth import get_current_user
|
||||
from luxx.utils.helpers import generate_id, success_response, error_response, paginate
|
||||
|
||||
|
||||
|
|
@ -14,7 +15,6 @@ router = APIRouter(prefix="/conversations", tags=["Conversations"])
|
|||
|
||||
|
||||
class ConversationCreate(BaseModel):
|
||||
"""Create conversation model"""
|
||||
project_id: Optional[str] = None
|
||||
provider_id: Optional[int] = None
|
||||
title: Optional[str] = None
|
||||
|
|
@ -26,7 +26,6 @@ class ConversationCreate(BaseModel):
|
|||
|
||||
|
||||
class ConversationUpdate(BaseModel):
|
||||
"""Update conversation model"""
|
||||
title: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
system_prompt: Optional[str] = None
|
||||
|
|
@ -50,7 +49,6 @@ def list_conversations(
|
|||
items = []
|
||||
for c in result["items"]:
|
||||
conv_dict = c.to_dict()
|
||||
# Get first user message for fallback title
|
||||
first_msg = db.query(Message).filter(
|
||||
Message.conversation_id == c.id,
|
||||
Message.role == 'user'
|
||||
|
|
@ -58,7 +56,6 @@ def list_conversations(
|
|||
if first_msg:
|
||||
conv_dict['first_message'] = first_msg.content[:50] + ('...' if len(first_msg.content) > 50 else '')
|
||||
|
||||
# Calculate total tokens from all assistant messages in this conversation
|
||||
assistant_messages = db.query(Message).filter(
|
||||
Message.conversation_id == c.id,
|
||||
Message.role == 'assistant'
|
||||
|
|
@ -66,7 +63,6 @@ def list_conversations(
|
|||
total_tokens = 0
|
||||
for msg in assistant_messages:
|
||||
total_tokens += msg.token_count or 0
|
||||
# Also try to get usage from the usage field
|
||||
if msg.usage:
|
||||
try:
|
||||
usage_obj = json.loads(msg.usage)
|
||||
|
|
@ -91,14 +87,12 @@ def create_conversation(
|
|||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create conversation"""
|
||||
from luxx.models import LLMProvider
|
||||
from luxx.models.user import LLMProvider
|
||||
|
||||
# Get provider info - use default provider if not specified
|
||||
provider_id = data.provider_id
|
||||
model = data.model
|
||||
|
||||
if not provider_id:
|
||||
# Find default provider
|
||||
default_provider = db.query(LLMProvider).filter(
|
||||
LLMProvider.user_id == current_user.id,
|
||||
LLMProvider.is_default == True
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
"""Message routes"""
|
||||
import json
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, Response
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
|
||||
from luxx.database import get_db
|
||||
from luxx.models import Conversation, Message, User
|
||||
from luxx.routes.auth import get_current_user
|
||||
from luxx.core.database import get_db
|
||||
from luxx.models.chat import Conversation, Message
|
||||
from luxx.models.user import User
|
||||
from luxx.api.auth import get_current_user
|
||||
from luxx.services.chat import chat_service
|
||||
from luxx.utils.helpers import generate_id, success_response, error_response
|
||||
|
||||
|
|
@ -18,15 +18,13 @@ router = APIRouter(prefix="/messages", tags=["Messages"])
|
|||
|
||||
|
||||
class MessageCreate(BaseModel):
|
||||
"""Create message model"""
|
||||
conversation_id: str
|
||||
content: str
|
||||
thinking_enabled: bool = False
|
||||
enabled_tools: List[str] = [] # 启用的工具名称列表
|
||||
enabled_tools: List[str] = []
|
||||
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
"""Message response model"""
|
||||
id: str
|
||||
role: str
|
||||
content: str
|
||||
|
|
@ -81,6 +79,7 @@ def send_message(
|
|||
content=data.content,
|
||||
token_count=len(data.content) // 4
|
||||
)
|
||||
|
||||
db.add(user_message)
|
||||
|
||||
conversation.updated_at = datetime.now()
|
||||
|
|
@ -103,6 +102,7 @@ def send_message(
|
|||
content=ai_content,
|
||||
token_count=len(ai_content) // 4
|
||||
)
|
||||
|
||||
db.add(ai_message)
|
||||
db.commit()
|
||||
|
||||
|
|
@ -134,6 +134,7 @@ async def stream_message(
|
|||
content=data.content,
|
||||
token_count=len(data.content) // 4
|
||||
)
|
||||
|
||||
db.add(user_message)
|
||||
conversation.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
|
@ -151,7 +152,6 @@ async def stream_message(
|
|||
workspace=workspace,
|
||||
user_permission_level=current_user.permission_level
|
||||
):
|
||||
# Chat service returns raw SSE strings (including done event)
|
||||
yield sse_str
|
||||
|
||||
return StreamingResponse(
|
||||
|
|
@ -3,9 +3,9 @@ from typing import Optional
|
|||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from luxx.database import get_db, SessionLocal
|
||||
from luxx.models import User, LLMProvider
|
||||
from luxx.routes.auth import get_current_user
|
||||
from luxx.core.database import SessionLocal
|
||||
from luxx.models.user import User, LLMProvider
|
||||
from luxx.api.auth import get_current_user
|
||||
from luxx.utils.helpers import success_response
|
||||
import httpx
|
||||
import asyncio
|
||||
|
|
@ -62,7 +62,6 @@ def create_provider(
|
|||
"""Create a new LLM provider"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
|
||||
db_provider = LLMProvider(
|
||||
user_id=current_user.id,
|
||||
name=provider.name,
|
||||
|
|
@ -122,16 +121,13 @@ def update_provider(
|
|||
if not provider:
|
||||
raise HTTPException(status_code=404, detail="Provider not found")
|
||||
|
||||
# If setting as default, unset others
|
||||
if update.is_default:
|
||||
db.query(LLMProvider).filter(
|
||||
LLMProvider.user_id == current_user.id,
|
||||
LLMProvider.id != provider_id
|
||||
).update({"is_default": False})
|
||||
|
||||
# Update fields
|
||||
update_data = update.dict(exclude_unset=True)
|
||||
# Keep existing API key if the new one is empty
|
||||
if update_data.get('api_key') == '':
|
||||
update_data.pop('api_key')
|
||||
for key, value in update_data.items():
|
||||
|
|
@ -180,7 +176,6 @@ def test_provider(
|
|||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Test provider connection"""
|
||||
|
||||
try:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
|
|
@ -192,7 +187,6 @@ def test_provider(
|
|||
if not provider:
|
||||
return {"success": False, "message": "Provider not found"}
|
||||
|
||||
# Test the connection
|
||||
async def test():
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.post(
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
"""Chat Room API Routes"""
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from luxx.services.room import chat_room_service
|
||||
|
||||
router = APIRouter(prefix="/chat-rooms", tags=["chat-rooms"])
|
||||
|
||||
|
||||
class CreateChatRoomRequest(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
agent_ids: Optional[List[str]] = None
|
||||
|
||||
|
||||
class UpdateChatRoomRequest(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class SendMessageRequest(BaseModel):
|
||||
content: str
|
||||
|
||||
|
||||
class AddAgentRequest(BaseModel):
|
||||
agent_id: str
|
||||
|
||||
|
||||
def get_current_user_id() -> int:
|
||||
"""Get current user ID from auth context"""
|
||||
return 1
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_chat_rooms():
|
||||
"""List all chat rooms"""
|
||||
user_id = get_current_user_id()
|
||||
rooms = chat_room_service.list_rooms(user_id=user_id)
|
||||
return {"rooms": rooms}
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_chat_room(request: CreateChatRoomRequest):
|
||||
"""Create a new chat room"""
|
||||
user_id = get_current_user_id()
|
||||
room = chat_room_service.create_room(
|
||||
name=request.name,
|
||||
owner_id=user_id,
|
||||
description=request.description,
|
||||
agent_ids=request.agent_ids
|
||||
)
|
||||
return {"room": room}
|
||||
|
||||
|
||||
@router.get("/{room_id}")
|
||||
async def get_chat_room(room_id: str):
|
||||
"""Get a chat room by ID"""
|
||||
room = chat_room_service.get_room(room_id)
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail="Chat room not found")
|
||||
return {"room": room.to_dict(include_agents=True)}
|
||||
|
||||
|
||||
@router.put("/{room_id}")
|
||||
async def update_chat_room(room_id: str, request: UpdateChatRoomRequest):
|
||||
"""Update a chat room"""
|
||||
room = chat_room_service.update_room(
|
||||
room_id=room_id,
|
||||
name=request.name,
|
||||
description=request.description,
|
||||
is_active=request.is_active
|
||||
)
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail="Chat room not found")
|
||||
return {"room": room}
|
||||
|
||||
|
||||
@router.delete("/{room_id}")
|
||||
async def delete_chat_room(room_id: str):
|
||||
"""Delete a chat room"""
|
||||
success = chat_room_service.delete_room(room_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Chat room not found")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.get("/{room_id}/agents")
|
||||
async def get_room_agents(room_id: str):
|
||||
"""Get all agents in a chat room"""
|
||||
agents = chat_room_service.get_room_agents(room_id)
|
||||
return {"agents": [a.to_dict() for a in agents]}
|
||||
|
||||
|
||||
@router.post("/{room_id}/agents")
|
||||
async def add_agent_to_room(room_id: str, request: AddAgentRequest):
|
||||
"""Add an agent to a chat room"""
|
||||
success = chat_room_service.add_agent_to_room(room_id, request.agent_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="Failed to add agent")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.delete("/{room_id}/agents/{agent_id}")
|
||||
async def remove_agent_from_room(room_id: str, agent_id: str):
|
||||
"""Remove an agent from a chat room"""
|
||||
success = chat_room_service.remove_agent_from_room(room_id, agent_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Agent not found in room")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.get("/{room_id}/messages")
|
||||
async def get_room_messages(room_id: str, limit: int = 50, before_id: str = None):
|
||||
"""Get messages from a chat room"""
|
||||
messages = chat_room_service.get_messages(room_id, limit=limit, before_id=before_id)
|
||||
return {"messages": messages}
|
||||
|
||||
|
||||
@router.post("/{room_id}/messages")
|
||||
async def send_message(room_id: str, request: SendMessageRequest):
|
||||
"""Send a message to a chat room. Returns a streaming response via SSE."""
|
||||
from fastapi.responses import StreamingResponse
|
||||
import json
|
||||
|
||||
user_id = str(get_current_user_id())
|
||||
user_name = "User"
|
||||
|
||||
async def generate():
|
||||
async for event in chat_room_service.process_message(
|
||||
room_id=room_id,
|
||||
user_message=request.content,
|
||||
user_id=user_id,
|
||||
user_name=user_name
|
||||
):
|
||||
if event.get("event") in ["process_step", "done"]:
|
||||
chat_room_service.save_message(
|
||||
room_id=room_id,
|
||||
sender_type="user",
|
||||
sender_id=user_id,
|
||||
sender_name=user_name,
|
||||
content=request.content
|
||||
)
|
||||
|
||||
yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
generate(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no"
|
||||
}
|
||||
)
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
"""Tool routes"""
|
||||
from typing import Optional, List, Dict, Any
|
||||
from typing import Optional, Dict, Any
|
||||
from fastapi import APIRouter, Depends, Body
|
||||
from pydantic import BaseModel
|
||||
|
||||
from luxx.database import get_db
|
||||
from luxx.models import User
|
||||
from luxx.routes.auth import get_current_user
|
||||
from luxx.models.user import User
|
||||
from luxx.api.auth import get_current_user
|
||||
from luxx.tools.core import registry
|
||||
from luxx.utils.helpers import success_response
|
||||
|
||||
|
|
@ -19,12 +18,10 @@ def list_tools(
|
|||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get available tools list"""
|
||||
# Get tool definitions directly from registry to access category
|
||||
|
||||
if category:
|
||||
all_tools = [t for t in registry._tools.values() if t.category == category]
|
||||
tools = [t.to_openai_format() for t in all_tools]
|
||||
categorized_tools = [t for t in registry._tools.values() if t.category == category]
|
||||
categorized_tools = all_tools
|
||||
else:
|
||||
all_tools = list(registry._tools.values())
|
||||
tools = registry.list_all()
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
"""Core package - configuration and database"""
|
||||
from luxx.core.config import config
|
||||
from luxx.core.database import Base, SessionLocal, get_db, init_db, engine
|
||||
|
||||
__all__ = ["config", "Base", "SessionLocal", "get_db", "init_db", "engine"]
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
"""Database connection module"""
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, declarative_base, Mapped
|
||||
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||
from typing import Generator
|
||||
|
||||
from luxx.config import config
|
||||
from luxx.core.config import config
|
||||
|
||||
|
||||
# Create database engine
|
||||
|
|
@ -13,9 +13,11 @@ engine = create_engine(
|
|||
echo=False
|
||||
)
|
||||
|
||||
|
||||
# Create session factory
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
|
||||
# Create base class
|
||||
Base = declarative_base()
|
||||
|
||||
223
luxx/models.py
223
luxx/models.py
|
|
@ -1,223 +0,0 @@
|
|||
"""ORM model definitions"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import String, Integer, Boolean, Float, Text, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from luxx.database import Base
|
||||
|
||||
|
||||
def local_now():
|
||||
return datetime.now()
|
||||
|
||||
|
||||
class LLMProvider(Base):
|
||||
"""LLM Provider configuration model"""
|
||||
__tablename__ = "llm_providers"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
provider_type: Mapped[str] = mapped_column(String(50), nullable=False, default="openai") # openai, deepseek, glm, etc.
|
||||
base_url: Mapped[str] = mapped_column(String(500), nullable=False)
|
||||
api_key: Mapped[str] = mapped_column(String(500), nullable=False)
|
||||
default_model: Mapped[str] = mapped_column(String(100), nullable=False, default="gpt-4")
|
||||
max_tokens: Mapped[int] = mapped_column(Integer, default=8192) # 默认 8192
|
||||
is_default: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=local_now, onupdate=local_now)
|
||||
|
||||
# Relationships
|
||||
user: Mapped["User"] = relationship("User", backref="llm_providers")
|
||||
|
||||
def to_dict(self, include_key: bool = False):
|
||||
"""Convert to dictionary, optionally include API key"""
|
||||
result = {
|
||||
"id": self.id,
|
||||
"user_id": self.user_id,
|
||||
"name": self.name,
|
||||
"provider_type": self.provider_type,
|
||||
"base_url": self.base_url,
|
||||
"default_model": self.default_model,
|
||||
"max_tokens": self.max_tokens,
|
||||
"is_default": self.is_default,
|
||||
"enabled": self.enabled,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
if include_key:
|
||||
result["api_key"] = self.api_key
|
||||
return result
|
||||
|
||||
|
||||
class Project(Base):
|
||||
"""Project model"""
|
||||
__tablename__ = "projects"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=local_now, onupdate=local_now)
|
||||
|
||||
# Relationships
|
||||
user: Mapped["User"] = relationship("User", backref="projects")
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""User model"""
|
||||
__tablename__ = "users"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
|
||||
email: Mapped[Optional[str]] = mapped_column(String(120), unique=True, nullable=True)
|
||||
password_hash: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
role: Mapped[str] = mapped_column(String(20), default="user")
|
||||
permission_level: Mapped[int] = mapped_column(Integer, default=1) # 1=READ_ONLY, 2=WRITE, 3=EXECUTE, 4=ADMIN
|
||||
workspace_path: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) # 用户工作空间路径
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
||||
|
||||
# Relationships
|
||||
conversations: Mapped[List["Conversation"]] = relationship(
|
||||
"Conversation", back_populates="user", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"username": self.username,
|
||||
"email": self.email,
|
||||
"role": self.role,
|
||||
"permission_level": self.permission_level,
|
||||
"workspace_path": self.workspace_path,
|
||||
"is_active": self.is_active,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
|
||||
class Conversation(Base):
|
||||
"""Conversation model"""
|
||||
__tablename__ = "conversations"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
provider_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("llm_providers.id"), nullable=True)
|
||||
project_id: Mapped[Optional[str]] = mapped_column(String(64), ForeignKey("projects.id"), nullable=True)
|
||||
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
model: Mapped[str] = mapped_column(String(64), nullable=False, default="deepseek-chat")
|
||||
system_prompt: Mapped[str] = mapped_column(Text, nullable=False, default="You are a helpful assistant.")
|
||||
temperature: Mapped[float] = mapped_column(Float, default=0.7)
|
||||
max_tokens: Mapped[int] = mapped_column(Integer, default=2000)
|
||||
thinking_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=local_now, onupdate=local_now)
|
||||
|
||||
# Relationships
|
||||
user: Mapped["User"] = relationship("User", back_populates="conversations")
|
||||
provider: Mapped[Optional["LLMProvider"]] = relationship("LLMProvider")
|
||||
messages: Mapped[List["Message"]] = relationship(
|
||||
"Message", back_populates="conversation", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"user_id": self.user_id,
|
||||
"provider_id": self.provider_id,
|
||||
"project_id": self.project_id,
|
||||
"title": self.title,
|
||||
"model": self.model,
|
||||
"system_prompt": self.system_prompt,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
"thinking_enabled": self.thinking_enabled,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
|
||||
class Message(Base):
|
||||
"""Message model
|
||||
|
||||
content 字段统一使用 JSON 格式存储:
|
||||
|
||||
**User 消息:**
|
||||
{
|
||||
"text": "用户输入的文本内容",
|
||||
"attachments": [
|
||||
{"name": "utils.py", "extension": "py", "content": "..."}
|
||||
]
|
||||
}
|
||||
|
||||
**Assistant 消息:**
|
||||
{
|
||||
"text": "AI 回复的文本内容",
|
||||
"tool_calls": [...], // 遗留的扁平结构
|
||||
"steps": [ // 有序步骤,用于渲染(主要数据源)
|
||||
{"id": "step-0", "index": 0, "type": "thinking", "content": "..."},
|
||||
{"id": "step-1", "index": 1, "type": "text", "content": "..."},
|
||||
{"id": "step-2", "index": 2, "type": "tool_call", "id_ref": "call_xxx", "name": "...", "arguments": "..."},
|
||||
{"id": "step-3", "index": 3, "type": "tool_result", "id_ref": "call_xxx", "name": "...", "content": "..."}
|
||||
]
|
||||
}
|
||||
"""
|
||||
__tablename__ = "messages"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
conversation_id: Mapped[str] = mapped_column(String(64), ForeignKey("conversations.id"), nullable=False)
|
||||
role: Mapped[str] = mapped_column(String(16), nullable=False) # user, assistant, system, tool
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
||||
token_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||
usage: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # JSON string for usage info
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
||||
|
||||
# Relationships
|
||||
conversation: Mapped["Conversation"] = relationship("Conversation", back_populates="messages")
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert to dictionary, extracting process_steps for frontend"""
|
||||
import json
|
||||
|
||||
result = {
|
||||
"id": self.id,
|
||||
"conversation_id": self.conversation_id,
|
||||
"role": self.role,
|
||||
"token_count": self.token_count,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
# Parse usage JSON
|
||||
if self.usage:
|
||||
try:
|
||||
result["usage"] = json.loads(self.usage)
|
||||
except json.JSONDecodeError:
|
||||
result["usage"] = None
|
||||
|
||||
# Parse content JSON
|
||||
try:
|
||||
content_obj = json.loads(self.content) if self.content else {}
|
||||
except json.JSONDecodeError:
|
||||
# Legacy plain text content
|
||||
result["content"] = self.content
|
||||
result["text"] = self.content
|
||||
result["attachments"] = []
|
||||
result["tool_calls"] = []
|
||||
result["process_steps"] = []
|
||||
return result
|
||||
|
||||
# Extract common fields
|
||||
result["text"] = content_obj.get("text", "")
|
||||
result["attachments"] = content_obj.get("attachments", [])
|
||||
result["tool_calls"] = content_obj.get("tool_calls", [])
|
||||
|
||||
# Extract steps as process_steps for frontend rendering
|
||||
result["process_steps"] = content_obj.get("steps", [])
|
||||
|
||||
# For backward compatibility
|
||||
if "content" not in result:
|
||||
result["content"] = result["text"]
|
||||
|
||||
return result
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
"""Models package"""
|
||||
from luxx.models.user import User, LLMProvider, Project
|
||||
from luxx.models.chat import Conversation, Message
|
||||
from luxx.models.room import ChatRoom, Agent, ChatRoomAgent, ChatRoomMessage
|
||||
|
||||
__all__ = [
|
||||
"User", "LLMProvider", "Project",
|
||||
"Conversation", "Message",
|
||||
"ChatRoom", "Agent", "ChatRoomAgent", "ChatRoomMessage"
|
||||
]
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
"""Chat-related models"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import String, Integer, Boolean, Float, Text, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from luxx.core.database import Base
|
||||
|
||||
|
||||
def local_now():
|
||||
return datetime.now()
|
||||
|
||||
|
||||
class Conversation(Base):
|
||||
"""Conversation model"""
|
||||
__tablename__ = "conversations"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
provider_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("llm_providers.id"), nullable=True)
|
||||
project_id: Mapped[Optional[str]] = mapped_column(String(64), ForeignKey("projects.id"), nullable=True)
|
||||
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
model: Mapped[str] = mapped_column(String(64), nullable=False, default="deepseek-chat")
|
||||
system_prompt: Mapped[str] = mapped_column(Text, nullable=False, default="You are a helpful assistant.")
|
||||
temperature: Mapped[float] = mapped_column(Float, default=0.7)
|
||||
max_tokens: Mapped[int] = mapped_column(Integer, default=2000)
|
||||
thinking_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=local_now, onupdate=local_now)
|
||||
|
||||
# Relationships
|
||||
user: Mapped["User"] = relationship("User", back_populates="conversations")
|
||||
provider: Mapped[Optional["LLMProvider"]] = relationship("LLMProvider")
|
||||
messages: Mapped[List["Message"]] = relationship(
|
||||
"Message", back_populates="conversation", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"user_id": self.user_id,
|
||||
"provider_id": self.provider_id,
|
||||
"project_id": self.project_id,
|
||||
"title": self.title,
|
||||
"model": self.model,
|
||||
"system_prompt": self.system_prompt,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
"thinking_enabled": self.thinking_enabled,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
|
||||
class Message(Base):
|
||||
"""Message model.
|
||||
|
||||
content 字段统一使用 JSON 格式存储:
|
||||
"""
|
||||
__tablename__ = "messages"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
conversation_id: Mapped[str] = mapped_column(String(64), ForeignKey("conversations.id"), nullable=False)
|
||||
role: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
||||
token_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||
usage: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
||||
|
||||
# Relationships
|
||||
conversation: Mapped["Conversation"] = relationship("Conversation", back_populates="messages")
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert to dictionary, extracting process_steps for frontend"""
|
||||
import json
|
||||
|
||||
result = {
|
||||
"id": self.id,
|
||||
"conversation_id": self.conversation_id,
|
||||
"role": self.role,
|
||||
"token_count": self.token_count,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
# Parse usage JSON
|
||||
if self.usage:
|
||||
try:
|
||||
result["usage"] = json.loads(self.usage)
|
||||
except json.JSONDecodeError:
|
||||
result["usage"] = None
|
||||
|
||||
# Parse content JSON
|
||||
try:
|
||||
content_obj = json.loads(self.content) if self.content else {}
|
||||
except json.JSONDecodeError:
|
||||
result["content"] = self.content
|
||||
result["text"] = self.content
|
||||
result["attachments"] = []
|
||||
result["tool_calls"] = []
|
||||
result["process_steps"] = []
|
||||
return result
|
||||
|
||||
result["text"] = content_obj.get("text", "")
|
||||
result["attachments"] = content_obj.get("attachments", [])
|
||||
result["tool_calls"] = content_obj.get("tool_calls", [])
|
||||
result["process_steps"] = content_obj.get("steps", [])
|
||||
|
||||
if "content" not in result:
|
||||
result["content"] = result["text"]
|
||||
|
||||
return result
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
"""ChatRoom models"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import String, Integer, Boolean, Text, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from luxx.core.database import Base
|
||||
|
||||
|
||||
def local_now():
|
||||
return datetime.now()
|
||||
|
||||
|
||||
class ChatRoom(Base):
|
||||
"""Chat Room model - like a group chat for multiple agents"""
|
||||
__tablename__ = "chat_rooms"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
owner_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=local_now, onupdate=local_now)
|
||||
|
||||
# Relationships
|
||||
owner: Mapped["User"] = relationship("User", backref="chat_rooms")
|
||||
agents: Mapped[List["ChatRoomAgent"]] = relationship(
|
||||
"ChatRoomAgent", back_populates="chat_room", cascade="all, delete-orphan"
|
||||
)
|
||||
messages: Mapped[List["ChatRoomMessage"]] = relationship(
|
||||
"ChatRoomMessage", back_populates="chat_room", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def to_dict(self, include_agents: bool = False):
|
||||
result = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"owner_id": self.owner_id,
|
||||
"is_active": self.is_active,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
if include_agents and self.agents:
|
||||
result["agents"] = [ca.to_dict() for ca in self.agents]
|
||||
return result
|
||||
|
||||
|
||||
class Agent(Base):
|
||||
"""Agent model - defines an AI agent with specific role"""
|
||||
__tablename__ = "agents"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
role: Mapped[str] = mapped_column(String(50), nullable=False, default="helper")
|
||||
avatar: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
|
||||
system_prompt: Mapped[str] = mapped_column(Text, nullable=False, default="You are a helpful assistant.")
|
||||
provider_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("llm_providers.id"), nullable=True)
|
||||
model: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||
tools: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
priority: Mapped[int] = mapped_column(Integer, default=5)
|
||||
auto_response: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
mention_trigger: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
temperature: Mapped[float] = mapped_column(Text, default="0.7")
|
||||
max_tokens: Mapped[int] = mapped_column(Integer, default=2048)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=local_now, onupdate=local_now)
|
||||
|
||||
# Relationships
|
||||
provider: Mapped[Optional["LLMProvider"]] = relationship("LLMProvider")
|
||||
chat_rooms: Mapped[List["ChatRoomAgent"]] = relationship(
|
||||
"ChatRoomAgent", back_populates="agent", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def to_dict(self, include_secrets: bool = False):
|
||||
import json
|
||||
result = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"role": self.role,
|
||||
"avatar": self.avatar,
|
||||
"system_prompt": self.system_prompt,
|
||||
"provider_id": self.provider_id,
|
||||
"model": self.model,
|
||||
"is_active": self.is_active,
|
||||
"priority": self.priority,
|
||||
"auto_response": self.auto_response,
|
||||
"mention_trigger": self.mention_trigger,
|
||||
"temperature": float(self.temperature) if self.temperature else 0.7,
|
||||
"max_tokens": self.max_tokens,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
if self.tools:
|
||||
try:
|
||||
result["tools"] = json.loads(self.tools)
|
||||
except json.JSONDecodeError:
|
||||
result["tools"] = []
|
||||
else:
|
||||
result["tools"] = []
|
||||
|
||||
if include_secrets and self.provider:
|
||||
result["provider"] = self.provider.to_dict(include_key=True)
|
||||
return result
|
||||
|
||||
|
||||
class ChatRoomAgent(Base):
|
||||
"""Association table for ChatRoom and Agent"""
|
||||
__tablename__ = "chat_room_agents"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
chat_room_id: Mapped[str] = mapped_column(String(64), ForeignKey("chat_rooms.id"), nullable=False)
|
||||
agent_id: Mapped[str] = mapped_column(String(64), ForeignKey("agents.id"), nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
joined_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
||||
|
||||
# Relationships
|
||||
chat_room: Mapped["ChatRoom"] = relationship("ChatRoom", back_populates="agents")
|
||||
agent: Mapped["Agent"] = relationship("Agent", back_populates="chat_rooms")
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"chat_room_id": self.chat_room_id,
|
||||
"agent_id": self.agent_id,
|
||||
"is_active": self.is_active,
|
||||
"joined_at": self.joined_at.isoformat() if self.joined_at else None,
|
||||
"agent": self.agent.to_dict() if self.agent else None
|
||||
}
|
||||
|
||||
|
||||
class ChatRoomMessage(Base):
|
||||
"""Chat Room Message model"""
|
||||
__tablename__ = "chat_room_messages"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
room_id: Mapped[str] = mapped_column(String(64), ForeignKey("chat_rooms.id"), nullable=False)
|
||||
sender_type: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
sender_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
sender_name: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
||||
mentions: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
parent_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
|
||||
token_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
||||
|
||||
# Relationships
|
||||
chat_room: Mapped["ChatRoom"] = relationship("ChatRoom", back_populates="messages")
|
||||
|
||||
def to_dict(self):
|
||||
import json
|
||||
result = {
|
||||
"id": self.id,
|
||||
"room_id": self.room_id,
|
||||
"sender_type": self.sender_type,
|
||||
"sender_id": self.sender_id,
|
||||
"sender_name": self.sender_name,
|
||||
"content": self.content,
|
||||
"parent_id": self.parent_id,
|
||||
"token_count": self.token_count,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
if self.mentions:
|
||||
try:
|
||||
result["mentions"] = json.loads(self.mentions)
|
||||
except json.JSONDecodeError:
|
||||
result["mentions"] = []
|
||||
else:
|
||||
result["mentions"] = []
|
||||
return result
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
"""User-related models"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import String, Integer, Boolean, Text, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from luxx.core.database import Base
|
||||
|
||||
|
||||
def local_now():
|
||||
return datetime.now()
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""User model"""
|
||||
__tablename__ = "users"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
|
||||
email: Mapped[Optional[str]] = mapped_column(String(120), unique=True, nullable=True)
|
||||
password_hash: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
role: Mapped[str] = mapped_column(String(20), default="user")
|
||||
permission_level: Mapped[int] = mapped_column(Integer, default=1)
|
||||
workspace_path: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
||||
|
||||
# Relationships
|
||||
conversations: Mapped[List["Conversation"]] = relationship(
|
||||
"Conversation", back_populates="user", cascade="all, delete-orphan"
|
||||
)
|
||||
llm_providers: Mapped[List["LLMProvider"]] = relationship("LLMProvider", back_populates="user")
|
||||
projects: Mapped[List["Project"]] = relationship("Project", back_populates="user")
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"username": self.username,
|
||||
"email": self.email,
|
||||
"role": self.role,
|
||||
"permission_level": self.permission_level,
|
||||
"workspace_path": self.workspace_path,
|
||||
"is_active": self.is_active,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
|
||||
class LLMProvider(Base):
|
||||
"""LLM Provider configuration model"""
|
||||
__tablename__ = "llm_providers"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
provider_type: Mapped[str] = mapped_column(String(50), nullable=False, default="openai")
|
||||
base_url: Mapped[str] = mapped_column(String(500), nullable=False)
|
||||
api_key: Mapped[str] = mapped_column(String(500), nullable=False)
|
||||
default_model: Mapped[str] = mapped_column(String(100), nullable=False, default="gpt-4")
|
||||
max_tokens: Mapped[int] = mapped_column(Integer, default=8192)
|
||||
is_default: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=local_now, onupdate=local_now)
|
||||
|
||||
# Relationships
|
||||
user: Mapped["User"] = relationship("User", back_populates="llm_providers")
|
||||
|
||||
def to_dict(self, include_key: bool = False):
|
||||
result = {
|
||||
"id": self.id,
|
||||
"user_id": self.user_id,
|
||||
"name": self.name,
|
||||
"provider_type": self.provider_type,
|
||||
"base_url": self.base_url,
|
||||
"default_model": self.default_model,
|
||||
"max_tokens": self.max_tokens,
|
||||
"is_default": self.is_default,
|
||||
"enabled": self.enabled,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
if include_key:
|
||||
result["api_key"] = self.api_key
|
||||
return result
|
||||
|
||||
|
||||
class Project(Base):
|
||||
"""Project model"""
|
||||
__tablename__ = "projects"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=local_now)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=local_now, onupdate=local_now)
|
||||
|
||||
# Relationships
|
||||
user: Mapped["User"] = relationship("User", back_populates="projects")
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
"""Services module"""
|
||||
from luxx.services.llm_client import LLMClient, llm_client, LLMResponse
|
||||
from luxx.services.chat import ChatService, chat_service
|
||||
"""Services package"""
|
||||
from luxx.services.chat import chat_service
|
||||
from luxx.services.room import chat_room_service
|
||||
from luxx.services.agent import agent_manager
|
||||
|
||||
__all__ = ["chat_service", "chat_room_service", "agent_manager"]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
"""Agent Manager Service"""
|
||||
import uuid
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
from luxx.core.database import SessionLocal
|
||||
from luxx.models.room import Agent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AgentManager:
|
||||
"""Service for managing agents"""
|
||||
|
||||
def get_agent(self, agent_id: str) -> Optional[Dict]:
|
||||
"""Get an agent by ID"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
agent = db.query(Agent).filter(Agent.id == agent_id).first()
|
||||
return agent.to_dict() if agent else None
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def list_agents(self, include_inactive: bool = False) -> List[Dict]:
|
||||
"""List all agents"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
query = db.query(Agent)
|
||||
if not include_inactive:
|
||||
query = query.filter(Agent.is_active == True)
|
||||
agents = query.order_by(Agent.priority).all()
|
||||
return [a.to_dict() for a in agents]
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def create_agent(self, name: str, role: str, system_prompt: str, provider_id: int = None, model: str = None,
|
||||
tools: List[str] = None, priority: int = 5, auto_response: bool = True, mention_trigger: bool = False,
|
||||
temperature: float = 0.7, max_tokens: int = 2048, avatar: str = None) -> Dict:
|
||||
"""Create a new agent"""
|
||||
if not system_prompt:
|
||||
system_prompt = "You are a helpful assistant."
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
agent = Agent(
|
||||
id=str(uuid.uuid4()),
|
||||
name=name,
|
||||
role=role,
|
||||
system_prompt=system_prompt,
|
||||
provider_id=provider_id,
|
||||
model=model,
|
||||
tools=json.dumps(tools) if tools else None,
|
||||
priority=priority,
|
||||
auto_response=auto_response,
|
||||
mention_trigger=mention_trigger,
|
||||
temperature=str(temperature),
|
||||
max_tokens=max_tokens,
|
||||
avatar=avatar
|
||||
)
|
||||
db.add(agent)
|
||||
db.commit()
|
||||
return agent.to_dict()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def update_agent(self, agent_id: str, name: str = None, role: str = None, system_prompt: str = None,
|
||||
provider_id: int = None, model: str = None, tools: List[str] = None, priority: int = None,
|
||||
auto_response: bool = None, mention_trigger: bool = None, temperature: float = None,
|
||||
max_tokens: int = None, is_active: bool = None, avatar: str = None) -> Optional[Dict]:
|
||||
"""Update an agent"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
agent = db.query(Agent).filter(Agent.id == agent_id).first()
|
||||
if not agent:
|
||||
return None
|
||||
|
||||
if name is not None:
|
||||
agent.name = name
|
||||
if role is not None:
|
||||
agent.role = role
|
||||
if system_prompt is not None:
|
||||
agent.system_prompt = system_prompt
|
||||
if provider_id is not None:
|
||||
agent.provider_id = provider_id
|
||||
if model is not None:
|
||||
agent.model = model
|
||||
if tools is not None:
|
||||
agent.tools = json.dumps(tools)
|
||||
if priority is not None:
|
||||
agent.priority = priority
|
||||
if auto_response is not None:
|
||||
agent.auto_response = auto_response
|
||||
if mention_trigger is not None:
|
||||
agent.mention_trigger = mention_trigger
|
||||
if temperature is not None:
|
||||
agent.temperature = str(temperature)
|
||||
if max_tokens is not None:
|
||||
agent.max_tokens = max_tokens
|
||||
if is_active is not None:
|
||||
agent.is_active = is_active
|
||||
if avatar is not None:
|
||||
agent.avatar = avatar
|
||||
|
||||
db.commit()
|
||||
return agent.to_dict()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def delete_agent(self, agent_id: str) -> bool:
|
||||
"""Delete an agent"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
agent = db.query(Agent).filter(Agent.id == agent_id).first()
|
||||
if not agent:
|
||||
return False
|
||||
db.delete(agent)
|
||||
db.commit()
|
||||
return True
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# Global manager instance
|
||||
agent_manager = AgentManager()
|
||||
|
|
@ -8,7 +8,7 @@ from luxx.models import Conversation, Message
|
|||
from luxx.tools.executor import ToolExecutor
|
||||
from luxx.tools.core import registry
|
||||
from luxx.services.llm_client import LLMClient
|
||||
from luxx.config import config
|
||||
from luxx.core.config import config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# Maximum iterations to prevent infinite loops
|
||||
|
|
@ -25,7 +25,7 @@ def get_llm_client(conversation: Conversation = None):
|
|||
max_tokens = None
|
||||
if conversation and conversation.provider_id:
|
||||
from luxx.models import LLMProvider
|
||||
from luxx.database import SessionLocal
|
||||
from luxx.core.database import SessionLocal
|
||||
db = SessionLocal()
|
||||
try:
|
||||
provider = db.query(LLMProvider).filter(LLMProvider.id == conversation.provider_id).first()
|
||||
|
|
@ -208,7 +208,7 @@ class ChatService:
|
|||
include_system: bool = True
|
||||
) -> List[Dict[str, str]]:
|
||||
"""Build message list"""
|
||||
from luxx.database import SessionLocal
|
||||
from luxx.core.database import SessionLocal
|
||||
from luxx.models import Message
|
||||
|
||||
messages = []
|
||||
|
|
@ -498,7 +498,7 @@ class ChatService:
|
|||
usage: dict = None
|
||||
):
|
||||
"""Save the assistant message to database."""
|
||||
from luxx.database import SessionLocal
|
||||
from luxx.core.database import SessionLocal
|
||||
from luxx.models import Message
|
||||
|
||||
content_json = {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import httpx
|
|||
import logging
|
||||
from typing import Dict, Any, Optional, List, AsyncGenerator
|
||||
|
||||
from luxx.config import config
|
||||
from luxx.core.config import config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,361 @@
|
|||
"""Chat Room Service - orchestrates multi-agent chat"""
|
||||
import json
|
||||
import re
|
||||
import uuid
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional, AsyncGenerator
|
||||
from dataclasses import dataclass
|
||||
|
||||
from luxx.core.database import SessionLocal
|
||||
from luxx.models.room import ChatRoom, Agent, ChatRoomAgent, ChatRoomMessage
|
||||
from luxx.agents.base import BaseAgent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ==================== Dispatcher ====================
|
||||
|
||||
@dataclass
|
||||
class DispatchResult:
|
||||
"""Result of message dispatch"""
|
||||
triggered_agents: List[BaseAgent]
|
||||
mentions: List[str]
|
||||
should_respond: bool
|
||||
|
||||
|
||||
class MessageDispatcher:
|
||||
"""Dispatcher for routing messages to agents"""
|
||||
|
||||
@staticmethod
|
||||
def parse_mentions(content: str) -> List[str]:
|
||||
"""Parse @mentions from message content"""
|
||||
pattern = r'@(\w+)'
|
||||
return re.findall(pattern, content)
|
||||
|
||||
@staticmethod
|
||||
def get_agents_by_names(names: List[str], room_agents: List[BaseAgent]) -> List[BaseAgent]:
|
||||
"""Get agents by their names (case-insensitive)"""
|
||||
name_lower_map = {a.name.lower(): a for a in room_agents}
|
||||
matched = []
|
||||
for name in names:
|
||||
agent = name_lower_map.get(name.lower())
|
||||
if agent:
|
||||
matched.append(agent)
|
||||
return matched
|
||||
|
||||
@staticmethod
|
||||
def get_agents_by_ids(agent_ids: List[str], room_agents: List[BaseAgent]) -> List[BaseAgent]:
|
||||
"""Get agents by their IDs"""
|
||||
id_set = set(agent_ids)
|
||||
return [a for a in room_agents if a.agent_id in id_set]
|
||||
|
||||
def dispatch(self, content: str, room_agents: List[BaseAgent], sender_id: str, sender_type: str = "user") -> DispatchResult:
|
||||
"""Dispatch a message to appropriate agents."""
|
||||
available_agents = [a for a in room_agents if a.agent_id != sender_id]
|
||||
mentions = self.parse_mentions(content)
|
||||
|
||||
if mentions:
|
||||
triggered = self.get_agents_by_names(mentions, available_agents)
|
||||
logger.info(f"Message with mentions: {mentions} -> triggered: {[a.name for a in triggered]}")
|
||||
return DispatchResult(triggered_agents=triggered, mentions=mentions, should_respond=len(triggered) > 0)
|
||||
|
||||
auto_agents = [a for a in available_agents if a.auto_response]
|
||||
auto_agents.sort(key=lambda a: a.priority)
|
||||
logger.info(f"Auto-response agents triggered: {[a.name for a in auto_agents]}")
|
||||
return DispatchResult(triggered_agents=auto_agents, mentions=[], should_respond=len(auto_agents) > 0)
|
||||
|
||||
|
||||
# ==================== Aggregator ====================
|
||||
|
||||
class ResponseAggregator:
|
||||
"""Aggregates responses from multiple agents"""
|
||||
|
||||
def __init__(self, room_id: str):
|
||||
self.room_id = room_id
|
||||
self._agent_responses: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
async def aggregate_stream(self, agent_streams: Dict[str, AsyncGenerator]) -> AsyncGenerator[Dict[str, Any], None]:
|
||||
"""Aggregate streaming responses from multiple agents."""
|
||||
if not agent_streams:
|
||||
return
|
||||
|
||||
async def collect_agent_stream(agent_id: str, stream):
|
||||
try:
|
||||
async for event in stream:
|
||||
event["agent_id"] = agent_id
|
||||
yield event
|
||||
except Exception as e:
|
||||
logger.error(f"Agent {agent_id} stream error: {e}")
|
||||
yield {"event": "error", "agent_id": agent_id, "data": {"content": str(e)}}
|
||||
|
||||
tasks = [collect_agent_stream(agent_id, stream) for agent_id, stream in agent_streams.items()]
|
||||
async for event in asyncio.merge(*tasks):
|
||||
yield event
|
||||
|
||||
def aggregate_final(self, responses: Dict[str, Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""Aggregate final responses from agents."""
|
||||
results = []
|
||||
for agent_id, response in responses.items():
|
||||
if response.get("event") == "done":
|
||||
results.append({
|
||||
"agent_id": agent_id,
|
||||
"agent_name": response.get("agent_name"),
|
||||
"message_id": response.get("message_id"),
|
||||
"content": response.get("content"),
|
||||
"token_count": response.get("token_count", 0)
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
# ==================== Chat Room Service ====================
|
||||
|
||||
class ChatRoomService:
|
||||
"""Service for managing chat rooms with multi-agent support"""
|
||||
|
||||
def __init__(self):
|
||||
self.dispatcher = MessageDispatcher()
|
||||
|
||||
def get_room(self, room_id: str) -> Optional[ChatRoom]:
|
||||
"""Get a chat room by ID"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
return db.query(ChatRoom).filter(ChatRoom.id == room_id).first()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def get_room_agents(self, room_id: str) -> List[BaseAgent]:
|
||||
"""Get all active agents in a chat room"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
room_agents = db.query(ChatRoomAgent).filter(
|
||||
ChatRoomAgent.chat_room_id == room_id,
|
||||
ChatRoomAgent.is_active == True
|
||||
).all()
|
||||
|
||||
agents = []
|
||||
for ra in room_agents:
|
||||
agent_db = db.query(Agent).filter(Agent.id == ra.agent_id, Agent.is_active == True).first()
|
||||
if agent_db:
|
||||
agents.append(BaseAgent.from_model(agent_db))
|
||||
|
||||
agents.sort(key=lambda a: a.priority)
|
||||
return agents
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def get_agent(self, agent_id: str) -> Optional[BaseAgent]:
|
||||
"""Get an agent by ID"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
agent_db = db.query(Agent).filter(Agent.id == agent_id).first()
|
||||
if agent_db:
|
||||
return BaseAgent.from_model(agent_db)
|
||||
return None
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def list_rooms(self, user_id: int = None, include_agents: bool = True) -> List[Dict]:
|
||||
"""List all chat rooms"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
query = db.query(ChatRoom)
|
||||
if user_id:
|
||||
query = query.filter(ChatRoom.owner_id == user_id)
|
||||
rooms = query.order_by(ChatRoom.updated_at.desc()).all()
|
||||
return [r.to_dict(include_agents=include_agents) for r in rooms]
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def create_room(self, name: str, owner_id: int, description: str = None, agent_ids: List[str] = None) -> Dict:
|
||||
"""Create a new chat room"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
room = ChatRoom(
|
||||
id=str(uuid.uuid4()),
|
||||
name=name,
|
||||
description=description,
|
||||
owner_id=owner_id
|
||||
)
|
||||
db.add(room)
|
||||
|
||||
if agent_ids:
|
||||
for agent_id in agent_ids:
|
||||
room_agent = ChatRoomAgent(
|
||||
id=str(uuid.uuid4()),
|
||||
chat_room_id=room.id,
|
||||
agent_id=agent_id
|
||||
)
|
||||
db.add(room_agent)
|
||||
|
||||
db.commit()
|
||||
return room.to_dict(include_agents=True)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def update_room(self, room_id: str, name: str = None, description: str = None, is_active: bool = None) -> Optional[Dict]:
|
||||
"""Update a chat room"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
room = db.query(ChatRoom).filter(ChatRoom.id == room_id).first()
|
||||
if not room:
|
||||
return None
|
||||
|
||||
if name is not None:
|
||||
room.name = name
|
||||
if description is not None:
|
||||
room.description = description
|
||||
if is_active is not None:
|
||||
room.is_active = is_active
|
||||
|
||||
db.commit()
|
||||
return room.to_dict(include_agents=True)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def delete_room(self, room_id: str) -> bool:
|
||||
"""Delete a chat room"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
room = db.query(ChatRoom).filter(ChatRoom.id == room_id).first()
|
||||
if not room:
|
||||
return False
|
||||
db.delete(room)
|
||||
db.commit()
|
||||
return True
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def add_agent_to_room(self, room_id: str, agent_id: str) -> bool:
|
||||
"""Add an agent to a chat room"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
existing = db.query(ChatRoomAgent).filter(
|
||||
ChatRoomAgent.chat_room_id == room_id,
|
||||
ChatRoomAgent.agent_id == agent_id
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
existing.is_active = True
|
||||
else:
|
||||
room_agent = ChatRoomAgent(
|
||||
id=str(uuid.uuid4()),
|
||||
chat_room_id=room_id,
|
||||
agent_id=agent_id
|
||||
)
|
||||
db.add(room_agent)
|
||||
|
||||
db.commit()
|
||||
return True
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def remove_agent_from_room(self, room_id: str, agent_id: str) -> bool:
|
||||
"""Remove an agent from a chat room"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
room_agent = db.query(ChatRoomAgent).filter(
|
||||
ChatRoomAgent.chat_room_id == room_id,
|
||||
ChatRoomAgent.agent_id == agent_id
|
||||
).first()
|
||||
|
||||
if room_agent:
|
||||
room_agent.is_active = False
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def get_messages(self, room_id: str, limit: int = 50, before_id: str = None) -> List[Dict]:
|
||||
"""Get messages from a chat room"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
query = db.query(ChatRoomMessage).filter(
|
||||
ChatRoomMessage.room_id == room_id
|
||||
).order_by(ChatRoomMessage.created_at.desc())
|
||||
|
||||
if before_id:
|
||||
before_msg = db.query(ChatRoomMessage).filter(
|
||||
ChatRoomMessage.id == before_id
|
||||
).first()
|
||||
if before_msg:
|
||||
query = query.filter(ChatRoomMessage.created_at < before_msg.created_at)
|
||||
|
||||
messages = query.limit(limit).all()
|
||||
return [m.to_dict() for m in reversed(messages)]
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def save_message(self, room_id: str, sender_type: str, sender_id: str, sender_name: str, content: str,
|
||||
mentions: List[str] = None, parent_id: str = None, token_count: int = 0) -> Dict:
|
||||
"""Save a message to a chat room"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
msg = ChatRoomMessage(
|
||||
id=str(uuid.uuid4()),
|
||||
room_id=room_id,
|
||||
sender_type=sender_type,
|
||||
sender_id=sender_id,
|
||||
sender_name=sender_name,
|
||||
content=content,
|
||||
mentions=json.dumps(mentions) if mentions else None,
|
||||
parent_id=parent_id,
|
||||
token_count=token_count
|
||||
)
|
||||
db.add(msg)
|
||||
|
||||
room = db.query(ChatRoom).filter(ChatRoom.id == room_id).first()
|
||||
if room:
|
||||
from datetime import datetime
|
||||
room.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
return msg.to_dict()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
async def process_message(self, room_id: str, user_message: str, user_id: str, user_name: str, context: Dict = None) -> AsyncGenerator[Dict[str, Any], None]:
|
||||
"""Process a user message and dispatch to appropriate agents."""
|
||||
room = self.get_room(room_id)
|
||||
if not room:
|
||||
yield {"event": "error", "data": {"content": "Chat room not found"}}
|
||||
return
|
||||
|
||||
room_agents = self.get_room_agents(room_id)
|
||||
if not room_agents:
|
||||
yield {"event": "error", "data": {"content": "No agents available in this room"}}
|
||||
return
|
||||
|
||||
dispatch_result = self.dispatcher.dispatch(
|
||||
content=user_message,
|
||||
room_agents=room_agents,
|
||||
sender_id=user_id,
|
||||
sender_type="user"
|
||||
)
|
||||
|
||||
if not dispatch_result.should_respond:
|
||||
yield {"event": "no_response", "data": {"message": "No agents triggered"}}
|
||||
return
|
||||
|
||||
messages = self.get_messages(room_id, limit=20)
|
||||
|
||||
agent_streams = {}
|
||||
for agent in dispatch_result.triggered_agents:
|
||||
stream = agent.stream_response(
|
||||
user_message=user_message,
|
||||
conversation_history=messages,
|
||||
context=context
|
||||
)
|
||||
agent_streams[agent.agent_id] = stream
|
||||
|
||||
aggregator = ResponseAggregator(room_id)
|
||||
async for event in aggregator.aggregate_stream(agent_streams):
|
||||
yield event
|
||||
|
||||
|
||||
# Global service instance
|
||||
chat_room_service = ChatRoomService()
|
||||
|
||||
# Export for backward compatibility
|
||||
dispatcher = chat_room_service.dispatcher
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
"""WebSocket handler for Chat Rooms"""
|
||||
import json
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Dict, Set
|
||||
from fastapi import WebSocket, WebSocketDisconnect
|
||||
|
||||
from luxx.services.room import chat_room_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChatRoomConnectionManager:
|
||||
"""Manages WebSocket connections for chat rooms"""
|
||||
|
||||
def __init__(self):
|
||||
self._room_connections: Dict[str, Set[WebSocket]] = {}
|
||||
self._connection_rooms: Dict[WebSocket, str] = {}
|
||||
|
||||
async def connect(self, websocket: WebSocket, room_id: str):
|
||||
"""Connect a WebSocket to a chat room"""
|
||||
await websocket.accept()
|
||||
|
||||
if room_id not in self._room_connections:
|
||||
self._room_connections[room_id] = set()
|
||||
|
||||
self._room_connections[room_id].add(websocket)
|
||||
self._connection_rooms[websocket] = room_id
|
||||
|
||||
logger.info(f"WebSocket connected to room: {room_id}")
|
||||
|
||||
await websocket.send_json({
|
||||
"event": "connected",
|
||||
"data": {"room_id": room_id, "message": "Connected to chat room"}
|
||||
})
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
"""Disconnect a WebSocket from its room"""
|
||||
room_id = self._connection_rooms.pop(websocket, None)
|
||||
if room_id and room_id in self._room_connections:
|
||||
self._room_connections[room_id].discard(websocket)
|
||||
if not self._room_connections[room_id]:
|
||||
del self._room_connections[room_id]
|
||||
logger.info(f"WebSocket disconnected from room: {room_id}")
|
||||
|
||||
async def broadcast_to_room(self, room_id: str, message: dict, exclude: WebSocket = None):
|
||||
"""Broadcast a message to all connections in a room"""
|
||||
if room_id not in self._room_connections:
|
||||
return
|
||||
|
||||
disconnected = []
|
||||
for connection in self._room_connections[room_id]:
|
||||
if connection == exclude:
|
||||
continue
|
||||
try:
|
||||
await connection.send_json(message)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send to connection: {e}")
|
||||
disconnected.append(connection)
|
||||
|
||||
for conn in disconnected:
|
||||
self.disconnect(conn)
|
||||
|
||||
async def send_to_room(self, room_id: str, message: dict):
|
||||
"""Send a message to all connections in a room"""
|
||||
await self.broadcast_to_room(room_id, message)
|
||||
|
||||
def get_room_size(self, room_id: str) -> int:
|
||||
"""Get the number of connections in a room"""
|
||||
return len(self._room_connections.get(room_id, set()))
|
||||
|
||||
|
||||
# Global connection manager
|
||||
connection_manager = ChatRoomConnectionManager()
|
||||
|
||||
|
||||
async def websocket_handler(websocket: WebSocket, room_id: str):
|
||||
"""Handle WebSocket connection for a chat room."""
|
||||
await connection_manager.connect(websocket, room_id)
|
||||
|
||||
room = chat_room_service.get_room(room_id)
|
||||
if not room:
|
||||
await websocket.send_json({"event": "error", "data": {"content": "Chat room not found"}})
|
||||
await websocket.close()
|
||||
return
|
||||
|
||||
try:
|
||||
messages = chat_room_service.get_messages(room_id, limit=50)
|
||||
await websocket.send_json({"event": "history", "data": {"messages": messages}})
|
||||
|
||||
agents = chat_room_service.get_room_agents(room_id)
|
||||
await websocket.send_json({"event": "agents", "data": {"agents": [a.to_dict() for a in agents]}})
|
||||
|
||||
await connection_manager.broadcast_to_room(room_id, {
|
||||
"event": "system",
|
||||
"data": {"content": "User joined the room", "type": "join"}
|
||||
}, exclude=websocket)
|
||||
|
||||
while True:
|
||||
data = await websocket.receive_json()
|
||||
action = data.get("action")
|
||||
|
||||
if action == "send_message":
|
||||
content = data.get("content", "")
|
||||
if not content:
|
||||
continue
|
||||
|
||||
user_id = str(data.get("user_id", "anonymous"))
|
||||
user_name = data.get("user_name", "Anonymous")
|
||||
|
||||
msg = chat_room_service.save_message(
|
||||
room_id=room_id, sender_type="user", sender_id=user_id,
|
||||
sender_name=user_name, content=content
|
||||
)
|
||||
|
||||
await connection_manager.broadcast_to_room(room_id, {"event": "message", "data": msg})
|
||||
|
||||
agents = chat_room_service.get_room_agents(room_id)
|
||||
for agent in agents:
|
||||
await connection_manager.broadcast_to_room(room_id, {
|
||||
"event": "typing",
|
||||
"data": {"agent_id": agent.agent_id, "agent_name": agent.name, "is_typing": True}
|
||||
})
|
||||
|
||||
context = {"user_id": user_id, "username": user_name}
|
||||
|
||||
async for event in chat_room_service.process_message(
|
||||
room_id=room_id, user_message=content, user_id=user_id,
|
||||
user_name=user_name, context=context
|
||||
):
|
||||
if event.get("event") in ["process_step", "done", "error"]:
|
||||
await connection_manager.broadcast_to_room(room_id, {
|
||||
"event": event.get("event"),
|
||||
"data": event.get("data", {}),
|
||||
"agent_id": event.get("agent_id"),
|
||||
"agent_name": event.get("agent_name")
|
||||
})
|
||||
|
||||
if event.get("event") == "done":
|
||||
chat_room_service.save_message(
|
||||
room_id=room_id, sender_type="agent",
|
||||
sender_id=event.get("agent_id"),
|
||||
sender_name=event.get("agent_name"),
|
||||
content=event.get("content", ""),
|
||||
token_count=event.get("token_count", 0)
|
||||
)
|
||||
|
||||
for agent in agents:
|
||||
await connection_manager.broadcast_to_room(room_id, {
|
||||
"event": "typing",
|
||||
"data": {"agent_id": agent.agent_id, "agent_name": agent.name, "is_typing": False}
|
||||
})
|
||||
|
||||
elif action == "ping":
|
||||
await websocket.send_json({"event": "pong", "data": {}})
|
||||
|
||||
except WebSocketDisconnect:
|
||||
logger.info(f"WebSocket disconnected from room: {room_id}")
|
||||
connection_manager.disconnect(websocket)
|
||||
await connection_manager.broadcast_to_room(room_id, {
|
||||
"event": "system",
|
||||
"data": {"content": "User left the room", "type": "leave"}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"WebSocket error in room {room_id}: {e}")
|
||||
connection_manager.disconnect(websocket)
|
||||
await connection_manager.broadcast_to_room(room_id, {
|
||||
"event": "error",
|
||||
"data": {"content": str(e)}
|
||||
})
|
||||
|
|
@ -6,7 +6,7 @@ from typing import Dict, Any, Optional
|
|||
|
||||
from luxx.tools.factory import tool
|
||||
from luxx.tools.core import ToolContext, CommandPermission
|
||||
from luxx.config import config
|
||||
from luxx.core.config import config
|
||||
|
||||
|
||||
def get_user_workspace(user_id: int) -> str:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from typing import Dict, Any
|
|||
|
||||
from luxx.tools.factory import tool
|
||||
from luxx.tools.core import ToolContext, CommandPermission
|
||||
from luxx.config import config
|
||||
from luxx.core.config import config
|
||||
from pathlib import Path
|
||||
|
||||
def get_workspace_from_context(context: ToolContext) -> str:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import hashlib
|
|||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from luxx.config import config
|
||||
from luxx.core.config import config
|
||||
|
||||
|
||||
def generate_id(prefix: str = "") -> str:
|
||||
|
|
|
|||
Loading…
Reference in New Issue