From 46e0f305993482f335e33e90c6190e927fcf26b6 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 8 Nov 2025 11:14:08 +0800 Subject: [PATCH] Implement filtering for assistant tags retrieval - Enhanced the HandleAssistantTags function to support multiple filtering options including type, connector, built-in status, mentionable status, automated status, and keywords. - Updated the GetAssistantTags method in the store interfaces to accept a filter parameter, allowing for more granular tag retrieval. - Refactored the Xun, Mongo, and Redis implementations of GetAssistantTags to utilize the new filtering capabilities. - Added comprehensive tests for the new filtering functionality, ensuring correct behavior across various scenarios. - Updated API endpoint documentation to reflect the new filtering options for assistant tags. --- agent/api.go | 46 ++++-- agent/store/mongo/mongo.go | 4 +- agent/store/redis/redis.go | 4 +- agent/store/types/store.go | 6 +- agent/store/xun/assistant.go | 46 +++++- agent/store/xun/assistant_test.go | 165 ++++++++++++++++++++- openapi/agent/agent.go | 2 +- openapi/agent/assistant.go | 74 ++++++++++ openapi/tests/agent/assistant_test.go | 200 ++++++++++++++++++++++++++ 9 files changed, 526 insertions(+), 21 deletions(-) diff --git a/agent/api.go b/agent/api.go index 431aab88..371f3909 100644 --- a/agent/api.go +++ b/agent/api.go @@ -1003,19 +1003,49 @@ func (agent *DSL) handleConnectors(c *gin.Context) { // HandleAssistantTags handles getting all assistant tags (exported for use in openapi/agent) func (agent *DSL) HandleAssistantTags(c *gin.Context) { - sid := c.GetString("__sid") - if sid == "" { - c.JSON(400, gin.H{"message": "sid is required", "code": 400}) - c.Done() - return - } - locale := "en-us" // Default locale if loc := c.Query("locale"); loc != "" { locale = strings.ToLower(strings.TrimSpace(loc)) } - tags, err := agent.Store.GetAssistantTags(locale) + // Build filter for tags query + filter := store.AssistantFilter{} + + // Apply type filter (default to "assistant") + typeParam := strings.TrimSpace(c.Query("type")) + if typeParam == "" { + typeParam = "assistant" + } + filter.Type = typeParam + + // Apply other optional filters + if connector := strings.TrimSpace(c.Query("connector")); connector != "" { + filter.Connector = connector + } + + if builtInParam := c.Query("built_in"); builtInParam != "" { + if val, err := strconv.ParseBool(builtInParam); err == nil { + filter.BuiltIn = &val + } + } + + if mentionableParam := c.Query("mentionable"); mentionableParam != "" { + if val, err := strconv.ParseBool(mentionableParam); err == nil { + filter.Mentionable = &val + } + } + + if automatedParam := c.Query("automated"); automatedParam != "" { + if val, err := strconv.ParseBool(automatedParam); err == nil { + filter.Automated = &val + } + } + + if keywords := strings.TrimSpace(c.Query("keywords")); keywords != "" { + filter.Keywords = keywords + } + + tags, err := agent.Store.GetAssistantTags(filter, locale) if err != nil { c.JSON(500, gin.H{"message": err.Error(), "code": 500}) c.Done() diff --git a/agent/store/mongo/mongo.go b/agent/store/mongo/mongo.go index 01f0bd2d..14fa6621 100644 --- a/agent/store/mongo/mongo.go +++ b/agent/store/mongo/mongo.go @@ -80,8 +80,8 @@ func (m *Mongo) DeleteAssistants(filter types.AssistantFilter) (int64, error) { return 0, nil } -// GetAssistantTags retrieves all unique tags from assistants -func (m *Mongo) GetAssistantTags(locale ...string) ([]types.Tag, error) { +// GetAssistantTags retrieves all unique tags from assistants with filtering +func (m *Mongo) GetAssistantTags(filter types.AssistantFilter, locale ...string) ([]types.Tag, error) { return []types.Tag{}, nil } diff --git a/agent/store/redis/redis.go b/agent/store/redis/redis.go index b596591c..cdfc1810 100644 --- a/agent/store/redis/redis.go +++ b/agent/store/redis/redis.go @@ -80,8 +80,8 @@ func (r *Redis) DeleteAssistants(filter types.AssistantFilter) (int64, error) { return 0, nil } -// GetAssistantTags retrieves all unique tags from assistants -func (r *Redis) GetAssistantTags(locale ...string) ([]types.Tag, error) { +// GetAssistantTags retrieves all unique tags from assistants with filtering +func (r *Redis) GetAssistantTags(filter types.AssistantFilter, locale ...string) ([]types.Tag, error) { return []types.Tag{}, nil } diff --git a/agent/store/types/store.go b/agent/store/types/store.go index 58e6ef11..b7d15482 100644 --- a/agent/store/types/store.go +++ b/agent/store/types/store.go @@ -77,9 +77,11 @@ type Store interface { // Returns: Paginated assistant list and potential error GetAssistants(filter AssistantFilter, locale ...string) (*AssistantList, error) - // GetAssistantTags retrieves all unique tags from assistants + // GetAssistantTags retrieves all unique tags from assistants with filtering + // filter: Filter conditions including QueryFilter for permission filtering + // locale: Optional locale for i18n translations // Returns: List of tags and potential error - GetAssistantTags(locale ...string) ([]Tag, error) + GetAssistantTags(filter AssistantFilter, locale ...string) ([]Tag, error) // GetAssistant retrieves a single assistant by ID // assistantID: Assistant ID diff --git a/agent/store/xun/assistant.go b/agent/store/xun/assistant.go index bbcdaee4..d991cade 100644 --- a/agent/store/xun/assistant.go +++ b/agent/store/xun/assistant.go @@ -544,10 +544,48 @@ func (conv *Xun) DeleteAssistants(filter types.AssistantFilter) (int64, error) { return qb.Delete() } -// GetAssistantTags retrieves all unique tags from assistants -func (conv *Xun) GetAssistantTags(locale ...string) ([]types.Tag, error) { - q := conv.newQuery().Table(conv.getAssistantTable()) - rows, err := q.Select("tags").Where("type", "assistant").GroupBy("tags").Get() +// GetAssistantTags retrieves all unique tags from assistants with filtering +func (conv *Xun) GetAssistantTags(filter types.AssistantFilter, locale ...string) ([]types.Tag, error) { + qb := conv.query.New().Table(conv.getAssistantTable()) + + // Apply type filter (default to "assistant") + typeFilter := "assistant" + if filter.Type != "" { + typeFilter = filter.Type + } + qb.Where("type", typeFilter) + + // Apply custom query filter function (for permission filtering) + if filter.QueryFilter != nil { + qb.Where(filter.QueryFilter) + } + + // Apply other filters if provided + if filter.Connector != "" { + qb.Where("connector", filter.Connector) + } + + if filter.BuiltIn != nil { + qb.Where("built_in", *filter.BuiltIn) + } + + if filter.Mentionable != nil { + qb.Where("mentionable", *filter.Mentionable) + } + + if filter.Automated != nil { + qb.Where("automated", *filter.Automated) + } + + // Apply keyword filter if provided + if filter.Keywords != "" { + qb.Where(func(qb query.Query) { + qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Keywords)). + OrWhere("description", "like", fmt.Sprintf("%%%s%%", filter.Keywords)) + }) + } + + rows, err := qb.Select("tags").GroupBy("tags").Get() if err != nil { return nil, err } diff --git a/agent/store/xun/assistant_test.go b/agent/store/xun/assistant_test.go index aaf323c3..47f48ef8 100644 --- a/agent/store/xun/assistant_test.go +++ b/agent/store/xun/assistant_test.go @@ -803,7 +803,7 @@ func TestGetAssistantTags(t *testing.T) { } // Get all tags - tags, err := store.GetAssistantTags() + tags, err := store.GetAssistantTags(types.AssistantFilter{}) if err != nil { t.Fatalf("Failed to get tags: %v", err) } @@ -825,6 +825,167 @@ func TestGetAssistantTags(t *testing.T) { t.Logf("Found %d unique tags", len(tags)) }) + + t.Run("GetTagsWithFilter", func(t *testing.T) { + // Create test assistants with specific tags and attributes + uniqueTag := fmt.Sprintf("filter-tag-%d", time.Now().UnixNano()) + assistants := []types.AssistantModel{ + { + Name: "Filtered Tags Test 1", + Type: "assistant", + Connector: "openai", + Tags: []string{uniqueTag, "ai"}, + Share: "private", + BuiltIn: false, + Mentionable: true, + }, + { + Name: "Filtered Tags Test 2", + Type: "assistant", + Connector: "anthropic", + Tags: []string{uniqueTag, "coding"}, + Share: "private", + BuiltIn: true, + Mentionable: false, + }, + { + Name: "Filtered Tags Test 3", + Type: "assistant", + Connector: "openai", + Tags: []string{uniqueTag, "search"}, + Share: "private", + BuiltIn: false, + Automated: true, + }, + } + + for _, asst := range assistants { + _, err := store.SaveAssistant(&asst) + if err != nil { + t.Fatalf("Failed to create assistant: %v", err) + } + } + + // Test: Get tags filtered by connector + tagsOpenAI, err := store.GetAssistantTags(types.AssistantFilter{ + Connector: "openai", + }) + if err != nil { + t.Fatalf("Failed to get tags with connector filter: %v", err) + } + t.Logf("Found %d tags for openai connector", len(tagsOpenAI)) + + // Test: Get tags filtered by built_in + builtInFalse := false + tagsNonBuiltIn, err := store.GetAssistantTags(types.AssistantFilter{ + BuiltIn: &builtInFalse, + }) + if err != nil { + t.Fatalf("Failed to get tags with built_in filter: %v", err) + } + t.Logf("Found %d tags for non-built-in assistants", len(tagsNonBuiltIn)) + + // Test: Get tags filtered by mentionable + mentionableTrue := true + tagsMentionable, err := store.GetAssistantTags(types.AssistantFilter{ + Mentionable: &mentionableTrue, + }) + if err != nil { + t.Fatalf("Failed to get tags with mentionable filter: %v", err) + } + t.Logf("Found %d tags for mentionable assistants", len(tagsMentionable)) + + // Test: Get tags filtered by keywords + tagsWithKeywords, err := store.GetAssistantTags(types.AssistantFilter{ + Keywords: "Filtered Tags Test", + }) + if err != nil { + t.Fatalf("Failed to get tags with keywords filter: %v", err) + } + t.Logf("Found %d tags with keywords filter", len(tagsWithKeywords)) + }) + + t.Run("GetTagsWithQueryFilter", func(t *testing.T) { + // Create test assistants with permission fields + permTag := fmt.Sprintf("perm-tag-%d", time.Now().UnixNano()) + assistants := []types.AssistantModel{ + { + Name: "Permission Tags Test 1", + Type: "assistant", + Connector: "openai", + Tags: []string{permTag, "public-tag"}, + Share: "private", + Public: true, + YaoCreatedBy: "user-1", + YaoTeamID: "team-1", + }, + { + Name: "Permission Tags Test 2", + Type: "assistant", + Connector: "openai", + Tags: []string{permTag, "team-tag"}, + Share: "team", + Public: false, + YaoCreatedBy: "user-2", + YaoTeamID: "team-1", + }, + { + Name: "Permission Tags Test 3", + Type: "assistant", + Connector: "openai", + Tags: []string{permTag, "private-tag"}, + Share: "private", + Public: false, + YaoCreatedBy: "user-3", + YaoTeamID: "team-2", + }, + } + + for _, asst := range assistants { + _, err := store.SaveAssistant(&asst) + if err != nil { + t.Fatalf("Failed to create assistant: %v", err) + } + } + + // Test: Get tags for public assistants only + tagsPublic, err := store.GetAssistantTags(types.AssistantFilter{ + QueryFilter: func(qb query.Query) { + qb.Where("public", true) + }, + }) + if err != nil { + t.Fatalf("Failed to get tags for public assistants: %v", err) + } + t.Logf("Found %d tags for public assistants", len(tagsPublic)) + + // Test: Get tags for team-1 assistants + tagsTeam1, err := store.GetAssistantTags(types.AssistantFilter{ + QueryFilter: func(qb query.Query) { + qb.Where("__yao_team_id", "team-1") + }, + }) + if err != nil { + t.Fatalf("Failed to get tags for team-1: %v", err) + } + t.Logf("Found %d tags for team-1 assistants", len(tagsTeam1)) + + // Test: Complex permission filter (public OR team-1 with share=team) + tagsComplex, err := store.GetAssistantTags(types.AssistantFilter{ + QueryFilter: func(qb query.Query) { + qb.Where(func(qb query.Query) { + qb.Where("public", true) + }).OrWhere(func(qb query.Query) { + qb.Where("__yao_team_id", "team-1"). + Where("share", "team") + }) + }, + }) + if err != nil { + t.Fatalf("Failed to get tags with complex filter: %v", err) + } + t.Logf("Found %d tags with complex permission filter", len(tagsComplex)) + }) } // TestGenerateAssistantID tests the ID generation function @@ -1672,7 +1833,7 @@ func TestAssistantCompleteWorkflow(t *testing.T) { } // Step 5: Get tags - tags, err := store.GetAssistantTags() + tags, err := store.GetAssistantTags(types.AssistantFilter{}) if err != nil { t.Fatalf("Failed to get tags: %v", err) } diff --git a/openapi/agent/agent.go b/openapi/agent/agent.go index 46a8f3d6..9e89bf62 100644 --- a/openapi/agent/agent.go +++ b/openapi/agent/agent.go @@ -20,7 +20,7 @@ func Attach(group *gin.RouterGroup, oauth types.OAuth) { // Assistant CRUD - Standard REST endpoints assistants.GET("/", ListAssistants) // GET /assistants - List assistants assistants.POST("/", n.HandleAssistantSave) // POST /assistants - Create/Update assistant - assistants.GET("/tags", n.HandleAssistantTags) // GET /assistants/tags - Get all assistant tags + assistants.GET("/tags", ListAssistantTags) // GET /assistants/tags - Get all assistant tags with permission filtering assistants.GET("/:id", n.HandleAssistantDetail) // GET /assistants/:id - Get assistant details assistants.DELETE("/:id", n.HandleAssistantDelete) // DELETE /assistants/:id - Delete assistant diff --git a/openapi/agent/assistant.go b/openapi/agent/assistant.go index 7c48b648..7e5b5f3b 100644 --- a/openapi/agent/assistant.go +++ b/openapi/agent/assistant.go @@ -159,3 +159,77 @@ func ListAssistants(c *gin.Context) { // Return the result with standard response format response.RespondWithSuccess(c, response.StatusOK, result) } + +// ListAssistantTags lists assistant tags with permission-based filtering +func ListAssistantTags(c *gin.Context) { + + // Get authorized information + authInfo := authorized.GetInfo(c) + + // Get Agent instance from global variable + agentInstance := agent.GetAgent() + if agentInstance == nil || agentInstance.Store == nil { + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Agent store not initialized", + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Parse locale + locale := "en-us" // Default locale + if loc := c.Query("locale"); loc != "" { + locale = strings.ToLower(strings.TrimSpace(loc)) + } + + // Parse filter parameters + typeParam := strings.TrimSpace(c.Query("type")) + if typeParam == "" { + typeParam = "assistant" // Default type + } + connector := strings.TrimSpace(c.Query("connector")) + keywords := strings.TrimSpace(c.Query("keywords")) + + // Parse boolean filters + var builtIn, mentionable, automated *bool + if builtInParam := c.Query("built_in"); builtInParam != "" { + builtIn = parseBoolValue(builtInParam) + } + if mentionableParam := c.Query("mentionable"); mentionableParam != "" { + mentionable = parseBoolValue(mentionableParam) + } + if automatedParam := c.Query("automated"); automatedParam != "" { + automated = parseBoolValue(automatedParam) + } + + // Build filter + filter := BuildAssistantFilter(AssistantFilterParams{ + Type: typeParam, + Connector: connector, + Keywords: keywords, + BuiltIn: builtIn, + Mentionable: mentionable, + Automated: automated, + }) + + // Apply permission-based filtering (Scope filtering) + filter.QueryFilter = AuthQueryFilter(c, authInfo) + + // Get tags with filter + tags, err := agentInstance.Store.GetAssistantTags(filter, locale) + if err != nil { + log.Error("Failed to get assistant tags: %v", err) + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Failed to get assistant tags: " + err.Error(), + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Return the result with standard response format + response.RespondWithSuccess(c, response.StatusOK, map[string]interface{}{ + "data": tags, + }) +} diff --git a/openapi/tests/agent/assistant_test.go b/openapi/tests/agent/assistant_test.go index 116f56b6..0b8b4c61 100644 --- a/openapi/tests/agent/assistant_test.go +++ b/openapi/tests/agent/assistant_test.go @@ -720,6 +720,206 @@ func TestAssistantEdgeCases(t *testing.T) { }) } +// TestListAssistantTags tests the assistant tags endpoint +func TestListAssistantTags(t *testing.T) { + serverURL := testutils.Prepare(t) + defer testutils.Clean() + + baseURL := "" + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL + } + + client := testutils.RegisterTestClient(t, "Agent Tags Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) + tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + + t.Run("ListAssistantTagsSuccess", func(t *testing.T) { + // Test listing all assistant tags + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/tags", nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode, "Should successfully retrieve tags") + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + + data, hasData := response["data"].([]interface{}) + if hasData { + t.Logf("Successfully retrieved %d tags", len(data)) + + // Verify tag structure + if len(data) > 0 { + tag, ok := data[0].(map[string]interface{}) + if ok { + assert.Contains(t, tag, "value", "Tag should have value field") + assert.Contains(t, tag, "label", "Tag should have label field") + } + } + } + }) + + t.Run("ListAssistantTagsWithLocale", func(t *testing.T) { + // Test with locale parameter + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/tags?locale=zh-cn", nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + + data, hasData := response["data"].([]interface{}) + if hasData { + t.Logf("Successfully retrieved %d tags with zh-cn locale", len(data)) + } + }) + + t.Run("ListAssistantTagsWithFilters", func(t *testing.T) { + // Test with type filter + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/tags?type=assistant", nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + + data, hasData := response["data"].([]interface{}) + if hasData { + t.Logf("Successfully retrieved %d tags with type filter", len(data)) + } + }) + + t.Run("ListAssistantTagsWithConnector", func(t *testing.T) { + // Test with connector filter + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/tags?connector=openai", nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + + data, hasData := response["data"].([]interface{}) + if hasData { + t.Logf("Successfully retrieved %d tags for openai connector", len(data)) + } + }) + + t.Run("ListAssistantTagsWithBuiltInFilter", func(t *testing.T) { + // Test with built_in filter + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/tags?built_in=false", nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + + data, hasData := response["data"].([]interface{}) + if hasData { + t.Logf("Successfully retrieved %d tags for non-built-in assistants", len(data)) + } + }) + + t.Run("ListAssistantTagsWithMentionableFilter", func(t *testing.T) { + // Test with mentionable filter + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/tags?mentionable=true", nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + + data, hasData := response["data"].([]interface{}) + if hasData { + t.Logf("Successfully retrieved %d tags for mentionable assistants", len(data)) + } + }) + + t.Run("ListAssistantTagsWithKeywords", func(t *testing.T) { + // Test with keywords filter + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/tags?keywords=test", nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + + data, hasData := response["data"].([]interface{}) + if hasData { + t.Logf("Successfully retrieved %d tags with keywords filter", len(data)) + } + }) + + t.Run("ListAssistantTagsUnauthorized", func(t *testing.T) { + // Test without authentication + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/tags", nil) + assert.NoError(t, err) + // No Authorization header + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + t.Logf("Correctly rejected unauthorized request") + }) +} + // BenchmarkListAssistants benchmarks the list assistants endpoint func BenchmarkListAssistants(b *testing.B) { // Convert testing.B to testing.T for Prepare/Clean