From 1bf6a5e0a2b7e7457c47f5275e4ae5ce9384291d Mon Sep 17 00:00:00 2001 From: Kavirubc Date: Thu, 18 Dec 2025 20:31:46 +0530 Subject: [PATCH 1/7] feat(amp-api): add agent filter model and repository method --- agent-manager-service/models/agent_filter.go | 37 +++++++++++++ agent-manager-service/repositories/agent.go | 57 ++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 agent-manager-service/models/agent_filter.go diff --git a/agent-manager-service/models/agent_filter.go b/agent-manager-service/models/agent_filter.go new file mode 100644 index 000000000..6159a1464 --- /dev/null +++ b/agent-manager-service/models/agent_filter.go @@ -0,0 +1,37 @@ +// Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package models + +// AgentFilter holds filter options for listing agents +type AgentFilter struct { + Search string // search in name, displayName, description + ProvisioningType string // "internal", "external" + SortBy string // "name", "createdAt", "updatedAt" + SortOrder string // "asc", "desc" + Limit int + Offset int +} + +// DefaultAgentFilter returns filter with sensible defaults +func DefaultAgentFilter() AgentFilter { + return AgentFilter{ + SortBy: "created_at", + SortOrder: "desc", + Limit: 20, + Offset: 0, + } +} diff --git a/agent-manager-service/repositories/agent.go b/agent-manager-service/repositories/agent.go index cfbbad2ac..8c4562bd6 100644 --- a/agent-manager-service/repositories/agent.go +++ b/agent-manager-service/repositories/agent.go @@ -29,6 +29,7 @@ import ( type AgentRepository interface { ListAgents(ctx context.Context, orgId uuid.UUID, projectId uuid.UUID) ([]*models.Agent, error) + ListAgentsWithFilter(ctx context.Context, orgId uuid.UUID, projectId uuid.UUID, filter models.AgentFilter) ([]*models.Agent, int64, error) GetAgentByName(ctx context.Context, orgId uuid.UUID, projectId uuid.UUID, agentName string) (*models.Agent, error) CreateAgent(ctx context.Context, agent *models.Agent) error SoftDeleteAgentByName(ctx context.Context, orgId uuid.UUID, projectId uuid.UUID, agentName string) error @@ -56,6 +57,62 @@ func (r *agentRepository) ListAgents(ctx context.Context, orgId uuid.UUID, proje return agents, nil } +func (r *agentRepository) ListAgentsWithFilter(ctx context.Context, orgId uuid.UUID, projectId uuid.UUID, filter models.AgentFilter) ([]*models.Agent, int64, error) { + var agents []*models.Agent + var total int64 + + query := db.DB(ctx).Model(&models.Agent{}).Where("org_id = ? AND project_id = ?", orgId, projectId) + + // Apply search filter (case-insensitive search on name, display_name, description) + if filter.Search != "" { + searchPattern := "%" + filter.Search + "%" + query = query.Where("name ILIKE ? OR display_name ILIKE ? OR description ILIKE ?", searchPattern, searchPattern, searchPattern) + } + + // Apply provisioning type filter + if filter.ProvisioningType != "" { + query = query.Where("provisioning_type = ?", filter.ProvisioningType) + } + + // Get total count before pagination + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("agentRepository.ListAgentsWithFilter count: %w", err) + } + + // Apply sorting + sortColumn := "created_at" + if filter.SortBy == "name" || filter.SortBy == "updatedAt" || filter.SortBy == "createdAt" { + switch filter.SortBy { + case "name": + sortColumn = "name" + case "updatedAt": + sortColumn = "updated_at" + case "createdAt": + sortColumn = "created_at" + } + } + sortOrder := "DESC" + if filter.SortOrder == "asc" { + sortOrder = "ASC" + } + query = query.Order(fmt.Sprintf("%s %s", sortColumn, sortOrder)) + + // Apply pagination + if filter.Limit > 0 { + query = query.Limit(filter.Limit) + } + if filter.Offset > 0 { + query = query.Offset(filter.Offset) + } + + // Execute query with preload + if err := query.Preload("AgentDetails").Find(&agents).Error; err != nil { + return nil, 0, fmt.Errorf("agentRepository.ListAgentsWithFilter: %w", err) + } + + return agents, total, nil +} + func (r *agentRepository) GetAgentByName(ctx context.Context, orgId uuid.UUID, projectId uuid.UUID, agentName string) (*models.Agent, error) { var agent models.Agent if err := db.DB(ctx). From 84168f7de0e0dc3f45412af79f4e0cad1c002fdc Mon Sep 17 00:00:00 2001 From: Kavirubc Date: Thu, 18 Dec 2025 20:36:40 +0530 Subject: [PATCH 2/7] feat(amp-api): update service and controller to use filter --- .../controllers/agent_controller.go | 19 +++++++++- .../services/agent_manager.go | 38 +++++++------------ 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/agent-manager-service/controllers/agent_controller.go b/agent-manager-service/controllers/agent_controller.go index 642fe17ef..a663406a3 100644 --- a/agent-manager-service/controllers/agent_controller.go +++ b/agent-manager-service/controllers/agent_controller.go @@ -26,6 +26,7 @@ import ( "github.com/wso2/ai-agent-management-platform/agent-manager-service/middleware/jwtassertion" "github.com/wso2/ai-agent-management-platform/agent-manager-service/middleware/logger" + "github.com/wso2/ai-agent-management-platform/agent-manager-service/models" "github.com/wso2/ai-agent-management-platform/agent-manager-service/services" "github.com/wso2/ai-agent-management-platform/agent-manager-service/spec" "github.com/wso2/ai-agent-management-platform/agent-manager-service/utils" @@ -125,11 +126,27 @@ func (c *agentController) ListAgents(w http.ResponseWriter, r *http.Request) { return } + // Parse filter parameters + search := r.URL.Query().Get("search") + provisioningType := r.URL.Query().Get("provisioningType") + sortBy := r.URL.Query().Get("sortBy") + sortOrder := r.URL.Query().Get("sortOrder") + + // Build filter + filter := models.AgentFilter{ + Search: search, + ProvisioningType: provisioningType, + SortBy: sortBy, + SortOrder: sortOrder, + Limit: limit, + Offset: offset, + } + // Extract user info from JWT token tokenClaims := jwtassertion.GetTokenClaims(ctx) userIdpId := tokenClaims.Sub - agents, total, err := c.agentService.ListAgents(ctx, userIdpId, orgName, projName, int32(limit), int32(offset)) + agents, total, err := c.agentService.ListAgents(ctx, userIdpId, orgName, projName, filter) if err != nil { log.Error("ListAgents: failed to list agents", "error", err) if errors.Is(err, utils.ErrOrganizationNotFound) { diff --git a/agent-manager-service/services/agent_manager.go b/agent-manager-service/services/agent_manager.go index e1d44f973..df23a3540 100644 --- a/agent-manager-service/services/agent_manager.go +++ b/agent-manager-service/services/agent_manager.go @@ -37,7 +37,7 @@ import ( ) type AgentManagerService interface { - ListAgents(ctx context.Context, userIdpId uuid.UUID, orgName string, projName string, limit int32, offset int32) ([]*models.AgentResponse, int32, error) + ListAgents(ctx context.Context, userIdpId uuid.UUID, orgName string, projName string, filter models.AgentFilter) ([]*models.AgentResponse, int32, error) CreateAgent(ctx context.Context, userIdpId uuid.UUID, orgName string, projectName string, req *spec.CreateAgentRequest) error BuildAgent(ctx context.Context, userIdpId uuid.UUID, orgName string, projectName string, agentName string, commitId string) (*models.BuildResponse, error) DeleteAgent(ctx context.Context, userIdpId uuid.UUID, orgName string, projectName string, agentName string) error @@ -126,8 +126,8 @@ func (s *agentManagerService) GetAgent(ctx context.Context, userIdpId uuid.UUID, return s.convertManagedAgentToAgentResponse(ocAgentComponent, agent), nil } -func (s *agentManagerService) ListAgents(ctx context.Context, userIdpId uuid.UUID, orgName string, projName string, limit int32, offset int32) ([]*models.AgentResponse, int32, error) { - s.logger.Info("Listing agents", "orgName", orgName, "projectName", projName, "limit", limit, "offset", offset, "userIdpId", userIdpId) +func (s *agentManagerService) ListAgents(ctx context.Context, userIdpId uuid.UUID, orgName string, projName string, filter models.AgentFilter) ([]*models.AgentResponse, int32, error) { + s.logger.Info("Listing agents", "orgName", orgName, "projectName", projName, "filter", filter, "userIdpId", userIdpId) // Validate organization exists org, err := s.OrganizationRepository.GetOrganizationByOrgName(ctx, userIdpId, orgName) if err != nil { @@ -146,34 +146,22 @@ func (s *agentManagerService) ListAgents(ctx context.Context, userIdpId uuid.UUI s.logger.Error("Failed to find project", "projectName", projName, "orgId", org.ID, "error", err) return nil, 0, fmt.Errorf("failed to find project %s: %w", projName, err) } - // Fetch all agents from the database - agents, err := s.AgentRepository.ListAgents(ctx, org.ID, project.ID) + + // Fetch agents with filter from the database + agents, total, err := s.AgentRepository.ListAgentsWithFilter(ctx, org.ID, project.ID, filter) if err != nil { s.logger.Error("Failed to list agents from repository", "orgId", org.ID, "projectId", project.ID, "error", err) - return nil, 0, fmt.Errorf("failed to list external agents: %w", err) + return nil, 0, fmt.Errorf("failed to list agents: %w", err) } - var allAgents []*models.AgentResponse + + // Convert to response format + var agentResponses []*models.AgentResponse for _, agent := range agents { - allAgents = append(allAgents, s.convertToAgentListItem(agent, project.Name)) + agentResponses = append(agentResponses, s.convertToAgentListItem(agent, project.Name)) } - // Calculate total count - total := int32(len(allAgents)) - - // Apply pagination - var paginatedAgents []*models.AgentResponse - if offset >= total { - // If offset is beyond available data, return empty slice - paginatedAgents = []*models.AgentResponse{} - } else { - endIndex := offset + limit - if endIndex > total { - endIndex = total - } - paginatedAgents = allAgents[offset:endIndex] - } - s.logger.Info("Listed agents successfully", "orgName", orgName, "projName", projName, "totalAgents", total, "returnedAgents", len(paginatedAgents)) - return paginatedAgents, total, nil + s.logger.Info("Listed agents successfully", "orgName", orgName, "projName", projName, "totalAgents", total, "returnedAgents", len(agentResponses)) + return agentResponses, int32(total), nil } func (s *agentManagerService) CreateAgent(ctx context.Context, userIdpId uuid.UUID, orgName string, projectName string, req *spec.CreateAgentRequest) error { From 343f203cf26789ed123059e172830e3085ead677 Mon Sep 17 00:00:00 2001 From: Kavirubc Date: Thu, 18 Dec 2025 20:42:30 +0530 Subject: [PATCH 3/7] feat(console): add search params to ListAgentsQuery type --- console/workspaces/libs/types/src/api/agents.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/console/workspaces/libs/types/src/api/agents.ts b/console/workspaces/libs/types/src/api/agents.ts index baeb55162..d7afcd14f 100644 --- a/console/workspaces/libs/types/src/api/agents.ts +++ b/console/workspaces/libs/types/src/api/agents.ts @@ -61,6 +61,12 @@ export type ListAgentsPathParams = OrgProjPathParams; export type CreateAgentPathParams = OrgProjPathParams; export type GetAgentPathParams = AgentPathParams; export type DeleteAgentPathParams = AgentPathParams; -export type ListAgentsQuery = ListQuery; + +export interface ListAgentsQuery extends ListQuery { + search?: string; + provisioningType?: string; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; +} From 02f7f1c1042b089692bc17f1b976df600e1bf1b7 Mon Sep 17 00:00:00 2001 From: Kavirubc Date: Thu, 18 Dec 2025 20:43:14 +0530 Subject: [PATCH 4/7] feat(console): use server-side search in AgentsList --- .../overview/src/AgentsList/AgentsList.tsx | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/console/workspaces/pages/overview/src/AgentsList/AgentsList.tsx b/console/workspaces/pages/overview/src/AgentsList/AgentsList.tsx index fdb151bea..60998fc19 100644 --- a/console/workspaces/pages/overview/src/AgentsList/AgentsList.tsx +++ b/console/workspaces/pages/overview/src/AgentsList/AgentsList.tsx @@ -100,8 +100,17 @@ export interface AgentWithHref extends AgentResponse { export const AgentsList: React.FC = () => { const theme = useTheme(); const [search, setSearch] = useState(""); + const [debouncedSearch, setDebouncedSearch] = useState(""); const [hoveredAgentId, setHoveredAgentId] = useState(null); + // Debounce search input to reduce API calls + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearch(search); + }, 300); + return () => clearTimeout(timer); + }, [search]); + // Detect touch device for alternative interaction pattern const isTouchDevice = typeof window !== "undefined" && @@ -118,10 +127,10 @@ export const AgentsList: React.FC = () => { error, isRefetching, refetch: refetchAgents, - } = useListAgents({ - orgName: orgId, - projName: projectId, - }); + } = useListAgents( + { orgName: orgId, projName: projectId }, + { search: debouncedSearch || undefined } + ); const { mutate: deleteAgent, isPending: isDeletingAgent } = useDeleteAgent(); const { data: project, isLoading: isProjectLoading } = useGetProject({ orgName: orgId, @@ -180,30 +189,24 @@ export const AgentsList: React.FC = () => { const agentsWithHref: AgentWithHref[] = useMemo( () => - data?.agents - ?.filter( - (agent: AgentResponse) => - agent.displayName.toLowerCase().includes(search.toLowerCase()) || - agent.name.toLowerCase().includes(search.toLowerCase()) - ) - .map((agent) => ({ - ...agent, - href: generatePath( - getAgentPath(agent.provisioning.type === "internal"), - { - orgId: orgId ?? "", - projectId: agent.projectName, - agentId: agent.name, - } - ), - id: agent.name, - agentInfo: { - name: agent.name, - displayName: agent.displayName, - description: agent.description, - }, - })) ?? [], - [data?.agents, search, orgId] + data?.agents?.map((agent) => ({ + ...agent, + href: generatePath( + getAgentPath(agent.provisioning.type === "internal"), + { + orgId: orgId ?? "", + projectId: agent.projectName, + agentId: agent.name, + } + ), + id: agent.name, + agentInfo: { + name: agent.name, + displayName: agent.displayName, + description: agent.description, + }, + })) ?? [], + [data?.agents, orgId] ); const columns = useMemo( @@ -416,7 +419,6 @@ export const AgentsList: React.FC = () => { size="small" variant="outlined" placeholder="Search agents" - disabled={!data?.agents?.length} />