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.
This commit is contained in:
Max 2025-11-08 11:14:08 +08:00
parent bed36b754f
commit 46e0f30599
9 changed files with 526 additions and 21 deletions

View file

@ -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()

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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)
}

View file

@ -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

View file

@ -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,
})
}

View file

@ -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