diff --git a/neo/api.go b/neo/api.go index 8ccf97ec..be5640a4 100644 --- a/neo/api.go +++ b/neo/api.go @@ -863,6 +863,37 @@ func (neo *DSL) handleAssistantList(c *gin.Context) { filter.Tags = strings.Split(tags, ",") } + // Parse keywords + if keywords := c.Query("keywords"); keywords != "" { + filter.Keywords = keywords + } + + // Parse connector + if connector := c.Query("connector"); connector != "" { + filter.Connector = connector + } + + // Parse select fields + if selectFields := c.Query("select"); selectFields != "" { + filter.Select = strings.Split(selectFields, ",") + } + + // Parse mentionable (support various boolean formats) + if mentionable := c.Query("mentionable"); mentionable != "" { + val := parseBoolValue(mentionable) + if val != nil { + filter.Mentionable = val + } + } + + // Parse automated (support various boolean formats) + if automated := c.Query("automated"); automated != "" { + val := parseBoolValue(automated) + if val != nil { + filter.Automated = val + } + } + response, err := neo.Conversation.GetAssistants(filter) if err != nil { c.JSON(500, gin.H{"message": err.Error(), "code": 500}) @@ -874,6 +905,22 @@ func (neo *DSL) handleAssistantList(c *gin.Context) { c.Done() } +// parseBoolValue parses various string formats into a boolean pointer +// Supports: 1, 0, "1", "0", "true", "false", etc. +func parseBoolValue(value string) *bool { + value = strings.ToLower(strings.TrimSpace(value)) + switch value { + case "1", "true", "yes", "on": + v := true + return &v + case "0", "false", "no", "off": + v := false + return &v + default: + return nil + } +} + // handleAssistantDetail handles getting a single assistant's details func (neo *DSL) handleAssistantDetail(c *gin.Context) { assistantID := c.Param("id") @@ -884,8 +931,9 @@ func (neo *DSL) handleAssistantDetail(c *gin.Context) { } filter := conversation.AssistantFilter{ - Page: 1, - PageSize: 1, + AssistantID: assistantID, + Page: 1, + PageSize: 1, } response, err := neo.Conversation.GetAssistants(filter) @@ -895,22 +943,13 @@ func (neo *DSL) handleAssistantDetail(c *gin.Context) { return } - // Find the assistant by ID - var assistant map[string]interface{} - for _, item := range response.Data { - if id, ok := item["id"].(string); ok && id == assistantID { - assistant = item - break - } - } - - if assistant == nil { + if len(response.Data) == 0 { c.JSON(404, gin.H{"message": "assistant not found", "code": 404}) c.Done() return } - c.JSON(200, map[string]interface{}{"data": assistant}) + c.JSON(200, map[string]interface{}{"data": response.Data[0]}) c.Done() } diff --git a/neo/conversation/types.go b/neo/conversation/types.go index c23fd83c..521fc176 100644 --- a/neo/conversation/types.go +++ b/neo/conversation/types.go @@ -46,13 +46,15 @@ type ChatGroupResponse struct { // AssistantFilter represents the assistant filter structure // Used for filtering and pagination when retrieving assistant lists type AssistantFilter struct { - Tags []string `json:"tags,omitempty"` // Filter by tags - Keywords string `json:"keywords,omitempty"` // Search in name and description - Connector string `json:"connector,omitempty"` // Filter by connector - Mentionable *bool `json:"mentionable,omitempty"` // Filter by mentionable status - Automated *bool `json:"automated,omitempty"` // Filter by automation status - Page int `json:"page,omitempty"` // Page number, starting from 1 - PageSize int `json:"pagesize,omitempty"` // Items per page + Tags []string `json:"tags,omitempty"` // Filter by tags + Keywords string `json:"keywords,omitempty"` // Search in name and description + Connector string `json:"connector,omitempty"` // Filter by connector + AssistantID string `json:"assistant_id,omitempty"` // Filter by assistant ID + Mentionable *bool `json:"mentionable,omitempty"` // Filter by mentionable status + Automated *bool `json:"automated,omitempty"` // Filter by automation status + Page int `json:"page,omitempty"` // Page number, starting from 1 + PageSize int `json:"pagesize,omitempty"` // Items per page + Select []string `json:"select,omitempty"` // Fields to return, returns all fields if empty } // AssistantResponse represents the assistant response structure diff --git a/neo/conversation/xun.go b/neo/conversation/xun.go index 6e017e90..d6ee4efa 100644 --- a/neo/conversation/xun.go +++ b/neo/conversation/xun.go @@ -829,6 +829,11 @@ func (conv *Xun) GetAssistants(filter AssistantFilter) (*AssistantResponse, erro 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) @@ -865,6 +870,15 @@ func (conv *Xun) GetAssistants(filter AssistantFilter) (*AssistantResponse, erro prevPage = 0 } + // Apply select fields if provided + if filter.Select != nil && len(filter.Select) > 0 { + selectFields := make([]interface{}, len(filter.Select)) + for i, field := range filter.Select { + selectFields[i] = field + } + qb.Select(selectFields...) + } + // Get paginated results rows, err := qb.OrderBy("created_at", "desc"). Offset(offset). @@ -879,7 +893,24 @@ func (conv *Xun) GetAssistants(filter AssistantFilter) (*AssistantResponse, erro jsonFields := []string{"tags", "options", "prompts", "flows", "files", "functions", "permissions"} for i, row := range rows { data[i] = row - conv.parseJSONFields(data[i], jsonFields) + // Only parse JSON fields if they are selected or no select filter is provided + if filter.Select == nil || len(filter.Select) == 0 { + conv.parseJSONFields(data[i], jsonFields) + } else { + // Parse only selected JSON fields + selectedJSONFields := []string{} + for _, field := range jsonFields { + for _, selected := range filter.Select { + if selected == field { + selectedJSONFields = append(selectedJSONFields, field) + break + } + } + } + if len(selectedJSONFields) > 0 { + conv.parseJSONFields(data[i], selectedJSONFields) + } + } } return &AssistantResponse{ diff --git a/neo/conversation/xun_test.go b/neo/conversation/xun_test.go index da3b4892..5b74c8a5 100644 --- a/neo/conversation/xun_test.go +++ b/neo/conversation/xun_test.go @@ -775,6 +775,46 @@ func TestXunAssistantPagination(t *testing.T) { assert.Nil(t, err) assert.Greater(t, len(resp.Data), 0) + // Test filtering by assistant_id + // First get an assistant_id from previous results + firstAssistantID := resp.Data[0]["assistant_id"].(string) + + // Test exact match with assistant_id + resp, err = conv.GetAssistants(AssistantFilter{ + AssistantID: firstAssistantID, + Page: 1, + PageSize: 10, + }) + assert.Nil(t, err) + assert.Equal(t, 1, len(resp.Data)) + assert.Equal(t, firstAssistantID, resp.Data[0]["assistant_id"]) + + // Test assistant_id with other filters + resp, err = conv.GetAssistants(AssistantFilter{ + AssistantID: firstAssistantID, + Select: []string{"name", "assistant_id", "description"}, + Page: 1, + PageSize: 10, + }) + assert.Nil(t, err) + assert.Equal(t, 1, len(resp.Data)) + assert.Equal(t, firstAssistantID, resp.Data[0]["assistant_id"]) + // Verify only selected fields are returned + assert.Contains(t, resp.Data[0], "name") + assert.Contains(t, resp.Data[0], "assistant_id") + assert.Contains(t, resp.Data[0], "description") + assert.NotContains(t, resp.Data[0], "tags") + assert.NotContains(t, resp.Data[0], "options") + + // Test non-existent assistant_id + resp, err = conv.GetAssistants(AssistantFilter{ + AssistantID: "non-existent-id", + Page: 1, + PageSize: 10, + }) + assert.Nil(t, err) + assert.Equal(t, 0, len(resp.Data)) + // Test combined filters resp, err = conv.GetAssistants(AssistantFilter{ Tags: []string{"tag0"}, @@ -786,4 +826,51 @@ func TestXunAssistantPagination(t *testing.T) { PageSize: 10, }) assert.Nil(t, err) + + // Test filtering with select fields + resp, err = conv.GetAssistants(AssistantFilter{ + Select: []string{"name", "description", "tags"}, + Page: 1, + PageSize: 10, + }) + assert.Nil(t, err) + assert.Equal(t, 10, len(resp.Data)) + // 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 = conv.GetAssistants(AssistantFilter{ + Tags: []string{"tag0"}, + 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") + } } diff --git a/neo/process.go b/neo/process.go index ac631428..7a27cbac 100644 --- a/neo/process.go +++ b/neo/process.go @@ -22,10 +22,11 @@ func GetNeo() *DSL { func init() { process.RegisterGroup("neo", map[string]process.Handler{ "write": ProcessWrite, - "assistant.add": processAssistantAdd, + "assistant.create": processAssistantCreate, "assistant.save": processAssistantSave, "assistant.delete": processAssistantDelete, "assistant.search": processAssistantSearch, + "assistant.find": processAssistantFind, }) } @@ -56,8 +57,8 @@ func ProcessWrite(process *process.Process) interface{} { return nil } -// processAssistantAdd process the assistant add request -func processAssistantAdd(process *process.Process) interface{} { +// processAssistantCreate process the assistant create request +func processAssistantCreate(process *process.Process) interface{} { process.ValidateArgNums(1) data := process.ArgsMap(0) @@ -68,7 +69,7 @@ func processAssistantAdd(process *process.Process) interface{} { id, err := neo.Conversation.SaveAssistant(data) if err != nil { - exception.New("Failed to add assistant: %s", 500, err.Error()).Throw() + exception.New("Failed to create assistant: %s", 500, err.Error()).Throw() } return id @@ -175,3 +176,31 @@ func processAssistantSearch(process *process.Process) interface{} { return res } + +// processAssistantFind process the assistant find request +func processAssistantFind(process *process.Process) interface{} { + process.ValidateArgNums(1) + assistantID := process.ArgsString(0) + + neo := GetNeo() + if neo.Conversation == nil { + exception.New("Neo conversation is not initialized", 500).Throw() + } + + filter := conversation.AssistantFilter{ + AssistantID: assistantID, + Page: 1, + PageSize: 1, + } + + res, err := neo.Conversation.GetAssistants(filter) + if err != nil { + exception.New("Failed to find assistant: %s", 500, err.Error()).Throw() + } + + if len(res.Data) == 0 { + exception.New("Assistant not found: %s", 404, assistantID).Throw() + } + + return res.Data[0] +} diff --git a/neo/process_test.go b/neo/process_test.go index 7dce8f1c..5f1e43e9 100644 --- a/neo/process_test.go +++ b/neo/process_test.go @@ -83,8 +83,8 @@ func TestProcessAssistantCRUD(t *testing.T) { "automated": true, } - // Test processAssistantAdd with string JSON - p, err := process.Of("neo.assistant.add", assistant) + // Test processAssistantCreate with string JSON + p, err := process.Of("neo.assistant.create", assistant) if err != nil { t.Fatal(err) } @@ -97,6 +97,33 @@ func TestProcessAssistantCRUD(t *testing.T) { assistantID := output assert.NotNil(t, assistantID) + // Test processAssistantFind + p, err = process.Of("neo.assistant.find", assistantID) + if err != nil { + t.Fatal(err) + } + + output, err = p.Exec() + if err != nil { + t.Fatal(err) + } + + foundAssistant := output.(map[string]interface{}) + assert.Equal(t, assistantID, foundAssistant["assistant_id"]) + assert.Equal(t, "Test Assistant", foundAssistant["name"]) + assert.Equal(t, []interface{}{"tag1", "tag2", "tag3"}, foundAssistant["tags"]) + assert.Equal(t, map[string]interface{}{"model": "gpt-4"}, foundAssistant["options"]) + + // Test processAssistantFind with non-existent ID + p, err = process.Of("neo.assistant.find", "non-existent-id") + if err != nil { + t.Fatal(err) + } + + _, err = p.Exec() + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "Assistant not found") + // Test with native type JSON fields assistant2 := map[string]interface{}{ "name": "Test Assistant 2", @@ -115,8 +142,8 @@ func TestProcessAssistantCRUD(t *testing.T) { "automated": true, } - // Test processAssistantAdd with native types - p, err = process.Of("neo.assistant.add", assistant2) + // Test processAssistantCreate with native types + p, err = process.Of("neo.assistant.create", assistant2) if err != nil { t.Fatal(err) } @@ -146,8 +173,8 @@ func TestProcessAssistantCRUD(t *testing.T) { "automated": true, } - // Test processAssistantAdd with nil fields - p, err = process.Of("neo.assistant.add", assistant3) + // Test processAssistantCreate with nil fields + p, err = process.Of("neo.assistant.create", assistant3) if err != nil { t.Fatal(err) } @@ -324,7 +351,7 @@ func TestProcessAssistantSearchPagination(t *testing.T) { "automated": i%3 == 0, } - p, err := process.Of("neo.assistant.add", assistant) + p, err := process.Of("neo.assistant.create", assistant) if err != nil { t.Fatal(err) } @@ -438,7 +465,7 @@ func TestProcessAssistantValidation(t *testing.T) { defer test.Clean() // Test missing required fields - p, err := process.Of("neo.assistant.add", map[string]interface{}{}) + p, err := process.Of("neo.assistant.create", map[string]interface{}{}) if err != nil { t.Fatal(err) } @@ -455,6 +482,16 @@ func TestProcessAssistantValidation(t *testing.T) { _, err = p.Exec() assert.NotNil(t, err) + // Test invalid assistant ID for find + p, err = process.Of("neo.assistant.find", "non-existent-id") + if err != nil { + t.Fatal(err) + } + + _, err = p.Exec() + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "Assistant not found") + // Test invalid page number p, err = process.Of("neo.assistant.search", map[string]interface{}{ "page": -1,