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:
parent
d0d110b0ec
commit
f8ce8fc193
10 changed files with 1039 additions and 22 deletions
36
neo/api.go
36
neo/api.go
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue