Remove deprecated studio package and refactor agent integration - Deleted the studio package, which is no longer in use, to streamline the codebase. - Updated references in the agent and chat modules to utilize the new agent package instead of the deprecated neo package. - Ensured that all related middleware and routing functionalities are now aligned with the agent architecture, enhancing overall system coherence.
2253 lines
59 KiB
Go
2253 lines
59 KiB
Go
package store
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
jsoniter "github.com/json-iterator/go"
|
|
"github.com/yaoapp/gou/connector"
|
|
"github.com/yaoapp/gou/session"
|
|
"github.com/yaoapp/kun/log"
|
|
"github.com/yaoapp/xun/capsule"
|
|
"github.com/yaoapp/xun/dbal/query"
|
|
"github.com/yaoapp/xun/dbal/schema"
|
|
"github.com/yaoapp/yao/agent/i18n"
|
|
)
|
|
|
|
// 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
|
|
// - Managing file attachments with metadata and access control
|
|
// - Managing knowledge collections for AI assistants
|
|
// - Supporting data expiration through TTL settings
|
|
type Xun struct {
|
|
query query.Query
|
|
schema schema.Schema
|
|
setting Setting
|
|
cleanTicker *time.Ticker
|
|
cleanStop chan bool
|
|
}
|
|
|
|
// 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
|
|
// GetAssistant retrieves a single assistant by assistant_id
|
|
// SaveAttachment creates or updates an attachment
|
|
// DeleteAttachment deletes an attachment by file_id
|
|
// GetAttachments retrieves a paginated list of attachments with filtering
|
|
// GetAttachment retrieves a single attachment by file_id
|
|
// SaveKnowledge creates or updates a knowledge collection
|
|
// DeleteKnowledge deletes a knowledge collection by collection_id
|
|
// GetKnowledges retrieves a paginated list of knowledge collections with filtering
|
|
// GetKnowledge retrieves a single knowledge collection by collection_id
|
|
|
|
// NewXun create a new xun store
|
|
func NewXun(setting Setting) (Store, error) {
|
|
conv := &Xun{setting: setting}
|
|
if setting.Connector == "default" {
|
|
conv.query = capsule.Global.Query()
|
|
conv.schema = capsule.Global.Schema()
|
|
} else {
|
|
conn, err := connector.Select(setting.Connector)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
conv.query, err = conn.Query()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
conv.schema, err = conn.Schema()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err := conv.initialize()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return conv, nil
|
|
}
|
|
|
|
// Rename the following functions to start with lowercase letters to make them private:
|
|
|
|
func (conv *Xun) newQuery() query.Query {
|
|
qb := conv.query.New()
|
|
qb.Table(conv.getHistoryTable())
|
|
return qb
|
|
}
|
|
|
|
func (conv *Xun) newQueryChat() query.Query {
|
|
qb := conv.query.New()
|
|
qb.Table(conv.getChatTable())
|
|
return qb
|
|
}
|
|
|
|
func (conv *Xun) clean() {
|
|
nums, err := conv.newQuery().Where("expired_at", "<=", time.Now()).Delete()
|
|
if err != nil {
|
|
log.Error("Clean the conversation table error: %s", err.Error())
|
|
return
|
|
}
|
|
|
|
if nums > 0 {
|
|
log.Trace("Clean the conversation table: %s %d", conv.setting.Prefix, nums)
|
|
}
|
|
}
|
|
|
|
// startAutoClean starts the automatic cleanup routine
|
|
func (conv *Xun) startAutoClean() {
|
|
if conv.cleanTicker != nil {
|
|
conv.stopAutoClean() // Stop existing ticker if any
|
|
}
|
|
|
|
conv.cleanTicker = time.NewTicker(1 * time.Hour) // Clean every hour
|
|
conv.cleanStop = make(chan bool)
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-conv.cleanTicker.C:
|
|
conv.clean()
|
|
case <-conv.cleanStop:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
log.Trace("Started automatic cleanup for: %s", conv.setting.Prefix)
|
|
}
|
|
|
|
// stopAutoClean stops the automatic cleanup routine
|
|
func (conv *Xun) stopAutoClean() {
|
|
if conv.cleanTicker != nil {
|
|
conv.cleanTicker.Stop()
|
|
conv.cleanTicker = nil
|
|
}
|
|
|
|
if conv.cleanStop != nil {
|
|
close(conv.cleanStop)
|
|
conv.cleanStop = nil
|
|
}
|
|
|
|
log.Trace("Stopped automatic cleanup for: %s", conv.setting.Prefix)
|
|
}
|
|
|
|
// Close stops the automatic cleanup and closes resources
|
|
func (conv *Xun) Close() error {
|
|
conv.stopAutoClean()
|
|
return nil
|
|
}
|
|
|
|
// Rename Init to initialize to avoid conflicts
|
|
func (conv *Xun) initialize() error {
|
|
// Initialize history table
|
|
if err := conv.initHistoryTable(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Initialize chat table
|
|
if err := conv.initChatTable(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Initialize assistant table
|
|
if err := conv.initAssistantTable(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Initialize attachment table
|
|
if err := conv.initAttachmentTable(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Initialize knowledge table
|
|
if err := conv.initKnowledgeTable(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Start automatic cleanup if TTL is enabled
|
|
if conv.setting.TTL > 0 {
|
|
conv.startAutoClean()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (conv *Xun) initHistoryTable() error {
|
|
historyTable := conv.getHistoryTable()
|
|
has, err := conv.schema.HasTable(historyTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create the history table
|
|
if !has {
|
|
err = conv.schema.CreateTable(historyTable, func(table schema.Blueprint) {
|
|
table.ID("id")
|
|
table.String("sid", 255).Index()
|
|
table.String("cid", 200).Null().Index()
|
|
table.String("uid", 255).Null().Index()
|
|
table.String("role", 200).Null().Index()
|
|
table.String("name", 200).Null().Index()
|
|
table.Text("content").Null()
|
|
table.JSON("context").Null()
|
|
table.String("assistant_id", 200).Null().Index()
|
|
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("CURRENT_TIMESTAMP").Index()
|
|
table.TimestampTz("updated_at").Null().Index()
|
|
table.TimestampTz("expired_at").Null().Index()
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Trace("Create the conversation history table: %s", historyTable)
|
|
}
|
|
|
|
// Validate the table
|
|
tab, err := conv.schema.GetTable(historyTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (conv *Xun) initChatTable() error {
|
|
chatTable := conv.getChatTable()
|
|
has, err := conv.schema.HasTable(chatTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create the chat table
|
|
if !has {
|
|
err = conv.schema.CreateTable(chatTable, func(table schema.Blueprint) {
|
|
table.ID("id")
|
|
table.String("chat_id", 200).Unique().Index()
|
|
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("CURRENT_TIMESTAMP").Index()
|
|
table.TimestampTz("updated_at").Null().Index()
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Trace("Create the chat table: %s", chatTable)
|
|
}
|
|
|
|
// Validate the table
|
|
tab, err := conv.schema.GetTable(chatTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (conv *Xun) initAssistantTable() error {
|
|
assistantTable := conv.getAssistantTable()
|
|
has, err := conv.schema.HasTable(assistantTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create the assistant table
|
|
if !has {
|
|
err = conv.schema.CreateTable(assistantTable, func(table schema.Blueprint) {
|
|
table.ID("id")
|
|
table.String("assistant_id", 200).Unique().Index()
|
|
table.String("type", 200).SetDefault("assistant").Index() // default is assistant
|
|
table.String("name", 200).Null() // assistant name
|
|
table.String("avatar", 200).Null() // assistant avatar
|
|
table.String("connector", 200).NotNull() // assistant connector
|
|
table.String("description", 600).Null().Index() // assistant description
|
|
table.String("path", 200).Null() // assistant storage path
|
|
table.Integer("sort").SetDefault(9999).Index() // assistant sort order
|
|
table.Boolean("built_in").SetDefault(false).Index() // whether this is a built-in assistant
|
|
table.JSON("placeholder").Null() // assistant placeholder
|
|
table.JSON("options").Null() // assistant options
|
|
table.JSON("prompts").Null() // assistant prompts
|
|
table.JSON("workflow").Null() // assistant workflow
|
|
table.JSON("knowledge").Null() // assistant knowledge
|
|
table.JSON("tools").Null() // assistant tools
|
|
table.JSON("tags").Null() // assistant tags
|
|
table.Boolean("readonly").SetDefault(false).Index() // assistant readonly
|
|
table.JSON("permissions").Null() // assistant permissions
|
|
table.JSON("locales").Null() // assistant i18n
|
|
table.Boolean("automated").SetDefault(true).Index() // assistant autoable
|
|
table.Boolean("mentionable").SetDefault(true).Index() // Whether this assistant can appear in @ mention list
|
|
table.TimestampTz("created_at").SetDefaultRaw("CURRENT_TIMESTAMP").Index()
|
|
table.TimestampTz("updated_at").Null().Index()
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Trace("Create the assistant table: %s", assistantTable)
|
|
}
|
|
|
|
// Validate the table
|
|
tab, err := conv.schema.GetTable(assistantTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fields := []string{"id", "assistant_id", "type", "name", "avatar", "connector", "description", "path", "sort", "built_in", "placeholder", "options", "prompts", "workflow", "knowledge", "tools", "tags", "readonly", "permissions", "locales", "automated", "mentionable", "created_at", "updated_at"}
|
|
for _, field := range fields {
|
|
if !tab.HasColumn(field) {
|
|
return fmt.Errorf("%s is required", field)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (conv *Xun) initAttachmentTable() error {
|
|
attachmentTable := conv.getAttachmentTable()
|
|
has, err := conv.schema.HasTable(attachmentTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create the attachment table
|
|
if !has {
|
|
err = conv.schema.CreateTable(attachmentTable, func(table schema.Blueprint) {
|
|
table.ID("id")
|
|
table.String("file_id", 255).Unique().Index()
|
|
table.String("uid", 255).Index()
|
|
table.Boolean("guest").SetDefault(false).Index()
|
|
table.String("manager", 200).Index()
|
|
table.String("content_type", 200).Index()
|
|
table.String("name", 500).Index()
|
|
table.Boolean("public").SetDefault(false).Index()
|
|
table.JSON("scope").Null()
|
|
table.Boolean("gzip").SetDefault(false).Index()
|
|
table.BigInteger("bytes").Index()
|
|
table.String("collection_id", 200).Null().Index()
|
|
table.Enum("status", []string{"uploading", "uploaded", "indexing", "indexed", "upload_failed", "index_failed"}).SetDefault("uploading").Index() // Status field enum
|
|
table.String("progress", 200).Null() // Progress information
|
|
table.String("error", 600).Null() // Error information
|
|
table.TimestampTz("created_at").SetDefaultRaw("CURRENT_TIMESTAMP").Index()
|
|
table.TimestampTz("updated_at").Null().Index()
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Trace("Create the attachment table: %s", attachmentTable)
|
|
}
|
|
|
|
// Validate the table
|
|
tab, err := conv.schema.GetTable(attachmentTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fields := []string{"id", "file_id", "uid", "guest", "manager", "content_type", "name", "public", "scope", "gzip", "bytes", "collection_id", "status", "progress", "error", "created_at", "updated_at"}
|
|
for _, field := range fields {
|
|
if !tab.HasColumn(field) {
|
|
return fmt.Errorf("%s is required", field)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (conv *Xun) initKnowledgeTable() error {
|
|
knowledgeTable := conv.getKnowledgeTable()
|
|
has, err := conv.schema.HasTable(knowledgeTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create the knowledge table
|
|
if !has {
|
|
err = conv.schema.CreateTable(knowledgeTable, func(table schema.Blueprint) {
|
|
table.ID("id")
|
|
table.String("collection_id", 200).Unique().Index()
|
|
table.String("name", 200).Index()
|
|
table.String("description", 600).Null().Index() // knowledge description
|
|
table.String("uid", 255).Index()
|
|
table.Boolean("public").SetDefault(false).Index()
|
|
table.JSON("scope").Null()
|
|
table.Boolean("readonly").SetDefault(false).Index()
|
|
table.JSON("option").Null()
|
|
table.Boolean("system").SetDefault(false).Index()
|
|
table.Integer("sort").SetDefault(9999).Index() // knowledge sort order
|
|
table.String("cover", 500).Null()
|
|
table.TimestampTz("created_at").SetDefaultRaw("CURRENT_TIMESTAMP").Index()
|
|
table.TimestampTz("updated_at").Null().Index()
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Trace("Create the knowledge table: %s", knowledgeTable)
|
|
}
|
|
|
|
// Validate the table
|
|
tab, err := conv.schema.GetTable(knowledgeTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fields := []string{"id", "collection_id", "name", "description", "uid", "public", "scope", "readonly", "option", "system", "sort", "cover", "created_at", "updated_at"}
|
|
for _, field := range fields {
|
|
if !tab.HasColumn(field) {
|
|
return fmt.Errorf("%s is required", field)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (conv *Xun) getUserID(sid string) (string, error) {
|
|
field := "user_id"
|
|
if conv.setting.UserField != "" {
|
|
field = conv.setting.UserField
|
|
}
|
|
|
|
id, err := session.Global().ID(sid).Get(field)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if id == nil || id == "" {
|
|
return sid, nil
|
|
}
|
|
|
|
return fmt.Sprintf("%v", id), nil
|
|
}
|
|
|
|
func (conv *Xun) getHistoryTable() string {
|
|
return conv.setting.Prefix + "history"
|
|
}
|
|
|
|
func (conv *Xun) getChatTable() string {
|
|
return conv.setting.Prefix + "chat"
|
|
}
|
|
|
|
func (conv *Xun) getAssistantTable() string {
|
|
return conv.setting.Prefix + "assistant"
|
|
}
|
|
|
|
func (conv *Xun) getAttachmentTable() string {
|
|
return conv.setting.Prefix + "attachment"
|
|
}
|
|
|
|
func (conv *Xun) getKnowledgeTable() string {
|
|
return conv.setting.Prefix + "knowledge"
|
|
}
|
|
|
|
func (conv *Xun) newQueryAttachment() query.Query {
|
|
qb := conv.query.New()
|
|
qb.Table(conv.getAttachmentTable())
|
|
return qb
|
|
}
|
|
|
|
func (conv *Xun) newQueryKnowledge() query.Query {
|
|
qb := conv.query.New()
|
|
qb.Table(conv.getKnowledgeTable())
|
|
return qb
|
|
}
|
|
|
|
// UpdateChatTitle update the chat title
|
|
func (conv *Xun) UpdateChatTitle(sid string, cid string, title string) error {
|
|
userID, err := conv.getUserID(sid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = conv.newQueryChat().
|
|
Where("sid", userID).
|
|
Where("chat_id", cid).
|
|
Update(map[string]interface{}{
|
|
"title": title,
|
|
"updated_at": time.Now(),
|
|
})
|
|
return err
|
|
}
|
|
|
|
// GetChats get the chat list with grouping by date
|
|
func (conv *Xun) GetChats(sid string, filter ChatFilter, locale ...string) (*ChatGroupResponse, error) {
|
|
// Default behavior: exclude silent chats
|
|
if filter.Silent == nil {
|
|
silentFalse := false
|
|
filter.Silent = &silentFalse
|
|
}
|
|
|
|
return conv.getChatsWithFilter(sid, filter, locale...)
|
|
}
|
|
|
|
// getChatsWithFilter get the chats with filter options
|
|
func (conv *Xun) getChatsWithFilter(sid string, filter ChatFilter, locale ...string) (*ChatGroupResponse, error) {
|
|
userID, err := conv.getUserID(sid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set default values
|
|
if filter.Page <= 0 {
|
|
filter.Page = 1
|
|
}
|
|
if filter.PageSize <= 0 {
|
|
filter.PageSize = 20
|
|
}
|
|
if filter.Order == "" {
|
|
filter.Order = "desc"
|
|
}
|
|
|
|
// Get total count
|
|
qbCount := conv.newQueryChat().
|
|
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
|
|
qbCount.Where("silent", false)
|
|
}
|
|
}
|
|
|
|
// 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 last page
|
|
lastPage := int(math.Ceil(float64(total) / float64(filter.PageSize)))
|
|
if lastPage < 1 {
|
|
lastPage = 1
|
|
}
|
|
|
|
// 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)
|
|
|
|
rows, err := qb.Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Group chats by date
|
|
today := time.Now().Truncate(24 * time.Hour)
|
|
yesterday := today.AddDate(0, 0, -1)
|
|
thisWeekStart := today.AddDate(0, 0, -int(today.Weekday()))
|
|
lastWeekStart := thisWeekStart.AddDate(0, 0, -7)
|
|
lastWeekEnd := thisWeekStart.AddDate(0, 0, -1)
|
|
|
|
groups := map[string][]map[string]interface{}{
|
|
"Today": {},
|
|
"Yesterday": {},
|
|
"This Week": {},
|
|
"Last Week": {},
|
|
"Even Earlier": {},
|
|
}
|
|
|
|
// Collect assistant IDs to fetch their details
|
|
assistantIDs := []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()).
|
|
Select("assistant_id", "name", "avatar").
|
|
WhereIn("assistant_id", assistantIDs).
|
|
Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, assistant := range assistants {
|
|
if id := assistant.Get("assistant_id"); id != nil {
|
|
name := assistant.Get("name")
|
|
if len(locale) > 0 {
|
|
lang := strings.ToLower(locale[0])
|
|
name = i18n.Translate(id.(string), lang, name).(string)
|
|
}
|
|
assistantMap[fmt.Sprintf("%v", id)] = map[string]interface{}{
|
|
"name": name,
|
|
"avatar": assistant.Get("avatar"),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, row := range rows {
|
|
chatID := row.Get("chat_id")
|
|
if chatID == nil || chatID == "" {
|
|
continue
|
|
}
|
|
|
|
chat := map[string]interface{}{
|
|
"chat_id": chatID,
|
|
"title": row.Get("title"),
|
|
"assistant_id": row.Get("assistant_id"),
|
|
"silent": row.Get("silent"),
|
|
}
|
|
|
|
// Add assistant details if available
|
|
if assistantID := row.Get("assistant_id"); assistantID != nil && assistantID != "" {
|
|
if assistant, ok := assistantMap[fmt.Sprintf("%v", assistantID)]; ok {
|
|
name := assistant["name"]
|
|
if len(locale) > 0 {
|
|
lang := strings.ToLower(locale[0])
|
|
name = i18n.Translate(assistantID.(string), lang, name).(string)
|
|
}
|
|
chat["assistant_name"] = name
|
|
chat["assistant_avatar"] = assistant["avatar"]
|
|
}
|
|
}
|
|
|
|
var dbDatetime = row.Get("updated_at")
|
|
if dbDatetime == nil {
|
|
dbDatetime = row.Get("created_at")
|
|
}
|
|
|
|
var createdAt time.Time
|
|
switch v := dbDatetime.(type) {
|
|
case time.Time:
|
|
createdAt = v
|
|
case string:
|
|
parsed, err := time.Parse("2006-01-02 15:04:05.999999-07:00", v)
|
|
if err != nil {
|
|
// Try alternative format
|
|
parsed, err = time.Parse(time.RFC3339, v)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
}
|
|
createdAt = parsed
|
|
default:
|
|
continue
|
|
}
|
|
|
|
createdDate := createdAt.Truncate(24 * time.Hour)
|
|
|
|
switch {
|
|
case createdDate.Equal(today):
|
|
groups["Today"] = append(groups["Today"], chat)
|
|
case createdDate.Equal(yesterday):
|
|
groups["Yesterday"] = append(groups["Yesterday"], chat)
|
|
case createdDate.After(thisWeekStart) && createdDate.Before(today):
|
|
groups["This Week"] = append(groups["This Week"], chat)
|
|
case createdDate.After(lastWeekStart) && createdDate.Before(lastWeekEnd.AddDate(0, 0, 1)):
|
|
groups["Last Week"] = append(groups["Last Week"], chat)
|
|
default:
|
|
groups["Even Earlier"] = append(groups["Even Earlier"], chat)
|
|
}
|
|
}
|
|
|
|
// Convert to ordered slice and apply i18n
|
|
result := []ChatGroup{}
|
|
for _, label := range []string{"Today", "Yesterday", "This Week", "Last Week", "Even Earlier"} {
|
|
if len(groups[label]) > 0 {
|
|
translatedLabel := label
|
|
if len(locale) > 0 {
|
|
lang := strings.ToLower(locale[0])
|
|
translatedLabel = i18n.TranslateGlobal(lang, label).(string)
|
|
}
|
|
result = append(result, ChatGroup{
|
|
Label: translatedLabel,
|
|
Chats: groups[label],
|
|
})
|
|
}
|
|
}
|
|
|
|
return &ChatGroupResponse{
|
|
Groups: result,
|
|
Page: filter.Page,
|
|
PageSize: filter.PageSize,
|
|
Total: total,
|
|
LastPage: lastPage,
|
|
}, nil
|
|
}
|
|
|
|
// GetHistory get the history
|
|
func (conv *Xun) GetHistory(sid string, cid string, locale ...string) ([]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")
|
|
|
|
// By default, 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
|
|
}
|
|
|
|
rows, err := qb.Limit(limit).Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res := []map[string]interface{}{}
|
|
for _, row := range rows {
|
|
assistantName := row.Get("assistant_name")
|
|
assistantID := row.Get("assistant_id")
|
|
if len(locale) > 0 && assistantID != nil {
|
|
lang := strings.ToLower(locale[0])
|
|
assistantName = i18n.Translate(assistantID.(string), lang, assistantName).(string)
|
|
}
|
|
|
|
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": assistantName,
|
|
"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
|
|
}
|
|
|
|
// SaveHistory save the history
|
|
func (conv *Xun) SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error {
|
|
|
|
if cid == "" {
|
|
cid = uuid.New().String() // Generate a new UUID if cid is empty
|
|
}
|
|
|
|
userID, err := conv.getUserID(sid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get assistant_id from context
|
|
var assistantID interface{} = nil
|
|
if context != nil {
|
|
if id, ok := context["assistant_id"].(string); ok && id != "" {
|
|
assistantID = id
|
|
}
|
|
}
|
|
|
|
// Get silent flag from context
|
|
var silent bool = false
|
|
var historyVisible bool = true
|
|
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
|
|
}
|
|
}
|
|
|
|
// Get history visible from context
|
|
if historyVisibleVal, ok := context["history_visible"]; ok {
|
|
switch v := historyVisibleVal.(type) {
|
|
case bool:
|
|
historyVisible = v
|
|
case string:
|
|
historyVisible = v == "true" || v == "1" || v == "yes"
|
|
case int:
|
|
historyVisible = v != 0
|
|
case float64:
|
|
historyVisible = v != 0
|
|
}
|
|
}
|
|
}
|
|
|
|
// First ensure chat record exists
|
|
exists, err := conv.newQueryChat().
|
|
Where("chat_id", cid).
|
|
Where("sid", userID).
|
|
Exists()
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !exists {
|
|
// Create new chat record
|
|
err = conv.newQueryChat().
|
|
Insert(map[string]interface{}{
|
|
"chat_id": cid,
|
|
"sid": userID,
|
|
"assistant_id": assistantID,
|
|
"silent": silent || historyVisible == false,
|
|
"created_at": time.Now(),
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// 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 || historyVisible == false,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Save message history
|
|
var expiredAt interface{} = nil
|
|
values := []map[string]interface{}{}
|
|
if conv.setting.TTL > 0 {
|
|
expiredAt = time.Now().Add(time.Duration(conv.setting.TTL) * time.Second)
|
|
}
|
|
|
|
now := time.Now()
|
|
for _, message := range messages {
|
|
// Type assertion safety checks
|
|
role, ok := message["role"].(string)
|
|
if !ok {
|
|
return fmt.Errorf("invalid role type in message: %v", message["role"])
|
|
}
|
|
|
|
content, ok := message["content"].(string)
|
|
if !ok {
|
|
return fmt.Errorf("invalid content type in message: %v", message["content"])
|
|
}
|
|
|
|
var contextRaw interface{} = nil
|
|
if context != nil {
|
|
contextRaw, err = jsoniter.MarshalToString(context)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Process mentions if present
|
|
var mentionsRaw interface{} = nil
|
|
if mentions, ok := message["mentions"].([]interface{}); ok && len(mentions) > 0 {
|
|
mentionsRaw, err = jsoniter.MarshalToString(mentions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
value := map[string]interface{}{
|
|
"role": role,
|
|
"name": "",
|
|
"content": content,
|
|
"sid": userID,
|
|
"cid": cid,
|
|
"uid": userID,
|
|
"context": contextRaw,
|
|
"mentions": mentionsRaw,
|
|
"assistant_id": nil,
|
|
"assistant_name": nil,
|
|
"assistant_avatar": nil,
|
|
"silent": silent,
|
|
"created_at": now,
|
|
"updated_at": nil,
|
|
"expired_at": expiredAt,
|
|
}
|
|
|
|
if name, ok := message["name"].(string); ok {
|
|
value["name"] = name
|
|
}
|
|
|
|
// Add assistant fields if present
|
|
if assistantID, ok := message["assistant_id"].(string); ok {
|
|
value["assistant_id"] = assistantID
|
|
}
|
|
if assistantName, ok := message["assistant_name"].(string); ok {
|
|
value["assistant_name"] = assistantName
|
|
}
|
|
if assistantAvatar, ok := message["assistant_avatar"].(string); ok {
|
|
value["assistant_avatar"] = assistantAvatar
|
|
}
|
|
|
|
values = append(values, value)
|
|
}
|
|
|
|
err = conv.newQuery().Insert(values)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update Chat updated_at
|
|
_, err = conv.newQueryChat().
|
|
Where("chat_id", cid).
|
|
Where("sid", userID).
|
|
Update(map[string]interface{}{"updated_at": now})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetChat get the chat info and its history
|
|
func (conv *Xun) GetChat(sid string, cid string, locale ...string) (*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
|
|
}
|
|
|
|
name := assistant.Get("name")
|
|
if len(locale) > 0 {
|
|
lang := strings.ToLower(locale[0])
|
|
name = i18n.Translate(assistantID.(string), lang, name).(string)
|
|
}
|
|
|
|
if assistant != nil {
|
|
chat["assistant_name"] = name
|
|
chat["assistant_avatar"] = assistant.Get("avatar")
|
|
}
|
|
}
|
|
|
|
// Get chat history with default filter (silent=false)
|
|
history, err := conv.GetHistory(sid, cid, locale...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ChatInfo{
|
|
Chat: chat,
|
|
History: history,
|
|
}, nil
|
|
}
|
|
|
|
// GetChatWithFilter get the chat info and its history with filter options
|
|
func (conv *Xun) GetChatWithFilter(sid string, cid string, filter ChatFilter, locale ...string) (*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, locale...)
|
|
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)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Delete history records first
|
|
_, err = conv.newQuery().
|
|
Where("sid", userID).
|
|
Where("cid", cid).
|
|
Delete()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Then delete the chat
|
|
_, err = conv.newQueryChat().
|
|
Where("sid", userID).
|
|
Where("chat_id", cid).
|
|
Limit(1).
|
|
Delete()
|
|
return err
|
|
}
|
|
|
|
// DeleteAllChats deletes all chats and their histories for a user
|
|
func (conv *Xun) DeleteAllChats(sid string) error {
|
|
userID, err := conv.getUserID(sid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Delete history records first
|
|
_, err = conv.newQuery().
|
|
Where("sid", userID).
|
|
Delete()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Then delete all chats
|
|
_, err = conv.newQueryChat().
|
|
Where("sid", userID).
|
|
Delete()
|
|
return err
|
|
}
|
|
|
|
// parseJSONFields parses JSON string fields into their corresponding Go types
|
|
func (conv *Xun) parseJSONFields(data map[string]interface{}, fields []string) {
|
|
for _, field := range fields {
|
|
if val := data[field]; val != nil {
|
|
if strVal, ok := val.(string); ok && strVal != "" {
|
|
var parsed interface{}
|
|
if err := jsoniter.UnmarshalFromString(strVal, &parsed); err == nil {
|
|
data[field] = parsed
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// SaveAssistant saves assistant information
|
|
func (conv *Xun) SaveAssistant(assistant map[string]interface{}) (interface{}, error) {
|
|
// Validate required fields
|
|
requiredFields := []string{"name", "type", "connector"}
|
|
for _, field := range requiredFields {
|
|
if _, ok := assistant[field]; !ok {
|
|
return nil, fmt.Errorf("field %s is required", field)
|
|
}
|
|
if assistant[field] == nil || assistant[field] == "" {
|
|
return nil, fmt.Errorf("field %s cannot be empty", field)
|
|
}
|
|
}
|
|
|
|
// Create a copy of the assistant map to avoid modifying the original
|
|
assistantCopy := make(map[string]interface{})
|
|
for k, v := range assistant {
|
|
assistantCopy[k] = v
|
|
}
|
|
|
|
// Process JSON fields
|
|
jsonFields := []string{"tags", "options", "prompts", "workflow", "knowledge", "tools", "permissions", "placeholder", "locales"}
|
|
for _, field := range jsonFields {
|
|
if val, ok := assistantCopy[field]; ok && val != nil {
|
|
// If it's a string, try to parse it first
|
|
if strVal, ok := val.(string); ok && strVal != "" {
|
|
var parsed interface{}
|
|
if err := jsoniter.UnmarshalFromString(strVal, &parsed); err == nil {
|
|
assistantCopy[field] = parsed
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate assistant_id if not provided
|
|
if _, ok := assistantCopy["assistant_id"]; !ok {
|
|
var err error
|
|
assistantCopy["assistant_id"], err = conv.GenerateAssistantID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Check if assistant exists
|
|
exists, err := conv.query.New().
|
|
Table(conv.getAssistantTable()).
|
|
Where("assistant_id", assistantCopy["assistant_id"]).
|
|
Exists()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert JSON fields to strings for storage
|
|
for _, field := range jsonFields {
|
|
if val, ok := assistantCopy[field]; ok && val != nil {
|
|
jsonStr, err := jsoniter.MarshalToString(val)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal %s to JSON: %v", field, err)
|
|
}
|
|
assistantCopy[field] = jsonStr
|
|
}
|
|
}
|
|
|
|
// Update or insert
|
|
if exists {
|
|
_, err := conv.query.New().
|
|
Table(conv.getAssistantTable()).
|
|
Where("assistant_id", assistantCopy["assistant_id"]).
|
|
Update(assistantCopy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return assistantCopy["assistant_id"], nil
|
|
}
|
|
|
|
err = conv.query.New().
|
|
Table(conv.getAssistantTable()).
|
|
Insert(assistantCopy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return assistantCopy["assistant_id"], nil
|
|
}
|
|
|
|
// DeleteAssistant deletes an assistant by assistant_id
|
|
func (conv *Xun) DeleteAssistant(assistantID string) error {
|
|
// Check if assistant exists
|
|
exists, err := conv.query.New().
|
|
Table(conv.getAssistantTable()).
|
|
Where("assistant_id", assistantID).
|
|
Exists()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !exists {
|
|
return fmt.Errorf("assistant %s not found", assistantID)
|
|
}
|
|
|
|
_, err = conv.query.New().
|
|
Table(conv.getAssistantTable()).
|
|
Where("assistant_id", assistantID).
|
|
Delete()
|
|
return err
|
|
}
|
|
|
|
// GetAssistants retrieves assistants with pagination and filtering
|
|
func (conv *Xun) GetAssistants(filter AssistantFilter, locale ...string) (*AssistantResponse, error) {
|
|
qb := conv.query.New().
|
|
Table(conv.getAssistantTable())
|
|
|
|
// Apply tag filter if provided
|
|
if filter.Tags != nil && len(filter.Tags) > 0 {
|
|
qb.Where(func(qb query.Query) {
|
|
for i, tag := range filter.Tags {
|
|
// For each tag, we need to match it as part of a JSON array
|
|
// This will match both single tag arrays ["tag1"] and multi-tag arrays ["tag1","tag2"]
|
|
pattern := fmt.Sprintf("%%\"%s\"%%", tag)
|
|
if i == 0 {
|
|
qb.Where("tags", "like", pattern)
|
|
} else {
|
|
qb.OrWhere("tags", "like", pattern)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// 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 type filter if provided
|
|
if filter.Type != "" {
|
|
qb.Where("type", filter.Type)
|
|
}
|
|
|
|
// Apply connector filter if provided
|
|
if filter.Connector != "" {
|
|
qb.Where("connector", filter.Connector)
|
|
}
|
|
|
|
// Apply assistant_id filter if provided
|
|
if filter.AssistantID != "" {
|
|
qb.Where("assistant_id", filter.AssistantID)
|
|
}
|
|
|
|
// Apply assistantIDs filter if provided
|
|
if filter.AssistantIDs != nil && len(filter.AssistantIDs) > 0 {
|
|
qb.WhereIn("assistant_id", filter.AssistantIDs)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// Apply built_in filter if provided
|
|
if filter.BuiltIn != nil {
|
|
qb.Where("built_in", *filter.BuiltIn)
|
|
}
|
|
|
|
// Set defaults for pagination
|
|
if filter.PageSize <= 0 {
|
|
filter.PageSize = 20
|
|
}
|
|
if filter.Page <= 0 {
|
|
filter.Page = 1
|
|
}
|
|
|
|
// Get total count
|
|
total, err := qb.Clone().Count()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Calculate pagination
|
|
offset := (filter.Page - 1) * filter.PageSize
|
|
totalPages := int(math.Ceil(float64(total) / float64(filter.PageSize)))
|
|
nextPage := filter.Page + 1
|
|
if nextPage > totalPages {
|
|
nextPage = 0
|
|
}
|
|
prevPage := filter.Page - 1
|
|
if prevPage < 1 {
|
|
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("sort", "asc").
|
|
OrderBy("updated_at", "desc").
|
|
Offset(offset).
|
|
Limit(filter.PageSize).
|
|
Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert rows to map slice and parse JSON fields
|
|
data := make([]map[string]interface{}, len(rows))
|
|
jsonFields := []string{"tags", "options", "prompts", "workflow", "knowledge", "tools", "permissions", "placeholder"}
|
|
for i, row := range rows {
|
|
data[i] = row
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Translate Data
|
|
if len(locale) > 0 {
|
|
lang := strings.ToLower(locale[0])
|
|
for i, row := range data {
|
|
assistantID := row["assistant_id"].(string)
|
|
data[i] = i18n.Translate(assistantID, lang, row).(map[string]interface{})
|
|
}
|
|
}
|
|
|
|
return &AssistantResponse{
|
|
Data: data,
|
|
Page: filter.Page,
|
|
PageSize: filter.PageSize,
|
|
PageCnt: totalPages,
|
|
Next: nextPage,
|
|
Prev: prevPage,
|
|
Total: total,
|
|
}, nil
|
|
}
|
|
|
|
// GetAssistant retrieves a single assistant by ID
|
|
func (conv *Xun) GetAssistant(assistantID string, locale ...string) (map[string]interface{}, error) {
|
|
row, err := conv.query.New().
|
|
Table(conv.getAssistantTable()).
|
|
Where("assistant_id", assistantID).
|
|
First()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if row == nil {
|
|
return nil, fmt.Errorf("assistant %s not found", assistantID)
|
|
}
|
|
|
|
data := row.ToMap()
|
|
if data == nil || len(data) == 0 {
|
|
return nil, fmt.Errorf("the assistant %s is empty", assistantID)
|
|
}
|
|
|
|
// Parse JSON fields
|
|
jsonFields := []string{"tags", "options", "prompts", "workflow", "knowledge", "tools", "permissions", "placeholder"}
|
|
conv.parseJSONFields(data, jsonFields)
|
|
if len(locale) > 0 {
|
|
lang := strings.ToLower(locale[0])
|
|
return i18n.Translate(assistantID, lang, data).(map[string]interface{}), nil
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// DeleteAssistants deletes assistants based on filter conditions
|
|
func (conv *Xun) DeleteAssistants(filter AssistantFilter) (int64, error) {
|
|
qb := conv.query.New().
|
|
Table(conv.getAssistantTable())
|
|
|
|
// Apply tag filter if provided
|
|
if filter.Tags != nil && len(filter.Tags) > 0 {
|
|
qb.Where(func(qb query.Query) {
|
|
for i, tag := range filter.Tags {
|
|
pattern := fmt.Sprintf("%%\"%s\"%%", tag)
|
|
if i == 0 {
|
|
qb.Where("tags", "like", pattern)
|
|
} else {
|
|
qb.OrWhere("tags", "like", pattern)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// 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 assistant_id filter if provided
|
|
if filter.AssistantID != "" {
|
|
qb.Where("assistant_id", filter.AssistantID)
|
|
}
|
|
|
|
// Apply assistantIDs filter if provided
|
|
if filter.AssistantIDs != nil && len(filter.AssistantIDs) > 0 {
|
|
qb.WhereIn("assistant_id", filter.AssistantIDs)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// Apply built_in filter if provided
|
|
if filter.BuiltIn != nil {
|
|
qb.Where("built_in", *filter.BuiltIn)
|
|
}
|
|
|
|
// Execute delete and return number of deleted records
|
|
return qb.Delete()
|
|
}
|
|
|
|
// GetAssistantTags retrieves all unique tags from assistants
|
|
func (conv *Xun) GetAssistantTags(locale ...string) ([]Tag, error) {
|
|
q := conv.newQuery().Table(conv.getAssistantTable())
|
|
rows, err := q.Select("tags").Where("type", "assistant").GroupBy("tags").Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tagSet := map[string]bool{}
|
|
for _, row := range rows {
|
|
if tags, ok := row["tags"].(string); ok && tags != "" {
|
|
var tagList []string
|
|
if err := jsoniter.UnmarshalFromString(tags, &tagList); err == nil {
|
|
for _, tag := range tagList {
|
|
tagSet[tag] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
lang := "en"
|
|
if len(locale) > 0 {
|
|
lang = locale[0]
|
|
}
|
|
|
|
// Convert map keys to slice
|
|
tags := make([]Tag, 0, len(tagSet))
|
|
for tag := range tagSet {
|
|
tags = append(tags, Tag{
|
|
Value: tag,
|
|
Label: i18n.TranslateGlobal(lang, tag).(string),
|
|
})
|
|
}
|
|
return tags, nil
|
|
}
|
|
|
|
// GetHistoryWithFilter get the history with filter options
|
|
func (conv *Xun) GetHistoryWithFilter(sid string, cid string, filter ChatFilter, locale ...string) ([]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
|
|
}
|
|
|
|
// GenerateAssistantID generates a random-looking 6-digit ID
|
|
func (conv *Xun) GenerateAssistantID() (string, error) {
|
|
maxAttempts := 10 // Maximum number of attempts to generate a unique ID
|
|
for i := 0; i < maxAttempts; i++ {
|
|
// Generate a random number using timestamp and some bit operations
|
|
timestamp := time.Now().UnixNano()
|
|
random := (timestamp ^ (timestamp >> 12)) % 1000000
|
|
hash := fmt.Sprintf("%06d", random)
|
|
|
|
// Check if this ID already exists
|
|
exists, err := conv.query.New().
|
|
Table(conv.getAssistantTable()).
|
|
Where("assistant_id", hash).
|
|
Exists()
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if !exists {
|
|
return hash, nil
|
|
}
|
|
|
|
// If ID exists, wait a bit and try again
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
|
|
return "", fmt.Errorf("failed to generate unique ID after %d attempts", maxAttempts)
|
|
}
|
|
|
|
// SaveAttachment saves attachment information
|
|
func (conv *Xun) SaveAttachment(attachment map[string]interface{}) (interface{}, error) {
|
|
// Validate required fields
|
|
requiredFields := []string{"file_id", "uid", "manager", "content_type", "name"}
|
|
for _, field := range requiredFields {
|
|
if _, ok := attachment[field]; !ok {
|
|
return nil, fmt.Errorf("field %s is required", field)
|
|
}
|
|
if attachment[field] == nil || attachment[field] == "" {
|
|
return nil, fmt.Errorf("field %s cannot be empty", field)
|
|
}
|
|
}
|
|
|
|
// Create a copy of the attachment map to avoid modifying the original
|
|
attachmentCopy := make(map[string]interface{})
|
|
for k, v := range attachment {
|
|
attachmentCopy[k] = v
|
|
}
|
|
|
|
// Process JSON fields
|
|
jsonFields := []string{"scope"}
|
|
for _, field := range jsonFields {
|
|
if val, ok := attachmentCopy[field]; ok && val != nil {
|
|
// If it's a string, try to parse it first
|
|
if strVal, ok := val.(string); ok && strVal != "" {
|
|
var parsed interface{}
|
|
if err := jsoniter.UnmarshalFromString(strVal, &parsed); err == nil {
|
|
attachmentCopy[field] = parsed
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if attachment exists
|
|
exists, err := conv.query.New().
|
|
Table(conv.getAttachmentTable()).
|
|
Where("file_id", attachmentCopy["file_id"]).
|
|
Exists()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert JSON fields to strings for storage
|
|
for _, field := range jsonFields {
|
|
if val, ok := attachmentCopy[field]; ok && val != nil {
|
|
jsonStr, err := jsoniter.MarshalToString(val)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal %s to JSON: %v", field, err)
|
|
}
|
|
attachmentCopy[field] = jsonStr
|
|
}
|
|
}
|
|
|
|
// Update or insert
|
|
if exists {
|
|
attachmentCopy["updated_at"] = time.Now()
|
|
_, err := conv.query.New().
|
|
Table(conv.getAttachmentTable()).
|
|
Where("file_id", attachmentCopy["file_id"]).
|
|
Update(attachmentCopy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return attachmentCopy["file_id"], nil
|
|
}
|
|
|
|
attachmentCopy["created_at"] = time.Now()
|
|
err = conv.query.New().
|
|
Table(conv.getAttachmentTable()).
|
|
Insert(attachmentCopy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return attachmentCopy["file_id"], nil
|
|
}
|
|
|
|
// DeleteAttachment deletes an attachment by file_id
|
|
func (conv *Xun) DeleteAttachment(fileID string) error {
|
|
// Check if attachment exists
|
|
exists, err := conv.query.New().
|
|
Table(conv.getAttachmentTable()).
|
|
Where("file_id", fileID).
|
|
Exists()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !exists {
|
|
return fmt.Errorf("attachment %s not found", fileID)
|
|
}
|
|
|
|
_, err = conv.query.New().
|
|
Table(conv.getAttachmentTable()).
|
|
Where("file_id", fileID).
|
|
Delete()
|
|
return err
|
|
}
|
|
|
|
// GetAttachments retrieves attachments with pagination and filtering
|
|
func (conv *Xun) GetAttachments(filter AttachmentFilter, locale ...string) (*AttachmentResponse, error) {
|
|
qb := conv.query.New().
|
|
Table(conv.getAttachmentTable())
|
|
|
|
// Apply UID filter if provided
|
|
if filter.UID != "" {
|
|
qb.Where("uid", filter.UID)
|
|
}
|
|
|
|
// Apply guest filter if provided
|
|
if filter.Guest != nil {
|
|
qb.Where("guest", *filter.Guest)
|
|
}
|
|
|
|
// Apply manager filter if provided
|
|
if filter.Manager != "" {
|
|
qb.Where("manager", filter.Manager)
|
|
}
|
|
|
|
// Apply content_type filter if provided
|
|
if filter.ContentType != "" {
|
|
qb.Where("content_type", filter.ContentType)
|
|
}
|
|
|
|
// Apply name filter if provided
|
|
if filter.Name != "" {
|
|
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Name))
|
|
}
|
|
|
|
// Apply public filter if provided
|
|
if filter.Public != nil {
|
|
qb.Where("public", *filter.Public)
|
|
}
|
|
|
|
// Apply gzip filter if provided
|
|
if filter.Gzip != nil {
|
|
qb.Where("gzip", *filter.Gzip)
|
|
}
|
|
|
|
// Apply collection_id filter if provided
|
|
if filter.CollectionID != "" {
|
|
qb.Where("collection_id", filter.CollectionID)
|
|
}
|
|
|
|
// Apply status filter if provided
|
|
if filter.Status != "" {
|
|
qb.Where("status", filter.Status)
|
|
}
|
|
|
|
// Apply keyword filter if provided
|
|
if filter.Keywords != "" {
|
|
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Keywords))
|
|
}
|
|
|
|
// Set defaults for pagination
|
|
if filter.PageSize <= 0 {
|
|
filter.PageSize = 20
|
|
}
|
|
if filter.Page <= 0 {
|
|
filter.Page = 1
|
|
}
|
|
|
|
// Get total count
|
|
total, err := qb.Clone().Count()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Calculate pagination
|
|
offset := (filter.Page - 1) * filter.PageSize
|
|
totalPages := int(math.Ceil(float64(total) / float64(filter.PageSize)))
|
|
nextPage := filter.Page + 1
|
|
if nextPage > totalPages {
|
|
nextPage = 0
|
|
}
|
|
prevPage := filter.Page - 1
|
|
if prevPage < 1 {
|
|
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).
|
|
Limit(filter.PageSize).
|
|
Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert rows to map slice and parse JSON fields
|
|
data := make([]map[string]interface{}, len(rows))
|
|
jsonFields := []string{"scope"}
|
|
for i, row := range rows {
|
|
data[i] = row
|
|
// 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 &AttachmentResponse{
|
|
Data: data,
|
|
Page: filter.Page,
|
|
PageSize: filter.PageSize,
|
|
PageCnt: totalPages,
|
|
Next: nextPage,
|
|
Prev: prevPage,
|
|
Total: total,
|
|
}, nil
|
|
}
|
|
|
|
// GetAttachment retrieves a single attachment by file_id
|
|
func (conv *Xun) GetAttachment(fileID string, locale ...string) (map[string]interface{}, error) {
|
|
row, err := conv.query.New().
|
|
Table(conv.getAttachmentTable()).
|
|
Where("file_id", fileID).
|
|
First()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if row == nil {
|
|
return nil, fmt.Errorf("attachment %s not found", fileID)
|
|
}
|
|
|
|
data := row.ToMap()
|
|
if data == nil || len(data) == 0 {
|
|
return nil, fmt.Errorf("the attachment %s is empty", fileID)
|
|
}
|
|
|
|
// Parse JSON fields
|
|
jsonFields := []string{"scope"}
|
|
conv.parseJSONFields(data, jsonFields)
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// DeleteAttachments deletes attachments based on filter conditions
|
|
func (conv *Xun) DeleteAttachments(filter AttachmentFilter) (int64, error) {
|
|
qb := conv.query.New().
|
|
Table(conv.getAttachmentTable())
|
|
|
|
// Apply UID filter if provided
|
|
if filter.UID != "" {
|
|
qb.Where("uid", filter.UID)
|
|
}
|
|
|
|
// Apply guest filter if provided
|
|
if filter.Guest != nil {
|
|
qb.Where("guest", *filter.Guest)
|
|
}
|
|
|
|
// Apply manager filter if provided
|
|
if filter.Manager != "" {
|
|
qb.Where("manager", filter.Manager)
|
|
}
|
|
|
|
// Apply content_type filter if provided
|
|
if filter.ContentType != "" {
|
|
qb.Where("content_type", filter.ContentType)
|
|
}
|
|
|
|
// Apply name filter if provided
|
|
if filter.Name != "" {
|
|
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Name))
|
|
}
|
|
|
|
// Apply public filter if provided
|
|
if filter.Public != nil {
|
|
qb.Where("public", *filter.Public)
|
|
}
|
|
|
|
// Apply gzip filter if provided
|
|
if filter.Gzip != nil {
|
|
qb.Where("gzip", *filter.Gzip)
|
|
}
|
|
|
|
// Apply collection_id filter if provided
|
|
if filter.CollectionID != "" {
|
|
qb.Where("collection_id", filter.CollectionID)
|
|
}
|
|
|
|
// Apply status filter if provided
|
|
if filter.Status != "" {
|
|
qb.Where("status", filter.Status)
|
|
}
|
|
|
|
// Apply keyword filter if provided
|
|
if filter.Keywords != "" {
|
|
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Keywords))
|
|
}
|
|
|
|
// Execute delete and return number of deleted records
|
|
return qb.Delete()
|
|
}
|
|
|
|
// SaveKnowledge saves knowledge collection information
|
|
func (conv *Xun) SaveKnowledge(knowledge map[string]interface{}) (interface{}, error) {
|
|
// Validate required fields
|
|
requiredFields := []string{"collection_id", "name", "uid"}
|
|
for _, field := range requiredFields {
|
|
if _, ok := knowledge[field]; !ok {
|
|
return nil, fmt.Errorf("field %s is required", field)
|
|
}
|
|
if knowledge[field] == nil || knowledge[field] == "" {
|
|
return nil, fmt.Errorf("field %s cannot be empty", field)
|
|
}
|
|
}
|
|
|
|
// Create a copy of the knowledge map to avoid modifying the original
|
|
knowledgeCopy := make(map[string]interface{})
|
|
for k, v := range knowledge {
|
|
knowledgeCopy[k] = v
|
|
}
|
|
|
|
// Process JSON fields
|
|
jsonFields := []string{"scope", "option"}
|
|
for _, field := range jsonFields {
|
|
if val, ok := knowledgeCopy[field]; ok && val != nil {
|
|
// If it's a string, try to parse it first
|
|
if strVal, ok := val.(string); ok && strVal != "" {
|
|
var parsed interface{}
|
|
if err := jsoniter.UnmarshalFromString(strVal, &parsed); err == nil {
|
|
knowledgeCopy[field] = parsed
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if knowledge exists
|
|
exists, err := conv.query.New().
|
|
Table(conv.getKnowledgeTable()).
|
|
Where("collection_id", knowledgeCopy["collection_id"]).
|
|
Exists()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert JSON fields to strings for storage
|
|
for _, field := range jsonFields {
|
|
if val, ok := knowledgeCopy[field]; ok && val != nil {
|
|
jsonStr, err := jsoniter.MarshalToString(val)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal %s to JSON: %v", field, err)
|
|
}
|
|
knowledgeCopy[field] = jsonStr
|
|
}
|
|
}
|
|
|
|
// Update or insert
|
|
if exists {
|
|
knowledgeCopy["updated_at"] = time.Now()
|
|
_, err := conv.query.New().
|
|
Table(conv.getKnowledgeTable()).
|
|
Where("collection_id", knowledgeCopy["collection_id"]).
|
|
Update(knowledgeCopy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return knowledgeCopy["collection_id"], nil
|
|
}
|
|
|
|
knowledgeCopy["created_at"] = time.Now()
|
|
err = conv.query.New().
|
|
Table(conv.getKnowledgeTable()).
|
|
Insert(knowledgeCopy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return knowledgeCopy["collection_id"], nil
|
|
}
|
|
|
|
// DeleteKnowledge deletes a knowledge collection by collection_id
|
|
func (conv *Xun) DeleteKnowledge(collectionID string) error {
|
|
// Check if knowledge exists
|
|
exists, err := conv.query.New().
|
|
Table(conv.getKnowledgeTable()).
|
|
Where("collection_id", collectionID).
|
|
Exists()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !exists {
|
|
return fmt.Errorf("knowledge collection %s not found", collectionID)
|
|
}
|
|
|
|
_, err = conv.query.New().
|
|
Table(conv.getKnowledgeTable()).
|
|
Where("collection_id", collectionID).
|
|
Delete()
|
|
return err
|
|
}
|
|
|
|
// GetKnowledges retrieves knowledge collections with pagination and filtering
|
|
func (conv *Xun) GetKnowledges(filter KnowledgeFilter, locale ...string) (*KnowledgeResponse, error) {
|
|
qb := conv.query.New().
|
|
Table(conv.getKnowledgeTable())
|
|
|
|
// Apply UID filter if provided
|
|
if filter.UID != "" {
|
|
qb.Where("uid", filter.UID)
|
|
}
|
|
|
|
// Apply name filter if provided
|
|
if filter.Name != "" {
|
|
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Name))
|
|
}
|
|
|
|
// 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 public filter if provided
|
|
if filter.Public != nil {
|
|
qb.Where("public", *filter.Public)
|
|
}
|
|
|
|
// Apply readonly filter if provided
|
|
if filter.Readonly != nil {
|
|
qb.Where("readonly", *filter.Readonly)
|
|
}
|
|
|
|
// Apply system filter if provided
|
|
if filter.System != nil {
|
|
qb.Where("system", *filter.System)
|
|
}
|
|
|
|
// Set defaults for pagination
|
|
if filter.PageSize <= 0 {
|
|
filter.PageSize = 20
|
|
}
|
|
if filter.Page <= 0 {
|
|
filter.Page = 1
|
|
}
|
|
|
|
// Get total count
|
|
total, err := qb.Clone().Count()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Calculate pagination
|
|
offset := (filter.Page - 1) * filter.PageSize
|
|
totalPages := int(math.Ceil(float64(total) / float64(filter.PageSize)))
|
|
nextPage := filter.Page + 1
|
|
if nextPage > totalPages {
|
|
nextPage = 0
|
|
}
|
|
prevPage := filter.Page - 1
|
|
if prevPage < 1 {
|
|
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("sort", "asc").
|
|
OrderBy("created_at", "desc").
|
|
Offset(offset).
|
|
Limit(filter.PageSize).
|
|
Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert rows to map slice and parse JSON fields
|
|
data := make([]map[string]interface{}, len(rows))
|
|
jsonFields := []string{"scope", "option"}
|
|
for i, row := range rows {
|
|
data[i] = row
|
|
// 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 &KnowledgeResponse{
|
|
Data: data,
|
|
Page: filter.Page,
|
|
PageSize: filter.PageSize,
|
|
PageCnt: totalPages,
|
|
Next: nextPage,
|
|
Prev: prevPage,
|
|
Total: total,
|
|
}, nil
|
|
}
|
|
|
|
// GetKnowledge retrieves a single knowledge collection by collection_id
|
|
func (conv *Xun) GetKnowledge(collectionID string, locale ...string) (map[string]interface{}, error) {
|
|
row, err := conv.query.New().
|
|
Table(conv.getKnowledgeTable()).
|
|
Where("collection_id", collectionID).
|
|
First()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if row == nil {
|
|
return nil, fmt.Errorf("knowledge collection %s not found", collectionID)
|
|
}
|
|
|
|
data := row.ToMap()
|
|
if data == nil || len(data) == 0 {
|
|
return nil, fmt.Errorf("the knowledge collection %s is empty", collectionID)
|
|
}
|
|
|
|
// Parse JSON fields
|
|
jsonFields := []string{"scope", "option"}
|
|
conv.parseJSONFields(data, jsonFields)
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// DeleteKnowledges deletes knowledge collections based on filter conditions
|
|
func (conv *Xun) DeleteKnowledges(filter KnowledgeFilter) (int64, error) {
|
|
qb := conv.query.New().
|
|
Table(conv.getKnowledgeTable())
|
|
|
|
// Apply UID filter if provided
|
|
if filter.UID != "" {
|
|
qb.Where("uid", filter.UID)
|
|
}
|
|
|
|
// Apply name filter if provided
|
|
if filter.Name != "" {
|
|
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Name))
|
|
}
|
|
|
|
// 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 public filter if provided
|
|
if filter.Public != nil {
|
|
qb.Where("public", *filter.Public)
|
|
}
|
|
|
|
// Apply readonly filter if provided
|
|
if filter.Readonly != nil {
|
|
qb.Where("readonly", *filter.Readonly)
|
|
}
|
|
|
|
// Apply system filter if provided
|
|
if filter.System != nil {
|
|
qb.Where("system", *filter.System)
|
|
}
|
|
|
|
// Execute delete and return number of deleted records
|
|
return qb.Delete()
|
|
}
|