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.
This commit is contained in:
Max 2024-12-29 11:06:41 +08:00
parent b17778afea
commit 8fafdc5855
5 changed files with 398 additions and 89 deletions

View file

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

View file

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

View file

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

View file

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

View file

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