Enhance assistant management and JSON field handling in Neo API

- Updated SaveAssistant and processAssistantAdd methods to return assistant IDs, improving response consistency.
- Implemented logic to handle JSON fields as both strings and native types, ensuring proper storage and retrieval of assistant attributes.
- Refactored tests to cover various JSON field formats, including string, native types, and nil values, enhancing test coverage and reliability.
- Improved the assistant management structure to support mixed JSON formats, ensuring robust functionality across different storage backends.
This commit is contained in:
Max 2024-12-29 17:14:01 +08:00
parent af65a2d5fa
commit 94f3ddb158
9 changed files with 463 additions and 336 deletions

View file

@ -923,13 +923,18 @@ func (neo *DSL) handleAssistantSave(c *gin.Context) {
return
}
err := neo.Conversation.SaveAssistant(assistant)
id, err := neo.Conversation.SaveAssistant(assistant)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
return
}
// Update the assistant map with the returned ID if it's not already set
if _, ok := assistant["assistant_id"]; !ok {
assistant["assistant_id"] = id
}
c.JSON(200, gin.H{"message": "ok", "data": assistant})
c.Done()
}

View file

@ -1,83 +1,59 @@
package conversation
// Mongo conversation
// Mongo represents a MongoDB-based conversation storage
type Mongo struct{}
// NewMongo create a new conversation
// NewMongo creates a new MongoDB conversation storage
func NewMongo() *Mongo {
return &Mongo{}
}
// UpdateChatTitle update the chat title
func (conv *Mongo) UpdateChatTitle(sid string, cid string, title string) error {
return nil
// GetChats retrieves a list of chats
func (m *Mongo) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) {
return &ChatGroupResponse{}, nil
}
// GetChats get the chat list
func (conv *Mongo) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) {
return &ChatGroupResponse{
Groups: []ChatGroup{},
Page: filter.Page,
PageSize: filter.PageSize,
Total: 0,
LastPage: 1,
}, nil
// GetChat retrieves a single chat's information
func (m *Mongo) GetChat(sid string, cid string) (*ChatInfo, error) {
return &ChatInfo{}, nil
}
// GetHistory get the history
func (conv *Mongo) GetHistory(sid string, cid string) ([]map[string]interface{}, error) {
// GetHistory retrieves chat history
func (m *Mongo) GetHistory(sid string, cid string) ([]map[string]interface{}, error) {
return []map[string]interface{}{}, nil
}
// SaveHistory save the history
func (conv *Mongo) SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error {
// SaveHistory saves chat history
func (m *Mongo) SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error {
return nil
}
// GetRequest get the request
func (conv *Mongo) GetRequest(sid string, rid string) ([]map[string]interface{}, error) {
return nil, nil
}
// SaveRequest save the request
func (conv *Mongo) SaveRequest(sid string, rid string, cid string, messages []map[string]interface{}) error {
// DeleteChat deletes a single chat
func (m *Mongo) DeleteChat(sid string, cid string) error {
return nil
}
// GetChat get the chat info and its history
func (conv *Mongo) GetChat(sid string, cid string) (*ChatInfo, error) {
return nil, nil
}
// DeleteChat deletes a specific chat and its history
func (conv *Mongo) DeleteChat(sid string, cid string) error {
// DeleteAllChats deletes all chats
func (m *Mongo) DeleteAllChats(sid string) error {
return nil
}
// DeleteAllChats deletes all chats and their histories for a user
func (conv *Mongo) DeleteAllChats(sid string) error {
// UpdateChatTitle updates chat title
func (m *Mongo) UpdateChatTitle(sid string, cid string, title string) error {
return nil
}
// SaveAssistant creates or updates an assistant
func (conv *Mongo) SaveAssistant(assistant map[string]interface{}) error {
// SaveAssistant saves assistant information
func (m *Mongo) SaveAssistant(assistant map[string]interface{}) (interface{}, error) {
return assistant["assistant_id"], nil
}
// DeleteAssistant deletes an assistant
func (m *Mongo) DeleteAssistant(assistantID string) error {
return nil
}
// DeleteAssistant deletes an assistant by assistant_id
func (conv *Mongo) DeleteAssistant(assistantID string) error {
return nil
}
// GetAssistants retrieves assistants with pagination and tag filtering
func (conv *Mongo) GetAssistants(filter AssistantFilter) (*AssistantResponse, error) {
return &AssistantResponse{
Data: []map[string]interface{}{},
Page: filter.Page,
PageSize: filter.PageSize,
PageCnt: 0,
Next: 0,
Prev: 0,
Total: 0,
}, nil
// GetAssistants retrieves a list of assistants
func (m *Mongo) GetAssistants(filter AssistantFilter) (*AssistantResponse, error) {
return &AssistantResponse{}, nil
}

View file

@ -1,83 +1,59 @@
package conversation
// Redis conversation
// Redis represents a Redis-based conversation storage
type Redis struct{}
// NewRedis create a new conversation
// NewRedis creates a new Redis conversation storage
func NewRedis() *Redis {
return &Redis{}
}
// UpdateChatTitle update the chat title
func (conv *Redis) UpdateChatTitle(sid string, cid string, title string) error {
return nil
// GetChats retrieves a list of chats
func (r *Redis) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) {
return &ChatGroupResponse{}, nil
}
// GetChats get the chat list
func (conv *Redis) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) {
return &ChatGroupResponse{
Groups: []ChatGroup{},
Page: filter.Page,
PageSize: filter.PageSize,
Total: 0,
LastPage: 1,
}, nil
// GetChat retrieves a single chat's information
func (r *Redis) GetChat(sid string, cid string) (*ChatInfo, error) {
return &ChatInfo{}, nil
}
// GetHistory get the history
func (conv *Redis) GetHistory(sid string, cid string) ([]map[string]interface{}, error) {
// GetHistory retrieves chat history
func (r *Redis) GetHistory(sid string, cid string) ([]map[string]interface{}, error) {
return []map[string]interface{}{}, nil
}
// SaveHistory save the history
func (conv *Redis) SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error {
// SaveHistory saves chat history
func (r *Redis) SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error {
return nil
}
// GetRequest get the request
func (conv *Redis) GetRequest(sid string, rid string) ([]map[string]interface{}, error) {
return nil, nil
}
// SaveRequest save the request
func (conv *Redis) SaveRequest(sid string, rid string, cid string, messages []map[string]interface{}) error {
// DeleteChat deletes a single chat
func (r *Redis) DeleteChat(sid string, cid string) error {
return nil
}
// GetChat get the chat info and its history
func (conv *Redis) GetChat(sid string, cid string) (*ChatInfo, error) {
return nil, nil
}
// DeleteChat deletes a specific chat and its history
func (conv *Redis) DeleteChat(sid string, cid string) error {
// DeleteAllChats deletes all chats
func (r *Redis) DeleteAllChats(sid string) error {
return nil
}
// DeleteAllChats deletes all chats and their histories for a user
func (conv *Redis) DeleteAllChats(sid string) error {
// UpdateChatTitle updates chat title
func (r *Redis) UpdateChatTitle(sid string, cid string, title string) error {
return nil
}
// SaveAssistant creates or updates an assistant
func (conv *Redis) SaveAssistant(assistant map[string]interface{}) error {
// SaveAssistant saves assistant information
func (r *Redis) SaveAssistant(assistant map[string]interface{}) (interface{}, error) {
return assistant["assistant_id"], nil
}
// DeleteAssistant deletes an assistant
func (r *Redis) DeleteAssistant(assistantID string) error {
return nil
}
// DeleteAssistant deletes an assistant by assistant_id
func (conv *Redis) DeleteAssistant(assistantID string) error {
return nil
}
// GetAssistants retrieves assistants with pagination and tag filtering
func (conv *Redis) GetAssistants(filter AssistantFilter) (*AssistantResponse, error) {
return &AssistantResponse{
Data: []map[string]interface{}{},
Page: filter.Page,
PageSize: filter.PageSize,
PageCnt: 0,
Next: 0,
Prev: 0,
Total: 0,
}, nil
// GetAssistants retrieves a list of assistants
func (r *Redis) GetAssistants(filter AssistantFilter) (*AssistantResponse, error) {
return &AssistantResponse{}, nil
}

View file

@ -117,7 +117,7 @@ type Conversation interface {
// SaveAssistant saves assistant information
// assistant: Assistant information
// Returns: Potential error
SaveAssistant(assistant map[string]interface{}) error
SaveAssistant(assistant map[string]interface{}) (interface{}, error)
// DeleteAssistant deletes an assistant
// assistantID: Assistant ID

View file

@ -1,84 +1,59 @@
package conversation
// Weaviate Database conversation
// Weaviate represents a Weaviate-based conversation storage
type Weaviate struct{}
// NewWeaviate create a new conversation
// NewWeaviate creates a new Weaviate conversation storage
func NewWeaviate() *Weaviate {
return &Weaviate{}
}
// UpdateChatTitle update the chat title
func (conv *Weaviate) UpdateChatTitle(sid string, cid string, title string) error {
return nil
// GetChats retrieves a list of chats
func (w *Weaviate) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) {
return &ChatGroupResponse{}, nil
}
// GetChats get the chat list
func (conv *Weaviate) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) {
return &ChatGroupResponse{
Groups: []ChatGroup{},
Page: filter.Page,
PageSize: filter.PageSize,
Total: 0,
LastPage: 1,
}, nil
// GetChat retrieves a single chat's information
func (w *Weaviate) GetChat(sid string, cid string) (*ChatInfo, error) {
return &ChatInfo{}, nil
}
// GetHistory get the history
func (conv *Weaviate) GetHistory(sid string, cid string) ([]map[string]interface{}, error) {
// GetHistory retrieves chat history
func (w *Weaviate) GetHistory(sid string, cid string) ([]map[string]interface{}, error) {
return []map[string]interface{}{}, nil
}
// SaveHistory save the history
func (conv *Weaviate) SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error {
// SaveHistory saves chat history
func (w *Weaviate) SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error {
return nil
}
// GetRequest get the request
func (conv *Weaviate) GetRequest(sid string, rid string) ([]map[string]interface{}, error) {
return nil, nil
}
// SaveRequest save the request
func (conv *Weaviate) SaveRequest(sid string, rid string, cid string, messages []map[string]interface{}) error {
// DeleteChat deletes a single chat
func (w *Weaviate) DeleteChat(sid string, cid string) error {
return nil
}
// GetChat get the chat info and its history
func (conv *Weaviate) GetChat(sid string, cid string) (*ChatInfo, error) {
return nil, nil
}
// DeleteChat deletes a specific chat and its history
func (conv *Weaviate) DeleteChat(sid string, cid string) error {
// DeleteAllChats deletes all chats
func (w *Weaviate) DeleteAllChats(sid string) error {
return nil
}
// DeleteAllChats deletes all chats and their histories for a user
func (conv *Weaviate) DeleteAllChats(sid string) error {
// UpdateChatTitle updates chat title
func (w *Weaviate) UpdateChatTitle(sid string, cid string, title string) error {
return nil
}
// SaveAssistant creates or updates an assistant
func (conv *Weaviate) SaveAssistant(assistant map[string]interface{}) error {
// SaveAssistant saves assistant information
func (w *Weaviate) SaveAssistant(assistant map[string]interface{}) (interface{}, error) {
return assistant["assistant_id"], nil
}
// DeleteAssistant deletes an assistant
func (w *Weaviate) DeleteAssistant(assistantID string) error {
return nil
}
// DeleteAssistant deletes an assistant by assistant_id
func (conv *Weaviate) DeleteAssistant(assistantID string) error {
return nil
}
// GetAssistants retrieves assistants with pagination and tag filtering
func (conv *Weaviate) GetAssistants(filter AssistantFilter) (*AssistantResponse, error) {
return &AssistantResponse{
Data: []map[string]interface{}{},
Page: filter.Page,
PageSize: filter.PageSize,
PageCnt: 0,
Next: 0,
Prev: 0,
Total: 0,
}, nil
// GetAssistants retrieves a list of assistants
func (w *Weaviate) GetAssistants(filter AssistantFilter) (*AssistantResponse, error) {
return &AssistantResponse{}, nil
}

View file

@ -662,53 +662,115 @@ func (conv *Xun) DeleteAllChats(sid string) error {
return err
}
// processJSONField processes a field that should be stored as JSON string
func (conv *Xun) processJSONField(field interface{}) (interface{}, error) {
if field == nil {
return nil, nil
}
switch v := field.(type) {
case string:
return v, nil
default:
jsonStr, err := jsoniter.MarshalToString(v)
if err != nil {
return nil, fmt.Errorf("failed to marshal %v to JSON: %v", field, err)
}
return jsonStr, nil
}
}
// parseJSONFields parses JSON string fields into their corresponding Go types
func (conv *Xun) parseJSONFields(data map[string]interface{}, fields []string) {
for _, field := range fields {
if val := data[field]; val != nil {
if strVal, ok := val.(string); ok && strVal != "" {
var parsed interface{}
if err := jsoniter.UnmarshalFromString(strVal, &parsed); err == nil {
data[field] = parsed
}
}
}
}
}
// SaveAssistant saves assistant information
func (conv *Xun) SaveAssistant(assistant map[string]interface{}) error {
func (conv *Xun) SaveAssistant(assistant map[string]interface{}) (interface{}, error) {
// Validate required fields
requiredFields := []string{"name", "type", "connector"}
for _, field := range requiredFields {
if _, ok := assistant[field]; !ok {
return fmt.Errorf("field %s is required", field)
return nil, fmt.Errorf("field %s is required", field)
}
if assistant[field] == nil || assistant[field] == "" {
return fmt.Errorf("field %s cannot be empty", field)
return nil, fmt.Errorf("field %s cannot be empty", field)
}
}
// Validate tags format
if tags, ok := assistant["tags"].(string); ok {
log.Trace("Saving assistant with tags: %s", tags)
// Create a copy of the assistant map to avoid modifying the original
assistantCopy := make(map[string]interface{})
for k, v := range assistant {
assistantCopy[k] = v
}
// Process JSON fields
jsonFields := []string{"tags", "options", "prompts", "flows", "files", "functions", "permissions"}
for _, field := range jsonFields {
if val, ok := assistantCopy[field]; ok && val != nil {
// If it's a string, try to parse it first
if strVal, ok := val.(string); ok && strVal != "" {
var parsed interface{}
if err := jsoniter.UnmarshalFromString(strVal, &parsed); err == nil {
assistantCopy[field] = parsed
}
}
}
}
// Generate assistant_id if not provided
if _, ok := assistant["assistant_id"]; !ok {
assistant["assistant_id"] = uuid.New().String()
if _, ok := assistantCopy["assistant_id"]; !ok {
assistantCopy["assistant_id"] = uuid.New().String()
}
// Check if assistant exists
exists, err := conv.query.New().
Table(conv.getAssistantTable()).
Where("assistant_id", assistant["assistant_id"]).
Where("assistant_id", assistantCopy["assistant_id"]).
Exists()
if err != nil {
return err
return nil, err
}
// Convert JSON fields to strings for storage
for _, field := range jsonFields {
if val, ok := assistantCopy[field]; ok && val != nil {
jsonStr, err := jsoniter.MarshalToString(val)
if err != nil {
return nil, fmt.Errorf("failed to marshal %s to JSON: %v", field, err)
}
assistantCopy[field] = jsonStr
}
}
// Update or insert
if exists {
assistant["updated_at"] = time.Now()
_, err = conv.query.New().
_, err := conv.query.New().
Table(conv.getAssistantTable()).
Where("assistant_id", assistant["assistant_id"]).
Update(assistant)
} else {
assistant["created_at"] = time.Now()
err = conv.query.New().
Table(conv.getAssistantTable()).
Insert(assistant)
Where("assistant_id", assistantCopy["assistant_id"]).
Update(assistantCopy)
if err != nil {
return nil, err
}
return assistantCopy["assistant_id"], nil
}
return err
err = conv.query.New().
Table(conv.getAssistantTable()).
Insert(assistantCopy)
if err != nil {
return nil, err
}
return assistantCopy["assistant_id"], nil
}
// DeleteAssistant deletes an assistant by assistant_id
@ -812,10 +874,12 @@ func (conv *Xun) GetAssistants(filter AssistantFilter) (*AssistantResponse, erro
return nil, err
}
// Convert rows to map slice
// Convert rows to map slice and parse JSON fields
data := make([]map[string]interface{}, len(rows))
jsonFields := []string{"tags", "options", "prompts", "flows", "files", "functions", "permissions"}
for i, row := range rows {
data[i] = row
conv.parseJSONFields(data[i], jsonFields)
}
return &AssistantResponse{

View file

@ -478,22 +478,10 @@ func TestXunAssistantCRUD(t *testing.T) {
t.Fatal(err)
}
// Test creating a new assistant
tagsJSON, err := jsoniter.MarshalToString([]string{"tag1", "tag2", "tag3"})
if err != nil {
t.Fatal(err)
}
optionsJSON, err := jsoniter.MarshalToString(map[string]interface{}{
"model": "gpt-4",
})
if err != nil {
t.Fatal(err)
}
mentionable := true
automated := true
// Test creating a new assistant with different JSON field formats
// Test case 1: JSON fields as strings
tagsJSON := `["tag1", "tag2", "tag3"]`
optionsJSON := `{"model": "gpt-4"}`
assistant := map[string]interface{}{
"name": "Test Assistant",
"type": "assistant",
@ -502,95 +490,152 @@ func TestXunAssistantCRUD(t *testing.T) {
"description": "Test Description",
"tags": tagsJSON,
"options": optionsJSON,
"mentionable": mentionable,
"automated": automated,
"mentionable": true,
"automated": true,
}
// Test SaveAssistant (Create)
err = conv.SaveAssistant(assistant)
// Test SaveAssistant (Create) with string JSON
v, err := conv.SaveAssistant(assistant)
assert.Nil(t, err)
assistantID := assistant["assistant_id"].(string)
assistantID := v.(string)
assert.NotEmpty(t, assistantID)
// Test GetAssistants with no filter
// Test case 2: JSON fields as native types
assistant2 := map[string]interface{}{
"name": "Test Assistant 2",
"type": "assistant",
"avatar": "https://example.com/avatar2.png",
"connector": "openai",
"description": "Test Description 2",
"tags": []string{"tag1", "tag2", "tag3"},
"options": map[string]interface{}{"model": "gpt-4"},
"prompts": []string{"prompt1", "prompt2"},
"flows": []string{"flow1", "flow2"},
"files": []string{"file1", "file2"},
"functions": []map[string]interface{}{{"name": "func1"}, {"name": "func2"}},
"permissions": map[string]interface{}{"read": true, "write": true},
"mentionable": true,
"automated": true,
}
// Test SaveAssistant (Create) with native types
v, err = conv.SaveAssistant(assistant2)
assert.Nil(t, err)
assistant2ID := v.(string)
assert.NotEmpty(t, assistant2ID)
// Test case 3: Test with nil JSON fields
assistant3 := map[string]interface{}{
"name": "Test Assistant 3",
"type": "assistant",
"connector": "openai",
"description": "Test Description 3",
"tags": nil,
"options": nil,
"prompts": nil,
"flows": nil,
"files": nil,
"functions": nil,
"permissions": nil,
"mentionable": true,
"automated": true,
}
// Test SaveAssistant (Create) with nil fields
v, err = conv.SaveAssistant(assistant3)
assert.Nil(t, err)
assistant3ID := v.(string)
assert.NotEmpty(t, assistant3ID)
// Test GetAssistants to verify JSON fields are properly stored
resp, err := conv.GetAssistants(AssistantFilter{})
assert.Nil(t, err)
assert.Equal(t, 1, len(resp.Data))
assert.Equal(t, 3, len(resp.Data))
// Test GetAssistants with tag filter (single tag)
resp, err = conv.GetAssistants(AssistantFilter{
Tags: []string{"tag1"},
})
assert.Nil(t, err)
assert.Equal(t, 1, len(resp.Data))
// Verify first assistant (string JSON)
found := false
for _, item := range resp.Data {
if item["assistant_id"].(string) == assistantID {
found = true
// Now we expect parsed JSON values instead of JSON strings
assert.Equal(t, []interface{}{"tag1", "tag2", "tag3"}, item["tags"])
assert.Equal(t, map[string]interface{}{"model": "gpt-4"}, item["options"])
break
}
}
assert.True(t, found)
// Test GetAssistants with tag filter (multiple tags)
resp, err = conv.GetAssistants(AssistantFilter{
Tags: []string{"tag1", "tag4"},
})
assert.Nil(t, err)
assert.Equal(t, 1, len(resp.Data))
// Verify second assistant (native types converted to JSON)
found = false
for _, item := range resp.Data {
if item["assistant_id"].(string) == assistant2ID {
found = true
// Now we expect parsed JSON values directly
assert.Equal(t, []interface{}{"tag1", "tag2", "tag3"}, item["tags"])
assert.Equal(t, map[string]interface{}{"model": "gpt-4"}, item["options"])
// Test GetAssistants with non-existent tag
resp, err = conv.GetAssistants(AssistantFilter{
Tags: []string{"nonexistent"},
})
assert.Nil(t, err)
assert.Equal(t, 0, len(resp.Data))
// Verify other JSON fields
assert.Equal(t, []interface{}{"prompt1", "prompt2"}, item["prompts"])
assert.Equal(t, []interface{}{"flow1", "flow2"}, item["flows"])
assert.Equal(t, []interface{}{"file1", "file2"}, item["files"])
assert.Equal(t,
[]interface{}{
map[string]interface{}{"name": "func1"},
map[string]interface{}{"name": "func2"},
},
item["functions"])
assert.Equal(t,
map[string]interface{}{
"read": true,
"write": true,
},
item["permissions"])
break
}
}
assert.True(t, found)
// Test GetAssistants with keyword filter
resp, err = conv.GetAssistants(AssistantFilter{
Keywords: "Test",
})
assert.Nil(t, err)
assert.Equal(t, 1, len(resp.Data))
// Verify third assistant (nil fields)
found = false
for _, item := range resp.Data {
if item["assistant_id"].(string) == assistant3ID {
found = true
assert.Nil(t, item["tags"])
assert.Nil(t, item["options"])
assert.Nil(t, item["prompts"])
assert.Nil(t, item["flows"])
assert.Nil(t, item["files"])
assert.Nil(t, item["functions"])
assert.Nil(t, item["permissions"])
break
}
}
assert.True(t, found)
// Test GetAssistants with connector filter
resp, err = conv.GetAssistants(AssistantFilter{
Connector: "openai",
})
assert.Nil(t, err)
assert.Equal(t, 1, len(resp.Data))
// Test GetAssistants with mentionable filter
resp, err = conv.GetAssistants(AssistantFilter{
Mentionable: &mentionable,
})
assert.Nil(t, err)
assert.Equal(t, 1, len(resp.Data))
// Test GetAssistants with automated filter
resp, err = conv.GetAssistants(AssistantFilter{
Automated: &automated,
})
assert.Nil(t, err)
assert.Equal(t, 1, len(resp.Data))
// Test GetAssistants with combined filters
resp, err = conv.GetAssistants(AssistantFilter{
Keywords: "Test",
Connector: "openai",
Mentionable: &mentionable,
Automated: &automated,
Tags: []string{"tag1"},
})
assert.Nil(t, err)
assert.Equal(t, 1, len(resp.Data))
// Test SaveAssistant (Update)
assistant["name"] = "Updated Assistant"
err = conv.SaveAssistant(assistant)
// Test updating with mixed JSON formats
assistant2["assistant_id"] = assistant2ID
_, err = conv.SaveAssistant(assistant2)
assert.Nil(t, err)
// Verify update
resp, err = conv.GetAssistants(AssistantFilter{})
assert.Nil(t, err)
assert.Equal(t, 1, len(resp.Data))
item := resp.Data[0]
assert.Equal(t, "Updated Assistant", item["name"])
for _, item := range resp.Data {
if item["assistant_id"].(string) == assistant2ID {
// Now we expect parsed JSON values
assert.Equal(t, []interface{}{"tag1", "tag2", "tag3"}, item["tags"])
assert.Equal(t, map[string]interface{}{"model": "gpt-4"}, item["options"])
break
}
}
// Test DeleteAssistant
err = conv.DeleteAssistant(assistantID)
assert.Nil(t, err)
err = conv.DeleteAssistant(assistant2ID)
assert.Nil(t, err)
err = conv.DeleteAssistant(assistant3ID)
assert.Nil(t, err)
resp, err = conv.GetAssistants(AssistantFilter{})
assert.Nil(t, err)
@ -647,7 +692,7 @@ func TestXunAssistantPagination(t *testing.T) {
"mentionable": mentionable,
"automated": automated,
}
err = conv.SaveAssistant(assistant)
_, err = conv.SaveAssistant(assistant)
assert.Nil(t, err)
}

View file

@ -66,12 +66,12 @@ func processAssistantAdd(process *process.Process) interface{} {
exception.New("Neo conversation is not initialized", 500).Throw()
}
err := neo.Conversation.SaveAssistant(data)
id, err := neo.Conversation.SaveAssistant(data)
if err != nil {
exception.New("Failed to add assistant: %s", 500, err.Error()).Throw()
}
return data
return id
}
// processAssistantSave process the assistant save request
@ -84,12 +84,12 @@ func processAssistantSave(process *process.Process) interface{} {
exception.New("Neo conversation is not initialized", 500).Throw()
}
err := neo.Conversation.SaveAssistant(data)
id, err := neo.Conversation.SaveAssistant(data)
if err != nil {
exception.New("Failed to save assistant: %s", 500, err.Error()).Throw()
}
return data
return id
}
// processAssistantDelete process the assistant delete request

View file

@ -4,7 +4,6 @@ import (
"fmt"
"testing"
jsoniter "github.com/json-iterator/go"
"github.com/stretchr/testify/assert"
"github.com/yaoapp/gou/process"
"github.com/yaoapp/kun/any"
@ -69,19 +68,9 @@ func TestProcessAssistantCRUD(t *testing.T) {
prepare(t)
defer test.Clean()
// Create an assistant
tagsJSON, err := jsoniter.MarshalToString([]string{"tag1", "tag2", "tag3"})
if err != nil {
t.Fatal(err)
}
optionsJSON, err := jsoniter.MarshalToString(map[string]interface{}{
"model": "gpt-4",
})
if err != nil {
t.Fatal(err)
}
// Create an assistant with string JSON fields
tagsJSON := `["tag1", "tag2", "tag3"]`
optionsJSON := `{"model": "gpt-4"}`
assistant := map[string]interface{}{
"name": "Test Assistant",
"type": "assistant",
@ -94,7 +83,7 @@ func TestProcessAssistantCRUD(t *testing.T) {
"automated": true,
}
// Test processAssistantAdd
// Test processAssistantAdd with string JSON
p, err := process.Of("neo.assistant.add", assistant)
if err != nil {
t.Fatal(err)
@ -105,12 +94,73 @@ func TestProcessAssistantCRUD(t *testing.T) {
t.Fatal(err)
}
res := any.Of(output).Map()
assert.Equal(t, "Test Assistant", res.Get("name"))
assert.NotEmpty(t, res.Get("assistant_id"))
assistantID := res.Get("assistant_id").(string)
assistantID := output
assert.NotNil(t, assistantID)
// Test processAssistantSearch - no filter
// Test with native type JSON fields
assistant2 := map[string]interface{}{
"name": "Test Assistant 2",
"type": "assistant",
"avatar": "https://example.com/avatar2.png",
"connector": "openai",
"description": "Test Description 2",
"tags": []string{"tag1", "tag2", "tag3"},
"options": map[string]interface{}{"model": "gpt-4"},
"prompts": []string{"prompt1", "prompt2"},
"flows": []string{"flow1", "flow2"},
"files": []string{"file1", "file2"},
"functions": []map[string]interface{}{{"name": "func1"}, {"name": "func2"}},
"permissions": map[string]interface{}{"read": true, "write": true},
"mentionable": true,
"automated": true,
}
// Test processAssistantAdd with native types
p, err = process.Of("neo.assistant.add", assistant2)
if err != nil {
t.Fatal(err)
}
output, err = p.Exec()
if err != nil {
t.Fatal(err)
}
assistant2ID := output
assert.NotNil(t, assistant2ID)
// Test with nil JSON fields
assistant3 := map[string]interface{}{
"name": "Test Assistant 3",
"type": "assistant",
"connector": "openai",
"description": "Test Description 3",
"tags": nil,
"options": nil,
"prompts": nil,
"flows": nil,
"files": nil,
"functions": nil,
"permissions": nil,
"mentionable": true,
"automated": true,
}
// Test processAssistantAdd with nil fields
p, err = process.Of("neo.assistant.add", assistant3)
if err != nil {
t.Fatal(err)
}
output, err = p.Exec()
if err != nil {
t.Fatal(err)
}
assistant3ID := output
assert.NotNil(t, assistant3ID)
// Test processAssistantSearch to verify all assistants
p, err = process.Of("neo.assistant.search")
if err != nil {
t.Fatal(err)
@ -126,20 +176,68 @@ func TestProcessAssistantCRUD(t *testing.T) {
if total == nil {
total = int64(0)
}
assert.Equal(t, int64(1), total)
assert.Equal(t, int64(3), total)
items := searchRes.Get("data")
if items == nil {
items = []map[string]interface{}{}
}
assert.Equal(t, 1, len(items.([]map[string]interface{})))
assert.Equal(t, 3, len(items.([]map[string]interface{})))
// Test processAssistantSearch - with filter
p, err = process.Of("neo.assistant.search", map[string]interface{}{
"tags": []string{"tag1"},
"page": 1,
"pagesize": 10,
})
// Verify each assistant's JSON fields
for _, item := range items.([]map[string]interface{}) {
switch item["assistant_id"].(string) {
case assistantID:
assert.Equal(t, []interface{}{"tag1", "tag2", "tag3"}, item["tags"])
assert.Equal(t, map[string]interface{}{"model": "gpt-4"}, item["options"])
case assistant2ID:
assert.Equal(t, []interface{}{"tag1", "tag2", "tag3"}, item["tags"])
assert.Equal(t, map[string]interface{}{"model": "gpt-4"}, item["options"])
assert.Equal(t, []interface{}{"prompt1", "prompt2"}, item["prompts"])
assert.Equal(t, []interface{}{"flow1", "flow2"}, item["flows"])
assert.Equal(t, []interface{}{"file1", "file2"}, item["files"])
assert.Equal(t,
[]interface{}{
map[string]interface{}{"name": "func1"},
map[string]interface{}{"name": "func2"},
},
item["functions"])
assert.Equal(t,
map[string]interface{}{
"read": true,
"write": true,
},
item["permissions"])
case assistant3ID:
assert.Nil(t, item["tags"])
assert.Nil(t, item["options"])
assert.Nil(t, item["prompts"])
assert.Nil(t, item["flows"])
assert.Nil(t, item["files"])
assert.Nil(t, item["functions"])
assert.Nil(t, item["permissions"])
}
}
// Test updating with mixed JSON formats
assistant2["assistant_id"] = assistant2ID
assistant2["tags"] = `["tag4", "tag5"]`
assistant2["options"] = map[string]interface{}{"model": "gpt-3.5"}
p, err = process.Of("neo.assistant.save", assistant2)
if err != nil {
t.Fatal(err)
}
output, err = p.Exec()
if err != nil {
t.Fatal(err)
}
savedID := output
assert.NotNil(t, savedID)
// Double check with a new search
p, err = process.Of("neo.assistant.search")
if err != nil {
t.Fatal(err)
}
@ -150,33 +248,17 @@ func TestProcessAssistantCRUD(t *testing.T) {
}
searchRes = any.Of(output).Map()
total = searchRes.Get("total")
if total == nil {
total = int64(0)
}
assert.Equal(t, int64(1), total)
items = searchRes.Get("data")
if items == nil {
items = []map[string]interface{}{}
found := false
for _, item := range items.([]map[string]interface{}) {
if item["assistant_id"].(string) == assistant2ID {
found = true
assert.Equal(t, []interface{}{"tag4", "tag5"}, item["tags"])
assert.Equal(t, map[string]interface{}{"model": "gpt-3.5"}, item["options"])
break
}
}
assert.Equal(t, 1, len(items.([]map[string]interface{})))
// Test processAssistantSave (Update)
assistant["assistant_id"] = assistantID
assistant["name"] = "Updated Assistant"
p, err = process.Of("neo.assistant.save", assistant)
if err != nil {
t.Fatal(err)
}
output, err = p.Exec()
if err != nil {
t.Fatal(err)
}
res = any.Of(output).Map()
assert.Equal(t, "Updated Assistant", res.Get("name"))
assert.True(t, found)
// Test processAssistantDelete
p, err = process.Of("neo.assistant.delete", assistantID)
@ -192,7 +274,22 @@ func TestProcessAssistantCRUD(t *testing.T) {
deleteRes := any.Of(output).Map()
assert.Equal(t, "ok", deleteRes.Get("message"))
// Verify deletion with search
// Delete remaining assistants
p, err = process.Of("neo.assistant.delete", assistant2ID)
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
assert.Nil(t, err)
p, err = process.Of("neo.assistant.delete", assistant3ID)
if err != nil {
t.Fatal(err)
}
_, err = p.Exec()
assert.Nil(t, err)
// Verify all assistants are deleted
p, err = process.Of("neo.assistant.search")
if err != nil {
t.Fatal(err)
@ -209,12 +306,6 @@ func TestProcessAssistantCRUD(t *testing.T) {
total = int64(0)
}
assert.Equal(t, int64(0), total)
items = searchRes.Get("data")
if items == nil {
items = []map[string]interface{}{}
}
assert.Equal(t, 0, len(items.([]map[string]interface{})))
}
func TestProcessAssistantSearchPagination(t *testing.T) {
@ -223,17 +314,12 @@ func TestProcessAssistantSearchPagination(t *testing.T) {
// Create multiple assistants for pagination testing
for i := 0; i < 25; i++ {
tagsJSON, err := jsoniter.MarshalToString([]string{fmt.Sprintf("tag%d", i%5)})
if err != nil {
t.Fatal(err)
}
assistant := map[string]interface{}{
"name": fmt.Sprintf("Assistant %d", i),
"type": "assistant",
"connector": fmt.Sprintf("connector%d", i%3),
"description": fmt.Sprintf("Description %d", i),
"tags": tagsJSON,
"tags": []string{fmt.Sprintf("tag%d", i%5)},
"mentionable": i%2 == 0,
"automated": i%3 == 0,
}