Enhance assistant management and API functionality in Neo

- Added a new endpoint to retrieve all assistant tags, improving data accessibility for clients.
- Updated the assistant list handling to support filtering by built-in status and assistant ID, enhancing the filtering capabilities.
- Introduced a method to load built-in assistants, streamlining the assistant initialization process.
- Enhanced the Assistant struct with new fields for path, built-in status, and sorting, improving data organization.
- Implemented validation and cloning methods for the Assistant struct, ensuring data integrity and ease of use.
- Updated tests to cover new functionalities, including validation, cloning, and tag retrieval, ensuring robust functionality across the assistant management operations.
This commit is contained in:
Max 2025-01-01 16:48:06 +08:00
parent d0d110b0ec
commit f8ce8fc193
10 changed files with 1039 additions and 22 deletions

View file

@ -62,6 +62,9 @@ func (neo *DSL) API(router *gin.Engine, path string) error {
// List assistants example: // List assistants example:
// curl -X GET 'http://localhost:5099/api/__yao/neo/assistants?page=1&pagesize=20&tags=tag1,tag2&token=xxx' // curl -X GET 'http://localhost:5099/api/__yao/neo/assistants?page=1&pagesize=20&tags=tag1,tag2&token=xxx'
router.GET(path+"/assistants", append(middlewares, neo.handleAssistantList)...) router.GET(path+"/assistants", append(middlewares, neo.handleAssistantList)...)
// Get all assistant tags example:
// curl -X GET 'http://localhost:5099/api/__yao/neo/assistants/tags?token=xxx'
router.GET(path+"/assistants/tags", append(middlewares, neo.handleAssistantTags)...)
// Get assistant details example: // Get assistant details example:
// curl -X GET 'http://localhost:5099/api/__yao/neo/assistants/assistant_123?token=xxx' // curl -X GET 'http://localhost:5099/api/__yao/neo/assistants/assistant_123?token=xxx'
@ -878,6 +881,14 @@ func (neo *DSL) handleAssistantList(c *gin.Context) {
filter.Select = strings.Split(selectFields, ",") filter.Select = strings.Split(selectFields, ",")
} }
// Parse built_in (support various boolean formats)
if builtIn := c.Query("built_in"); builtIn != "" {
val := parseBoolValue(builtIn)
if val != nil {
filter.BuiltIn = val
}
}
// Parse mentionable (support various boolean formats) // Parse mentionable (support various boolean formats)
if mentionable := c.Query("mentionable"); mentionable != "" { if mentionable := c.Query("mentionable"); mentionable != "" {
val := parseBoolValue(mentionable) val := parseBoolValue(mentionable)
@ -894,6 +905,11 @@ func (neo *DSL) handleAssistantList(c *gin.Context) {
} }
} }
// Parse assistant_id
if assistantID := c.Query("assistant_id"); assistantID != "" {
filter.AssistantID = assistantID
}
response, err := neo.Store.GetAssistants(filter) response, err := neo.Store.GetAssistants(filter)
if err != nil { if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500}) c.JSON(500, gin.H{"message": err.Error(), "code": 500})
@ -1023,3 +1039,23 @@ func (neo *DSL) handleConnectors(c *gin.Context) {
c.JSON(200, gin.H{"data": options}) c.JSON(200, gin.H{"data": options})
c.Done() c.Done()
} }
// handleAssistantTags handles getting all assistant tags
func (neo *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
}
tags, err := neo.Store.GetAssistantTags()
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
c.JSON(200, gin.H{"data": tags})
c.Done()
}

View file

@ -19,6 +19,63 @@ import (
var loaded = NewCache(200) // 200 is the default capacity var loaded = NewCache(200) // 200 is the default capacity
var storage store.Store = nil var storage store.Store = nil
// LoadBuiltIn load the built-in assistants
func LoadBuiltIn() error {
root := `/assistants`
app, err := fs.Get("app")
if err != nil {
return err
}
// Remove the built-in assistants
if storage != nil {
builtIn := true
_, err := storage.DeleteAssistants(store.AssistantFilter{BuiltIn: &builtIn})
if err != nil {
return err
}
}
// Check if the assistant is built-in
if exists, _ := app.Exists(root); !exists {
return nil
}
paths, err := app.ReadDir(root, true)
if err != nil {
return err
}
sort := 1
for _, path := range paths {
pkgfile := filepath.Join(path, "package.yao")
if has, _ := app.Exists(pkgfile); !has {
continue
}
assistant, err := LoadPath(path)
if err != nil {
return err
}
assistant.Readonly = true
assistant.BuiltIn = true
assistant.Sort = sort
sort++
loaded.Put(assistant)
// Save the assistant
if storage != nil {
_, err := storage.SaveAssistant(assistant.Map())
if err != nil {
return err
}
}
}
return nil
}
// SetStorage set the storage // SetStorage set the storage
func SetStorage(s store.Store) { func SetStorage(s store.Store) {
storage = s storage = s
@ -89,7 +146,8 @@ func LoadPath(path string) (*Assistant, error) {
// assistant_id // assistant_id
data["assistant_id"] = id data["assistant_id"] = id
data["type"] = "assistant"
data["path"] = path
// prompts // prompts
promptsfile := filepath.Join(path, "prompts.yml") promptsfile := filepath.Join(path, "prompts.yml")
if has, _ := app.Exists(promptsfile); has { if has, _ := app.Exists(promptsfile); has {
@ -140,6 +198,41 @@ func loadMap(data map[string]interface{}) (*Assistant, error) {
assistant.Avatar = avatar assistant.Avatar = avatar
} }
// Type
if v, ok := data["type"].(string); ok {
assistant.Type = v
}
// Mentionable
if v, ok := data["mentionable"].(bool); ok {
assistant.Mentionable = v
}
// Automated
if v, ok := data["automated"].(bool); ok {
assistant.Automated = v
}
// Readonly
if v, ok := data["readonly"].(bool); ok {
assistant.Readonly = v
}
// built_in
if v, ok := data["built_in"].(bool); ok {
assistant.BuiltIn = v
}
// sort
if v, ok := data["sort"].(int); ok {
assistant.Sort = v
}
// path
if v, ok := data["path"].(string); ok {
assistant.Path = v
}
// connector // connector
if connector, ok := data["connector"].(string); ok { if connector, ok := data["connector"].(string); ok {
assistant.Connector = connector assistant.Connector = connector
@ -216,3 +309,151 @@ func loadScriptSource(source string, file string) (*v8.Script, error) {
} }
return script, nil return script, nil
} }
// Save save the assistant
func (ast *Assistant) Save() error {
if storage == nil {
return fmt.Errorf("storage is not set")
}
_, err := storage.SaveAssistant(ast.Map())
return err
}
// Map convert the assistant to a map
func (ast *Assistant) Map() map[string]interface{} {
if ast == nil {
return nil
}
return map[string]interface{}{
"assistant_id": ast.ID,
"type": ast.Type,
"name": ast.Name,
"readonly": ast.Readonly,
"avatar": ast.Avatar,
"connector": ast.Connector,
"path": ast.Path,
"built_in": ast.BuiltIn,
"sort": ast.Sort,
"description": ast.Description,
"options": ast.Options,
"prompts": ast.Prompts,
"tags": ast.Tags,
"mentionable": ast.Mentionable,
"automated": ast.Automated,
}
}
// Validate validates the assistant configuration
func (ast *Assistant) Validate() error {
if ast.ID == "" {
return fmt.Errorf("assistant_id is required")
}
if ast.Name == "" {
return fmt.Errorf("name is required")
}
if ast.Connector == "" {
return fmt.Errorf("connector is required")
}
return nil
}
// Clone creates a deep copy of the assistant
func (ast *Assistant) Clone() *Assistant {
if ast == nil {
return nil
}
clone := &Assistant{
ID: ast.ID,
Type: ast.Type,
Name: ast.Name,
Avatar: ast.Avatar,
Connector: ast.Connector,
Path: ast.Path,
BuiltIn: ast.BuiltIn,
Sort: ast.Sort,
Description: ast.Description,
Readonly: ast.Readonly,
Mentionable: ast.Mentionable,
Automated: ast.Automated,
Script: ast.Script,
API: ast.API,
}
// Deep copy tags
if ast.Tags != nil {
clone.Tags = make([]string, len(ast.Tags))
copy(clone.Tags, ast.Tags)
}
// Deep copy options
if ast.Options != nil {
clone.Options = make(map[string]interface{})
for k, v := range ast.Options {
clone.Options[k] = v
}
}
// Deep copy prompts
if ast.Prompts != nil {
clone.Prompts = make([]Prompt, len(ast.Prompts))
copy(clone.Prompts, ast.Prompts)
}
// Deep copy flows
if ast.Flows != nil {
clone.Flows = make([]map[string]interface{}, len(ast.Flows))
for i, flow := range ast.Flows {
cloneFlow := make(map[string]interface{})
for k, v := range flow {
cloneFlow[k] = v
}
clone.Flows[i] = cloneFlow
}
}
return clone
}
// Update updates the assistant properties
func (ast *Assistant) Update(data map[string]interface{}) error {
if ast == nil {
return fmt.Errorf("assistant is nil")
}
if v, ok := data["name"].(string); ok {
ast.Name = v
}
if v, ok := data["avatar"].(string); ok {
ast.Avatar = v
}
if v, ok := data["description"].(string); ok {
ast.Description = v
}
if v, ok := data["connector"].(string); ok {
ast.Connector = v
}
if v, ok := data["type"].(string); ok {
ast.Type = v
}
if v, ok := data["sort"].(int); ok {
ast.Sort = v
}
if v, ok := data["mentionable"].(bool); ok {
ast.Mentionable = v
}
if v, ok := data["automated"].(bool); ok {
ast.Automated = v
}
if v, ok := data["tags"].([]string); ok {
ast.Tags = v
}
if v, ok := data["options"].(map[string]interface{}); ok {
ast.Options = v
}
return ast.Validate()
}

View file

@ -127,6 +127,196 @@ func TestAssistant_Cache(t *testing.T) {
assert.NotNil(t, loaded) assert.NotNil(t, loaded)
} }
func TestAssistant_Validate(t *testing.T) {
tests := []struct {
name string
ast *Assistant
wantErr bool
}{
{
name: "valid assistant",
ast: &Assistant{
ID: "test-id",
Name: "Test Assistant",
Connector: "test-connector",
},
wantErr: false,
},
{
name: "missing id",
ast: &Assistant{
Name: "Test Assistant",
Connector: "test-connector",
},
wantErr: true,
},
{
name: "missing name",
ast: &Assistant{
ID: "test-id",
Connector: "test-connector",
},
wantErr: true,
},
{
name: "missing connector",
ast: &Assistant{
ID: "test-id",
Name: "Test Assistant",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.ast.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Assistant.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestAssistant_Clone(t *testing.T) {
// Create a test assistant with all fields populated
original := &Assistant{
ID: "test-id",
Type: "test-type",
Name: "Test Assistant",
Avatar: "test-avatar",
Connector: "test-connector",
Path: "test-path",
BuiltIn: true,
Sort: 1,
Description: "test description",
Tags: []string{"tag1", "tag2"},
Readonly: true,
Mentionable: true,
Automated: true,
Options: map[string]interface{}{"key": "value"},
Prompts: []Prompt{{Role: "system", Content: "test"}},
Flows: []map[string]interface{}{{"step": "test"}},
}
// Clone the assistant
clone := original.Clone()
// Verify all fields are correctly cloned
assert.Equal(t, original.ID, clone.ID)
assert.Equal(t, original.Type, clone.Type)
assert.Equal(t, original.Name, clone.Name)
assert.Equal(t, original.Avatar, clone.Avatar)
assert.Equal(t, original.Connector, clone.Connector)
assert.Equal(t, original.Path, clone.Path)
assert.Equal(t, original.BuiltIn, clone.BuiltIn)
assert.Equal(t, original.Sort, clone.Sort)
assert.Equal(t, original.Description, clone.Description)
assert.Equal(t, original.Tags, clone.Tags)
assert.Equal(t, original.Readonly, clone.Readonly)
assert.Equal(t, original.Mentionable, clone.Mentionable)
assert.Equal(t, original.Automated, clone.Automated)
assert.Equal(t, original.Options, clone.Options)
assert.Equal(t, original.Prompts, clone.Prompts)
assert.Equal(t, original.Flows, clone.Flows)
// Verify deep copy by modifying original
original.Tags[0] = "modified"
original.Options["key"] = "modified"
original.Flows[0]["step"] = "modified"
assert.NotEqual(t, original.Tags[0], clone.Tags[0])
assert.NotEqual(t, original.Options["key"], clone.Options["key"])
assert.NotEqual(t, original.Flows[0]["step"], clone.Flows[0]["step"])
// Test nil case
var nilAssistant *Assistant
assert.Nil(t, nilAssistant.Clone())
}
func TestAssistant_Update(t *testing.T) {
// Create a test assistant
ast := &Assistant{
ID: "test-id",
Name: "Original Name",
Connector: "original-connector",
}
// Test updating various fields
updates := map[string]interface{}{
"name": "Updated Name",
"avatar": "updated-avatar",
"description": "Updated description",
"connector": "updated-connector",
"type": "updated-type",
"sort": 2,
"mentionable": true,
"automated": true,
"tags": []string{"new-tag"},
"options": map[string]interface{}{"new": "value"},
}
err := ast.Update(updates)
assert.NoError(t, err)
// Verify updates
assert.Equal(t, "Updated Name", ast.Name)
assert.Equal(t, "updated-avatar", ast.Avatar)
assert.Equal(t, "Updated description", ast.Description)
assert.Equal(t, "updated-connector", ast.Connector)
assert.Equal(t, "updated-type", ast.Type)
assert.Equal(t, 2, ast.Sort)
assert.True(t, ast.Mentionable)
assert.True(t, ast.Automated)
assert.Equal(t, []string{"new-tag"}, ast.Tags)
assert.Equal(t, map[string]interface{}{"new": "value"}, ast.Options)
// Test nil assistant
var nilAssistant *Assistant
err = nilAssistant.Update(updates)
assert.Error(t, err)
// Test invalid update that would make the assistant invalid
invalidUpdates := map[string]interface{}{
"name": "",
}
err = ast.Update(invalidUpdates)
assert.Error(t, err)
}
func TestLoadBuiltIn(t *testing.T) {
prepare(t)
defer test.Clean()
// Clear any existing cache and storage
ClearCache()
SetStorage(nil)
// Create a mock store to verify built-in assistants are saved
mockStore := &mockStore{
data: make(map[string]map[string]interface{}),
}
SetStorage(mockStore)
SetCache(100)
// Test loading built-in assistants
err := LoadBuiltIn()
assert.NoError(t, err)
// Verify Modi assistant was loaded
assistant, exists := loaded.Get("modi")
assert.True(t, exists, "Modi assistant should be loaded in cache")
if exists {
assert.Equal(t, "modi", assistant.ID)
assert.Equal(t, "Modi", assistant.Name)
assert.Equal(t, "deepseek", assistant.Connector)
assert.True(t, assistant.BuiltIn)
assert.True(t, assistant.Readonly)
assert.NotNil(t, assistant.Prompts)
assert.NotNil(t, assistant.Script)
}
}
// mockStore implements store.Store interface for testing // mockStore implements store.Store interface for testing
type mockStore struct { type mockStore struct {
data map[string]map[string]interface{} data map[string]map[string]interface{}
@ -194,3 +384,5 @@ func (m *mockStore) SaveHistory(sid string, messages []map[string]interface{}, c
return nil return nil
} }
func (m *mockStore) UpdateChatTitle(sid string, cid string, title string) error { return nil } func (m *mockStore) UpdateChatTitle(sid string, cid string, title string) error { return nil }
func (m *mockStore) DeleteAssistants(filter store.AssistantFilter) (int64, error) { return 0, nil }
func (m *mockStore) GetAssistantTags() ([]string, error) { return []string{}, nil }

View file

@ -38,8 +38,15 @@ type Assistant struct {
Name string `json:"name,omitempty"` // Assistant Name Name string `json:"name,omitempty"` // Assistant Name
Avatar string `json:"avatar,omitempty"` // Assistant Avatar Avatar string `json:"avatar,omitempty"` // Assistant Avatar
Connector string `json:"connector"` // AI Connector Connector string `json:"connector"` // AI Connector
Path string `json:"path,omitempty"` // Assistant Path
BuiltIn bool `json:"built_in,omitempty"` // Whether this is a built-in assistant
Sort int `json:"sort,omitempty"` // Assistant Sort
Description string `json:"description,omitempty"` // Assistant Description Description string `json:"description,omitempty"` // Assistant Description
Option map[string]interface{} `json:"option,omitempty"` // AI Option Tags []string `json:"tags,omitempty"` // Assistant Tags
Readonly bool `json:"readonly,omitempty"` // Whether this assistant is readonly
Mentionable bool `json:"mentionable,omitempty"` // Whether this assistant is mentionable
Automated bool `json:"automated,omitempty"` // Whether this assistant is automated
Options map[string]interface{} `json:"options,omitempty"` // AI Options
Prompts []Prompt `json:"prompts,omitempty"` // AI Prompts Prompts []Prompt `json:"prompts,omitempty"` // AI Prompts
Flows []map[string]interface{} `json:"flows,omitempty"` // Assistant Flows Flows []map[string]interface{} `json:"flows,omitempty"` // Assistant Flows
Script *v8.Script `json:"-" yaml:"-"` // Assistant Script Script *v8.Script `json:"-" yaml:"-"` // Assistant Script

View file

@ -51,6 +51,13 @@ func Load(cfg config.Config) error {
return err return err
} }
// Load Built-in Assistants
assistant.SetStorage(Neo.Store)
err = assistant.LoadBuiltIn()
if err != nil {
return err
}
// Query Assistant List // Query Assistant List
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()

View file

@ -62,3 +62,13 @@ func (m *Mongo) GetAssistants(filter AssistantFilter) (*AssistantResponse, error
func (m *Mongo) GetAssistant(assistantID string) (map[string]interface{}, error) { func (m *Mongo) GetAssistant(assistantID string) (map[string]interface{}, error) {
return map[string]interface{}{}, nil return map[string]interface{}{}, nil
} }
// DeleteAssistants deletes assistants based on filter conditions (not implemented)
func (mongo *Mongo) DeleteAssistants(filter AssistantFilter) (int64, error) {
return 0, nil
}
// GetAssistantTags retrieves all unique tags from assistants
func (conv *Mongo) GetAssistantTags() ([]string, error) {
return []string{}, nil
}

View file

@ -62,3 +62,13 @@ func (r *Redis) GetAssistants(filter AssistantFilter) (*AssistantResponse, error
func (r *Redis) GetAssistant(assistantID string) (map[string]interface{}, error) { func (r *Redis) GetAssistant(assistantID string) (map[string]interface{}, error) {
return map[string]interface{}{}, nil return map[string]interface{}{}, nil
} }
// DeleteAssistants deletes assistants based on filter conditions (not implemented)
func (redis *Redis) DeleteAssistants(filter AssistantFilter) (int64, error) {
return 0, nil
}
// GetAssistantTags retrieves all unique tags from assistants
func (conv *Redis) GetAssistantTags() ([]string, error) {
return []string{}, nil
}

View file

@ -52,6 +52,7 @@ type AssistantFilter struct {
AssistantID string `json:"assistant_id,omitempty"` // Filter by assistant ID AssistantID string `json:"assistant_id,omitempty"` // Filter by assistant ID
Mentionable *bool `json:"mentionable,omitempty"` // Filter by mentionable status Mentionable *bool `json:"mentionable,omitempty"` // Filter by mentionable status
Automated *bool `json:"automated,omitempty"` // Filter by automation status Automated *bool `json:"automated,omitempty"` // Filter by automation status
BuiltIn *bool `json:"built_in,omitempty"` // Filter by built-in status
Page int `json:"page,omitempty"` // Page number, starting from 1 Page int `json:"page,omitempty"` // Page number, starting from 1
PageSize int `json:"pagesize,omitempty"` // Items per page PageSize int `json:"pagesize,omitempty"` // Items per page
Select []string `json:"select,omitempty"` // Fields to return, returns all fields if empty Select []string `json:"select,omitempty"` // Fields to return, returns all fields if empty
@ -135,4 +136,13 @@ type Store interface {
// assistantID: Assistant ID // assistantID: Assistant ID
// Returns: Assistant information and potential error // Returns: Assistant information and potential error
GetAssistant(assistantID string) (map[string]interface{}, error) GetAssistant(assistantID string) (map[string]interface{}, error)
// DeleteAssistants deletes assistants based on filter conditions
// filter: Filter conditions
// Returns: Number of deleted records and potential error
DeleteAssistants(filter AssistantFilter) (int64, error)
// GetAssistantTags retrieves all unique tags from assistants
// Returns: List of tags and potential error
GetAssistantTags() ([]string, error)
} }

View file

@ -225,6 +225,9 @@ func (conv *Xun) initAssistantTable() error {
table.String("avatar", 200).Null() // assistant avatar table.String("avatar", 200).Null() // assistant avatar
table.String("connector", 200).NotNull() // assistant connector table.String("connector", 200).NotNull() // assistant connector
table.Text("description").Null() // assistant description table.Text("description").Null() // assistant description
table.String("path", 200).Null() // assistant storage path
table.Integer("sort").SetDefault(9999).Index() // assistant sort order
table.Boolean("built_in").SetDefault(false).Index() // whether this is a built-in assistant
table.JSON("options").Null() // assistant options table.JSON("options").Null() // assistant options
table.JSON("prompts").Null() // assistant prompts table.JSON("prompts").Null() // assistant prompts
table.JSON("flows").Null() // assistant flows table.JSON("flows").Null() // assistant flows
@ -251,7 +254,7 @@ func (conv *Xun) initAssistantTable() error {
return err return err
} }
fields := []string{"id", "assistant_id", "type", "name", "avatar", "connector", "description", "options", "prompts", "flows", "files", "functions", "tags", "mentionable", "created_at", "updated_at"} fields := []string{"id", "assistant_id", "type", "name", "avatar", "connector", "description", "path", "sort", "built_in", "options", "prompts", "flows", "files", "functions", "tags", "mentionable", "created_at", "updated_at"}
for _, field := range fields { for _, field := range fields {
if !tab.HasColumn(field) { if !tab.HasColumn(field) {
return fmt.Errorf("%s is required", field) return fmt.Errorf("%s is required", field)
@ -845,6 +848,11 @@ func (conv *Xun) GetAssistants(filter AssistantFilter) (*AssistantResponse, erro
qb.Where("automated", *filter.Automated) qb.Where("automated", *filter.Automated)
} }
// Apply built_in filter if provided
if filter.BuiltIn != nil {
qb.Where("built_in", *filter.BuiltIn)
}
// Set defaults for pagination // Set defaults for pagination
if filter.PageSize <= 0 { if filter.PageSize <= 0 {
filter.PageSize = 20 filter.PageSize = 20
@ -881,7 +889,8 @@ func (conv *Xun) GetAssistants(filter AssistantFilter) (*AssistantResponse, erro
} }
// Get paginated results // Get paginated results
rows, err := qb.OrderBy("created_at", "desc"). rows, err := qb.OrderBy("sort", "asc").
OrderBy("updated_at", "desc").
Offset(offset). Offset(offset).
Limit(filter.PageSize). Limit(filter.PageSize).
Get() Get()
@ -950,3 +959,87 @@ func (conv *Xun) GetAssistant(assistantID string) (map[string]interface{}, error
return data, nil return data, nil
} }
// DeleteAssistants deletes assistants based on filter conditions
func (conv *Xun) DeleteAssistants(filter AssistantFilter) (int64, error) {
qb := conv.query.New().
Table(conv.getAssistantTable())
// Apply tag filter if provided
if filter.Tags != nil && len(filter.Tags) > 0 {
qb.Where(func(qb query.Query) {
for i, tag := range filter.Tags {
pattern := fmt.Sprintf("%%\"%s\"%%", tag)
if i == 0 {
qb.Where("tags", "like", pattern)
} else {
qb.OrWhere("tags", "like", pattern)
}
}
})
}
// 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))
})
}
// Apply connector filter if provided
if filter.Connector != "" {
qb.Where("connector", filter.Connector)
}
// Apply assistant_id filter if provided
if filter.AssistantID != "" {
qb.Where("assistant_id", filter.AssistantID)
}
// Apply mentionable filter if provided
if filter.Mentionable != nil {
qb.Where("mentionable", *filter.Mentionable)
}
// Apply automated filter if provided
if filter.Automated != nil {
qb.Where("automated", *filter.Automated)
}
// Apply built_in filter if provided
if filter.BuiltIn != nil {
qb.Where("built_in", *filter.BuiltIn)
}
// Execute delete and return number of deleted records
return qb.Delete()
}
// GetAssistantTags retrieves all unique tags from assistants
func (conv *Xun) GetAssistantTags() ([]string, error) {
q := conv.newQuery().Table(conv.getAssistantTable())
rows, err := q.Select("tags").GroupBy("tags").Get()
if err != nil {
return nil, err
}
tagSet := map[string]bool{}
for _, row := range rows {
if tags, ok := row["tags"].(string); ok && tags != "" {
var tagList []string
if err := jsoniter.UnmarshalFromString(tags, &tagList); err == nil {
for _, tag := range tagList {
tagSet[tag] = true
}
}
}
}
// Convert map keys to slice
tags := make([]string, 0, len(tagSet))
for tag := range tagSet {
tags = append(tags, tag)
}
return tags, nil
}

View file

@ -471,6 +471,9 @@ func TestXunAssistantCRUD(t *testing.T) {
"avatar": "https://example.com/avatar.png", "avatar": "https://example.com/avatar.png",
"connector": "openai", "connector": "openai",
"description": "Test Description", "description": "Test Description",
"path": "/assistants/test",
"sort": 100,
"built_in": true,
"tags": tagsJSON, "tags": tagsJSON,
"options": optionsJSON, "options": optionsJSON,
"mentionable": true, "mentionable": true,
@ -492,6 +495,9 @@ func TestXunAssistantCRUD(t *testing.T) {
assert.Equal(t, "https://example.com/avatar.png", assistantData["avatar"]) assert.Equal(t, "https://example.com/avatar.png", assistantData["avatar"])
assert.Equal(t, "openai", assistantData["connector"]) assert.Equal(t, "openai", assistantData["connector"])
assert.Equal(t, "Test Description", assistantData["description"]) assert.Equal(t, "Test Description", assistantData["description"])
assert.Equal(t, "/assistants/test", assistantData["path"])
assert.Equal(t, int64(100), assistantData["sort"])
assert.Equal(t, int64(1), assistantData["built_in"])
assert.Equal(t, []interface{}{"tag1", "tag2", "tag3"}, assistantData["tags"]) assert.Equal(t, []interface{}{"tag1", "tag2", "tag3"}, assistantData["tags"])
assert.Equal(t, map[string]interface{}{"model": "gpt-4"}, assistantData["options"]) assert.Equal(t, map[string]interface{}{"model": "gpt-4"}, assistantData["options"])
assert.Equal(t, int64(1), assistantData["mentionable"]) assert.Equal(t, int64(1), assistantData["mentionable"])
@ -504,6 +510,9 @@ func TestXunAssistantCRUD(t *testing.T) {
"avatar": "https://example.com/avatar2.png", "avatar": "https://example.com/avatar2.png",
"connector": "openai", "connector": "openai",
"description": "Test Description 2", "description": "Test Description 2",
"path": "/assistants/test2",
"sort": 200,
"built_in": false,
"tags": []string{"tag1", "tag2", "tag3"}, "tags": []string{"tag1", "tag2", "tag3"},
"options": map[string]interface{}{"model": "gpt-4"}, "options": map[string]interface{}{"model": "gpt-4"},
"prompts": []string{"prompt1", "prompt2"}, "prompts": []string{"prompt1", "prompt2"},
@ -545,6 +554,9 @@ func TestXunAssistantCRUD(t *testing.T) {
"type": "assistant", "type": "assistant",
"connector": "openai", "connector": "openai",
"description": "Test Description 3", "description": "Test Description 3",
"path": nil,
"sort": 9999,
"built_in": false,
"tags": nil, "tags": nil,
"options": nil, "options": nil,
"prompts": nil, "prompts": nil,
@ -665,14 +677,232 @@ func TestXunAssistantCRUD(t *testing.T) {
} }
} }
// Test DeleteAssistant // Test non-existent assistant_id
err = store.DeleteAssistant(assistantID) resp, err = store.GetAssistants(AssistantFilter{
AssistantID: "non-existent-id",
Page: 1,
PageSize: 10,
})
assert.Nil(t, err) assert.Nil(t, err)
err = store.DeleteAssistant(assistant2ID) assert.Equal(t, 0, len(resp.Data))
// Test filtering with select fields
resp, err = store.GetAssistants(AssistantFilter{
Select: []string{"name", "description", "tags"},
Page: 1,
PageSize: 10,
})
assert.Nil(t, err) assert.Nil(t, err)
err = store.DeleteAssistant(assistant3ID) // Verify only selected fields are returned
for _, item := range resp.Data {
// These fields should exist
assert.Contains(t, item, "name")
assert.Contains(t, item, "description")
assert.Contains(t, item, "tags")
// These fields should not exist
assert.NotContains(t, item, "options")
assert.NotContains(t, item, "prompts")
assert.NotContains(t, item, "flows")
assert.NotContains(t, item, "files")
assert.NotContains(t, item, "functions")
assert.NotContains(t, item, "permissions")
}
// Test filtering with select fields and other filters combined
resp, err = store.GetAssistants(AssistantFilter{
Tags: []string{"tag1"},
Keywords: "Assistant",
Select: []string{"name", "tags"},
Page: 1,
PageSize: 10,
})
assert.Nil(t, err)
// Verify only selected fields are returned
for _, item := range resp.Data {
// These fields should exist
assert.Contains(t, item, "name")
assert.Contains(t, item, "tags")
// These fields should not exist
assert.NotContains(t, item, "description")
assert.NotContains(t, item, "options")
assert.NotContains(t, item, "prompts")
assert.NotContains(t, item, "flows")
assert.NotContains(t, item, "files")
assert.NotContains(t, item, "functions")
assert.NotContains(t, item, "permissions")
}
// Test filtering with automated
automatedTrue := true
resp, err = store.GetAssistants(AssistantFilter{
Automated: &automatedTrue,
Page: 1,
PageSize: 10,
})
assert.Nil(t, err)
assert.Greater(t, len(resp.Data), 0)
// Test filtering with mentionable
mentionableTrue := true
resp, err = store.GetAssistants(AssistantFilter{
Mentionable: &mentionableTrue,
Page: 1,
PageSize: 10,
})
assert.Nil(t, err)
assert.Greater(t, len(resp.Data), 0)
// Test combined filters
resp, err = store.GetAssistants(AssistantFilter{
Tags: []string{"tag1"},
Keywords: "Assistant",
Connector: "openai",
Mentionable: &mentionableTrue,
Automated: &automatedTrue,
Page: 1,
PageSize: 10,
})
assert.Nil(t, err) assert.Nil(t, err)
// Test filtering with built_in
builtInTrue := true
resp, err = store.GetAssistants(AssistantFilter{
BuiltIn: &builtInTrue,
Page: 1,
PageSize: 10,
})
assert.Nil(t, err)
for _, assistant := range resp.Data {
assert.Equal(t, int64(1), assistant["built_in"], "All assistants should be built-in")
}
builtInFalse := false
resp, err = store.GetAssistants(AssistantFilter{
BuiltIn: &builtInFalse,
Page: 1,
PageSize: 10,
})
assert.Nil(t, err)
for _, assistant := range resp.Data {
assert.Equal(t, int64(0), assistant["built_in"], "All assistants should not be built-in")
}
// Now test the delete operations
// First create some test data for delete operations
for i := 0; i < 5; i++ {
assistant := map[string]interface{}{
"name": fmt.Sprintf("Delete Test Assistant %d", i),
"type": "assistant",
"connector": "openai",
"description": fmt.Sprintf("Delete Test Description %d", i),
"tags": []string{"delete-tag1", "delete-tag2"},
"built_in": i%2 == 0,
"mentionable": true,
"automated": true,
}
_, err = store.SaveAssistant(assistant)
assert.Nil(t, err)
}
// Test delete by connector
count, err := store.DeleteAssistants(AssistantFilter{
Connector: "openai",
})
assert.Nil(t, err)
assert.Greater(t, count, int64(0))
// Verify deletion
resp, err = store.GetAssistants(AssistantFilter{
Connector: "openai",
})
assert.Nil(t, err)
assert.Equal(t, 0, len(resp.Data))
// Create more test data for built_in test
for i := 0; i < 5; i++ {
assistant := map[string]interface{}{
"name": fmt.Sprintf("Built-in Test Assistant %d", i),
"type": "assistant",
"connector": "openai",
"description": fmt.Sprintf("Built-in Test Description %d", i),
"tags": []string{"builtin-tag1", "builtin-tag2"},
"built_in": true,
"mentionable": true,
"automated": true,
}
_, err = store.SaveAssistant(assistant)
assert.Nil(t, err)
}
// Test delete by built_in status
builtInTrue = true
count, err = store.DeleteAssistants(AssistantFilter{
BuiltIn: &builtInTrue,
})
assert.Nil(t, err)
assert.Greater(t, count, int64(0))
// Verify deletion
resp, err = store.GetAssistants(AssistantFilter{
BuiltIn: &builtInTrue,
})
assert.Nil(t, err)
assert.Equal(t, 0, len(resp.Data))
// Create more test data for tags test
for i := 0; i < 5; i++ {
assistant := map[string]interface{}{
"name": fmt.Sprintf("Tags Test Assistant %d", i),
"type": "assistant",
"connector": "openai",
"description": fmt.Sprintf("Tags Test Description %d", i),
"tags": []string{"tag1", "tag2"},
"built_in": false,
"mentionable": true,
"automated": true,
}
_, err = store.SaveAssistant(assistant)
assert.Nil(t, err)
}
// Test delete by tags
count, err = store.DeleteAssistants(AssistantFilter{
Tags: []string{"tag1"},
})
assert.Nil(t, err)
assert.Greater(t, count, int64(0))
// Verify deletion
resp, err = store.GetAssistants(AssistantFilter{
Tags: []string{"tag1"},
})
assert.Nil(t, err)
assert.Equal(t, 0, len(resp.Data))
// Create more test data for keywords test
for i := 0; i < 5; i++ {
assistant := map[string]interface{}{
"name": fmt.Sprintf("Keywords Test Assistant %d", i),
"type": "assistant",
"connector": "openai",
"description": fmt.Sprintf("Keywords Test Description %d", i),
"tags": []string{"keyword-tag1", "keyword-tag2"},
"built_in": false,
"mentionable": true,
"automated": true,
}
_, err = store.SaveAssistant(assistant)
assert.Nil(t, err)
}
// Test delete by keywords
count, err = store.DeleteAssistants(AssistantFilter{
Keywords: "Keywords Test",
})
assert.Nil(t, err)
assert.Greater(t, count, int64(0))
// Verify all assistants are deleted
resp, err = store.GetAssistants(AssistantFilter{}) resp, err = store.GetAssistants(AssistantFilter{})
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 0, len(resp.Data)) assert.Equal(t, 0, len(resp.Data))
@ -725,6 +955,9 @@ func TestXunAssistantPagination(t *testing.T) {
"connector": fmt.Sprintf("connector%d", i%3), "connector": fmt.Sprintf("connector%d", i%3),
"description": fmt.Sprintf("Description %d", i), "description": fmt.Sprintf("Description %d", i),
"tags": tagsJSON, "tags": tagsJSON,
"sort": 9999 - i,
"updated_at": time.Now().Add(time.Duration(-i) * time.Hour),
"built_in": i%2 == 0,
"mentionable": mentionable, "mentionable": mentionable,
"automated": automated, "automated": automated,
} }
@ -744,6 +977,25 @@ func TestXunAssistantPagination(t *testing.T) {
assert.Equal(t, 2, resp.Next) assert.Equal(t, 2, resp.Next)
assert.Equal(t, 0, resp.Prev) assert.Equal(t, 0, resp.Prev)
// Verify sorting order (sort ASC, updated_at DESC)
for i := 1; i < len(resp.Data); i++ {
curr := resp.Data[i]["sort"].(int64)
prev := resp.Data[i-1]["sort"].(int64)
assert.True(t, curr >= prev, "Results should be sorted by sort ASC")
// When sort values are equal, check updated_at if both values exist
if curr == prev {
currTime, currOk := resp.Data[i]["updated_at"].(time.Time)
prevTime, prevOk := resp.Data[i-1]["updated_at"].(time.Time)
// Only compare times if both values exist
if currOk && prevOk {
assert.True(t, currTime.Before(prevTime) || currTime.Equal(prevTime),
"Results with same sort should be ordered by updated_at DESC")
}
}
}
// Test second page // Test second page
resp, err = store.GetAssistants(AssistantFilter{ resp, err = store.GetAssistants(AssistantFilter{
Page: 2, Page: 2,
@ -811,7 +1063,30 @@ func TestXunAssistantPagination(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.Greater(t, len(resp.Data), 0) assert.Greater(t, len(resp.Data), 0)
// Test filtering by assistant_id // Test filtering with built_in
builtInTrue := true
resp, err = store.GetAssistants(AssistantFilter{
BuiltIn: &builtInTrue,
Page: 1,
PageSize: 10,
})
assert.Nil(t, err)
for _, assistant := range resp.Data {
assert.Equal(t, int64(1), assistant["built_in"], "All assistants should be built-in")
}
builtInFalse := false
resp, err = store.GetAssistants(AssistantFilter{
BuiltIn: &builtInFalse,
Page: 1,
PageSize: 10,
})
assert.Nil(t, err)
for _, assistant := range resp.Data {
assert.Equal(t, int64(0), assistant["built_in"], "All assistants should not be built-in")
}
// Test assistant_id with other filters
// First get an assistant_id from previous results // First get an assistant_id from previous results
firstAssistantID := resp.Data[0]["assistant_id"].(string) firstAssistantID := resp.Data[0]["assistant_id"].(string)
@ -851,18 +1126,6 @@ func TestXunAssistantPagination(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 0, len(resp.Data)) assert.Equal(t, 0, len(resp.Data))
// Test combined filters
resp, err = store.GetAssistants(AssistantFilter{
Tags: []string{"tag0"},
Keywords: "Assistant",
Connector: "connector0",
Mentionable: &mentionableTrue,
Automated: &automatedTrue,
Page: 1,
PageSize: 10,
})
assert.Nil(t, err)
// Test filtering with select fields // Test filtering with select fields
resp, err = store.GetAssistants(AssistantFilter{ resp, err = store.GetAssistants(AssistantFilter{
Select: []string{"name", "description", "tags"}, Select: []string{"name", "description", "tags"},
@ -909,4 +1172,152 @@ func TestXunAssistantPagination(t *testing.T) {
assert.NotContains(t, item, "functions") assert.NotContains(t, item, "functions")
assert.NotContains(t, item, "permissions") assert.NotContains(t, item, "permissions")
} }
// Test combined filters
resp, err = store.GetAssistants(AssistantFilter{
Tags: []string{"tag0"},
Keywords: "Assistant",
Connector: "connector0",
Mentionable: &mentionableTrue,
Automated: &automatedTrue,
Page: 1,
PageSize: 10,
})
assert.Nil(t, err)
// Now test the delete operations
// Test delete by connector
count, err := store.DeleteAssistants(AssistantFilter{
Connector: "connector0",
})
assert.Nil(t, err)
assert.Greater(t, count, int64(0))
// Verify deletion
resp, err = store.GetAssistants(AssistantFilter{
Connector: "connector0",
})
assert.Nil(t, err)
assert.Equal(t, 0, len(resp.Data))
// Test delete by built_in status
builtInTrue = true
count, err = store.DeleteAssistants(AssistantFilter{
BuiltIn: &builtInTrue,
})
assert.Nil(t, err)
assert.Greater(t, count, int64(0))
// Verify deletion
resp, err = store.GetAssistants(AssistantFilter{
BuiltIn: &builtInTrue,
})
assert.Nil(t, err)
assert.Equal(t, 0, len(resp.Data))
// Test delete by tags
count, err = store.DeleteAssistants(AssistantFilter{
Tags: []string{"tag1"},
})
assert.Nil(t, err)
assert.Greater(t, count, int64(0))
// Verify deletion
resp, err = store.GetAssistants(AssistantFilter{
Tags: []string{"tag1"},
})
assert.Nil(t, err)
assert.Equal(t, 0, len(resp.Data))
// Test delete by keywords
count, err = store.DeleteAssistants(AssistantFilter{
Keywords: "Assistant",
})
assert.Nil(t, err)
assert.Greater(t, count, int64(0))
// Verify all assistants are deleted
resp, err = store.GetAssistants(AssistantFilter{})
assert.Nil(t, err)
assert.Equal(t, 0, len(resp.Data))
}
func TestGetAssistantTags(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
defer capsule.Schema().DropTableIfExists("__unit_test_conversation_assistant")
store, err := NewXun(Setting{
Connector: "default",
Table: "__unit_test_conversation",
})
if err != nil {
t.Fatal(err)
}
// Create test assistants with tags
assistants := []map[string]interface{}{
{
"assistant_id": "test-assistant-1",
"type": "assistant",
"connector": "test",
"tags": []string{"tag1", "tag2"},
"name": "Test Assistant 1",
},
{
"assistant_id": "test-assistant-2",
"type": "assistant",
"connector": "test",
"tags": []string{"tag2", "tag3"},
"name": "Test Assistant 2",
},
{
"assistant_id": "test-assistant-3",
"type": "assistant",
"connector": "test",
"tags": []string{"tag1", "tag3", "tag4"},
"name": "Test Assistant 3",
},
}
// Save test assistants
for _, assistant := range assistants {
_, err := store.SaveAssistant(assistant)
if err != nil {
t.Fatal(err)
}
}
// Get tags
tags, err := store.GetAssistantTags()
if err != nil {
t.Fatal(err)
}
// Verify results
expectedTags := map[string]bool{
"tag1": true,
"tag2": true,
"tag3": true,
"tag4": true,
}
if len(tags) != len(expectedTags) {
t.Errorf("Expected %d tags, got %d", len(expectedTags), len(tags))
}
for _, tag := range tags {
if !expectedTags[tag] {
t.Errorf("Unexpected tag found: %s", tag)
}
}
// Cleanup
for _, assistant := range assistants {
err := store.DeleteAssistant(assistant["assistant_id"].(string))
if err != nil {
t.Fatal(err)
}
}
} }