From 94f3ddb15825847d784fd20ca62d93e7213376a1 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 29 Dec 2024 17:14:01 +0800 Subject: [PATCH] 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. --- neo/api.go | 7 +- neo/conversation/mongo.go | 80 +++++-------- neo/conversation/redis.go | 80 +++++-------- neo/conversation/types.go | 2 +- neo/conversation/weaviate.go | 81 +++++-------- neo/conversation/xun.go | 106 +++++++++++++---- neo/conversation/xun_test.go | 217 ++++++++++++++++++++-------------- neo/process.go | 8 +- neo/process_test.go | 218 ++++++++++++++++++++++++----------- 9 files changed, 463 insertions(+), 336 deletions(-) diff --git a/neo/api.go b/neo/api.go index 9da02e91..8ccf97ec 100644 --- a/neo/api.go +++ b/neo/api.go @@ -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() } diff --git a/neo/conversation/mongo.go b/neo/conversation/mongo.go index c96fddf6..d4b20a16 100644 --- a/neo/conversation/mongo.go +++ b/neo/conversation/mongo.go @@ -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 } diff --git a/neo/conversation/redis.go b/neo/conversation/redis.go index 7f88dbf5..fb150570 100644 --- a/neo/conversation/redis.go +++ b/neo/conversation/redis.go @@ -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 } diff --git a/neo/conversation/types.go b/neo/conversation/types.go index 87c0176a..c23fd83c 100644 --- a/neo/conversation/types.go +++ b/neo/conversation/types.go @@ -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 diff --git a/neo/conversation/weaviate.go b/neo/conversation/weaviate.go index 0b572111..8462e843 100644 --- a/neo/conversation/weaviate.go +++ b/neo/conversation/weaviate.go @@ -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 } diff --git a/neo/conversation/xun.go b/neo/conversation/xun.go index 752cda0e..6e017e90 100644 --- a/neo/conversation/xun.go +++ b/neo/conversation/xun.go @@ -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{ diff --git a/neo/conversation/xun_test.go b/neo/conversation/xun_test.go index a08c800b..da3b4892 100644 --- a/neo/conversation/xun_test.go +++ b/neo/conversation/xun_test.go @@ -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) } diff --git a/neo/process.go b/neo/process.go index 432c1ec4..ac631428 100644 --- a/neo/process.go +++ b/neo/process.go @@ -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 diff --git a/neo/process_test.go b/neo/process_test.go index b7eca7f1..7dce8f1c 100644 --- a/neo/process_test.go +++ b/neo/process_test.go @@ -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, }