Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions api/apps/restful_apis/agent_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ def list_agents(tenant_id):
keywords = request.args.get("keywords", "")
canvas_category = request.args.get("canvas_category")
owner_ids = [item for item in request.args.get("owner_ids", "").strip().split(",") if item]
tags = [item for item in request.args.get("tags", "").strip().split(",") if item]

page_number = int(request.args.get("page", 0))
items_per_page = int(request.args.get("page_size", 0))
Expand Down Expand Up @@ -347,11 +348,40 @@ def list_agents(tenant_id):
desc,
keywords,
canvas_category,
tags,
)

return get_json_result(data={"canvas": canvas, "total": total})


@manager.route("/agents/tags", methods=["GET"]) # noqa: F821
@login_required
@add_tenant_id_to_kwargs
def list_agent_tags(tenant_id):
"""Aggregate tag usage counts across agents visible to the caller."""
canvas_category = request.args.get("canvas_category")
tenants = TenantService.get_joined_tenants_by_user_id(tenant_id)
joined_ids = list({member["tenant_id"] for member in tenants} | {tenant_id})
counts = UserCanvasService.list_tags(joined_ids, tenant_id, canvas_category)
return get_json_result(data=[{"tag": k, "count": v} for k, v in sorted(counts.items(), key=lambda x: (-x[1], x[0]))])


@manager.route("/agent/<canvas_id>/tags", methods=["PUT"]) # noqa: F821
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@manager.route("/agents/<canvas_id>/tags", methods=["PUT"])

I found a bug in my testing. We need to add an s here.

@login_required
@add_tenant_id_to_kwargs
async def update_agent_tags(tenant_id, canvas_id):
if not UserCanvasService.accessible(canvas_id, tenant_id):
return get_json_result(
data=False,
message="Agent not found or no permission.",
code=RetCode.OPERATING_ERROR,
)
req = await get_request_json()
tags = req.get("tags", "")
UserCanvasService.update_tags(canvas_id, tags)
return get_json_result(data=True)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated


@manager.route("/agents", methods=["POST"]) # noqa: F821
@login_required
@add_tenant_id_to_kwargs
Expand Down
2 changes: 2 additions & 0 deletions api/db/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,7 @@ class UserCanvas(DataBaseModel):
description = TextField(null=True, help_text="Canvas description")
canvas_type = CharField(max_length=32, null=True, help_text="Canvas type", index=True)
canvas_category = CharField(max_length=32, null=False, default="agent_canvas", help_text="Canvas category: agent_canvas|dataflow_canvas", index=True)
tags = CharField(max_length=512, null=False, default="", help_text="Comma-separated tags for organizing agents", index=True)
dsl = JSONField(null=True, default={})

class Meta:
Expand Down Expand Up @@ -1647,6 +1648,7 @@ def migrate_db():
alter_db_add_column(migrator, "memory", "tenant_embd_id", IntegerField(null=True, help_text="id in tenant_llm", index=True))
alter_db_add_column(migrator, "memory", "tenant_llm_id", IntegerField(null=True, help_text="id in tenant_llm", index=True))
alter_db_add_column(migrator, "user_canvas_version", "release", BooleanField(null=False, help_text="is released", default=False, index=True))
alter_db_add_column(migrator, "user_canvas", "tags", CharField(max_length=512, null=False, default="", help_text="Comma-separated tags for organizing agents", index=True))
alter_db_add_column(migrator, "api_4_conversation", "version_title", CharField(max_length=255, null=True, help_text="canvas version title when session created", index=False))
alter_db_column_type(migrator, "document", "size", BigIntegerField(default=0, index=True))
alter_db_column_type(migrator, "file", "size", BigIntegerField(default=0, index=True))
Expand Down
61 changes: 61 additions & 0 deletions api/db/services/canvas_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import json
import logging
import time
from functools import reduce
from operator import or_
from uuid import uuid4
from agent.canvas import Canvas
from api.db import CanvasCategory, TenantPermission
Expand Down Expand Up @@ -149,6 +151,7 @@ def get_by_tenant_ids(
desc,
keywords,
canvas_category=None,
tags=None,
):
fields = [
cls.model.id,
Expand All @@ -161,6 +164,7 @@ def get_by_tenant_ids(
User.avatar.alias('tenant_avatar'),
cls.model.update_time,
cls.model.canvas_category,
cls.model.tags,
]
if keywords:
agents = cls.model.select(*fields).join(User, on=(cls.model.user_id == User.id)).where(
Expand All @@ -173,6 +177,13 @@ def get_by_tenant_ids(
)
if canvas_category:
agents = agents.where(cls.model.canvas_category == canvas_category)
if tags:
tag_list = [t.strip() for t in tags if t and t.strip()] if isinstance(tags, (list, tuple)) else [t.strip() for t in str(tags).split(",") if t.strip()]
if tag_list:
# Wrap value with commas so 'ml' doesn't match 'ml-ops'.
wrapped = fn.CONCAT(",", cls.model.tags, ",")
clauses = [wrapped.contains(f",{t},") for t in tag_list]
agents = agents.where(reduce(or_, clauses))
if desc:
agents = agents.order_by(cls.model.getter_by(orderby).desc())
else:
Expand All @@ -199,6 +210,56 @@ def get_by_tenant_ids(

return agents_list, count

@classmethod
@DB.connection_context()
def list_tags(cls, joined_tenant_ids, user_id, canvas_category=None):
"""Return {tag: agent_count} aggregated across agents visible to the user."""
query = cls.model.select(cls.model.tags).where(
((cls.model.user_id.in_(joined_tenant_ids)) & (cls.model.permission == TenantPermission.TEAM.value)) | (cls.model.user_id == user_id)
)
if canvas_category:
query = query.where(cls.model.canvas_category == canvas_category)

counts: dict[str, int] = {}
for row in query.dicts():
for t in (row.get("tags") or "").split(","):
t = t.strip()
if t:
counts[t] = counts.get(t, 0) + 1
return counts

# Tag storage is a single comma-separated CharField(max_length=512);
# commas inside a tag would corrupt the encoding, so strip them on write.
TAGS_FIELD_MAX = 512
TAG_MAX_LEN = 64

@classmethod
@DB.connection_context()
def update_tags(cls, canvas_id, tags):
"""Persist a normalized comma-separated tag string for the given canvas."""
if isinstance(tags, (list, tuple)):
cleaned = [str(t).replace(",", " ").strip() for t in tags if t and str(t).strip()]
else:
cleaned = [t.strip() for t in str(tags or "").split(",") if t.strip()]
# Dedupe (case-insensitive, preserve order), cap individual tag length,
# then truncate the joined value so it always fits the column.
seen = set()
normalized = []
used = 0
for t in cleaned:
t = t[: cls.TAG_MAX_LEN]
key = t.lower()
if key in seen:
continue
extra = len(t) + (1 if normalized else 0)
if used + extra > cls.TAGS_FIELD_MAX:
break
seen.add(key)
normalized.append(t)
used += extra
value = ",".join(normalized)
return cls.model.update(tags=value).where(cls.model.id == canvas_id).execute()

@classmethod
@DB.connection_context()
def accessible(cls, canvas_id, tenant_id):
Expand Down
3 changes: 3 additions & 0 deletions web/src/components/home-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface IProps {
icon?: React.ReactNode;
testId?: string;
showReleaseTime?: boolean;
extra?: ReactNode;
}

function Time({ time }: { time: string | number | undefined }) {
Expand All @@ -31,6 +32,7 @@ export function HomeCard({
icon,
testId,
showReleaseTime = false,
extra,
}: IProps) {
const { t } = useTranslation();

Expand Down Expand Up @@ -81,6 +83,7 @@ export function HomeCard({
<div className="whitespace-nowrap overflow-hidden text-ellipsis">
{data.description}
</div>
{extra}
<div className="flex justify-between items-center">
{showReleaseTime ? (
<section className="text-sm text-text-secondary space-y-1">
Expand Down
61 changes: 61 additions & 0 deletions web/src/hooks/use-agent-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import agentService, {
fetchTrace,
fetchWebhookTrace,
updateAgent,
updateAgentTags,
uploadAgentFile,
} from '@/services/agent-service';
import { buildMessageListWithUuid } from '@/utils/chat';
Expand Down Expand Up @@ -73,6 +74,8 @@ export const enum AgentApiAction {
FetchSessionByIdManually = 'fetchSessionByIdManually',
FetchAgentLog = 'fetchAgentLog',
FetchSharedAgent = 'fetchSharedAgent',
FetchAgentTags = 'fetchAgentTags',
UpdateAgentTags = 'updateAgentTags',
}

export const useFetchAgentTemplates = () => {
Expand All @@ -95,12 +98,14 @@ const buildAgentListParams = ({
keywords,
canvasCategory,
ownerIds,
tags,
}: {
page: number;
pageSize: number;
keywords?: string;
canvasCategory?: string;
ownerIds?: string[];
tags?: string[];
}) => {
const params: Record<string, unknown> = {
page,
Expand All @@ -116,6 +121,9 @@ const buildAgentListParams = ({
if (Array.isArray(ownerIds) && ownerIds.length > 0) {
params.owner_ids = ownerIds.join(',');
}
if (Array.isArray(tags) && tags.length > 0) {
params.tags = tags.join(',');
}

return params;
};
Expand All @@ -129,13 +137,15 @@ export const useFetchAgentListByPage = () => {
? filterValue.canvasCategory
: [];
const owner = filterValue.owner;
const tags = Array.isArray(filterValue.tags) ? filterValue.tags : undefined;

const requestParams = buildAgentListParams({
page: pagination.current,
pageSize: pagination.pageSize,
keywords: debouncedSearchString,
canvasCategory: canvasCategory.length === 1 ? canvasCategory[0] : undefined,
ownerIds: Array.isArray(owner) ? owner : undefined,
tags,
});

const { data, isFetching: loading } = useQuery<{
Expand Down Expand Up @@ -264,6 +274,57 @@ export const useDeleteAgent = () => {
return { data, loading, deleteAgent: mutateAsync };
};

export interface IAgentTagCount {
tag: string;
count: number;
}

export const useFetchAgentTags = (canvasCategory?: string) => {
const { data, isFetching: loading } = useQuery<IAgentTagCount[]>({
queryKey: [AgentApiAction.FetchAgentTags, canvasCategory],
initialData: [],
gcTime: 0,
queryFn: async () => {
const { data } = await agentService.listAgentTags(
{
params: canvasCategory ? { canvas_category: canvasCategory } : {},
},
true,
);
return data?.data ?? [];
},
});
return { data, loading };
};

export const useUpdateAgentTags = () => {
const queryClient = useQueryClient();
const { isPending: loading, mutateAsync } = useMutation({
mutationKey: [AgentApiAction.UpdateAgentTags],
mutationFn: async ({
agentId,
tags,
}: {
agentId: string;
tags: string[];
}) => {
const { data } = await updateAgentTags(agentId, tags);
if (data?.code === 0) {
queryClient.invalidateQueries({
queryKey: [AgentApiAction.FetchAgentListByPage],
});
queryClient.invalidateQueries({
queryKey: [AgentApiAction.FetchAgentTags],
});
} else {
message.error(data?.message || 'Update failed');
}
return data?.code === 0;
},
});
return { loading, updateAgentTags: mutateAsync };
};

export const useFetchAgent = (): {
data: IFlow;
loading: boolean;
Expand Down
1 change: 1 addition & 0 deletions web/src/interfaces/database/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export declare interface IFlow {
release_time?: number;
last_publish_time?: number;
datasets?: Pick<IDataset, 'id' | 'name' | 'avatar'>[];
tags?: string;
}

export interface IFlowTemplate {
Expand Down
4 changes: 4 additions & 0 deletions web/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1693,6 +1693,10 @@ Example: Virtual Hosted Style`,
author: 'Author',
sectionTitle: 'Section title',
},
editTags: 'Edit tags',
editTagsDescription:
'Add tags to organize and filter your agents. Press Enter or comma to add.',
tagsPlaceholder: 'Add a tag and press Enter',
includeHeadingContent: 'Separate parent-heading content',
includeHeadingContentTip:
'When enabled, chunks include only their heading path and content; content immediately following a parent heading is kept as a separate chunk.',
Expand Down
3 changes: 3 additions & 0 deletions web/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1420,6 +1420,9 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
author: '作者',
sectionTitle: '章节标题',
},
editTags: '编辑标签',
editTagsDescription: '添加标签以整理和筛选你的智能体。按回车或逗号添加。',
tagsPlaceholder: '输入标签后按回车',
includeHeadingContent: '分离上级标题正文',
includeHeadingContentTip:
'启用后,每个分块仅保留标题路径和自身内容,与上级标题紧挨着的内容将作为一个独立的块保留。',
Expand Down
19 changes: 19 additions & 0 deletions web/src/pages/agents/agent-card.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HomeCard } from '@/components/home-card';
import { MoreButton } from '@/components/more-button';
import { SharedBadge } from '@/components/shared-badge';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { AgentCategory } from '@/constants/agent';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
Expand All @@ -13,6 +14,23 @@ export type DatasetCardProps = {
data: IFlow;
} & Pick<ReturnType<typeof useRenameAgent>, 'showAgentRenameModal'>;

function AgentTags({ tags }: { tags?: string }) {
const list = (tags || '')
.split(',')
.map((t) => t.trim())
.filter(Boolean);
if (list.length === 0) return null;
return (
<div className="flex flex-wrap gap-1 mt-1">
{list.map((tag) => (
<Badge key={tag} variant="secondary" className="text-xs font-normal">
{tag}
</Badge>
))}
</div>
);
}

export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) {
const { navigateToAgent } = useNavigatePage();

Expand Down Expand Up @@ -44,6 +62,7 @@ export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) {
</Button>
)
}
extra={<AgentTags tags={data.tags} />}
showReleaseTime
/>
);
Expand Down
Loading