From 8fafdc5855c52d10b95607859bb2b02d5c37df52 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 29 Dec 2024 11:06:41 +0800 Subject: [PATCH] Enhance assistant management in Neo API and conversation module - Added new endpoints for managing assistants: list, detail, save, and delete. - Implemented filtering capabilities for assistants based on keywords, tags, mentionable status, and automation. - Refactored the assistant structure to include new fields for better management and filtering. - Removed obsolete test data file, streamlining the codebase. - Updated tests to cover new assistant functionalities and filtering options, ensuring robust functionality. --- neo/api.go | 160 ++++++++++++++++++++++++++++++++--- neo/conversation/types.go | 118 +++++++++++++++++++------- neo/conversation/xun.go | 71 ++++++++++++---- neo/conversation/xun_test.go | 108 ++++++++++++++++++++++- neo/test_data.go | 30 ------- 5 files changed, 398 insertions(+), 89 deletions(-) delete mode 100644 neo/test_data.go diff --git a/neo/api.go b/neo/api.go index 163b3fad..cddab1e0 100644 --- a/neo/api.go +++ b/neo/api.go @@ -40,6 +40,8 @@ func (neo *DSL) API(router *gin.Engine, path string) error { router.OPTIONS(path+"/generate/title", neo.optionsHandler) router.OPTIONS(path+"/generate/prompts", neo.optionsHandler) router.OPTIONS(path+"/dangerous/clear_chats", neo.optionsHandler) + router.OPTIONS(path+"/assistants", neo.optionsHandler) + router.OPTIONS(path+"/assistants/:id", neo.optionsHandler) // Register endpoints with middlewares router.GET(path, append(middlewares, neo.handleChat)...) @@ -48,6 +50,12 @@ func (neo *DSL) API(router *gin.Engine, path string) error { // Status check router.GET(path+"/status", append(middlewares, neo.handleStatus)...) + // Assistant API + router.GET(path+"/assistants", append(middlewares, neo.handleAssistantList)...) + router.GET(path+"/assistants/:id", append(middlewares, neo.handleAssistantDetail)...) + router.POST(path+"/assistants", append(middlewares, neo.handleAssistantSave)...) + router.DELETE(path+"/assistants/:id", append(middlewares, neo.handleAssistantDelete)...) + // Chat api router.GET(path+"/chats", append(middlewares, neo.handleChatList)...) router.GET(path+"/chats/:id", append(middlewares, neo.handleChatDetail)...) @@ -394,28 +402,37 @@ func (neo *DSL) handleMentions(c *gin.Context) { // Get keywords from query parameter keywords := strings.ToLower(c.Query("keywords")) - mentions, err := neo.GetMentions(keywords) + mentionable := true + + // Query mentionable assistants + filter := conversation.AssistantFilter{ + Keywords: keywords, + Mentionable: &mentionable, + Page: 1, + PageSize: 20, + } + + response, err := neo.Conversation.GetAssistants(filter) if err != nil { c.JSON(500, gin.H{"message": err.Error(), "code": 500}) c.Done() return } - // Filter mentions by keywords - testMentions := TestMentions - if keywords != "" { - filtered := []Mention{} - for _, m := range testMentions { - if strings.Contains(strings.ToLower(m.Name), keywords) { - filtered = append(filtered, m) + // Convert assistants to mentions + mentions := []Mention{} + for _, item := range response.Items { + if assistant, ok := item.(map[string]interface{}); ok { + mention := Mention{ + ID: assistant["assistant_id"].(string), + Name: assistant["name"].(string), + Type: assistant["type"].(string), + Avatar: assistant["avatar"].(string), } + mentions = append(mentions, mention) } - testMentions = filtered } - // Append test data to actual mentions - mentions = append(mentions, testMentions...) - c.JSON(200, map[string]interface{}{"data": mentions}) c.Done() } @@ -751,3 +768,122 @@ func (neo *DSL) handleGenerateCustom(c *gin.Context) { resp.result, resp.err = neo.GenerateWithAI(ctx, resp.content, genType, systemPrompt, c, silent) resp.send("result") } + +// handleAssistantList handles listing assistants +func (neo *DSL) handleAssistantList(c *gin.Context) { + // Parse filter parameters + filter := conversation.AssistantFilter{ + Page: 1, + PageSize: 20, + } + + // Parse page and pagesize + if page := c.Query("page"); page != "" { + if n, err := strconv.Atoi(page); err == nil { + filter.Page = n + } + } + + if pageSize := c.Query("pagesize"); pageSize != "" { + if n, err := strconv.Atoi(pageSize); err == nil { + filter.PageSize = n + } + } + + // Parse tags + if tags := c.Query("tags"); tags != "" { + filter.Tags = strings.Split(tags, ",") + } + + response, err := neo.Conversation.GetAssistants(filter) + if err != nil { + c.JSON(500, gin.H{"message": err.Error(), "code": 500}) + c.Done() + return + } + + c.JSON(200, map[string]interface{}{"data": response}) + c.Done() +} + +// handleAssistantDetail handles getting a single assistant's details +func (neo *DSL) handleAssistantDetail(c *gin.Context) { + assistantID := c.Param("id") + if assistantID == "" { + c.JSON(400, gin.H{"message": "assistant id is required", "code": 400}) + c.Done() + return + } + + filter := conversation.AssistantFilter{ + Page: 1, + PageSize: 1, + } + + response, err := neo.Conversation.GetAssistants(filter) + if err != nil { + c.JSON(500, gin.H{"message": err.Error(), "code": 500}) + c.Done() + return + } + + // Find the assistant by ID + var assistant map[string]interface{} + for _, item := range response.Items { + if a, ok := item.(map[string]interface{}); ok { + if id, ok := a["id"].(string); ok && id == assistantID { + assistant = a + break + } + } + } + + if assistant == nil { + c.JSON(404, gin.H{"message": "assistant not found", "code": 404}) + c.Done() + return + } + + c.JSON(200, map[string]interface{}{"data": assistant}) + c.Done() +} + +// handleAssistantSave handles creating or updating an assistant +func (neo *DSL) handleAssistantSave(c *gin.Context) { + var assistant map[string]interface{} + if err := c.BindJSON(&assistant); err != nil { + c.JSON(400, gin.H{"message": "invalid request body", "code": 400}) + c.Done() + return + } + + err := neo.Conversation.SaveAssistant(assistant) + if err != nil { + c.JSON(500, gin.H{"message": err.Error(), "code": 500}) + c.Done() + return + } + + c.JSON(200, gin.H{"message": "ok", "data": assistant}) + c.Done() +} + +// handleAssistantDelete handles deleting an assistant +func (neo *DSL) handleAssistantDelete(c *gin.Context) { + assistantID := c.Param("id") + if assistantID == "" { + c.JSON(400, gin.H{"message": "assistant id is required", "code": 400}) + c.Done() + return + } + + err := neo.Conversation.DeleteAssistant(assistantID) + if err != nil { + c.JSON(500, gin.H{"message": err.Error(), "code": 500}) + c.Done() + return + } + + c.JSON(200, gin.H{"message": "ok"}) + c.Done() +} diff --git a/neo/conversation/types.go b/neo/conversation/types.go index fd9e2d40..b85c8eeb 100644 --- a/neo/conversation/types.go +++ b/neo/conversation/types.go @@ -2,66 +2,126 @@ package conversation import "github.com/yaoapp/xun" -// Setting the conversation config +// Setting represents the conversation configuration structure +// Used to configure basic conversation parameters including connector, user field, table name, etc. type Setting struct { - Connector string `json:"connector,omitempty"` - UserField string `json:"user_field,omitempty"` // the user id field name, default is user_id - Table string `json:"table,omitempty"` - MaxSize int `json:"max_size,omitempty" yaml:"max_size,omitempty"` - TTL int `json:"ttl,omitempty" yaml:"ttl,omitempty"` + Connector string `json:"connector,omitempty"` // Name of the connector used to specify data storage method + UserField string `json:"user_field,omitempty"` // User ID field name, defaults to "user_id" + Table string `json:"table,omitempty"` // Database table name + MaxSize int `json:"max_size,omitempty" yaml:"max_size,omitempty"` // Maximum storage size limit + TTL int `json:"ttl,omitempty" yaml:"ttl,omitempty"` // Time To Live in seconds } -// ChatInfo represents the chat information and its history +// ChatInfo represents the chat information structure +// Contains basic information and history for a single chat type ChatInfo struct { - Chat map[string]interface{} `json:"chat"` - History []map[string]interface{} `json:"history"` + Chat map[string]interface{} `json:"chat"` // Basic chat information + History []map[string]interface{} `json:"history"` // Chat history records } -// ChatFilter represents the filter parameters for GetChats +// ChatFilter represents the chat filter structure +// Used for filtering and pagination when retrieving chat lists type ChatFilter struct { - Keywords string `json:"keywords,omitempty"` - Page int `json:"page,omitempty"` // 页码,从1开始 - PageSize int `json:"pagesize,omitempty"` // 每页数量 - Order string `json:"order,omitempty"` // desc/asc + Keywords string `json:"keywords,omitempty"` // Keyword search + Page int `json:"page,omitempty"` // Page number, starting from 1 + PageSize int `json:"pagesize,omitempty"` // Number of items per page + Order string `json:"order,omitempty"` // Sort order: desc/asc } -// ChatGroup represents a group of chats by date +// ChatGroup represents the chat group structure +// Groups chats by date type ChatGroup struct { - Label string `json:"label"` - Chats []map[string]interface{} `json:"chats"` + Label string `json:"label"` // Group label (typically a date) + Chats []map[string]interface{} `json:"chats"` // List of chats in this group } -// ChatGroupResponse represents paginated chat groups +// ChatGroupResponse represents the paginated chat group response +// Contains paginated chat group information type ChatGroupResponse struct { - Groups []ChatGroup `json:"groups"` - Page int `json:"page"` // 当前页码 - PageSize int `json:"pagesize"` // 每页数量 - Total int64 `json:"total"` // 总记录数 - LastPage int `json:"last_page"` // 最后一页页码 + Groups []ChatGroup `json:"groups"` // List of chat groups + Page int `json:"page"` // Current page number + PageSize int `json:"pagesize"` // Items per page + Total int64 `json:"total"` // Total number of records + LastPage int `json:"last_page"` // Last page number } -// AssistantFilter represents the filter parameters for GetAssistants +// AssistantFilter represents the assistant filter structure +// Used for filtering and pagination when retrieving assistant lists type AssistantFilter struct { - Tags []string `json:"tags,omitempty"` - 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 + 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 } -// AssistantResponse represents paginated assistant results +// AssistantResponse represents the assistant response structure +// Inherits from xun.P, used for returning paginated assistant lists type AssistantResponse struct { xun.P } -// Conversation the store interface +// Conversation defines the conversation storage interface +// Provides basic operations required for conversation management type Conversation interface { + // GetChats retrieves a list of chats + // sid: Session ID + // filter: Filter conditions + // Returns: Grouped chat list and potential error GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) + + // GetChat retrieves a single chat's information + // sid: Session ID + // cid: Chat ID + // Returns: Chat information and potential error GetChat(sid string, cid string) (*ChatInfo, error) + + // GetHistory retrieves chat history + // sid: Session ID + // cid: Chat ID + // Returns: History record list and potential error GetHistory(sid string, cid string) ([]map[string]interface{}, error) + + // SaveHistory saves chat history + // sid: Session ID + // messages: Message list + // cid: Chat ID + // context: Context information + // Returns: Potential error SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error + + // DeleteChat deletes a single chat + // sid: Session ID + // cid: Chat ID + // Returns: Potential error DeleteChat(sid string, cid string) error + + // DeleteAllChats deletes all chats + // sid: Session ID + // Returns: Potential error DeleteAllChats(sid string) error + + // UpdateChatTitle updates chat title + // sid: Session ID + // cid: Chat ID + // title: New title + // Returns: Potential error UpdateChatTitle(sid string, cid string, title string) error + + // SaveAssistant saves assistant information + // assistant: Assistant information + // Returns: Potential error SaveAssistant(assistant map[string]interface{}) error + + // DeleteAssistant deletes an assistant + // assistantID: Assistant ID + // Returns: Potential error DeleteAssistant(assistantID string) error + + // GetAssistants retrieves a list of assistants + // filter: Filter conditions + // Returns: Paginated assistant list and potential error GetAssistants(filter AssistantFilter) (*AssistantResponse, error) } diff --git a/neo/conversation/xun.go b/neo/conversation/xun.go index 33eb6180..5e439e73 100644 --- a/neo/conversation/xun.go +++ b/neo/conversation/xun.go @@ -16,22 +16,34 @@ import ( "github.com/yaoapp/xun/dbal/schema" ) -// Xun Database conversation +// Package conversation provides functionality for managing chat conversations and assistants. + +// Xun implements the Conversation interface using a database backend. +// It provides functionality for: +// - Managing chat conversations and their message histories +// - Organizing chats with pagination and date-based grouping +// - Handling chat metadata like titles and creation dates +// - Managing AI assistants with their configurations and metadata +// - Supporting data expiration through TTL settings type Xun struct { query query.Query schema schema.Schema setting Setting } -// Public interface methods and constructor remain exported: -// - NewXun -// - UpdateChatTitle -// - GetChats -// - GetChat -// - GetHistory -// - SaveHistory -// - GetRequest -// - SaveRequest +// Public interface methods: +// +// NewXun creates a new conversation instance with the given settings +// UpdateChatTitle updates the title of a specific chat +// GetChats retrieves a paginated list of chats grouped by date +// GetChat retrieves a specific chat and its message history +// GetHistory retrieves the message history for a specific chat +// SaveHistory saves new messages to a chat's history +// DeleteChat deletes a specific chat and its history +// DeleteAllChats deletes all chats and their histories for a user +// SaveAssistant creates or updates an assistant +// DeleteAssistant deletes an assistant by assistant_id +// GetAssistants retrieves a paginated list of assistants with filtering // NewXun create a new conversation func NewXun(setting Setting) (*Xun, error) { @@ -696,20 +708,45 @@ func (conv *Xun) DeleteAssistant(assistantID string) error { return err } -// GetAssistants retrieves assistants with pagination and tag filtering +// GetAssistants retrieves assistants with pagination and filtering func (conv *Xun) GetAssistants(filter AssistantFilter) (*AssistantResponse, error) { qb := conv.query.New(). Table(conv.getAssistantTable()) // Apply tag filter if provided if filter.Tags != nil && len(filter.Tags) > 0 { - for i, tag := range filter.Tags { - if i == 0 { - qb.Where("tags", "like", fmt.Sprintf("%%\"%s\"%%", tag)) - } else { - qb.OrWhere("tags", "like", fmt.Sprintf("%%\"%s\"%%", tag)) + qb.Where(func(qb query.Query) { + for i, tag := range filter.Tags { + if i == 0 { + qb.Where("tags", "like", fmt.Sprintf("%%\"%s\"%%", tag)) + } else { + qb.OrWhere("tags", "like", fmt.Sprintf("%%\"%s\"%%", tag)) + } } - } + }) + } + + // Apply keyword filter if provided + if filter.Keywords != "" { + qb.Where(func(qb query.Query) { + qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Keywords)). + OrWhere("description", "like", fmt.Sprintf("%%%s%%", filter.Keywords)) + }) + } + + // Apply connector filter if provided + if filter.Connector != "" { + qb.Where("connector", filter.Connector) + } + + // Apply mentionable filter if provided + if filter.Mentionable != nil { + qb.Where("mentionable", *filter.Mentionable) + } + + // Apply automated filter if provided + if filter.Automated != nil { + qb.Where("automated", *filter.Automated) } // Set defaults for pagination diff --git a/neo/conversation/xun_test.go b/neo/conversation/xun_test.go index adc9306b..90eacf74 100644 --- a/neo/conversation/xun_test.go +++ b/neo/conversation/xun_test.go @@ -483,6 +483,9 @@ func TestXunAssistantCRUD(t *testing.T) { t.Fatal(err) } + mentionable := true + automated := true + assistant := map[string]interface{}{ "name": "Test Assistant", "type": "assistant", @@ -491,6 +494,8 @@ func TestXunAssistantCRUD(t *testing.T) { "description": "Test Description", "tags": tagsJSON, "options": optionsJSON, + "mentionable": mentionable, + "automated": automated, } // Test SaveAssistant (Create) @@ -525,6 +530,45 @@ func TestXunAssistantCRUD(t *testing.T) { assert.Nil(t, err) assert.Equal(t, 0, len(resp.P.Items)) + // Test GetAssistants with keyword filter + resp, err = conv.GetAssistants(AssistantFilter{ + Keywords: "Test", + }) + assert.Nil(t, err) + assert.Equal(t, 1, len(resp.P.Items)) + + // Test GetAssistants with connector filter + resp, err = conv.GetAssistants(AssistantFilter{ + Connector: "openai", + }) + assert.Nil(t, err) + assert.Equal(t, 1, len(resp.P.Items)) + + // Test GetAssistants with mentionable filter + resp, err = conv.GetAssistants(AssistantFilter{ + Mentionable: &mentionable, + }) + assert.Nil(t, err) + assert.Equal(t, 1, len(resp.P.Items)) + + // Test GetAssistants with automated filter + resp, err = conv.GetAssistants(AssistantFilter{ + Automated: &automated, + }) + assert.Nil(t, err) + assert.Equal(t, 1, len(resp.P.Items)) + + // 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.P.Items)) + // Test SaveAssistant (Update) assistant["name"] = "Updated Assistant" err = conv.SaveAssistant(assistant) @@ -566,18 +610,30 @@ func TestXunAssistantPagination(t *testing.T) { } // Create multiple assistants for pagination testing + mentionable := true + automated := true for i := 0; i < 25; i++ { tagsJSON, err := jsoniter.MarshalToString([]string{fmt.Sprintf("tag%d", i%5)}) if err != nil { t.Fatal(err) } + // Alternate mentionable and automated flags + if i%2 == 0 { + mentionable = !mentionable + } + if i%3 == 0 { + automated = !automated + } + assistant := map[string]interface{}{ "name": fmt.Sprintf("Assistant %d", i), "type": "assistant", - "connector": "openai", + "connector": fmt.Sprintf("connector%d", i%3), "description": fmt.Sprintf("Description %d", i), "tags": tagsJSON, + "mentionable": mentionable, + "automated": automated, } err = conv.SaveAssistant(assistant) assert.Nil(t, err) @@ -617,4 +673,54 @@ func TestXunAssistantPagination(t *testing.T) { }) assert.Nil(t, err) assert.Equal(t, 5, len(resp.P.Items)) + + // Test filtering with keywords + resp, err = conv.GetAssistants(AssistantFilter{ + Keywords: "Assistant 1", + Page: 1, + PageSize: 10, + }) + assert.Nil(t, err) + assert.Greater(t, len(resp.P.Items), 0) + + // Test filtering with connector + resp, err = conv.GetAssistants(AssistantFilter{ + Connector: "connector0", + Page: 1, + PageSize: 10, + }) + assert.Nil(t, err) + assert.Greater(t, len(resp.P.Items), 0) + + // Test filtering with mentionable + mentionableTrue := true + resp, err = conv.GetAssistants(AssistantFilter{ + Mentionable: &mentionableTrue, + Page: 1, + PageSize: 10, + }) + assert.Nil(t, err) + assert.Greater(t, len(resp.P.Items), 0) + + // Test filtering with automated + automatedTrue := true + resp, err = conv.GetAssistants(AssistantFilter{ + Automated: &automatedTrue, + Page: 1, + PageSize: 10, + }) + assert.Nil(t, err) + assert.Greater(t, len(resp.P.Items), 0) + + // Test combined filters + resp, err = conv.GetAssistants(AssistantFilter{ + Tags: []string{"tag0"}, + Keywords: "Assistant", + Connector: "connector0", + Mentionable: &mentionableTrue, + Automated: &automatedTrue, + Page: 1, + PageSize: 10, + }) + assert.Nil(t, err) } diff --git a/neo/test_data.go b/neo/test_data.go deleted file mode 100644 index f783b8de..00000000 --- a/neo/test_data.go +++ /dev/null @@ -1,30 +0,0 @@ -package neo - -// TestMentions contains test data for mentions -var TestMentions = []Mention{ - { - ID: "assistant_1", - Name: "Alice AI", - Type: "assistant", - Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Alice", - }, - { - ID: "assistant_2", - Name: "Bob Bot", - Type: "assistant", - Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Bob", - }, - { - ID: "assistant_3", - Name: "Carol AI", - Type: "assistant", - Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Carol", - }, - // ... 其他测试数据 ... - { - ID: "assistant_20", - Name: "Tara Tech", - Type: "assistant", - Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Tara", - }, -}