Enhance assistant filtering and management in Neo API

- Added support for filtering assistants by keywords, connector, and mentionable status in the handleAssistantList function.
- Introduced a new parseBoolValue utility function to handle various boolean formats for filtering.
- Updated the AssistantFilter structure to include an AssistantID field and a Select field for specifying returned fields.
- Refactored process functions to replace 'add' with 'create' for consistency and clarity in assistant management.
- Implemented new processAssistantFind function to retrieve assistants by ID, improving data retrieval capabilities.
- Enhanced tests to cover new filtering options and ensure robust functionality across assistant management operations.
This commit is contained in:
Max 2024-12-29 17:30:33 +08:00
parent 94f3ddb158
commit 882277d974
6 changed files with 258 additions and 33 deletions

View file

@ -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()
}

View file

@ -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

View file

@ -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{

View file

@ -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")
}
}

View file

@ -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]
}

View file

@ -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,