Files
ragflow/web/src/services/agent-service.ts
plind dd76653dc1 feat: add tag management for Agents with filtering and sorting (#14774) (#14799)
## Summary
Closes #14774.

Adds free-form tags on agents (UserCanvas) with full UI + API:
- Stored as comma-separated `tags` column on `UserCanvas` with online
migration.
- New endpoints: `GET /v1/agents/tags` (aggregate counts) and `PUT
/v1/agent/<id>/tags` (write). `GET /v1/agents` accepts a `tags=` query.
- "Edit tags" item in agent dropdown opens a chip-style editor dialog;
tags render as badges on each agent card.
- New "Tags" facet in the agents filter bar, with counts.

## Implementation notes
- **Tag matching is exact-token**: the SQL filter wraps stored tags as
`,…,` and matches `,ml,` so `ml` doesn't match `ml-ops`.
- **Server-side normalization** in `UserCanvasService.update_tags`:
dedup (case-insensitive), per-tag cap of 64 chars, total length capped
at 512 chars to fit the column, commas inside tag values are replaced
with spaces.
- **Tenant authorization**: `PUT /v1/agent/<id>/tags` gates on
`UserCanvasService.accessible(canvas_id, tenant_id)`.
- **Tag listing scope**: `UserCanvasService.list_tags` follows the same
own + team-shared rule as `get_by_tenant_ids`.
- **i18n**: keys added to `en.ts` and `zh.ts` only (per project
convention; other locales fall back).
- **`HomeCard`** gets a non-breaking `extra?: ReactNode` slot for the
chip row; no `src/components/ui/` files modified.

## Test plan
- [ ] Backend boot runs `migrate_db` → confirm `user_canvas.tags` column
exists (`DESCRIBE user_canvas`).
- [ ] Agents page renders cards normally (no console error from missing
field).
- [ ] `⋯ → Edit tags` opens a dialog that stays open (regression: dialog
was unmounting with the dropdown).
- [ ] Typing a tag without pressing Enter and clicking Save persists it
(regression: last typed tag was being dropped).
- [ ] Chip input supports Enter/comma to commit, Backspace on empty to
remove, `×` to remove individual chip.
- [ ] Tag containing a comma sent via API is stored with the comma
replaced by a space.
- [ ] 20 long tags sent via API does not error (length cap silently
truncates).
- [ ] "Tags" filter in the filter bar shows counts and narrows the list.
- [ ] Filtering by `ml` does **not** return agents tagged `ml-ops`.
- [ ] UI in Chinese shows 编辑标签 / 添加标签以整理和筛选你的智能体 etc.
- [ ] `PUT /v1/agent/<other-tenant-id>/tags` returns `Agent not found or
no permission.`
2026-05-13 21:41:32 +08:00

195 lines
4.4 KiB
TypeScript

import {
IAgentLogsRequest,
IPipeLineListRequest,
} from '@/interfaces/database/agent';
import { IAgentWebhookTraceRequest } from '@/interfaces/request/agent';
import api from '@/utils/api';
import { registerNextServer } from '@/utils/register-server';
import request from '@/utils/request';
const {
createAgent,
updateAgent: updateAgentApi,
listAgents,
deleteAgent,
agentChatCompletion,
resetAgent,
listAgentTemplate,
testDbConnect,
getInputElements,
trace,
fetchVersionList,
fetchVersion,
getAgent,
fetchAgentSessions,
fetchExternalAgentInputs,
prompt,
cancelDataflow,
cancelCanvas,
} = api;
const methods = {
getAgent: {
url: getAgent,
method: 'get',
},
createAgent: {
url: createAgent,
method: 'post',
},
fetchVersionList: {
url: fetchVersionList,
method: 'get',
},
fetchVersion: {
url: (config: { agentId: string; versionId: string }) =>
fetchVersion(config.agentId, config.versionId),
method: 'get',
},
listAgents: {
url: listAgents,
method: 'get',
},
listAgentTags: {
url: api.listAgentTags,
method: 'get',
},
resetAgent: {
url: resetAgent,
method: 'post',
},
deleteAgent: {
url: deleteAgent,
method: 'delete',
},
agentChatCompletion: {
url: agentChatCompletion,
method: 'post',
},
listAgentTemplate: {
url: listAgentTemplate,
method: 'get',
},
testDbConnect: {
url: testDbConnect,
method: 'post',
},
getInputElements: {
url: getInputElements,
method: 'get',
},
debugSingle: {
url: (config: { agentId: string; componentId: string }) =>
api.debug(config.agentId, config.componentId),
method: 'post',
},
uploadAgentFile: {
url: (config: { agentId: string }) => api.uploadAgentFile(config.agentId),
method: 'post',
},
trace: {
url: (config: { agentId: string; messageId: string }) =>
trace(config.agentId, config.messageId),
method: 'get',
},
inputForm: {
url: (config: { agentId: string; componentId: string }) =>
api.inputForm(config.agentId, config.componentId),
method: 'get',
},
fetchAgentLogs: {
url: fetchAgentSessions,
method: 'get',
},
fetchExternalAgentInputs: {
url: fetchExternalAgentInputs,
method: 'get',
},
fetchPrompt: {
url: prompt,
method: 'get',
},
cancelDataflow: {
url: cancelDataflow,
method: 'post',
},
cancelCanvas: {
url: cancelCanvas,
method: 'post',
},
createAgentSession: {
url: api.createAgentSession,
method: 'post',
},
} as const;
const agentService = registerNextServer<keyof typeof methods>(methods);
export const updateAgent = (
agentId: string,
params: {
title?: string;
dsl?: Record<string, any>;
avatar?: string;
description?: string | null;
permission?: string;
release?: string;
},
) => {
return request(updateAgentApi(agentId), { method: 'put', data: params });
};
export const updateAgentTags = (agentId: string, tags: string[]) => {
return request(api.updateAgentTags(agentId), {
method: 'put',
data: { tags: tags.join(',') },
});
};
export const fetchTrace = (data: { canvas_id: string; message_id: string }) => {
return request.get(
methods.trace.url({
agentId: data.canvas_id,
messageId: data.message_id,
}),
);
};
export const fetchAgentLogsByCanvasId = (
canvasId: string,
params: IAgentLogsRequest,
) => {
return request.get(methods.fetchAgentLogs.url(canvasId), { params: params });
};
export const fetchAgentLogsById = (canvasId: string, sessionId: string) => {
return request.get(api.fetchAgentSessionById(canvasId, sessionId));
};
export const fetchPipeLineList = (params: IPipeLineListRequest) => {
return request.get(api.listAgents, { params: params });
};
export const fetchWebhookTrace = (
id: string,
params: IAgentWebhookTraceRequest,
) => {
return request.get(api.fetchWebhookTrace(id), { params: params });
};
export function createAgentSession({ id, name }: { id: string; name: string }) {
return request.post(api.createAgentSession(id), { data: { name } });
}
export const deleteAgentSession = (canvasId: string, sessionId: string) => {
return request.delete(api.fetchAgentSessionById(canvasId, sessionId));
};
export const uploadAgentFile = (agentId: string, data: FormData) => {
return request(api.uploadAgentFile(agentId), {
method: 'post',
data,
});
};
export default agentService;