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 { Box, List, ListItemButton, ListItemText, Typography } from '@mui/material';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import DashboardOutlinedIcon from '@mui/icons-material/DashboardOutlined';
|
import {
|
||||||
import LibraryBooksOutlinedIcon from '@mui/icons-material/LibraryBooksOutlined';
|
LibraryBooksOutlined as KnowledgeBasesIcon,
|
||||||
import AccountTreeOutlinedIcon from '@mui/icons-material/AccountTreeOutlined';
|
AccountTreeOutlined as AgentIcon,
|
||||||
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
|
} from '@mui/icons-material';
|
||||||
import StorageOutlinedIcon from '@mui/icons-material/StorageOutlined';
|
import { useTranslation } from 'react-i18next';
|
||||||
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 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const Sidebar = () => {
|
const Sidebar = () => {
|
||||||
const location = useLocation();
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -47,8 +43,10 @@ const Sidebar = () => {
|
|||||||
<List>
|
<List>
|
||||||
{navItems.map((item) => {
|
{navItems.map((item) => {
|
||||||
const IconComponent = item.icon;
|
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 (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={item.path}
|
key={item.path}
|
||||||
@@ -84,7 +82,6 @@ const Sidebar = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
marginTop: 'auto',
|
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 {
|
export interface IDebugSingleRequestBody {
|
||||||
component_id: string;
|
component_id: string;
|
||||||
params: Record<string, any>;
|
params: Record<string, any>;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface IPaginationRequestBody {
|
|||||||
* 分页请求参数
|
* 分页请求参数
|
||||||
*/
|
*/
|
||||||
export interface IPaginationBody {
|
export interface IPaginationBody {
|
||||||
|
keywords?: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
size?: number;
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ export default {
|
|||||||
setting: 'User settings',
|
setting: 'User settings',
|
||||||
logout: 'Log out',
|
logout: 'Log out',
|
||||||
fileManager: 'File Management',
|
fileManager: 'File Management',
|
||||||
flow: 'Agent',
|
agent: 'Agent',
|
||||||
search: 'Search',
|
search: 'Search',
|
||||||
welcome: 'Welcome to',
|
welcome: 'Welcome to',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ export default {
|
|||||||
setting: '用户设置',
|
setting: '用户设置',
|
||||||
logout: '登出',
|
logout: '登出',
|
||||||
fileManager: '文件管理',
|
fileManager: '文件管理',
|
||||||
flow: '智能体',
|
agent: '智能体',
|
||||||
search: '搜索',
|
search: '搜索',
|
||||||
welcome: '欢迎来到',
|
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 PipelineConfig from '../pages/PipelineConfig';
|
||||||
import Dashboard from '../pages/Dashboard';
|
import Dashboard from '../pages/Dashboard';
|
||||||
import ModelsResources from '../pages/ModelsResources';
|
import ModelsResources from '../pages/ModelsResources';
|
||||||
|
import AgentList from '../pages/agent/list';
|
||||||
import {
|
import {
|
||||||
KnowledgeBaseList,
|
KnowledgeBaseList,
|
||||||
KnowledgeBaseCreate,
|
KnowledgeBaseCreate,
|
||||||
@@ -32,7 +33,7 @@ const AppRoutes = () => {
|
|||||||
|
|
||||||
{/* 使用MainLayout作为受保护路由的布局 */}
|
{/* 使用MainLayout作为受保护路由的布局 */}
|
||||||
<Route path="/" element={<MainLayout />}>
|
<Route path="/" element={<MainLayout />}>
|
||||||
{/* <Route index element={<Home />} /> */}
|
<Route index element={<KnowledgeBaseList />} />
|
||||||
<Route path="knowledge">
|
<Route path="knowledge">
|
||||||
<Route index element={<KnowledgeBaseList />} />
|
<Route index element={<KnowledgeBaseList />} />
|
||||||
<Route path="create" element={<KnowledgeBaseCreate />} />
|
<Route path="create" element={<KnowledgeBaseCreate />} />
|
||||||
@@ -44,12 +45,9 @@ const AppRoutes = () => {
|
|||||||
<Route path="overview" element={<KnowledgeLogsPage />} />
|
<Route path="overview" element={<KnowledgeLogsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
<Route index element={<KnowledgeBaseList />} />
|
<Route path="agent">
|
||||||
<Route path="pipeline-config" element={<PipelineConfig />} />
|
<Route index element={<AgentList />} />
|
||||||
<Route path="dashboard" element={<Dashboard />} />
|
</Route>
|
||||||
<Route path="models-resources" element={<ModelsResources />} />
|
|
||||||
<Route path="mcp" element={<MCP />} />
|
|
||||||
<Route path="form-field-test" element={<FormFieldTest />} />
|
|
||||||
</Route>
|
</Route>
|
||||||
{/* 处理chunk相关路由 需要传入 kb_id doc_id */}
|
{/* 处理chunk相关路由 需要传入 kb_id doc_id */}
|
||||||
<Route path="chunk">
|
<Route path="chunk">
|
||||||
@@ -68,7 +66,7 @@ const AppRoutes = () => {
|
|||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* 处理未匹配的路由 */}
|
{/* 处理未匹配的路由 */}
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/knowledge" replace />} />
|
||||||
</Routes>
|
</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
|
// flow
|
||||||
listTemplates: `${api_host}/canvas/templates`,
|
listTemplates: `${api_host}/canvas/templates`,
|
||||||
listCanvas: `${api_host}/canvas/list`,
|
listCanvas: `${api_host}/canvas/list`,
|
||||||
|
listTeamCanvas: `${api_host}/canvas/listteam`,
|
||||||
getCanvas: `${api_host}/canvas/get`,
|
getCanvas: `${api_host}/canvas/get`,
|
||||||
getCanvasSSE: `${api_host}/canvas/getsse`,
|
getCanvasSSE: `${api_host}/canvas/getsse`,
|
||||||
removeCanvas: `${api_host}/canvas/rm`,
|
removeCanvas: `${api_host}/canvas/rm`,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": false,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": false,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true,
|
"noUncheckedSideEffectImports": true,
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user