mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-13 04:13:35 +08:00
Go: implement ?type=filter branch for GET /agents (#18111)
This commit is contained in:
@@ -59,6 +59,17 @@ func (dao *CompilationTemplateGroupDAO) ListSaved(ctx context.Context, db *gorm.
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
// CountSavedByTenant counts the tenant's own valid groups; built-in groups
|
||||
// (empty tenant_id) are excluded, mirroring the group_count query in Python
|
||||
// get_owner_filter / get_category_filter.
|
||||
func (dao *CompilationTemplateGroupDAO) CountSavedByTenant(ctx context.Context, db *gorm.DB, tenantID string) (int64, error) {
|
||||
var count int64
|
||||
err := db.WithContext(ctx).Model(&entity.CompilationTemplateGroup{}).
|
||||
Where("tenant_id = ? AND status = ?", tenantID, string(entity.StatusValid)).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetSaved returns a single valid group for the tenant (or built-in), or nil.
|
||||
func (dao *CompilationTemplateGroupDAO) GetSaved(ctx context.Context, db *gorm.DB, tenantID, groupID string) (*entity.CompilationTemplateGroup, error) {
|
||||
var g entity.CompilationTemplateGroup
|
||||
|
||||
@@ -410,6 +410,61 @@ func (dao *UserCanvasDAO) ListByTenantIDs(ctx context.Context, db *gorm.DB, owne
|
||||
return canvases, total, nil
|
||||
}
|
||||
|
||||
// OwnerFilterItem is one row of the owner aggregation backing the
|
||||
// ?type=filter branch of the agents list endpoint.
|
||||
type OwnerFilterItem struct {
|
||||
ID string `gorm:"column:id"`
|
||||
Label *string `gorm:"column:label"`
|
||||
Count int64 `gorm:"column:count"`
|
||||
}
|
||||
|
||||
// CategoryFilterItem is one row of the canvas_category aggregation backing
|
||||
// the ?type=filter branch of the agents list endpoint.
|
||||
type CategoryFilterItem struct {
|
||||
ID string `gorm:"column:id"`
|
||||
Count int64 `gorm:"column:count"`
|
||||
}
|
||||
|
||||
// visibleToUserScope scopes a user_canvas query to rows the user can see:
|
||||
// team-permission canvases owned by ownerIDs plus everything the user owns.
|
||||
func visibleToUserScope(db *gorm.DB, ownerIDs []string, userID string) *gorm.DB {
|
||||
return db.Model(&entity.UserCanvas{}).
|
||||
Where("user_canvas.user_id IN ?", ownerIDs).
|
||||
Where(
|
||||
db.Where("user_canvas.permission = ?", "team").
|
||||
Or("user_canvas.user_id = ?", userID))
|
||||
}
|
||||
|
||||
// GetOwnerFilter aggregates visible canvases by owner, joining the user
|
||||
// table for the display label. Mirrors Python
|
||||
// UserCanvasService.get_owner_filter.
|
||||
func (dao *UserCanvasDAO) GetOwnerFilter(ctx context.Context, db *gorm.DB, ownerIDs []string, userID string) ([]*OwnerFilterItem, error) {
|
||||
if len(ownerIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var items []*OwnerFilterItem
|
||||
err := visibleToUserScope(db.WithContext(ctx), ownerIDs, userID).
|
||||
Select("user_canvas.user_id AS id, user.nickname AS label, COUNT(user_canvas.id) AS count").
|
||||
Joins("LEFT JOIN user ON user_canvas.user_id = user.id").
|
||||
Group("user_canvas.user_id, user.nickname").
|
||||
Scan(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
// GetCategoryFilter aggregates visible canvases by canvas_category.
|
||||
// Mirrors Python UserCanvasService.get_category_filter.
|
||||
func (dao *UserCanvasDAO) GetCategoryFilter(ctx context.Context, db *gorm.DB, ownerIDs []string, userID string) ([]*CategoryFilterItem, error) {
|
||||
if len(ownerIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var items []*CategoryFilterItem
|
||||
err := visibleToUserScope(db.WithContext(ctx), ownerIDs, userID).
|
||||
Select("user_canvas.canvas_category AS id, COUNT(user_canvas.id) AS count").
|
||||
Group("user_canvas.canvas_category").
|
||||
Scan(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
// ListTags returns tag usage counts across canvases visible to userID.
|
||||
func (dao *UserCanvasDAO) ListTags(ctx context.Context, db *gorm.DB, ownerIDs []string, userID string, canvasCategory string) (map[string]int, error) {
|
||||
if len(ownerIDs) == 0 {
|
||||
|
||||
@@ -192,3 +192,75 @@ func TestUserCanvasDAOListTagsIncludesPipelineWhenCategoryIsEmpty(t *testing.T)
|
||||
t.Fatalf("agent-tag count with agent_canvas filter = %d, want 1", counts["agent-tag"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserCanvasDAOOwnerAndCategoryFilters(t *testing.T) {
|
||||
db := setupUserCanvasTestDB(t)
|
||||
if err := db.AutoMigrate(&entity.User{}); err != nil {
|
||||
t.Fatalf("failed to migrate user: %v", err)
|
||||
}
|
||||
pushDB(t, db)
|
||||
ctx := t.Context()
|
||||
d := NewUserCanvasDAO()
|
||||
|
||||
users := []entity.User{
|
||||
{ID: "user-1", Nickname: "Alice", Email: "alice@example.com"},
|
||||
{ID: "user-2", Nickname: "Bob", Email: "bob@example.com"},
|
||||
}
|
||||
for i := range users {
|
||||
if err := db.WithContext(ctx).Create(&users[i]).Error; err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
canvases := []entity.UserCanvas{
|
||||
{ID: "c1", UserID: "user-1", Permission: "me", CanvasCategory: "agent_canvas"},
|
||||
{ID: "c2", UserID: "user-1", Permission: "me", CanvasCategory: "dataflow_canvas"},
|
||||
{ID: "c3", UserID: "user-2", Permission: "team", CanvasCategory: "agent_canvas"},
|
||||
{ID: "c4", UserID: "user-2", Permission: "me", CanvasCategory: "agent_canvas"},
|
||||
}
|
||||
for i := range canvases {
|
||||
if err := db.WithContext(ctx).Create(&canvases[i]).Error; err != nil {
|
||||
t.Fatalf("failed to create canvas: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
ownerIDs := []string{"user-1", "user-2"}
|
||||
|
||||
owners, err := d.GetOwnerFilter(ctx, db, ownerIDs, "user-1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetOwnerFilter failed: %v", err)
|
||||
}
|
||||
if len(owners) != 2 {
|
||||
t.Fatalf("owner filter rows = %d, want 2", len(owners))
|
||||
}
|
||||
byID := make(map[string]*OwnerFilterItem, len(owners))
|
||||
for _, o := range owners {
|
||||
byID[o.ID] = o
|
||||
}
|
||||
// user-1 sees both of their own canvases; user-2 only the team one (c4 is "me").
|
||||
if byID["user-1"].Count != 2 {
|
||||
t.Fatalf("user-1 count = %d, want 2", byID["user-1"].Count)
|
||||
}
|
||||
if byID["user-2"].Count != 1 {
|
||||
t.Fatalf("user-2 count = %d, want 1", byID["user-2"].Count)
|
||||
}
|
||||
if byID["user-1"].Label == nil || *byID["user-1"].Label != "Alice" {
|
||||
t.Fatalf("user-1 label = %v, want Alice", byID["user-1"].Label)
|
||||
}
|
||||
|
||||
categories, err := d.GetCategoryFilter(ctx, db, ownerIDs, "user-1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetCategoryFilter failed: %v", err)
|
||||
}
|
||||
catByID := make(map[string]int64, len(categories))
|
||||
for _, c := range categories {
|
||||
catByID[c.ID] = c.Count
|
||||
}
|
||||
// agent_canvas: c1 (own) + c3 (team); c4 hidden. dataflow_canvas: c2.
|
||||
if catByID["agent_canvas"] != 2 {
|
||||
t.Fatalf("agent_canvas count = %d, want 2", catByID["agent_canvas"])
|
||||
}
|
||||
if catByID["dataflow_canvas"] != 1 {
|
||||
t.Fatalf("dataflow_canvas count = %d, want 1", catByID["dataflow_canvas"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +187,19 @@ func (h *AgentHandler) ListAgents(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Filter-aggregation mode: the agents page filter bar fetches
|
||||
// GET /api/v1/agents?type=filter and expects
|
||||
// {filter: {owner, canvas_category}, total} instead of a canvas list.
|
||||
if c.Query("type") == "filter" {
|
||||
filters, code, err := h.agentService.ListAgentFilters(c.Request.Context(), user.ID)
|
||||
if err != nil {
|
||||
common.ResponseWithCodeData(c, code, false, err.Error())
|
||||
return
|
||||
}
|
||||
common.SuccessWithData(c, filters, "success")
|
||||
return
|
||||
}
|
||||
|
||||
keywords := c.Query("keywords")
|
||||
canvasCategory := c.Query("canvas_category")
|
||||
canvasType := c.Query("canvas_type")
|
||||
|
||||
@@ -303,12 +303,13 @@ var ErrAgentStorageError = errors.New("agent storage error")
|
||||
|
||||
// AgentService agent service
|
||||
type AgentService struct {
|
||||
canvasDAO *dao.UserCanvasDAO
|
||||
canvasTemplateDAO *dao.CanvasTemplateDAO
|
||||
userDAO *dao.UserDAO
|
||||
userTenantDAO *dao.UserTenantDAO
|
||||
versionDAO *dao.UserCanvasVersionDAO
|
||||
api4ConversationDAO *dao.API4ConversationDAO
|
||||
canvasDAO *dao.UserCanvasDAO
|
||||
canvasTemplateDAO *dao.CanvasTemplateDAO
|
||||
userDAO *dao.UserDAO
|
||||
userTenantDAO *dao.UserTenantDAO
|
||||
versionDAO *dao.UserCanvasVersionDAO
|
||||
api4ConversationDAO *dao.API4ConversationDAO
|
||||
compilationTemplateGroupDAO *dao.CompilationTemplateGroupDAO
|
||||
|
||||
// driver is the per-process runner that drives canvas
|
||||
// invocations and produces SSE events. V1 persistence is
|
||||
@@ -375,17 +376,18 @@ func NewAgentServiceWithOptions(
|
||||
agenttool.SetSandboxClient(agentsandbox.NewManagerClient())
|
||||
}
|
||||
return &AgentService{
|
||||
canvasDAO: dao.NewUserCanvasDAO(),
|
||||
canvasTemplateDAO: dao.NewCanvasTemplateDAO(),
|
||||
userDAO: dao.NewUserDAO(),
|
||||
userTenantDAO: dao.NewUserTenantDAO(),
|
||||
versionDAO: dao.NewUserCanvasVersionDAO(),
|
||||
api4ConversationDAO: dao.NewAPI4ConversationDAO(),
|
||||
runner: canvas.NewRunner(),
|
||||
activeSessions: make(map[string]*activeAgentRun),
|
||||
checkpointStore: cp,
|
||||
stateSerializer: ser,
|
||||
runTracker: rt,
|
||||
canvasDAO: dao.NewUserCanvasDAO(),
|
||||
canvasTemplateDAO: dao.NewCanvasTemplateDAO(),
|
||||
userDAO: dao.NewUserDAO(),
|
||||
userTenantDAO: dao.NewUserTenantDAO(),
|
||||
versionDAO: dao.NewUserCanvasVersionDAO(),
|
||||
api4ConversationDAO: dao.NewAPI4ConversationDAO(),
|
||||
compilationTemplateGroupDAO: dao.NewCompilationTemplateGroupDAO(),
|
||||
runner: canvas.NewRunner(),
|
||||
activeSessions: make(map[string]*activeAgentRun),
|
||||
checkpointStore: cp,
|
||||
stateSerializer: ser,
|
||||
runTracker: rt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,6 +423,114 @@ type ListAgentsResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
// CompilationTemplateGroupCategory is the synthetic canvas_category the
|
||||
// frontend uses to filter compilation template groups through the merged
|
||||
// /agents endpoint. Mirrors Python _COMPILATION_TEMPLATE_GROUP_CATEGORY.
|
||||
const CompilationTemplateGroupCategory = "compilation_template_group"
|
||||
|
||||
// AgentOwnerFilter is one owner option in the agents filter response.
|
||||
type AgentOwnerFilter struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
// AgentCategoryFilter is one canvas_category option in the agents filter
|
||||
// response.
|
||||
type AgentCategoryFilter struct {
|
||||
ID string `json:"id"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
// AgentFiltersResponse is the response body for
|
||||
// GET /api/v1/agents?type=filter.
|
||||
type AgentFiltersResponse struct {
|
||||
Filter struct {
|
||||
Owner []AgentOwnerFilter `json:"owner"`
|
||||
CanvasCategory []AgentCategoryFilter `json:"canvas_category"`
|
||||
} `json:"filter"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
// ListAgentFilters returns the owner/category aggregations backing the
|
||||
// agents page filter bar. Mirrors the ?type=filter branch of Python
|
||||
// agent_api.list_agents.
|
||||
func (s *AgentService) ListAgentFilters(ctx context.Context, userID string) (*AgentFiltersResponse, common.ErrorCode, error) {
|
||||
tenantIDs, err := s.userTenantDAO.GetTenantIDsByUserID(ctx, dao.DB, userID)
|
||||
if err != nil {
|
||||
return nil, common.CodeServerError, fmt.Errorf("failed to get tenant IDs: %w", err)
|
||||
}
|
||||
ownerIDs := make([]string, 0, len(tenantIDs)+1)
|
||||
seen := make(map[string]struct{}, len(tenantIDs)+1)
|
||||
seen[userID] = struct{}{}
|
||||
ownerIDs = append(ownerIDs, userID)
|
||||
for _, id := range tenantIDs {
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
ownerIDs = append(ownerIDs, id)
|
||||
}
|
||||
|
||||
owners, err := s.canvasDAO.GetOwnerFilter(ctx, dao.DB, ownerIDs, userID)
|
||||
if err != nil {
|
||||
return nil, common.CodeServerError, fmt.Errorf("failed to aggregate agent owners: %w", err)
|
||||
}
|
||||
categories, err := s.canvasDAO.GetCategoryFilter(ctx, dao.DB, ownerIDs, userID)
|
||||
if err != nil {
|
||||
return nil, common.CodeServerError, fmt.Errorf("failed to aggregate agent categories: %w", err)
|
||||
}
|
||||
groupCount, err := s.compilationTemplateGroupDAO.CountSavedByTenant(ctx, dao.DB, userID)
|
||||
if err != nil {
|
||||
return nil, common.CodeServerError, fmt.Errorf("failed to count compilation template groups: %w", err)
|
||||
}
|
||||
|
||||
ownerFilters := make([]AgentOwnerFilter, 0, len(owners)+1)
|
||||
for _, o := range owners {
|
||||
label := o.ID
|
||||
if o.Label != nil && *o.Label != "" {
|
||||
label = *o.Label
|
||||
}
|
||||
ownerFilters = append(ownerFilters, AgentOwnerFilter{ID: o.ID, Label: label, Count: o.Count})
|
||||
}
|
||||
if groupCount > 0 {
|
||||
idx := -1
|
||||
for i := range ownerFilters {
|
||||
if ownerFilters[i].ID == userID {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx >= 0 {
|
||||
ownerFilters[idx].Count += groupCount
|
||||
} else {
|
||||
nickname, nerr := s.userDAO.GetNicknameByID(ctx, dao.DB, userID)
|
||||
if nerr != nil {
|
||||
nickname = ""
|
||||
}
|
||||
ownerFilters = append(ownerFilters, AgentOwnerFilter{ID: userID, Label: nickname, Count: groupCount})
|
||||
}
|
||||
}
|
||||
|
||||
categoryFilters := make([]AgentCategoryFilter, 0, len(categories)+1)
|
||||
for _, c := range categories {
|
||||
categoryFilters = append(categoryFilters, AgentCategoryFilter{ID: c.ID, Count: c.Count})
|
||||
}
|
||||
if groupCount > 0 {
|
||||
categoryFilters = append(categoryFilters, AgentCategoryFilter{ID: CompilationTemplateGroupCategory, Count: groupCount})
|
||||
}
|
||||
|
||||
var total int64
|
||||
for _, o := range ownerFilters {
|
||||
total += o.Count
|
||||
}
|
||||
|
||||
resp := &AgentFiltersResponse{Total: total}
|
||||
resp.Filter.Owner = ownerFilters
|
||||
resp.Filter.CanvasCategory = categoryFilters
|
||||
return resp, common.CodeSuccess, nil
|
||||
}
|
||||
|
||||
type AgentTagCount struct {
|
||||
Tag string `json:"tag"`
|
||||
Count int `json:"count"`
|
||||
|
||||
@@ -951,7 +951,10 @@ export const useFetchAgentFilters = () => {
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data.filter, loading };
|
||||
return {
|
||||
data: data?.filter ?? { owner: [], canvas_category: [] },
|
||||
loading,
|
||||
};
|
||||
};
|
||||
|
||||
export const BuiltinPipelineKeys = {
|
||||
|
||||
Reference in New Issue
Block a user