diff --git a/api/apps/restful_apis/agent_api.py b/api/apps/restful_apis/agent_api.py index 127b9f386a..c886ce6a6c 100644 --- a/api/apps/restful_apis/agent_api.py +++ b/api/apps/restful_apis/agent_api.py @@ -681,6 +681,18 @@ _COMPILATION_TEMPLATE_GROUP_CATEGORY = "compilation_template_group" @login_required @add_tenant_id_to_kwargs def list_agents(tenant_id): + if request.args.get("type") == "filter": + tenants = TenantService.get_joined_tenants_by_user_id(tenant_id) + joined_tenant_ids = list({member["tenant_id"] for member in tenants} | {tenant_id}) + owners = UserCanvasService.get_owner_filter(joined_tenant_ids, tenant_id) + categories = UserCanvasService.get_category_filter(joined_tenant_ids, tenant_id) + return get_json_result( + data={ + "filter": {"owner": owners, "canvas_category": categories}, + "total": sum(owner["count"] for owner in owners), + } + ) + keywords = request.args.get("keywords", "") canvas_category_list = [item for item in request.args.get("canvas_category", "").strip().split(",") if item] canvas_type = request.args.get("canvas_type") @@ -707,6 +719,7 @@ def list_agents(tenant_id): effective_owner_ids = list(requested_owner_ids) else: effective_owner_ids = list(authorized_owner_ids) + include_template_groups = tenant_id in effective_owner_ids # Groups-only: when ``compilation_template_group`` is the only selected # category, return just the caller's template groups (no agents) via @@ -715,11 +728,12 @@ def list_agents(tenant_id): if canvas_category_list == [_COMPILATION_TEMPLATE_GROUP_CATEGORY]: from api.db.services.compilation_template_group_service import CompilationTemplateGroupService - try: - groups = CompilationTemplateGroupService.list_saved(tenant_id, keywords, "", order_by, desc) - except Exception: - logging.exception("list_agents: compilation template group list failed for tenant=%s", tenant_id) - groups = [] + groups = [] + if include_template_groups: + try: + groups = CompilationTemplateGroupService.list_saved(tenant_id, keywords, "", order_by, desc) + except Exception: + logging.exception("list_agents: compilation template group list failed for tenant=%s", tenant_id) for group in groups: group["type"] = _COMPILATION_TEMPLATE_GROUP_CATEGORY total = len(groups) @@ -758,11 +772,12 @@ def list_agents(tenant_id): ) # Groups are owner-only (no team sharing), so they're scoped to the # caller. Keyword filters the group name; scope is left unfiltered. - try: - groups = CompilationTemplateGroupService.list_saved(tenant_id, keywords, "", order_by, desc) - except Exception: - logging.exception("list_agents: compilation template group merge failed for tenant=%s", tenant_id) - groups = [] + groups = [] + if include_template_groups: + try: + groups = CompilationTemplateGroupService.list_saved(tenant_id, keywords, "", order_by, desc) + except Exception: + logging.exception("list_agents: compilation template group merge failed for tenant=%s", tenant_id) items: list[dict] = [] for agent in agents: @@ -802,11 +817,12 @@ def list_agents(tenant_id): tags, canvas_type, ) - try: - groups = CompilationTemplateGroupService.list_saved(tenant_id, keywords, "", order_by, desc) - except Exception: - logging.exception("list_agents: compilation template group mixed failed for tenant=%s", tenant_id) - groups = [] + groups = [] + if include_template_groups: + try: + groups = CompilationTemplateGroupService.list_saved(tenant_id, keywords, "", order_by, desc) + except Exception: + logging.exception("list_agents: compilation template group mixed failed for tenant=%s", tenant_id) items = [] for agent in agents: diff --git a/api/db/services/canvas_service.py b/api/db/services/canvas_service.py index 4bc3fd9d85..6550638086 100644 --- a/api/db/services/canvas_service.py +++ b/api/db/services/canvas_service.py @@ -21,11 +21,12 @@ from operator import or_ from uuid import uuid4 from agent.canvas import Canvas from api.db import CanvasCategory, TenantPermission -from api.db.db_models import DB, CanvasTemplate, User, UserCanvas, API4Conversation, UserCanvasVersion +from api.db.db_models import DB, CanvasTemplate, CompilationTemplateGroup, User, UserCanvas, API4Conversation, UserCanvasVersion from api.db.services.api_service import API4ConversationService from api.db.services.common_service import CommonService from api.db.services.user_canvas_version import UserCanvasVersionService from common.misc_utils import get_uuid, thread_pool_exec +from common.constants import StatusEnum from api.utils.api_utils import get_data_openai import tiktoken from peewee import fn @@ -197,6 +198,63 @@ class UserCanvasService(CommonService): return agents_list, count + @classmethod + @DB.connection_context() + def get_owner_filter(cls, joined_tenant_ids, user_id): + owner_filter = cls.model.user_id.in_(joined_tenant_ids) & ((cls.model.permission == TenantPermission.TEAM.value) | (cls.model.user_id == user_id)) + owners = ( + cls.model.select( + cls.model.user_id.alias("id"), + User.nickname.alias("label"), + fn.COUNT(cls.model.id).alias("count"), + ) + .join(User, on=(cls.model.user_id == User.id)) + .where(owner_filter) + .group_by(cls.model.user_id, User.nickname) + ) + owner_list = list(owners.dicts()) + group_count = ( + CompilationTemplateGroup.select() + .where( + CompilationTemplateGroup.tenant_id == user_id, + CompilationTemplateGroup.status == StatusEnum.VALID.value, + ) + .count() + ) + if group_count: + current_owner = next((owner for owner in owner_list if owner["id"] == user_id), None) + if current_owner: + current_owner["count"] += group_count + else: + nickname = User.select(User.nickname).where(User.id == user_id).scalar() + owner_list.append({"id": user_id, "label": nickname or "", "count": group_count}) + return owner_list + + @classmethod + @DB.connection_context() + def get_category_filter(cls, joined_tenant_ids, user_id): + category_filter = cls.model.user_id.in_(joined_tenant_ids) & ((cls.model.permission == TenantPermission.TEAM.value) | (cls.model.user_id == user_id)) + categories = ( + cls.model.select( + cls.model.canvas_category.alias("id"), + fn.COUNT(cls.model.id).alias("count"), + ) + .where(category_filter) + .group_by(cls.model.canvas_category) + ) + category_list = list(categories.dicts()) + group_count = ( + CompilationTemplateGroup.select() + .where( + CompilationTemplateGroup.tenant_id == user_id, + CompilationTemplateGroup.status == StatusEnum.VALID.value, + ) + .count() + ) + if group_count: + category_list.append({"id": "compilation_template_group", "count": group_count}) + return category_list + @classmethod @DB.connection_context() def list_tags(cls, joined_tenant_ids, user_id, canvas_category=None): diff --git a/web/src/hooks/use-agent-request.ts b/web/src/hooks/use-agent-request.ts index 91f79e4c86..86e51d3e48 100644 --- a/web/src/hooks/use-agent-request.ts +++ b/web/src/hooks/use-agent-request.ts @@ -50,6 +50,7 @@ export const enum AgentApiAction { FetchAgentListByPage = 'fetchAgentListByPage', FetchAllAgentList = 'fetchAllAgentList', FetchAgentList = 'fetchAgentList', + FetchAgentFilters = 'fetchAgentFilters', UpdateAgentSetting = 'updateAgentSetting', DeleteAgent = 'deleteAgent', FetchAgentDetail = 'fetchAgentDetail', @@ -84,9 +85,27 @@ export const enum AgentApiAction { FetchBuiltinPipelineDetail = 'fetchBuiltinPipelineDetail', } +const AgentKeys = { + templates: () => [AgentApiAction.FetchAgentTemplates] as const, + list: (params?: unknown) => + params === undefined + ? ([AgentApiAction.FetchAgentListByPage] as const) + : ([AgentApiAction.FetchAgentListByPage, params] as const), + all: () => [AgentApiAction.FetchAllAgentList] as const, + listAll: (canvasCategory?: string) => + [AgentApiAction.FetchAgentList, canvasCategory] as const, + filters: () => [AgentApiAction.FetchAgentFilters] as const, + tags: (canvasCategory?: string) => + canvasCategory === undefined + ? ([AgentApiAction.FetchAgentTags] as const) + : ([AgentApiAction.FetchAgentTags, canvasCategory] as const), + detail: (agentId?: string) => + [AgentApiAction.FetchAgentDetail, agentId] as const, +}; + export const useFetchAgentTemplates = () => { const { data } = useQuery({ - queryKey: [AgentApiAction.FetchAgentTemplates], + queryKey: AgentKeys.templates(), initialData: [], queryFn: async () => { const { data } = await agentService.listAgentTemplate(); @@ -158,14 +177,11 @@ export const useFetchAgentListByPage = () => { canvas: AgentListItem[]; total: number; }>({ - queryKey: [ - AgentApiAction.FetchAgentListByPage, - { - debouncedSearchString, - ...pagination, - filterValue, - }, - ], + queryKey: AgentKeys.list({ + debouncedSearchString, + ...pagination, + filterValue, + }), placeholderData: (previousData) => { if (previousData === undefined) { return { canvas: [], total: 0 }; @@ -207,7 +223,7 @@ export const useFetchAgentListByPage = () => { export function useFetchAllAgentList() { const { data, isFetching: loading } = useQuery({ - queryKey: [AgentApiAction.FetchAllAgentList], + queryKey: AgentKeys.all(), queryFn: async () => { const { data } = await agentService.listAgents( { @@ -246,7 +262,7 @@ export const useUpdateAgentSetting = () => { if (ret?.data?.code === 0) { message.success('success'); queryClient.invalidateQueries({ - queryKey: [AgentApiAction.FetchAgentListByPage], + queryKey: AgentKeys.list(), }); } return ret?.data?.code; @@ -288,7 +304,10 @@ export const useDuplicateAgent = () => { if (data?.code === 0) { message.success(i18n.t('message.created')); queryClient.invalidateQueries({ - queryKey: [AgentApiAction.FetchAgentListByPage], + queryKey: AgentKeys.list(), + }); + queryClient.invalidateQueries({ + queryKey: AgentKeys.filters(), }); return data; } @@ -321,7 +340,10 @@ export const useDeleteAgent = () => { const { data } = await agentService.deleteAgent(agentId); if (data.code === 0) { queryClient.invalidateQueries({ - queryKey: [AgentApiAction.FetchAgentListByPage], + queryKey: AgentKeys.list(), + }); + queryClient.invalidateQueries({ + queryKey: AgentKeys.filters(), }); } return data?.data ?? false; @@ -338,7 +360,7 @@ export interface IAgentTagCount { export const useFetchAgentTags = (canvasCategory?: string) => { const { data, isFetching: loading } = useQuery({ - queryKey: [AgentApiAction.FetchAgentTags, canvasCategory], + queryKey: AgentKeys.tags(canvasCategory), initialData: [], gcTime: 0, queryFn: async () => { @@ -368,10 +390,10 @@ export const useUpdateAgentTags = () => { const { data } = await updateAgentTags(agentId, tags); if (data?.code === 0) { queryClient.invalidateQueries({ - queryKey: [AgentApiAction.FetchAgentListByPage], + queryKey: AgentKeys.list(), }); queryClient.invalidateQueries({ - queryKey: [AgentApiAction.FetchAgentTags], + queryKey: AgentKeys.tags(), }); } else { message.error(data?.message || 'Update failed'); @@ -395,7 +417,7 @@ export const useFetchAgent = (): { isFetching: loading, refetch, } = useQuery({ - queryKey: [AgentApiAction.FetchAgentDetail, sharedId || id], + queryKey: AgentKeys.detail(sharedId || id), initialData: {} as IFlow, refetchOnReconnect: false, refetchOnMount: false, @@ -484,11 +506,16 @@ export const useSetAgent = ( ); } queryClient.invalidateQueries({ - queryKey: [AgentApiAction.FetchAgentListByPage], + queryKey: AgentKeys.list(), }); + if (!agentId) { + queryClient.invalidateQueries({ + queryKey: AgentKeys.filters(), + }); + } if (agentId && !skipInvalidation) { queryClient.invalidateQueries({ - queryKey: [AgentApiAction.FetchAgentDetail, agentId], + queryKey: AgentKeys.detail(agentId), }); } } @@ -860,7 +887,7 @@ export const useFetchAgentList = ({ canvas: AgentListItem[]; total: number; }>({ - queryKey: [AgentApiAction.FetchAgentList], + queryKey: AgentKeys.listAll(canvas_category), initialData: { canvas: [], total: 0 }, gcTime: 0, queryFn: async () => { @@ -875,6 +902,42 @@ export const useFetchAgentList = ({ return { data, loading }; }; +export interface IAgentOwnerFilter { + id: string; + label: string; + count: number; +} + +export interface IAgentCategoryFilter { + id: string; + count: number; +} + +export const useFetchAgentFilters = () => { + const { data, isFetching: loading } = useQuery<{ + filter: { + owner: IAgentOwnerFilter[]; + canvas_category: IAgentCategoryFilter[]; + }; + total: number; + }>({ + queryKey: AgentKeys.filters(), + initialData: { filter: { owner: [], canvas_category: [] }, total: 0 }, + gcTime: 0, + queryFn: async () => { + const { data } = await agentService.listAgents( + { params: { type: 'filter' } }, + true, + ); + return ( + data?.data ?? { filter: { owner: [], canvas_category: [] }, total: 0 } + ); + }, + }); + + return { data: data.filter, loading }; +}; + export const BuiltinPipelineKeys = { list: (type: string) => [AgentApiAction.FetchBuiltinPipelineList, type] as const, diff --git a/web/src/pages/agents/hooks/use-select-filters.ts b/web/src/pages/agents/hooks/use-select-filters.ts index 1bbc6b4ed7..e6bc32682c 100644 --- a/web/src/pages/agents/hooks/use-select-filters.ts +++ b/web/src/pages/agents/hooks/use-select-filters.ts @@ -1,30 +1,18 @@ import { FilterCollection } from '@/components/list-filter-bar/interface'; import { AgentCategory } from '@/constants/agent'; import { - useFetchAgentList, + useFetchAgentFilters, useFetchAgentTags, } from '@/hooks/use-agent-request'; -import { AgentListItemType, IFlow } from '@/interfaces/database/agent'; -import { buildOwnersFilter } from '@/utils/list-filter-util'; +import { AgentListItemType } from '@/interfaces/database/agent'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; export function useSelectFilters() { const { t } = useTranslation(); - const { data } = useFetchAgentList({}); + const { data: agentFilters } = useFetchAgentFilters(); const { data: tagCounts } = useFetchAgentTags(); - // The merged /agents list also contains compilation template groups, which - // have no owner fields — drop them before building the owner filter. - const agents = useMemo(() => { - const canvas = (data?.canvas ?? []) as Array< - IFlow & { type?: AgentListItemType } - >; - return canvas.filter( - (x) => x.type !== AgentListItemType.CompilationTemplateGroup, - ); - }, [data?.canvas]); - const tagList = useMemo( () => (tagCounts ?? []).map((t) => ({ @@ -36,21 +24,37 @@ export function useSelectFilters() { ); const filters: FilterCollection[] = [ - buildOwnersFilter(agents, undefined, t('common.owner')), + { + field: 'owner', + list: agentFilters.owner, + label: t('common.owner'), + }, { field: 'canvasCategory', list: [ { id: AgentCategory.DataflowCanvas, label: t('flow.tabList.ingestionPipeline'), + count: + agentFilters.canvas_category.find( + (item) => item.id === AgentCategory.DataflowCanvas, + )?.count ?? 0, }, { id: AgentListItemType.CompilationTemplateGroup, label: t('flow.tabList.compilationOperator'), + count: + agentFilters.canvas_category.find( + (item) => item.id === AgentListItemType.CompilationTemplateGroup, + )?.count ?? 0, }, { id: AgentCategory.AgentCanvas, label: t('flow.tabList.workflow'), + count: + agentFilters.canvas_category.find( + (item) => item.id === AgentCategory.AgentCanvas, + )?.count ?? 0, }, ], label: t('flow.canvasCategory'),