feat(agent): add agent management feature with list view and routing
This commit is contained in:
125
docs/ragflow-agent-overview.md
Normal file
125
docs/ragflow-agent-overview.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# `agent` & `agents` 目录代码结构解析
|
||||
|
||||
本文档旨在详细解析 `ragflow_core_v0.21.1` 项目中 `src/pages/agent` 和 `src/pages/agents` 两个核心目录的结构、功能和关系。
|
||||
|
||||
- **`src/pages/agent`**: Agent/数据流的可视化编排器,是构建和调试单个 Agent 的工作区。
|
||||
- **`src/pages/agents`**: Agent 的管理中心,负责列表展示、创建、模板管理和日志查看。
|
||||
|
||||
---
|
||||
|
||||
## 1. `src/pages/agent` - Agent 可视化编排器
|
||||
|
||||
此目录是整个 RAGFlow 的核心功能所在,提供了一个基于 `@xyflow/react` 的可视化画布,用户可以通过拖拽节点和连接边的方式来构建复杂的数据处理流(DSL)。
|
||||
|
||||
### 1.1. 目录结构概览
|
||||
|
||||
```
|
||||
agent/
|
||||
├── canvas/ # 画布核心组件
|
||||
│ ├── node/ # 所有自定义节点的实现
|
||||
│ └── edge/ # 自定义边的实现
|
||||
├── form/ # 所有节点的配置表单
|
||||
│ ├── agent-form/
|
||||
│ └── ...
|
||||
├── hooks/ # 画布相关的 Hooks
|
||||
│ ├── use-build-dsl.ts
|
||||
│ └── use-save-graph.ts
|
||||
├── chat/ # 调试用的聊天面板
|
||||
├── constant/ # Agent 相关的常量
|
||||
├── index.tsx # Agent 页面主入口
|
||||
└── store.ts # Zustand store,管理画布状态
|
||||
```
|
||||
|
||||
### 1.2. 关键子目录解析
|
||||
|
||||
#### `canvas/` - 画布与节点
|
||||
|
||||
- **`canvas/index.tsx`**: 画布主组件,负责整合节点、边、背景、小地图等,并处理拖拽、连接、删除等基本交互。
|
||||
- **`canvas/node/`**: 定义了所有内置的节点类型。每个节点文件(如 `begin-node.tsx`, `retrieval-form/`) 负责节点的 UI 渲染、Handles(连接点)的定位和基本逻辑。
|
||||
- `node-wrapper.tsx`: 为所有节点提供统一的包裹层,处理选中、错误、运行状态等通用 UI 逻辑。
|
||||
- `card.tsx`: 节点内部内容的标准卡片布局。
|
||||
- **`canvas/edge/`**: 自定义边的实现,可能包含特殊的路径、箭头或交互。
|
||||
|
||||
#### `form/` - 节点配置表单
|
||||
|
||||
当用户在画布上选中一个节点时,会弹出一个表单用于配置该节点的参数。此目录存放了所有节点对应的表单组件。
|
||||
|
||||
- 目录结构与节点类型一一对应,例如 `retrieval-form/` 对应检索节点。
|
||||
- 每个表单组件负责:
|
||||
1. 渲染配置项(如输入框、下拉框、开关等)。
|
||||
2. 从节点数据中初始化表单。
|
||||
3. 将用户的输入实时或在保存时更新回节点的 `data` 属性中。
|
||||
- `form/components/` 包含了一些表单内复用的组件,如标准的输出配置 (`output.tsx`)。
|
||||
|
||||
#### `hooks/` - 核心逻辑 Hooks
|
||||
|
||||
此目录封装了画布的核心业务逻辑,使主页面 `index.tsx` 保持整洁。
|
||||
|
||||
- **`use-build-dsl.ts`**: 将画布上的节点和边(Graph a object)转换为后端可执行的领域特定语言(DSL JSON)。这是从前端图形表示到后端逻辑表示的关键转换。
|
||||
- **`use-save-graph.ts`**: 负责保存当前的图结构(节点和边的位置、数据等)到后端。通常在用户手动点击保存或自动保存时触发。
|
||||
- **`use-run-dataflow.ts`**: 触发当前 Agent 的运行,并处理返回的日志、结果等。
|
||||
- **`use-set-graph.ts`**: 从后端获取图数据并将其渲染到画布上,用于加载已保存的 Agent。
|
||||
|
||||
#### `chat/`, `debug-content/`, `log-sheet/` - 调试与运行
|
||||
|
||||
- **`chat/`**: Agent 调试时使用的聊天界面,用于发送输入并实时查看 Agent 的回复。
|
||||
- **`debug-content/`**: 调试抽屉的内容,可能包含更详细的输入/输出或日志信息。
|
||||
- **`log-sheet/`**: 展示 Agent 运行历史日志的面板。
|
||||
|
||||
#### `store.ts`
|
||||
|
||||
基于 Zustand 的状态管理,全局维护画布的状态,例如:
|
||||
- `nodes`, `edges`: 画布上的节点和边数组。
|
||||
- `onNodesChange`, `onEdgesChange`: `@xyflow/react` 需要的回调。
|
||||
- 当前选中的节点、画布的缩放/平移状态等。
|
||||
- 其他 UI 状态,如配置面板是否打开。
|
||||
|
||||
---
|
||||
|
||||
## 2. `src/pages/agents` - Agent 管理中心
|
||||
|
||||
此目录负责管理所有的 Agent 实例,是用户与 Agent 交互的入口页面。
|
||||
|
||||
### 2.1. 目录结构概览
|
||||
|
||||
```
|
||||
agents/
|
||||
├── hooks/
|
||||
│ ├── use-create-agent.ts
|
||||
│ └── use-selelct-filters.ts
|
||||
├── agent-card.tsx # 单个 Agent 的卡片展示
|
||||
├── agent-log-page.tsx # Agent 运行日志列表页
|
||||
├── agent-templates.tsx # Agent 模板页
|
||||
├── create-agent-dialog.tsx # 创建 Agent 的对话框
|
||||
├── index.tsx # Agent 列表页面主入口
|
||||
└── use-rename-agent.ts # 重命名 Agent 的 Hook
|
||||
```
|
||||
|
||||
### 2.2. 关键文件解析
|
||||
|
||||
- **`index.tsx`**: Agent 列表的主页面。通常会从后端获取所有 Agent 的列表,并使用 `agent-card.tsx` 将它们渲染出来。包含搜索、筛选和分页等功能。
|
||||
- **`agent-card.tsx`**: 以卡片形式展示单个 Agent 的摘要信息,如名称、描述、更新时间等。点击卡片通常会导航到 `src/pages/agent` 页面,并传入对应的 Agent ID。
|
||||
- **`create-agent-dialog.tsx` / `create-agent-form.tsx`**: 提供创建新 Agent 的表单和对话框。`use-create-agent.ts` Hook 封装了向后端发送创建请求的逻辑。
|
||||
- **`agent-templates.tsx`**: 展示可供用户选择的预置 Agent 模板,帮助用户快速启动。
|
||||
- **`agent-log-page.tsx`**: 展示所有 Agent 的历史运行日志,提供查询和筛选功能。点击某条日志可以查看详情。
|
||||
- **`hooks/`**: 存放与 Agent 列表管理相关的业务逻辑。
|
||||
- `use-create-agent.ts`: 封装创建 Agent 的 API 调用。
|
||||
- `use-selelct-filters.ts`: 管理列表页的筛选条件。
|
||||
|
||||
---
|
||||
|
||||
## 3. 关系与数据流
|
||||
|
||||
1. **从 `agents` 到 `agent`**:
|
||||
- 用户在 `agents` 列表页 (`/agents`) 点击一个 Agent 卡片。
|
||||
- 应用导航到 `agent` 编排页 (`/agent/{agent_id}`)。
|
||||
- `agent` 页面的 `use-set-graph.ts` Hook 被触发,使用 `agent_id` 从后端获取该 Agent 的图数据。
|
||||
- 获取到的图数据通过 `store.ts` 设置到全局状态,画布 (`canvas/index.tsx`) 监听到状态变化后,渲染出对应的节点和边。
|
||||
|
||||
2. **从 `agent` 保存与返回**:
|
||||
- 用户在 `agent` 页面修改了图结构。
|
||||
- `use-save-graph.ts` Hook 将新的图结构保存到后端。
|
||||
- `use-build-dsl.ts` 将图结构转换为 DSL 并一同保存。
|
||||
- 用户返回 `agents` 列表页,可以看到 Agent 的更新时间等信息已变化。
|
||||
|
||||
这两个目录共同构成了一个完整的 Agent 创建、管理、编排和调试的闭环。
|
||||
@@ -1,23 +1,19 @@
|
||||
import { Box, List, ListItemButton, ListItemText, Typography } from '@mui/material';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import DashboardOutlinedIcon from '@mui/icons-material/DashboardOutlined';
|
||||
import LibraryBooksOutlinedIcon from '@mui/icons-material/LibraryBooksOutlined';
|
||||
import AccountTreeOutlinedIcon from '@mui/icons-material/AccountTreeOutlined';
|
||||
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
|
||||
import StorageOutlinedIcon from '@mui/icons-material/StorageOutlined';
|
||||
import ExtensionOutlinedIcon from '@mui/icons-material/ExtensionOutlined';
|
||||
|
||||
const navItems = [
|
||||
// { text: 'Overview', path: '/', icon: DashboardOutlinedIcon },
|
||||
{ text: 'Knowledge Bases', path: '/', icon: LibraryBooksOutlinedIcon },
|
||||
// { text: 'RAG Pipeline', path: '/pipeline-config', icon: AccountTreeOutlinedIcon },
|
||||
// { text: 'Operations', path: '/dashboard', icon: SettingsOutlinedIcon },
|
||||
// { text: 'Models & Resources', path: '/models-resources', icon: StorageOutlinedIcon },
|
||||
// { text: 'MCP', path: '/mcp', icon: ExtensionOutlinedIcon },
|
||||
];
|
||||
import {
|
||||
LibraryBooksOutlined as KnowledgeBasesIcon,
|
||||
AccountTreeOutlined as AgentIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Sidebar = () => {
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const navItems = [
|
||||
{ text: t('header.knowledgeBase'), path: '/knowledge', icon: KnowledgeBasesIcon },
|
||||
{ text: t('header.agent'), path: '/agent', icon: AgentIcon },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -42,17 +38,19 @@ const Sidebar = () => {
|
||||
}}
|
||||
>
|
||||
T-Systems Enterprise
|
||||
</Typography>
|
||||
|
||||
</Typography>
|
||||
|
||||
<List>
|
||||
{navItems.map((item) => {
|
||||
const IconComponent = item.icon;
|
||||
const isActive = location.pathname === item.path;
|
||||
|
||||
let isActive = location.pathname === item.path;
|
||||
if (item.path === '/knowledge' && location.pathname === '/') {
|
||||
isActive = true;
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
to={item.path}
|
||||
style={{ textDecoration: 'none', color: 'inherit' }}
|
||||
>
|
||||
<ListItemButton
|
||||
@@ -71,12 +69,12 @@ const Sidebar = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
sx={{
|
||||
<IconComponent
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
marginRight: '12px',
|
||||
fontSize: '1.2rem'
|
||||
}}
|
||||
}}
|
||||
/>
|
||||
<ListItemText primary={item.text} />
|
||||
</ListItemButton>
|
||||
@@ -84,7 +82,6 @@ const Sidebar = () => {
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: 'auto',
|
||||
|
||||
125
src/components/agent/AgentCard.tsx
Normal file
125
src/components/agent/AgentCard.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Card,
|
||||
CardContent,
|
||||
IconButton,
|
||||
Avatar,
|
||||
Chip,
|
||||
} from '@mui/material';
|
||||
import { MoreVert as MoreVertIcon } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { IFlow } from '@/interfaces/database/agent';
|
||||
|
||||
interface AgentCardProps {
|
||||
agent: IFlow;
|
||||
onMenuClick: (event: React.MouseEvent<HTMLElement>, agent: IFlow) => void;
|
||||
onViewAgent: (agent: IFlow) => void;
|
||||
}
|
||||
|
||||
const AgentCard: React.FC<AgentCardProps> = ({ agent, onMenuClick, onViewAgent }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getPermissionInfo = (permission: string) => {
|
||||
switch (permission) {
|
||||
case 'me':
|
||||
return { label: t('common.private'), color: '#E3F2FD', textColor: '#1976D2' };
|
||||
case 'team':
|
||||
return { label: t('common.team'), color: '#E8F5E8', textColor: '#388E3C' };
|
||||
default:
|
||||
return { label: t('common.public'), color: '#FFF3E0', textColor: '#F57C00' };
|
||||
}
|
||||
};
|
||||
|
||||
const permissionInfo = getPermissionInfo(agent.permission || 'me');
|
||||
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return t('common.unknown');
|
||||
return dateStr;
|
||||
};
|
||||
|
||||
const nodeCount = agent.dsl?.graph?.nodes?.length ?? 0;
|
||||
const edgeCount = agent.dsl?.graph?.edges?.length ?? 0;
|
||||
|
||||
return (
|
||||
<Card sx={{ borderRadius: 2 }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Avatar src={agent.avatar} sx={{ bgcolor: 'primary.main' }}>{agent.title?.[0] || 'A'}</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h6" fontWeight={600}>{agent.title || t('common.untitled')}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 0.5 }}>
|
||||
{agent.canvas_category && (
|
||||
<Chip label={agent.canvas_category} size="small" />
|
||||
)}
|
||||
<Chip
|
||||
label={permissionInfo.label}
|
||||
size="small"
|
||||
sx={{ bgcolor: permissionInfo.color, color: permissionInfo.textColor }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<IconButton aria-label={t('common.more')} onClick={(e) => onMenuClick(e, agent)}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
mt: 1,
|
||||
mb: 2,
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{agent.description || t('common.noDescription')}
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
mt: 2,
|
||||
p: 1.5,
|
||||
backgroundColor: '#F8F9FA',
|
||||
borderRadius: 1,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="h6" sx={{ fontSize: '1.25rem', fontWeight: 600, color: 'primary.main' }}>
|
||||
{nodeCount}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{t('agent.nodes') || 'Nodes'}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="h6" sx={{ fontSize: '1.25rem', fontWeight: 600, color: 'primary.main' }}>
|
||||
{edgeCount}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{t('agent.edges') || 'Edges'}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
|
||||
{t('common.updatedAt') || 'Updated'}: {formatDate(agent.update_date)}
|
||||
</Typography>
|
||||
|
||||
{agent.nickname && (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
|
||||
{t('knowledge.creator') || 'Creator'}: {agent.nickname}
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentCard;
|
||||
125
src/components/agent/AgentGridView.tsx
Normal file
125
src/components/agent/AgentGridView.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography, Grid, Button, Menu, MenuItem } from '@mui/material';
|
||||
import { ArrowForward as ArrowForwardIcon, Add as AddIcon } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { IFlow } from '@/interfaces/database/agent';
|
||||
import AgentCard from './AgentCard';
|
||||
|
||||
interface AgentGridViewProps {
|
||||
agents: IFlow[];
|
||||
maxItems?: number;
|
||||
showSeeAll?: boolean;
|
||||
onSeeAll?: () => void;
|
||||
onEdit?: (agent: IFlow) => void;
|
||||
onDelete?: (agent: IFlow) => void;
|
||||
onView?: (agent: IFlow) => void;
|
||||
loading?: boolean;
|
||||
searchTerm?: string;
|
||||
onCreateAgent?: () => void;
|
||||
}
|
||||
|
||||
const AgentGridView: React.FC<AgentGridViewProps> = ({
|
||||
agents,
|
||||
maxItems,
|
||||
showSeeAll = false,
|
||||
onSeeAll,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onView,
|
||||
loading = false,
|
||||
searchTerm = '',
|
||||
onCreateAgent,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
const [selectedAgent, setSelectedAgent] = React.useState<IFlow | null>(null);
|
||||
|
||||
const handleMenuClick = (event: React.MouseEvent<HTMLElement>, agent: IFlow) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedAgent(agent);
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
setSelectedAgent(null);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (selectedAgent && onDelete) {
|
||||
onDelete(selectedAgent);
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleView = () => {
|
||||
if (selectedAgent && onView) {
|
||||
onView(selectedAgent);
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleViewAgent = (agent: IFlow) => {
|
||||
if (onView) {
|
||||
onView(agent);
|
||||
}
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const displayedAgents = maxItems ? agents.slice(0, maxItems) : agents;
|
||||
const hasMore = maxItems && agents.length > maxItems;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Typography>{t('common.loading')}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (agents.length === 0) {
|
||||
return (
|
||||
<Box sx={{ textAlign: 'center', py: 8 }}>
|
||||
<Typography variant="h6" color="text.secondary" gutterBottom>
|
||||
{searchTerm ? (t('agent.noMatchingAgents') || 'No matching agents') : (t('agent.noAgents') || 'No agents')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
{searchTerm ? (t('agent.tryAdjustingFilters') || 'Try adjusting filters') : (t('agent.createFirstAgent') || 'Create your first agent')}
|
||||
</Typography>
|
||||
{(!searchTerm && onCreateAgent) && (
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={onCreateAgent}>
|
||||
{t('agent.createAgent') || 'Create Agent'}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Grid container spacing={3}>
|
||||
{displayedAgents.map((agent) => (
|
||||
<Grid key={agent.id} size={{ xs: 12, sm: 6, md: 4 }}>
|
||||
<AgentCard agent={agent} onMenuClick={handleMenuClick} onViewAgent={handleViewAgent} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{showSeeAll && hasMore && (
|
||||
<Box sx={{ textAlign: 'center', mt: 3 }}>
|
||||
<Button variant="outlined" endIcon={<ArrowForwardIcon />} onClick={onSeeAll} sx={{ borderRadius: 2 }}>
|
||||
{t('common.viewAll')} ({agents.length})
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleMenuClose}>
|
||||
<MenuItem onClick={handleView}>{t('common.viewDetails')}</MenuItem>
|
||||
<MenuItem onClick={handleDelete} sx={{ color: 'error.main' }}>
|
||||
{t('common.delete')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentGridView;
|
||||
97
src/constants/agent.ts
Normal file
97
src/constants/agent.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
export enum ProgrammingLanguage {
|
||||
Python = 'python',
|
||||
Javascript = 'javascript',
|
||||
}
|
||||
|
||||
export const CodeTemplateStrMap = {
|
||||
[ProgrammingLanguage.Python]: `def main(arg1: str, arg2: str) -> str:
|
||||
return f"result: {arg1 + arg2}"
|
||||
`,
|
||||
[ProgrammingLanguage.Javascript]: `const axios = require('axios');
|
||||
async function main({}) {
|
||||
try {
|
||||
const response = await axios.get('https://github.com/infiniflow/ragflow');
|
||||
return 'Body:' + response.data;
|
||||
} catch (error) {
|
||||
return 'Error:' + error.message;
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
export enum AgentGlobals {
|
||||
SysQuery = 'sys.query',
|
||||
SysUserId = 'sys.user_id',
|
||||
SysConversationTurns = 'sys.conversation_turns',
|
||||
SysFiles = 'sys.files',
|
||||
}
|
||||
|
||||
export enum AgentCategory {
|
||||
AgentCanvas = 'agent_canvas',
|
||||
DataflowCanvas = 'dataflow_canvas',
|
||||
}
|
||||
|
||||
export enum AgentQuery {
|
||||
Category = 'category',
|
||||
}
|
||||
|
||||
export enum DataflowOperator {
|
||||
Begin = 'File',
|
||||
Note = 'Note',
|
||||
Parser = 'Parser',
|
||||
Tokenizer = 'Tokenizer',
|
||||
Splitter = 'Splitter',
|
||||
HierarchicalMerger = 'HierarchicalMerger',
|
||||
Extractor = 'Extractor',
|
||||
}
|
||||
|
||||
export enum Operator {
|
||||
Begin = 'Begin',
|
||||
Retrieval = 'Retrieval',
|
||||
Categorize = 'Categorize',
|
||||
Message = 'Message',
|
||||
Relevant = 'Relevant',
|
||||
RewriteQuestion = 'RewriteQuestion',
|
||||
KeywordExtract = 'KeywordExtract',
|
||||
Baidu = 'Baidu',
|
||||
DuckDuckGo = 'DuckDuckGo',
|
||||
Wikipedia = 'Wikipedia',
|
||||
PubMed = 'PubMed',
|
||||
ArXiv = 'ArXiv',
|
||||
Google = 'Google',
|
||||
Bing = 'Bing',
|
||||
GoogleScholar = 'GoogleScholar',
|
||||
DeepL = 'DeepL',
|
||||
GitHub = 'GitHub',
|
||||
BaiduFanyi = 'BaiduFanyi',
|
||||
QWeather = 'QWeather',
|
||||
ExeSQL = 'ExeSQL',
|
||||
Switch = 'Switch',
|
||||
WenCai = 'WenCai',
|
||||
AkShare = 'AkShare',
|
||||
YahooFinance = 'YahooFinance',
|
||||
Jin10 = 'Jin10',
|
||||
TuShare = 'TuShare',
|
||||
Note = 'Note',
|
||||
Crawler = 'Crawler',
|
||||
Invoke = 'Invoke',
|
||||
Email = 'Email',
|
||||
Iteration = 'Iteration',
|
||||
IterationStart = 'IterationItem',
|
||||
Code = 'CodeExec',
|
||||
WaitingDialogue = 'WaitingDialogue',
|
||||
Agent = 'Agent',
|
||||
Tool = 'Tool',
|
||||
TavilySearch = 'TavilySearch',
|
||||
TavilyExtract = 'TavilyExtract',
|
||||
UserFillUp = 'UserFillUp',
|
||||
StringTransform = 'StringTransform',
|
||||
SearXNG = 'SearXNG',
|
||||
Placeholder = 'Placeholder',
|
||||
File = 'File', // pipeline
|
||||
Parser = 'Parser',
|
||||
Tokenizer = 'Tokenizer',
|
||||
Splitter = 'Splitter',
|
||||
HierarchicalMerger = 'HierarchicalMerger',
|
||||
Extractor = 'Extractor',
|
||||
Generate = 'Generate',
|
||||
}
|
||||
82
src/hooks/agent-hooks.ts
Normal file
82
src/hooks/agent-hooks.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import agentService from '@/services/agent_service';
|
||||
import type { IFlow } from '@/interfaces/database/agent';
|
||||
import type { IAgentPaginationParams } from '@/interfaces/request/agent';
|
||||
import logger from '@/utils/logger';
|
||||
|
||||
export interface UseAgentListState {
|
||||
agents: IFlow[];
|
||||
total: number;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
keywords: string;
|
||||
orderby?: string;
|
||||
desc?: boolean;
|
||||
}
|
||||
|
||||
export interface UseAgentListReturn extends UseAgentListState {
|
||||
fetchAgents: (params?: IAgentPaginationParams) => Promise<void>;
|
||||
setKeywords: (keywords: string) => void;
|
||||
setCurrentPage: (page: number) => void;
|
||||
setPageSize: (size: number) => void;
|
||||
setOrder: (orderby?: string, desc?: boolean) => void;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能体列表钩子
|
||||
* @param initialParams 初始参数
|
||||
*/
|
||||
export const useAgentList = (initialParams?: IAgentPaginationParams) => {
|
||||
const [agents, setAgents] = useState<IFlow[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(initialParams?.page || 1);
|
||||
const [pageSize, setPageSize] = useState(initialParams?.page_size || 10);
|
||||
const [keywords, setKeywords] = useState(initialParams?.keywords || '');
|
||||
|
||||
const fetchAgentList = useCallback(async (params?: IAgentPaginationParams) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await agentService.listCanvas(params);
|
||||
const res = response.data || {};
|
||||
logger.info('useAgentList fetchAgentList', res);
|
||||
const data = res.data
|
||||
setAgents(data.canvas || []);
|
||||
setTotal(data.total || 0);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to fetch agent list');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refresh = useCallback(() => fetchAgentList({
|
||||
keywords,
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
}), [keywords, currentPage, pageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
return {
|
||||
agents,
|
||||
total,
|
||||
loading,
|
||||
error,
|
||||
currentPage,
|
||||
pageSize,
|
||||
keywords,
|
||||
fetchAgents: fetchAgentList,
|
||||
setKeywords,
|
||||
setCurrentPage,
|
||||
setPageSize,
|
||||
refresh,
|
||||
};
|
||||
};
|
||||
@@ -1,3 +1,13 @@
|
||||
/**
|
||||
* 分页请求参数
|
||||
*/
|
||||
export interface IAgentPaginationParams {
|
||||
keywords?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}
|
||||
|
||||
|
||||
export interface IDebugSingleRequestBody {
|
||||
component_id: string;
|
||||
params: Record<string, any>;
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface IPaginationRequestBody {
|
||||
* 分页请求参数
|
||||
*/
|
||||
export interface IPaginationBody {
|
||||
keywords?: string;
|
||||
page?: number;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
@@ -278,7 +278,7 @@ export default {
|
||||
setting: 'User settings',
|
||||
logout: 'Log out',
|
||||
fileManager: 'File Management',
|
||||
flow: 'Agent',
|
||||
agent: 'Agent',
|
||||
search: 'Search',
|
||||
welcome: 'Welcome to',
|
||||
},
|
||||
|
||||
@@ -262,7 +262,7 @@ export default {
|
||||
setting: '用户设置',
|
||||
logout: '登出',
|
||||
fileManager: '文件管理',
|
||||
flow: '智能体',
|
||||
agent: '智能体',
|
||||
search: '搜索',
|
||||
welcome: '欢迎来到',
|
||||
},
|
||||
|
||||
96
src/pages/agent/list.tsx
Normal file
96
src/pages/agent/list.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
TextField,
|
||||
InputAdornment,
|
||||
Paper,
|
||||
Button,
|
||||
Pagination,
|
||||
Stack,
|
||||
} from '@mui/material';
|
||||
import { Search as SearchIcon, Refresh as RefreshIcon } from '@mui/icons-material';
|
||||
import { useAgentList } from '@/hooks/agent-hooks';
|
||||
import AgentGridView from '@/components/agent/AgentGridView';
|
||||
|
||||
function AgentListPage() {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const {
|
||||
agents,
|
||||
total,
|
||||
loading,
|
||||
currentPage,
|
||||
pageSize,
|
||||
setCurrentPage,
|
||||
setKeywords,
|
||||
refresh,
|
||||
} = useAgentList({ page: 1, page_size: 10 });
|
||||
|
||||
const totalPages = useMemo(() => {
|
||||
return Math.ceil((agents?.length || 0) / pageSize) || 1;
|
||||
}, [agents, pageSize]);
|
||||
|
||||
const currentPageData = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
return (agents || []).slice(startIndex, endIndex);
|
||||
}, [agents, currentPage, pageSize]);
|
||||
|
||||
const handleSearch = useCallback(() => {
|
||||
setKeywords(searchValue);
|
||||
setCurrentPage(1);
|
||||
}, [searchValue, setKeywords, setCurrentPage]);
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography variant="h4" fontWeight={600} mb={2}>Agent 列表</Typography>
|
||||
|
||||
<Paper sx={{ p: 2, mb: 2 }}>
|
||||
<Box display="flex" gap={2} alignItems="center">
|
||||
<TextField
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
placeholder="搜索名称或描述"
|
||||
size="small"
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Button variant="contained" onClick={handleSearch}>搜索</Button>
|
||||
<Button variant="outlined" startIcon={<RefreshIcon />} onClick={refresh}>刷新</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<AgentGridView
|
||||
agents={currentPageData}
|
||||
loading={loading}
|
||||
searchTerm={searchValue}
|
||||
/>
|
||||
|
||||
{totalPages >= 1 && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
|
||||
<Stack spacing={2}>
|
||||
<Pagination
|
||||
count={totalPages}
|
||||
page={currentPage}
|
||||
onChange={(_, page) => setCurrentPage(page)}
|
||||
color="primary"
|
||||
size="large"
|
||||
showFirstButton
|
||||
showLastButton
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary" textAlign="center">
|
||||
共 {total} 个,当前第 {currentPage} / {totalPages} 页
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default AgentListPage;
|
||||
@@ -5,6 +5,7 @@ import Login from '../pages/login/Login';
|
||||
import PipelineConfig from '../pages/PipelineConfig';
|
||||
import Dashboard from '../pages/Dashboard';
|
||||
import ModelsResources from '../pages/ModelsResources';
|
||||
import AgentList from '../pages/agent/list';
|
||||
import {
|
||||
KnowledgeBaseList,
|
||||
KnowledgeBaseCreate,
|
||||
@@ -32,7 +33,7 @@ const AppRoutes = () => {
|
||||
|
||||
{/* 使用MainLayout作为受保护路由的布局 */}
|
||||
<Route path="/" element={<MainLayout />}>
|
||||
{/* <Route index element={<Home />} /> */}
|
||||
<Route index element={<KnowledgeBaseList />} />
|
||||
<Route path="knowledge">
|
||||
<Route index element={<KnowledgeBaseList />} />
|
||||
<Route path="create" element={<KnowledgeBaseCreate />} />
|
||||
@@ -44,12 +45,9 @@ const AppRoutes = () => {
|
||||
<Route path="overview" element={<KnowledgeLogsPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route index element={<KnowledgeBaseList />} />
|
||||
<Route path="pipeline-config" element={<PipelineConfig />} />
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
<Route path="models-resources" element={<ModelsResources />} />
|
||||
<Route path="mcp" element={<MCP />} />
|
||||
<Route path="form-field-test" element={<FormFieldTest />} />
|
||||
<Route path="agent">
|
||||
<Route index element={<AgentList />} />
|
||||
</Route>
|
||||
</Route>
|
||||
{/* 处理chunk相关路由 需要传入 kb_id doc_id */}
|
||||
<Route path="chunk">
|
||||
@@ -68,7 +66,7 @@ const AppRoutes = () => {
|
||||
</Route>
|
||||
|
||||
{/* 处理未匹配的路由 */}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
<Route path="*" element={<Navigate to="/knowledge" replace />} />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
19
src/services/agent_service.ts
Normal file
19
src/services/agent_service.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import api from './api';
|
||||
import request from '@/utils/request';
|
||||
import type { IAgentPaginationParams } from '@/interfaces/request/agent';
|
||||
|
||||
/**
|
||||
* 智能体服务
|
||||
*/
|
||||
const agentService = {
|
||||
/**
|
||||
* 获取团队下的Canvas列表
|
||||
*/
|
||||
listCanvas: (params?: IAgentPaginationParams) => {
|
||||
return request.get(api.listTeamCanvas, { params });
|
||||
},
|
||||
|
||||
|
||||
};
|
||||
|
||||
export default agentService;
|
||||
@@ -151,6 +151,7 @@ export default {
|
||||
// flow
|
||||
listTemplates: `${api_host}/canvas/templates`,
|
||||
listCanvas: `${api_host}/canvas/list`,
|
||||
listTeamCanvas: `${api_host}/canvas/listteam`,
|
||||
getCanvas: `${api_host}/canvas/get`,
|
||||
getCanvasSSE: `${api_host}/canvas/getsse`,
|
||||
removeCanvas: `${api_host}/canvas/rm`,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"verbatimModuleSyntax": false,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
@@ -20,7 +20,7 @@
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"erasableSyntaxOnly": true,
|
||||
"erasableSyntaxOnly": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user