diff --git a/neo/assistant/api.go b/neo/assistant/api.go index f333ab69..4e275572 100644 --- a/neo/assistant/api.go +++ b/neo/assistant/api.go @@ -314,20 +314,28 @@ func (ast *Assistant) Call(c *gin.Context, payload APIPayload) (interface{}, err func (ast *Assistant) handleChatStream(c *gin.Context, ctx chatctx.Context, messages []chatMessage.Message, options map[string]interface{}, contents *chatMessage.Contents, callback ...interface{}) (interface{}, error) { clientBreak := make(chan bool, 1) done := make(chan bool, 1) + var result interface{} = nil + var err error = nil // Chat with AI in background go func() { - err := ast.streamChat(c, ctx, messages, options, clientBreak, done, contents, callback...) + var res interface{} = nil + res, err = ast.streamChat(c, ctx, messages, options, clientBreak, contents, callback...) if err != nil { chatMessage.New().Error(err).Done().Write(c.Writer) + err = fmt.Errorf("stream chat error %s", err.Error()) } + result = res done <- true }() // Wait for completion or client disconnect select { case <-done: - return nil, nil + if err != nil { + return nil, err + } + return result, nil case <-c.Writer.CloseNotify(): clientBreak <- true return nil, nil @@ -341,10 +349,9 @@ func (ast *Assistant) streamChat( messages []chatMessage.Message, options map[string]interface{}, clientBreak chan bool, - done chan bool, contents *chatMessage.Contents, callback ...interface{}, -) error { +) (interface{}, error) { var cb interface{} if len(callback) > 0 { @@ -358,6 +365,8 @@ func (ast *Assistant) streamChat( toolsCount := 0 currentMessageID := "" + var result interface{} = nil // To save the result + var content string = "" // To save the content err := ast.Chat(c.Request.Context(), messages, options, func(data []byte) int { select { case <-clientBreak: @@ -511,6 +520,11 @@ func (ast *Assistant) streamChat( msgType = "tool" } + // Add the text content to the content + if msgType == "text" || msgType == "" { + content += msg.Text // Save the content + } + output := chatMessage.New().Map(map[string]interface{}{ "text": delta, "type": msgType, @@ -555,8 +569,6 @@ func (ast *Assistant) streamChat( // Some error occurred in the hook, return the error if hookErr != nil { chatMessage.New().Error(hookErr.Error()).Done().Callback(cb).Write(c.Writer) - - done <- true return 0 // break } @@ -569,10 +581,14 @@ func (ast *Assistant) streamChat( if err != nil { chatMessage.New().Error(err.Error()).Done().Callback(cb).Write(c.Writer) } - done <- true return 0 // break } + // if the result is not nil, save the result + if res != nil && res.Result != nil { + result = res.Result + } + // The default output output := chatMessage.New().Done() if res != nil && res.Output != nil { @@ -587,7 +603,6 @@ func (ast *Assistant) streamChat( } output.Callback(cb).Write(c.Writer) - done <- true return 0 // break } @@ -597,21 +612,27 @@ func (ast *Assistant) streamChat( // Handle error if err != nil { - return err + return nil, err } // raw error if errorRaw != "" { msg, err := chatMessage.NewStringError(errorRaw) if err != nil { - return fmt.Errorf("error: %s", err.Error()) + return nil, fmt.Errorf("stream chat error %s", err.Error()) } msg.Retry = ctx.Retry msg.Silent = ctx.Silent msg.Done().Callback(cb).Write(c.Writer) } - return nil + // If the result is not nil, return the result + if result != nil { + return result, nil + } + + // Return the content + return strings.TrimSpace(content), nil } // saveChatHistory saves the chat history if storage is available diff --git a/neo/assistant/call.go b/neo/assistant/call.go index 9a285f33..532e8a7e 100644 --- a/neo/assistant/call.go +++ b/neo/assistant/call.go @@ -23,23 +23,23 @@ type objectCall struct{} // OptionsCall is the options for the call function type OptionsCall struct { - Retry OptionsCallRetry `json:"retry,omitempty"` - Options map[string]interface{} `json:"options,omitempty"` + Retry OptionsCallRetry `json:"retry,omitempty"` // Retry options + Options map[string]interface{} `json:"options,omitempty"` // LLM API options + Silent bool `json:"silent,omitempty"` // Silent mode, default is true } // OptionsCallRetry is the retry options for the call function type OptionsCallRetry struct { - Times int `json:"times,omitempty"` - Delay int `json:"delay,omitempty"` - DelayMax int `json:"delay_max,omitempty"` - Prompt string `json:"prompt,omitempty"` + Times int `json:"times,omitempty"` // Retry times, default is 3 + Delay int `json:"delay,omitempty"` // Retry delay, default is 200 + DelayMax int `json:"delay_max,omitempty"` // Retry delay max, default is 5000 + Prompt string `json:"prompt,omitempty"` // Retry prompt, default is "Please fix the error. \n {{ error }}" } // allowedEvents is the allowed events for the call function var allowedEvents = map[string]bool{ "done": true, "retry": true, - "error": true, "message": true, } @@ -188,10 +188,11 @@ func (obj *objectCall) run(info *v8go.FunctionCallbackInfo) *v8go.Value { Retry: OptionsCallRetry{ Times: 3, Delay: 200, - DelayMax: 5000, - Prompt: "Please fix the error. \n {{ error }}", + DelayMax: 1000, + Prompt: "{{ input }}\n**Answer is not correct, please try again.**\nError:\n{{ error }} \nAssistant's last answer:\n{{ output }}", }, - Options: map[string]interface{}{}, + Silent: true, + Options: map[string]interface{}{}, // LLM API options } // Get the options @@ -241,8 +242,8 @@ func (obj *objectCall) run(info *v8go.FunctionCallbackInfo) *v8go.Value { // Update the chat context var chatCtx chatctx.Context = global.ChatContext chatCtx.AssistantID = assistantID - chatCtx.ChatID = fmt.Sprintf("chat_%s", uuid.New().String()) // New chat id - chatCtx.Silent = true + chatCtx.ChatID = fmt.Sprintf("call_%s", uuid.New().String()) // New chat id + chatCtx.Silent = options.Silent // Check the silent mode // Define the callback function var cb func(msg *chatMessage.Message) = nil @@ -274,7 +275,7 @@ func (obj *objectCall) run(info *v8go.FunctionCallbackInfo) *v8go.Value { } // Trigger the done event - _, err = obj.trigger(info, "done", jsArgs...) + doneResult, err := obj.trigger(info, "done", jsArgs...) if err != nil { result, err = obj.retry(jsArgs, err, input, output, info, options) if err != nil { @@ -282,6 +283,11 @@ func (obj *objectCall) run(info *v8go.FunctionCallbackInfo) *v8go.Value { } } + // Return the done result + if doneResult != nil && !doneResult.IsUndefined() { + return doneResult + } + // Return Value switch v := result.(type) { case *v8go.Value: diff --git a/neo/assistant/load_test.go b/neo/assistant/load_test.go index dd5088c8..1e55d31f 100644 --- a/neo/assistant/load_test.go +++ b/neo/assistant/load_test.go @@ -371,12 +371,18 @@ func (m *mockStore) GetAssistants(filter store.AssistantFilter) (*store.Assistan return nil, nil } func (m *mockStore) GetChat(id string, chatID string) (*store.ChatInfo, error) { return nil, nil } +func (m *mockStore) GetChatWithFilter(id string, chatID string, filter store.ChatFilter) (*store.ChatInfo, error) { + return nil, nil +} func (m *mockStore) GetChats(id string, filter store.ChatFilter) (*store.ChatGroupResponse, error) { return nil, nil } func (m *mockStore) GetHistory(id string, chatID string) ([]map[string]interface{}, error) { return nil, nil } +func (m *mockStore) GetHistoryWithFilter(id string, chatID string, filter store.ChatFilter) ([]map[string]interface{}, error) { + return nil, nil +} func (m *mockStore) SaveAssistant(assistant map[string]interface{}) (interface{}, error) { return nil, nil } diff --git a/neo/assistant/object.go b/neo/assistant/object.go index 3cd45491..c97041c3 100644 --- a/neo/assistant/object.go +++ b/neo/assistant/object.go @@ -59,10 +59,10 @@ func (ast *Assistant) InitObject(v8ctx *v8.Context, c *gin.Context, context chat v8ctx.WithGlobal("context", context.Map()) // Add methods to the script contexts - v8ctx.WithFunction("Plan", jsPlan) // Create a new plan object v8ctx.WithFunction("Send", jsSend) - v8ctx.WithFunction("Call", jsCall) v8ctx.WithFunction("Assets", jsAssets) + v8ctx.WithFunction("MakeCall", jsCall) // Create a new call object + v8ctx.WithFunction("MakePlan", jsPlan) // Create a new plan object // Shared space methods v8ctx.WithFunction("Set", jsSet) diff --git a/neo/context/context.go b/neo/context/context.go index 26cc5450..8d0d9442 100644 --- a/neo/context/context.go +++ b/neo/context/context.go @@ -113,6 +113,12 @@ func (ctx *Context) Map() map[string]interface{} { if ctx.Stack != "" { data["stack"] = ctx.Stack } + + // Silent mode + if ctx.Silent { + data["silent"] = ctx.Silent + } + if ctx.Path != "" { data["pathname"] = ctx.Path } diff --git a/neo/store/mongo.go b/neo/store/mongo.go index e94b4278..b6fc9146 100644 --- a/neo/store/mongo.go +++ b/neo/store/mongo.go @@ -18,11 +18,21 @@ func (m *Mongo) GetChat(sid string, cid string) (*ChatInfo, error) { return &ChatInfo{}, nil } +// GetChatWithFilter retrieves a single chat's information with filter options +func (m *Mongo) GetChatWithFilter(sid string, cid string, filter ChatFilter) (*ChatInfo, error) { + return &ChatInfo{}, nil +} + // GetHistory retrieves chat history func (m *Mongo) GetHistory(sid string, cid string) ([]map[string]interface{}, error) { return []map[string]interface{}{}, nil } +// GetHistoryWithFilter retrieves chat history with filter options +func (m *Mongo) GetHistoryWithFilter(sid string, cid string, filter ChatFilter) ([]map[string]interface{}, error) { + return []map[string]interface{}{}, nil +} + // SaveHistory saves chat history func (m *Mongo) SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error { return nil diff --git a/neo/store/redis.go b/neo/store/redis.go index 50206e1a..a5176b27 100644 --- a/neo/store/redis.go +++ b/neo/store/redis.go @@ -18,11 +18,21 @@ func (r *Redis) GetChat(sid string, cid string) (*ChatInfo, error) { return &ChatInfo{}, nil } +// GetChatWithFilter retrieves a single chat's information with filter options +func (r *Redis) GetChatWithFilter(sid string, cid string, filter ChatFilter) (*ChatInfo, error) { + return &ChatInfo{}, nil +} + // GetHistory retrieves chat history func (r *Redis) GetHistory(sid string, cid string) ([]map[string]interface{}, error) { return []map[string]interface{}{}, nil } +// GetHistoryWithFilter retrieves chat history with filter options +func (r *Redis) GetHistoryWithFilter(sid string, cid string, filter ChatFilter) ([]map[string]interface{}, error) { + return []map[string]interface{}{}, nil +} + // SaveHistory saves chat history func (r *Redis) SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error { return nil diff --git a/neo/store/types.go b/neo/store/types.go index d4dcdba3..22cab473 100644 --- a/neo/store/types.go +++ b/neo/store/types.go @@ -24,6 +24,7 @@ type ChatFilter struct { 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 + Silent *bool `json:"silent,omitempty"` // Include silent messages (default: false) } // ChatGroup represents the chat group structure @@ -86,12 +87,26 @@ type Store interface { // Returns: Chat information and potential error GetChat(sid string, cid string) (*ChatInfo, error) + // GetChatWithFilter retrieves a single chat's information with filter options + // sid: Session ID + // cid: Chat ID + // filter: Filter conditions + // Returns: Chat information and potential error + GetChatWithFilter(sid string, cid string, filter ChatFilter) (*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) + // GetHistoryWithFilter retrieves chat history with filter options + // sid: Session ID + // cid: Chat ID + // filter: Filter conditions + // Returns: History record list and potential error + GetHistoryWithFilter(sid string, cid string, filter ChatFilter) ([]map[string]interface{}, error) + // SaveHistory saves chat history // sid: Session ID // messages: Message list diff --git a/neo/store/xun.go b/neo/store/xun.go index 6f3b4c97..ceae2f0a 100644 --- a/neo/store/xun.go +++ b/neo/store/xun.go @@ -3,7 +3,6 @@ package store import ( "fmt" "math" - "strings" "time" "github.com/google/uuid" @@ -145,6 +144,7 @@ func (conv *Xun) initHistoryTable() error { table.String("assistant_name", 200).Null() table.String("assistant_avatar", 200).Null() table.JSON("mentions").Null() + table.Boolean("silent").SetDefault(false).Index() table.TimestampTz("created_at").SetDefaultRaw("NOW()").Index() table.TimestampTz("updated_at").Null().Index() table.TimestampTz("expired_at").Null().Index() @@ -162,7 +162,7 @@ func (conv *Xun) initHistoryTable() error { return err } - fields := []string{"id", "sid", "cid", "uid", "role", "name", "content", "context", "assistant_id", "assistant_name", "assistant_avatar", "mentions", "created_at", "updated_at", "expired_at"} + fields := []string{"id", "sid", "cid", "uid", "role", "name", "content", "context", "assistant_id", "assistant_name", "assistant_avatar", "mentions", "silent", "created_at", "updated_at", "expired_at"} for _, field := range fields { if !tab.HasColumn(field) { return fmt.Errorf("%s is required", field) @@ -187,6 +187,7 @@ func (conv *Xun) initChatTable() error { table.String("title", 200).Null() table.String("assistant_id", 200).Null().Index() table.String("sid", 255).Index() + table.Boolean("silent").SetDefault(false).Index() table.TimestampTz("created_at").SetDefaultRaw("NOW()").Index() table.TimestampTz("updated_at").Null().Index() }) @@ -203,7 +204,7 @@ func (conv *Xun) initChatTable() error { return err } - fields := []string{"id", "chat_id", "title", "assistant_id", "sid", "created_at", "updated_at"} + fields := []string{"id", "chat_id", "title", "assistant_id", "sid", "silent", "created_at", "updated_at"} for _, field := range fields { if !tab.HasColumn(field) { return fmt.Errorf("%s is required", field) @@ -319,53 +320,90 @@ func (conv *Xun) UpdateChatTitle(sid string, cid string, title string) error { // GetChats get the chat list with grouping by date func (conv *Xun) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, error) { + // Default behavior: exclude silent chats + if filter.Silent == nil { + silentFalse := false + filter.Silent = &silentFalse + } + + return conv.getChatsWithFilter(sid, filter) +} + +// getChatsWithFilter get the chats with filter options +func (conv *Xun) getChatsWithFilter(sid string, filter ChatFilter) (*ChatGroupResponse, error) { userID, err := conv.getUserID(sid) if err != nil { return nil, err } - // Set defaults - if filter.PageSize <= 0 { - filter.PageSize = 100 - } + // Set default values if filter.Page <= 0 { filter.Page = 1 } + if filter.PageSize <= 0 { + filter.PageSize = 20 + } if filter.Order == "" { filter.Order = "desc" } - // Build base query - qb := conv.newQueryChat(). - Select("chat_id", "title", "assistant_id", "created_at", "updated_at"). - Where("sid", userID). - Where("chat_id", "!=", "") + // Get total count + qbCount := conv.newQueryChat(). + Where("sid", userID) - // Add keyword filter - if filter.Keywords != "" { - keyword := strings.TrimSpace(filter.Keywords) - if keyword != "" { - qb.Where("title", "like", "%"+keyword+"%") + // Apply silent filter if provided + if filter.Silent != nil { + if *filter.Silent { + // Include all chats (both silent and non-silent) + } else { + // Only include non-silent chats + qbCount.Where("silent", false) } } - // Get total count - total, err := qb.Clone().Count() + // Apply keyword filter if provided + if filter.Keywords != "" { + qbCount.Where("title", "like", fmt.Sprintf("%%%s%%", filter.Keywords)) + } + + total, err := qbCount.Count() if err != nil { return nil, err } - // Calculate pagination - offset := (filter.Page - 1) * filter.PageSize + // Calculate last page lastPage := int(math.Ceil(float64(total) / float64(filter.PageSize))) + if lastPage < 1 { + lastPage = 1 + } - // Get paginated results - rows, err := qb. - OrderBy("updated_at", filter.Order). - OrderBy("created_at", filter.Order). + // Get chats with pagination + qb := conv.newQueryChat(). + Select("chat_id", "title", "assistant_id", "silent", "created_at", "updated_at"). + Where("sid", userID) + + // Apply silent filter if provided + if filter.Silent != nil { + if *filter.Silent { + // Include all chats (both silent and non-silent) + } else { + // Only include non-silent chats + qb.Where("silent", false) + } + } + + // Apply keyword filter if provided + if filter.Keywords != "" { + qb.Where("title", "like", fmt.Sprintf("%%%s%%", filter.Keywords)) + } + + // Apply pagination + offset := (filter.Page - 1) * filter.PageSize + qb.OrderBy("updated_at", filter.Order). Offset(offset). - Limit(filter.PageSize). - Get() + Limit(filter.PageSize) + + rows, err := qb.Get() if err != nil { return nil, err } @@ -385,16 +423,16 @@ func (conv *Xun) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, er "Even Earlier": {}, } - // Get assistant details for all chats + // Collect assistant IDs to fetch their details assistantIDs := []interface{}{} - assistantMap := make(map[string]map[string]interface{}) - for _, row := range rows { if assistantID := row.Get("assistant_id"); assistantID != nil && assistantID != "" { assistantIDs = append(assistantIDs, assistantID) } } + // Fetch assistant details + assistantMap := map[string]map[string]interface{}{} if len(assistantIDs) > 0 { assistants, err := conv.query.New(). Table(conv.getAssistantTable()). @@ -425,6 +463,7 @@ func (conv *Xun) GetChats(sid string, filter ChatFilter) (*ChatGroupResponse, er "chat_id": chatID, "title": row.Get("title"), "assistant_id": row.Get("assistant_id"), + "silent": row.Get("silent"), } // Add assistant details if available @@ -502,11 +541,14 @@ func (conv *Xun) GetHistory(sid string, cid string) ([]map[string]interface{}, e } qb := conv.newQuery(). - Select("role", "name", "content", "context", "assistant_id", "assistant_name", "assistant_avatar", "mentions", "uid", "created_at", "updated_at"). + Select("role", "name", "content", "context", "assistant_id", "assistant_name", "assistant_avatar", "mentions", "uid", "silent", "created_at", "updated_at"). Where("sid", userID). Where("cid", cid). OrderBy("id", "desc") + // By default, exclude silent messages + qb.Where("silent", false) + if conv.setting.TTL > 0 { qb.Where("expired_at", ">", time.Now()) } @@ -533,6 +575,7 @@ func (conv *Xun) GetHistory(sid string, cid string) ([]map[string]interface{}, e "assistant_avatar": row.Get("assistant_avatar"), "mentions": row.Get("mentions"), "uid": row.Get("uid"), + "silent": row.Get("silent"), "created_at": row.Get("created_at"), "updated_at": row.Get("updated_at"), } @@ -562,6 +605,23 @@ func (conv *Xun) SaveHistory(sid string, messages []map[string]interface{}, cid } } + // Get silent flag from context + var silent bool = false + if context != nil { + if silentVal, ok := context["silent"]; ok { + switch v := silentVal.(type) { + case bool: + silent = v + case string: + silent = v == "true" || v == "1" || v == "yes" + case int: + silent = v != 0 + case float64: + silent = v != 0 + } + } + } + // First ensure chat record exists exists, err := conv.newQueryChat(). Where("chat_id", cid). @@ -579,6 +639,7 @@ func (conv *Xun) SaveHistory(sid string, messages []map[string]interface{}, cid "chat_id": cid, "sid": userID, "assistant_id": assistantID, + "silent": silent, "created_at": time.Now(), }) @@ -586,17 +647,16 @@ func (conv *Xun) SaveHistory(sid string, messages []map[string]interface{}, cid return err } } else { - // Update assistant_id if it exists - if assistantID != nil { - _, err = conv.newQueryChat(). - Where("chat_id", cid). - Where("sid", userID). - Update(map[string]interface{}{ - "assistant_id": assistantID, - }) - if err != nil { - return err - } + // Update assistant_id and silent if needed + _, err = conv.newQueryChat(). + Where("chat_id", cid). + Where("sid", userID). + Update(map[string]interface{}{ + "assistant_id": assistantID, + "silent": silent, + }) + if err != nil { + return err } } @@ -650,6 +710,7 @@ func (conv *Xun) SaveHistory(sid string, messages []map[string]interface{}, cid "assistant_id": nil, "assistant_name": nil, "assistant_avatar": nil, + "silent": silent, "created_at": now, "updated_at": nil, "expired_at": expiredAt, @@ -736,7 +797,7 @@ func (conv *Xun) GetChat(sid string, cid string) (*ChatInfo, error) { } } - // Get chat history + // Get chat history with default filter (silent=false) history, err := conv.GetHistory(sid, cid) if err != nil { return nil, err @@ -748,6 +809,64 @@ func (conv *Xun) GetChat(sid string, cid string) (*ChatInfo, error) { }, nil } +// GetChatWithFilter get the chat info and its history with filter options +func (conv *Xun) GetChatWithFilter(sid string, cid string, filter ChatFilter) (*ChatInfo, error) { + userID, err := conv.getUserID(sid) + if err != nil { + return nil, err + } + + // Get chat info + qb := conv.newQueryChat(). + Select("chat_id", "title", "assistant_id"). + Where("sid", userID). + Where("chat_id", cid) + + row, err := qb.First() + if err != nil { + return nil, err + } + + // Return nil if chat_id is nil (means no chat found) + if row.Get("chat_id") == nil { + return nil, nil + } + + chat := map[string]interface{}{ + "chat_id": row.Get("chat_id"), + "title": row.Get("title"), + "assistant_id": row.Get("assistant_id"), + } + + // Get assistant details if assistant_id exists + if assistantID := row.Get("assistant_id"); assistantID != nil && assistantID != "" { + assistant, err := conv.query.New(). + Table(conv.getAssistantTable()). + Select("name", "avatar"). + Where("assistant_id", assistantID). + First() + if err != nil { + return nil, err + } + + if assistant != nil { + chat["assistant_name"] = assistant.Get("name") + chat["assistant_avatar"] = assistant.Get("avatar") + } + } + + // Get chat history with filter + history, err := conv.GetHistoryWithFilter(sid, cid, filter) + if err != nil { + return nil, err + } + + return &ChatInfo{ + Chat: chat, + History: history, + }, nil +} + // DeleteChat deletes a specific chat and its history func (conv *Xun) DeleteChat(sid string, cid string) error { userID, err := conv.getUserID(sid) @@ -1164,3 +1283,74 @@ func (conv *Xun) GetAssistantTags() ([]string, error) { } return tags, nil } + +// GetHistoryWithFilter get the history with filter options +func (conv *Xun) GetHistoryWithFilter(sid string, cid string, filter ChatFilter) ([]map[string]interface{}, error) { + userID, err := conv.getUserID(sid) + if err != nil { + return nil, err + } + + qb := conv.newQuery(). + Select("role", "name", "content", "context", "assistant_id", "assistant_name", "assistant_avatar", "mentions", "uid", "silent", "created_at", "updated_at"). + Where("sid", userID). + Where("cid", cid). + OrderBy("id", "desc") + + // Apply silent filter if provided, otherwise exclude silent messages by default + if filter.Silent != nil { + if *filter.Silent { + // Include all messages (both silent and non-silent) + } else { + // Only include non-silent messages + qb.Where("silent", false) + } + } else { + // Default behavior: exclude silent messages + qb.Where("silent", false) + } + + if conv.setting.TTL > 0 { + qb.Where("expired_at", ">", time.Now()) + } + + limit := 20 + if conv.setting.MaxSize > 0 { + limit = conv.setting.MaxSize + } + if filter.PageSize > 0 { + limit = filter.PageSize + } + + // Apply pagination if provided + if filter.Page > 0 { + offset := (filter.Page - 1) * limit + qb.Offset(offset) + } + + rows, err := qb.Limit(limit).Get() + if err != nil { + return nil, err + } + + res := []map[string]interface{}{} + for _, row := range rows { + message := map[string]interface{}{ + "role": row.Get("role"), + "name": row.Get("name"), + "content": row.Get("content"), + "context": row.Get("context"), + "assistant_id": row.Get("assistant_id"), + "assistant_name": row.Get("assistant_name"), + "assistant_avatar": row.Get("assistant_avatar"), + "mentions": row.Get("mentions"), + "uid": row.Get("uid"), + "silent": row.Get("silent"), + "created_at": row.Get("created_at"), + "updated_at": row.Get("updated_at"), + } + res = append([]map[string]interface{}{message}, res...) + } + + return res, nil +} diff --git a/neo/store/xun_test.go b/neo/store/xun_test.go index e1a4d7d7..e1d1f1bd 100644 --- a/neo/store/xun_test.go +++ b/neo/store/xun_test.go @@ -996,3 +996,252 @@ func TestGetAssistantTags(t *testing.T) { } } } + +func TestXunSaveAndGetHistoryWithSilent(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + defer capsule.Schema().DropTableIfExists("__unit_test_conversation_history") + defer capsule.Schema().DropTableIfExists("__unit_test_conversation_chat") + + err := capsule.Schema().DropTableIfExists("__unit_test_conversation_history") + if err != nil { + t.Fatal(err) + } + + err = capsule.Schema().DropTableIfExists("__unit_test_conversation_chat") + if err != nil { + t.Fatal(err) + } + + store, err := NewXun(Setting{ + Connector: "default", + Prefix: "__unit_test_conversation_", + TTL: 3600, + }) + + // save the history with silent messages + sid := "123456" + cid := "silent_test" + + // First save regular messages + messages := []map[string]interface{}{ + {"role": "user", "name": "user1", "content": "hello"}, + {"role": "assistant", "name": "assistant1", "content": "Hi! How can I help you?"}, + } + context := map[string]interface{}{ + "assistant_id": "test-assistant-1", + } + err = store.SaveHistory(sid, messages, cid, context) + assert.Nil(t, err) + + // Then save silent messages + silentMessages := []map[string]interface{}{ + {"role": "user", "name": "user1", "content": "silent message"}, + {"role": "assistant", "name": "assistant1", "content": "This is a silent response"}, + } + silentContext := map[string]interface{}{ + "assistant_id": "test-assistant-1", + "silent": true, + } + err = store.SaveHistory(sid, silentMessages, cid, silentContext) + assert.Nil(t, err) + + // Get history without filter (should only return non-silent messages) + data, err := store.GetHistory(sid, cid) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, 2, len(data)) + for _, msg := range data { + // Check if silent is false, handling different types + isSilent := false + switch v := msg["silent"].(type) { + case bool: + isSilent = v + case int: + isSilent = v != 0 + case int64: + isSilent = v != 0 + case float64: + isSilent = v != 0 + } + assert.False(t, isSilent, "message should not be silent") + } + + // Get history with silent=true filter (should return all messages) + silentTrue := true + filter := ChatFilter{ + Silent: &silentTrue, + } + allData, err := store.GetHistoryWithFilter(sid, cid, filter) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, 4, len(allData)) + + // Count silent messages + silentCount := 0 + for _, msg := range allData { + // Check if silent is true, handling different types + isSilent := false + switch v := msg["silent"].(type) { + case bool: + isSilent = v + case int: + isSilent = v != 0 + case int64: + isSilent = v != 0 + case float64: + isSilent = v != 0 + } + if isSilent { + silentCount++ + } + } + assert.Equal(t, 2, silentCount) + + // Get chat with filter (should include silent messages) + chat, err := store.GetChatWithFilter(sid, cid, filter) + assert.Nil(t, err) + assert.Equal(t, 4, len(chat.History)) + + // Get chat without filter (should exclude silent messages) + chatNoSilent, err := store.GetChat(sid, cid) + assert.Nil(t, err) + assert.Equal(t, 2, len(chatNoSilent.History)) +} + +func TestXunGetChatsWithSilent(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + defer capsule.Schema().DropTableIfExists("__unit_test_conversation_history") + defer capsule.Schema().DropTableIfExists("__unit_test_conversation_chat") + defer capsule.Schema().DropTableIfExists("__unit_test_conversation_assistant") + + // Drop tables before test + err := capsule.Schema().DropTableIfExists("__unit_test_conversation_history") + if err != nil { + t.Fatal(err) + } + err = capsule.Schema().DropTableIfExists("__unit_test_conversation_chat") + if err != nil { + t.Fatal(err) + } + err = capsule.Schema().DropTableIfExists("__unit_test_conversation_assistant") + if err != nil { + t.Fatal(err) + } + + store, err := NewXun(Setting{ + Connector: "default", + Prefix: "__unit_test_conversation_", + }) + if err != nil { + t.Fatal(err) + } + + // Create test assistant + assistant := map[string]interface{}{ + "assistant_id": "test-assistant-1", + "name": "Test Assistant 1", + "avatar": "avatar1.png", + "type": "assistant", + "connector": "test", + } + _, err = store.SaveAssistant(assistant) + assert.Nil(t, err) + + // Save some test chats + sid := "test_user" + messages := []map[string]interface{}{ + {"role": "user", "content": "test message"}, + } + + // Create regular chats + for i := 0; i < 3; i++ { + chatID := fmt.Sprintf("regular_chat_%d", i) + title := fmt.Sprintf("Regular Chat %d", i) + context := map[string]interface{}{ + "assistant_id": "test-assistant-1", + "silent": false, + } + + // Save history to create the chat + err = store.SaveHistory(sid, messages, chatID, context) + assert.Nil(t, err) + + // Update the chat title + err = store.UpdateChatTitle(sid, chatID, title) + assert.Nil(t, err) + } + + // Create silent chats + for i := 0; i < 2; i++ { + chatID := fmt.Sprintf("silent_chat_%d", i) + title := fmt.Sprintf("Silent Chat %d", i) + context := map[string]interface{}{ + "assistant_id": "test-assistant-1", + "silent": true, + } + + // Save history to create the chat + err = store.SaveHistory(sid, messages, chatID, context) + assert.Nil(t, err) + + // Update the chat title + err = store.UpdateChatTitle(sid, chatID, title) + assert.Nil(t, err) + } + + // Test GetChats with default filter (should exclude silent chats) + defaultFilter := ChatFilter{ + PageSize: 10, + Order: "desc", + } + defaultGroups, err := store.GetChats(sid, defaultFilter) + assert.Nil(t, err) + assert.NotNil(t, defaultGroups) + + // Count total chats in all groups + totalDefaultChats := 0 + for _, group := range defaultGroups.Groups { + totalDefaultChats += len(group.Chats) + } + assert.Equal(t, 3, totalDefaultChats, "Default filter should only return non-silent chats") + + // Test GetChats with silent=true filter (should include all chats) + silentTrue := true + silentFilter := ChatFilter{ + PageSize: 10, + Order: "desc", + Silent: &silentTrue, + } + silentGroups, err := store.GetChats(sid, silentFilter) + assert.Nil(t, err) + assert.NotNil(t, silentGroups) + + // Count total chats in all groups + totalSilentChats := 0 + for _, group := range silentGroups.Groups { + totalSilentChats += len(group.Chats) + } + assert.Equal(t, 5, totalSilentChats, "Silent filter should return all chats") + + // Test GetChats with silent=false filter (should only include non-silent chats) + silentFalse := false + nonSilentFilter := ChatFilter{ + PageSize: 10, + Order: "desc", + Silent: &silentFalse, + } + nonSilentGroups, err := store.GetChats(sid, nonSilentFilter) + assert.Nil(t, err) + assert.NotNil(t, nonSilentGroups) + + // Count total chats in all groups + totalNonSilentChats := 0 + for _, group := range nonSilentGroups.Groups { + totalNonSilentChats += len(group.Chats) + } + assert.Equal(t, 3, totalNonSilentChats, "Non-silent filter should only return non-silent chats") +}