diff --git a/neo/api.go b/neo/api.go index 349c6b6a..5e7c46ca 100644 --- a/neo/api.go +++ b/neo/api.go @@ -491,6 +491,11 @@ func (neo *DSL) handleChatLatest(c *gin.Context) { return } + locale := "en-us" + if loc := c.Query("locale"); loc != "" { + locale = strings.ToLower(strings.TrimSpace(loc)) + } + // Create a new chat if len(chats.Groups) == 0 || len(chats.Groups[0].Chats) == 0 { @@ -509,7 +514,7 @@ func (neo *DSL) handleChatLatest(c *gin.Context) { } c.JSON(200, map[string]interface{}{"data": map[string]interface{}{ - "placeholder": ast.GetPlaceholder(), + "placeholder": ast.GetPlaceholder(locale), "assistant_id": ast.ID, "assistant_name": ast.Name, "assistant_avatar": ast.Avatar, @@ -882,22 +887,18 @@ func (neo *DSL) handleAssistantList(c *gin.Context) { filter.AssistantID = assistantID } - response, err := neo.Store.GetAssistants(filter) + locale := "en-us" // Default locale + if loc := c.Query("locale"); loc != "" { + locale = strings.ToLower(strings.TrimSpace(loc)) + } + + response, err := neo.Store.GetAssistants(filter, locale) if err != nil { c.JSON(500, gin.H{"message": err.Error(), "code": 500}) c.Done() return } - // Translate the response - locale := "en-us" // Default locale - if loc := c.Query("locale"); loc != "" { - locale = strings.ToLower(strings.TrimSpace(loc)) - } - for i, ast := range response.Data { - id := ast["assistant_id"].(string) - response.Data[i] = assistant.Translate(locale, id, ast).(map[string]interface{}) - } c.JSON(200, response) c.Done() } @@ -974,7 +975,13 @@ func (neo *DSL) handleAssistantDetail(c *gin.Context) { PageSize: 1, } - response, err := neo.Store.GetAssistants(filter) + locale := "en-us" // Default locale + // Translate the response + if loc := c.Query("locale"); loc != "" { + locale = strings.ToLower(strings.TrimSpace(loc)) + } + + response, err := neo.Store.GetAssistants(filter, locale) if err != nil { c.JSON(500, gin.H{"message": err.Error(), "code": 500}) c.Done() @@ -987,14 +994,6 @@ func (neo *DSL) handleAssistantDetail(c *gin.Context) { return } - locale := "en-us" // Default locale - // Translate the response - if loc := c.Query("locale"); loc != "" { - locale = strings.ToLower(strings.TrimSpace(loc)) - } - id := response.Data[0]["assistant_id"].(string) - response.Data[0] = assistant.Translate(locale, id, response.Data[0]).(map[string]interface{}) - c.JSON(200, map[string]interface{}{"data": response.Data[0]}) c.Done() } @@ -1098,28 +1097,18 @@ func (neo *DSL) handleAssistantTags(c *gin.Context) { return } - tags, err := neo.Store.GetAssistantTags() + locale := "en-us" // Default locale + if loc := c.Query("locale"); loc != "" { + locale = strings.ToLower(strings.TrimSpace(loc)) + } + + tags, err := neo.Store.GetAssistantTags(locale) if err != nil { c.JSON(500, gin.H{"message": err.Error(), "code": 500}) c.Done() return } - // Translate the tags - locale := "en-us" // Default locale - if loc := c.Query("locale"); loc != "" { - locale = strings.ToLower(strings.TrimSpace(loc)) - } - - // Translate the tags - items := []map[string]interface{}{} - for _, value := range tags { - items = append(items, map[string]interface{}{ - "label": assistant.Translate(locale, "", value).(string), - "key": value, - }) - } - - c.JSON(200, gin.H{"data": items}) + c.JSON(200, gin.H{"data": tags}) c.Done() } diff --git a/neo/assistant/api.go b/neo/assistant/api.go index fdde4956..5028095e 100644 --- a/neo/assistant/api.go +++ b/neo/assistant/api.go @@ -15,6 +15,7 @@ import ( "github.com/yaoapp/kun/exception" "github.com/yaoapp/kun/log" chatctx "github.com/yaoapp/yao/neo/context" + "github.com/yaoapp/yao/neo/i18n" "github.com/yaoapp/yao/neo/message" chatMessage "github.com/yaoapp/yao/neo/message" ) @@ -243,20 +244,6 @@ func (next *NextAction) Execute(c *gin.Context, ctx chatctx.Context, contents *c if err != nil { return nil, fmt.Errorf("with history error: %s", err.Error()) } - - // Send the progress message from application side instead - // Create a new Text - // Send loading message and mark as new - // if !ctx.Silent { - // msg := chatMessage.New().Map(map[string]interface{}{ - // "new": true, - // "role": "assistant", - // "type": "loading", - // "props": map[string]interface{}{"placeholder": "Calling " + assistant.Name}, - // }) - // msg.Assistant(assistant.ID, assistant.Name, assistant.Avatar) - // msg.Write(c.Writer) - // } newContents := chatMessage.NewContents() // Update the context id @@ -272,8 +259,19 @@ func (next *NextAction) Execute(c *gin.Context, ctx chatctx.Context, contents *c } // GetPlaceholder returns the placeholder of the assistant -func (ast *Assistant) GetPlaceholder() *Placeholder { - return ast.Placeholder +func (ast *Assistant) GetPlaceholder(locale string) *Placeholder { + + prompts := []string{} + if ast.Placeholder.Prompts != nil { + prompts = i18n.Translate(ast.ID, locale, ast.Placeholder.Prompts).([]string) + } + title := i18n.Translate(ast.ID, locale, ast.Placeholder.Title).(string) + description := i18n.Translate(ast.ID, locale, ast.Placeholder.Description).(string) + return &Placeholder{ + Title: title, + Description: description, + Prompts: prompts, + } } // Call implements the call functionality diff --git a/neo/assistant/assistant.go b/neo/assistant/assistant.go index 7af06d0b..1be35738 100644 --- a/neo/assistant/assistant.go +++ b/neo/assistant/assistant.go @@ -140,6 +140,7 @@ func (ast *Assistant) Map() map[string]interface{} { "mentionable": ast.Mentionable, "automated": ast.Automated, "placeholder": ast.Placeholder, + "locales": ast.Locales, "created_at": timeToMySQLFormat(ast.CreatedAt), "updated_at": timeToMySQLFormat(ast.UpdatedAt), } diff --git a/neo/assistant/i18n.go b/neo/assistant/i18n.go deleted file mode 100644 index 93a27307..00000000 --- a/neo/assistant/i18n.go +++ /dev/null @@ -1,151 +0,0 @@ -package assistant - -import ( - "path/filepath" - "strings" - - "github.com/yaoapp/gou/application" - "github.com/yaoapp/gou/fs" - "github.com/yaoapp/kun/maps" -) - -// Locales the locales -var Locales = map[string]map[string]I18n{} - -// I18n the i18n struct -type I18n struct { - Locale string `json:"locale,omitempty" yaml:"locale,omitempty"` - Messages map[string]any `json:"messages,omitempty" yaml:"messages,omitempty"` -} - -// Parse parse the input -func (i18n I18n) Parse(input any) any { - - switch in := input.(type) { - case string: - trimed := strings.TrimSpace(in) - hasExp := strings.HasPrefix(trimed, "{{") && strings.HasSuffix(trimed, "}}") - if hasExp { - exp := strings.TrimSpace(strings.TrimPrefix(strings.TrimSuffix(trimed, "}}"), "{{")) - if _, ok := i18n.Messages[exp]; ok { - return i18n.Messages[exp] - } - return exp - } - - if _, ok := i18n.Messages[trimed]; ok { - return i18n.Messages[trimed] - } - - return in - - case map[string]any: - for key, value := range in { - in[key] = i18n.Parse(value) - } - return in - - case []any: - for i, value := range in { - in[i] = i18n.Parse(value) - } - return in - } - - return input -} - -// GetI18n load the i18n from path -func GetI18n(path string) (map[string]I18n, error) { - app, err := fs.Get("app") - if err != nil { - return nil, err - } - - // Get the global i18n - globalI18ns, hasGlobal := Locales["__global__"] - // i18ns - localesdir := filepath.Join(path, "locales") - var i18ns map[string]I18n = map[string]I18n{} - if has, _ := app.Exists(localesdir); has { - locales, err := app.ReadDir(localesdir, true) - if err != nil { - return nil, err - } - - // load locales - for _, locale := range locales { - localeData, err := app.ReadFile(locale) - if err != nil { - return nil, err - } - var messages maps.Map = map[string]any{} - err = application.Parse(locale, localeData, &messages) - if err != nil { - return nil, err - } - - name := strings.ToLower(strings.TrimSuffix(filepath.Base(locale), ".yml")) - // Merge the global i18n - if hasGlobal { - global, has := globalI18ns[name] - if has { - for key, value := range global.Messages { - if _, ok := messages[key]; !ok { - messages[key] = value - } - } - } - } - - i18ns[name] = I18n{Locale: name, Messages: messages.Dot()} - namer := strings.Split(name, "-") - if len(namer) > 1 { - // Merge the global i18n - if hasGlobal { - global, has := globalI18ns[namer[0]] - if has { - for key, value := range global.Messages { - if _, ok := messages[key]; !ok { - messages[key] = value - } - } - } - } - i18ns[namer[0]] = I18n{Locale: name, Messages: messages.Dot()} - } - - } - } - return i18ns, nil -} - -// Translate translate the input -func Translate(locale string, id string, input any) any { - - locale = strings.ToLower(strings.TrimSpace(locale)) - i18ns, has := Locales[id] - if !has { - i18ns = map[string]I18n{} - } - - i18n, has := i18ns[locale] - if !has { - namer := strings.Split(locale, "-") - lang := namer[0] - i18n, has = i18ns[lang] - } - if !has { - var hasGlobal bool = false - i18ns, hasGlobal = Locales["__global__"] - if hasGlobal { - i18n, has = i18ns[locale] - } - } - - if has { - return i18n.Parse(input) - } - - return input -} diff --git a/neo/assistant/load.go b/neo/assistant/load.go index 4a50ae6f..43b77c77 100644 --- a/neo/assistant/load.go +++ b/neo/assistant/load.go @@ -14,6 +14,7 @@ import ( "github.com/yaoapp/gou/fs" "github.com/yaoapp/gou/rag/driver" v8 "github.com/yaoapp/gou/runtime/v8" + "github.com/yaoapp/yao/neo/i18n" "github.com/yaoapp/yao/neo/store" neovision "github.com/yaoapp/yao/neo/vision" "github.com/yaoapp/yao/openai" @@ -314,7 +315,7 @@ func LoadPath(path string) (*Assistant, error) { } // i18ns - locales, err := GetI18n(path) + locales, err := i18n.GetLocales(path) if err != nil { return nil, err } @@ -460,8 +461,9 @@ func loadMap(data map[string]interface{}) (*Assistant, error) { } // locales - if locales, ok := data["locales"].(map[string]I18n); ok { - Locales[id] = locales + if locales, ok := data["locales"].(i18n.Map); ok { + assistant.Locales = locales + i18n.Locales[id] = locales.FlattenWithGlobal() } // Search options diff --git a/neo/assistant/load_test.go b/neo/assistant/load_test.go index 1e55d31f..513f70af 100644 --- a/neo/assistant/load_test.go +++ b/neo/assistant/load_test.go @@ -322,7 +322,7 @@ type mockStore struct { data map[string]map[string]interface{} } -func (m *mockStore) GetAssistant(id string) (map[string]interface{}, error) { +func (m *mockStore) GetAssistant(id string, locale ...string) (map[string]interface{}, error) { if data, ok := m.data[id]; ok { return data, nil } @@ -367,7 +367,7 @@ func (m *mockStore) ListFiles(query map[string]interface{}) ([]map[string]interf } func (m *mockStore) DeleteAllChats(id string) error { return nil } func (m *mockStore) DeleteChat(id string, chatID string) error { return nil } -func (m *mockStore) GetAssistants(filter store.AssistantFilter) (*store.AssistantResponse, error) { +func (m *mockStore) GetAssistants(filter store.AssistantFilter, locale ...string) (*store.AssistantResponse, error) { return nil, nil } func (m *mockStore) GetChat(id string, chatID string) (*store.ChatInfo, error) { return nil, nil } @@ -391,4 +391,6 @@ func (m *mockStore) SaveHistory(sid string, messages []map[string]interface{}, c } func (m *mockStore) UpdateChatTitle(sid string, cid string, title string) error { return nil } func (m *mockStore) DeleteAssistants(filter store.AssistantFilter) (int64, error) { return 0, nil } -func (m *mockStore) GetAssistantTags() ([]string, error) { return []string{}, nil } +func (m *mockStore) GetAssistantTags(locale ...string) ([]store.Tag, error) { + return []store.Tag{}, nil +} diff --git a/neo/assistant/types.go b/neo/assistant/types.go index a4166694..39fe92fc 100644 --- a/neo/assistant/types.go +++ b/neo/assistant/types.go @@ -9,6 +9,7 @@ import ( "github.com/yaoapp/gou/rag/driver" v8 "github.com/yaoapp/gou/runtime/v8" chatctx "github.com/yaoapp/yao/neo/context" + "github.com/yaoapp/yao/neo/i18n" "github.com/yaoapp/yao/neo/message" api "github.com/yaoapp/yao/openai" ) @@ -25,7 +26,7 @@ type API interface { Download(ctx context.Context, fileID string) (*FileResponse, error) ReadBase64(ctx context.Context, fileID string) (string, error) - GetPlaceholder() *Placeholder + GetPlaceholder(locale string) *Placeholder Execute(c *gin.Context, ctx chatctx.Context, input interface{}, options map[string]interface{}, callback ...interface{}) (interface{}, error) Call(c *gin.Context, payload APIPayload) (interface{}, error) } @@ -139,6 +140,7 @@ type Assistant struct { Tools *ToolCalls `json:"tools,omitempty"` // Assistant Tools Flows []map[string]interface{} `json:"flows,omitempty"` // Assistant Flows Placeholder *Placeholder `json:"placeholder,omitempty"` // Assistant Placeholder + Locales i18n.Map `json:"locales,omitempty"` // Assistant Locales Search *SearchOption `json:"search,omitempty" yaml:"search,omitempty"` // Whether this assistant supports search Knowledge *KnowledgeOption `json:"knowledge,omitempty" yaml:"knowledge,omitempty"` // Whether this assistant supports knowledge CreatedAt int64 `json:"created_at"` // Creation timestamp diff --git a/neo/i18n/i18n.go b/neo/i18n/i18n.go new file mode 100644 index 00000000..4f962199 --- /dev/null +++ b/neo/i18n/i18n.go @@ -0,0 +1,222 @@ +package i18n + +import ( + "path/filepath" + "strings" + + "github.com/yaoapp/gou/application" + "github.com/yaoapp/gou/fs" + "github.com/yaoapp/kun/maps" +) + +// Locales the locales +var Locales = map[string]map[string]I18n{} + +// I18n the i18n struct +type I18n struct { + Locale string `json:"locale,omitempty" yaml:"locale,omitempty"` + Messages map[string]any `json:"messages,omitempty" yaml:"messages,omitempty"` +} + +// Map the i18n map +type Map map[string]I18n + +// Parse parse the input +func (i18n I18n) Parse(input any) any { + + switch in := input.(type) { + case string: + trimed := strings.TrimSpace(in) + hasExp := strings.HasPrefix(trimed, "{{") && strings.HasSuffix(trimed, "}}") + if hasExp { + exp := strings.TrimSpace(strings.TrimPrefix(strings.TrimSuffix(trimed, "}}"), "{{")) + if _, ok := i18n.Messages[exp]; ok { + return i18n.Messages[exp] + } + return exp + } + + if _, ok := i18n.Messages[trimed]; ok { + return i18n.Messages[trimed] + } + + return in + + case map[string]any: + new := map[string]any{} + for key, value := range in { + new[key] = i18n.Parse(value) + } + return new + + case []any: + new := []any{} + for _, value := range in { + new = append(new, i18n.Parse(value)) + } + return new + + case []string: + new := []string{} + for _, value := range in { + new = append(new, i18n.Parse(value).(string)) + } + return new + } + + return input +} + +// GetLocales get the locales from path +func GetLocales(path string) (Map, error) { + app, err := fs.Get("app") + if err != nil { + return nil, err + } + + i18ns := Map{} + localesdir := filepath.Join(path, "locales") + if has, _ := app.Exists(localesdir); has { + locales, err := app.ReadDir(localesdir, true) + if err != nil { + return nil, err + } + + // load locales + for _, locale := range locales { + localeData, err := app.ReadFile(locale) + if err != nil { + return nil, err + } + var messages maps.Map = map[string]any{} + err = application.Parse(locale, localeData, &messages) + if err != nil { + return nil, err + } + name := strings.ToLower(strings.TrimSuffix(filepath.Base(locale), ".yml")) + i18ns[name] = I18n{Locale: name, Messages: messages} + } + } + return i18ns, nil +} + +// Flatten flatten the i18n map +func (i18ns Map) Flatten() Map { + new := Map{} + for lang, i18n := range i18ns { + new[lang] = I18n{Locale: lang, Messages: maps.MapOf(i18n.Messages).Dot()} + + // Add short lang + parts := strings.Split(lang, "-") + + // en + if parts[0] != lang { + new[parts[0]] = new[lang] + } + + // us + if len(parts) > 1 { + new[parts[1]] = new[lang] + } + } + return new +} + +// FlattenWithGlobal flatten the i18n map with global i18n +func (i18ns Map) FlattenWithGlobal() Map { + + // New i18n map + new := Map{} + + // Global i18n + globalI18ns, hasGlobal := Locales["__global__"] + + // Extend the i18n map with global i18n + for lang, i18n := range i18ns { + new[lang] = I18n{Locale: lang, Messages: maps.MapOf(i18n.Messages).Dot()} + if hasGlobal { + if global, has := globalI18ns[lang]; has { + for key, value := range global.Messages { + if _, ok := new[lang].Messages[key]; !ok { + new[lang].Messages[key] = value + } + } + } + } + + // Add short lang + parts := strings.Split(lang, "-") + + // en + if parts[0] != lang { + new[parts[0]] = new[lang] + } + + // us + if len(parts) > 1 { + new[parts[1]] = new[lang] + } + } + + return new +} + +// Translate translate the input +func Translate(assistantID string, locale string, input any) any { + + locale = strings.ToLower(strings.TrimSpace(locale)) + i18ns, has := Locales[assistantID] + if !has { + i18ns = map[string]I18n{} + } + + i18n, has := i18ns[locale] + if !has { + parts := strings.Split(locale, "-") + if len(parts) > 1 { + i18n, has = i18ns[parts[1]] + } + if !has { + i18n, has = i18ns[parts[0]] + } + } + + if !has { + var hasGlobal bool = false + i18ns, hasGlobal = Locales["__global__"] + if hasGlobal { + i18n, has = i18ns[locale] + } + } + + if has { + return i18n.Parse(input) + } + + return input +} + +// TranslateGlobal translate the input with global i18n +func TranslateGlobal(locale string, input any) any { + locale = strings.ToLower(strings.TrimSpace(locale)) + i18ns, has := Locales["__global__"] + if !has { + i18ns = map[string]I18n{} + } + + i18n, has := i18ns[locale] + if !has { + parts := strings.Split(locale, "-") + if len(parts) > 1 { + i18n, has = i18ns[parts[1]] + } + if !has { + i18n, has = i18ns[parts[0]] + } + } + + if has { + return i18n.Parse(input) + } + return input +} diff --git a/neo/load.go b/neo/load.go index b8c239d2..642fce01 100644 --- a/neo/load.go +++ b/neo/load.go @@ -8,6 +8,7 @@ import ( "github.com/yaoapp/gou/connector" "github.com/yaoapp/yao/config" "github.com/yaoapp/yao/neo/assistant" + "github.com/yaoapp/yao/neo/i18n" "github.com/yaoapp/yao/neo/store" ) @@ -86,11 +87,11 @@ func Load(cfg config.Config) error { // initGlobalI18n initialize the global i18n func initGlobalI18n() error { - locales, err := assistant.GetI18n("neo") + locales, err := i18n.GetLocales("neo") if err != nil { return err } - assistant.Locales["__global__"] = locales + i18n.Locales["__global__"] = locales.Flatten() return nil } diff --git a/neo/process.go b/neo/process.go index 79a377cf..eb011593 100644 --- a/neo/process.go +++ b/neo/process.go @@ -395,7 +395,12 @@ func processAssistantSearch(process *process.Process) interface{} { exception.New("Neo store is not initialized", 500).Throw() } - res, err := neo.Store.GetAssistants(filter) + locale := "en" + if len(process.Args) > 1 { + locale = process.ArgsString(1) + } + + res, err := neo.Store.GetAssistants(filter, locale) if err != nil { exception.New("get assistants error: %s", 500, err).Throw() } @@ -419,7 +424,11 @@ func processAssistantFind(process *process.Process) interface{} { PageSize: 1, } - res, err := neo.Store.GetAssistants(filter) + locale := "en" + if len(process.Args) > 1 { + locale = process.ArgsString(1) + } + res, err := neo.Store.GetAssistants(filter, locale) if err != nil { exception.New("Failed to find assistant: %s", 500, err.Error()).Throw() } diff --git a/neo/store/mongo.go b/neo/store/mongo.go index b6fc9146..2202d003 100644 --- a/neo/store/mongo.go +++ b/neo/store/mongo.go @@ -64,21 +64,21 @@ func (m *Mongo) DeleteAssistant(assistantID string) error { } // GetAssistants retrieves a list of assistants -func (m *Mongo) GetAssistants(filter AssistantFilter) (*AssistantResponse, error) { +func (m *Mongo) GetAssistants(filter AssistantFilter, locale ...string) (*AssistantResponse, error) { return &AssistantResponse{}, nil } // GetAssistant retrieves a single assistant by ID -func (m *Mongo) GetAssistant(assistantID string) (map[string]interface{}, error) { - return map[string]interface{}{}, nil +func (m *Mongo) GetAssistant(assistantID string, locale ...string) (map[string]interface{}, error) { + return nil, nil } // DeleteAssistants deletes assistants based on filter conditions (not implemented) -func (mongo *Mongo) DeleteAssistants(filter AssistantFilter) (int64, error) { +func (m *Mongo) DeleteAssistants(filter AssistantFilter) (int64, error) { return 0, nil } // GetAssistantTags retrieves all unique tags from assistants -func (conv *Mongo) GetAssistantTags() ([]string, error) { - return []string{}, nil +func (m *Mongo) GetAssistantTags(locale ...string) ([]Tag, error) { + return []Tag{}, nil } diff --git a/neo/store/redis.go b/neo/store/redis.go index a5176b27..94f4b3eb 100644 --- a/neo/store/redis.go +++ b/neo/store/redis.go @@ -64,21 +64,21 @@ func (r *Redis) DeleteAssistant(assistantID string) error { } // GetAssistants retrieves a list of assistants -func (r *Redis) GetAssistants(filter AssistantFilter) (*AssistantResponse, error) { +func (r *Redis) GetAssistants(filter AssistantFilter, locale ...string) (*AssistantResponse, error) { return &AssistantResponse{}, nil } // GetAssistant retrieves a single assistant by ID -func (r *Redis) GetAssistant(assistantID string) (map[string]interface{}, error) { - return map[string]interface{}{}, nil +func (r *Redis) GetAssistant(assistantID string, locale ...string) (map[string]interface{}, error) { + return nil, nil } // DeleteAssistants deletes assistants based on filter conditions (not implemented) -func (redis *Redis) DeleteAssistants(filter AssistantFilter) (int64, error) { +func (r *Redis) DeleteAssistants(filter AssistantFilter) (int64, error) { return 0, nil } // GetAssistantTags retrieves all unique tags from assistants -func (conv *Redis) GetAssistantTags() ([]string, error) { - return []string{}, nil +func (r *Redis) GetAssistantTags(locale ...string) ([]Tag, error) { + return []Tag{}, nil } diff --git a/neo/store/types.go b/neo/store/types.go index 104cfcf7..abfc6477 100644 --- a/neo/store/types.go +++ b/neo/store/types.go @@ -73,6 +73,12 @@ type AssistantResponse struct { Total int64 `json:"total"` // Total number of items } +// Tag represents a tag +type Tag struct { + Value string `json:"value"` + Label string `json:"label"` +} + // Store defines the conversation storage interface // Provides basic operations required for conversation management type Store interface { @@ -147,19 +153,19 @@ type Store interface { // GetAssistants retrieves a list of assistants // filter: Filter conditions // Returns: Paginated assistant list and potential error - GetAssistants(filter AssistantFilter) (*AssistantResponse, error) + GetAssistants(filter AssistantFilter, locale ...string) (*AssistantResponse, error) + + // GetAssistantTags retrieves all unique tags from assistants + // Returns: List of tags and potential error + GetAssistantTags(locale ...string) ([]Tag, error) // GetAssistant retrieves a single assistant by ID // assistantID: Assistant ID // Returns: Assistant information and potential error - GetAssistant(assistantID string) (map[string]interface{}, error) + GetAssistant(assistantID string, locale ...string) (map[string]interface{}, error) // DeleteAssistants deletes assistants based on filter conditions // filter: Filter conditions // Returns: Number of deleted records and potential error DeleteAssistants(filter AssistantFilter) (int64, error) - - // GetAssistantTags retrieves all unique tags from assistants - // Returns: List of tags and potential error - GetAssistantTags() ([]string, error) } diff --git a/neo/store/xun.go b/neo/store/xun.go index 354736d2..c82c19bc 100644 --- a/neo/store/xun.go +++ b/neo/store/xun.go @@ -3,6 +3,7 @@ package store import ( "fmt" "math" + "strings" "time" "github.com/google/uuid" @@ -13,6 +14,7 @@ import ( "github.com/yaoapp/xun/capsule" "github.com/yaoapp/xun/dbal/query" "github.com/yaoapp/xun/dbal/schema" + "github.com/yaoapp/yao/neo/i18n" ) // Package conversation provides functionality for managing chat conversations and assistants. @@ -145,7 +147,7 @@ func (conv *Xun) initHistoryTable() error { 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("created_at").SetDefaultRaw("CURRENT_TIMESTAMP").Index() table.TimestampTz("updated_at").Null().Index() table.TimestampTz("expired_at").Null().Index() }) @@ -188,7 +190,7 @@ func (conv *Xun) initChatTable() error { 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("created_at").SetDefaultRaw("CURRENT_TIMESTAMP").Index() table.TimestampTz("updated_at").Null().Index() }) @@ -237,15 +239,16 @@ func (conv *Xun) initAssistantTable() error { table.JSON("placeholder").Null() // assistant placeholder table.JSON("options").Null() // assistant options table.JSON("prompts").Null() // assistant prompts - table.JSON("flows").Null() // assistant flows - table.JSON("files").Null() // assistant files + 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("NOW()").Index() + table.TimestampTz("created_at").SetDefaultRaw("CURRENT_TIMESTAMP").Index() table.TimestampTz("updated_at").Null().Index() }) @@ -261,7 +264,7 @@ func (conv *Xun) initAssistantTable() error { return err } - fields := []string{"id", "assistant_id", "type", "name", "avatar", "connector", "description", "path", "sort", "built_in", "placeholder", "options", "prompts", "flows", "files", "tools", "tags", "mentionable", "created_at", "updated_at"} + 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) @@ -963,7 +966,7 @@ func (conv *Xun) SaveAssistant(assistant map[string]interface{}) (interface{}, e } // Process JSON fields - jsonFields := []string{"tags", "options", "prompts", "flows", "files", "tools", "permissions", "placeholder"} + 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 @@ -1049,7 +1052,7 @@ func (conv *Xun) DeleteAssistant(assistantID string) error { } // GetAssistants retrieves assistants with pagination and filtering -func (conv *Xun) GetAssistants(filter AssistantFilter) (*AssistantResponse, error) { +func (conv *Xun) GetAssistants(filter AssistantFilter, locale ...string) (*AssistantResponse, error) { qb := conv.query.New(). Table(conv.getAssistantTable()) @@ -1159,7 +1162,7 @@ func (conv *Xun) GetAssistants(filter AssistantFilter) (*AssistantResponse, erro // Convert rows to map slice and parse JSON fields data := make([]map[string]interface{}, len(rows)) - jsonFields := []string{"tags", "options", "prompts", "flows", "files", "tools", "permissions", "placeholder"} + 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 @@ -1182,6 +1185,15 @@ func (conv *Xun) GetAssistants(filter AssistantFilter) (*AssistantResponse, erro } } + // 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, @@ -1194,7 +1206,7 @@ func (conv *Xun) GetAssistants(filter AssistantFilter) (*AssistantResponse, erro } // GetAssistant retrieves a single assistant by ID -func (conv *Xun) GetAssistant(assistantID string) (map[string]interface{}, error) { +func (conv *Xun) GetAssistant(assistantID string, locale ...string) (map[string]interface{}, error) { row, err := conv.query.New(). Table(conv.getAssistantTable()). Where("assistant_id", assistantID). @@ -1213,9 +1225,12 @@ func (conv *Xun) GetAssistant(assistantID string) (map[string]interface{}, error } // Parse JSON fields - jsonFields := []string{"tags", "options", "prompts", "flows", "files", "tools", "permissions", "placeholder"} + 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 } @@ -1281,7 +1296,7 @@ func (conv *Xun) DeleteAssistants(filter AssistantFilter) (int64, error) { } // GetAssistantTags retrieves all unique tags from assistants -func (conv *Xun) GetAssistantTags() ([]string, error) { +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 { @@ -1300,10 +1315,18 @@ func (conv *Xun) GetAssistantTags() ([]string, error) { } } + lang := "en" + if len(locale) > 0 { + lang = locale[0] + } + // Convert map keys to slice - tags := make([]string, 0, len(tagSet)) + tags := make([]Tag, 0, len(tagSet)) for tag := range tagSet { - tags = append(tags, tag) + tags = append(tags, Tag{ + Value: tag, + Label: i18n.TranslateGlobal(lang, tag).(string), + }) } return tags, nil } diff --git a/neo/store/xun_test.go b/neo/store/xun_test.go index e1d1f1bd..99c1b6a6 100644 --- a/neo/store/xun_test.go +++ b/neo/store/xun_test.go @@ -983,7 +983,8 @@ func TestGetAssistantTags(t *testing.T) { } for _, tag := range tags { - if !expectedTags[tag] { + value := tag.Value + if !expectedTags[value] { t.Errorf("Unexpected tag found: %s", tag) } } diff --git a/widgets/app/app.go b/widgets/app/app.go index a7741dc7..40a68103 100644 --- a/widgets/app/app.go +++ b/widgets/app/app.go @@ -466,6 +466,7 @@ func processXgen(process *process.Process) interface{} { } // Set User ENV + lang := config.Conf.Lang if process.NumOfArgs() > 0 { payload := process.ArgsMap(0, map[string]interface{}{ "now": time.Now().Unix(), @@ -476,8 +477,7 @@ func processXgen(process *process.Process) interface{} { if v, ok := payload["sid"].(string); ok && v != "" { sid = v } - - lang := strings.ToLower(fmt.Sprintf("%v", payload["lang"])) + lang = strings.ToLower(fmt.Sprintf("%v", payload["lang"])) session.Global().ID(sid).Set("__yao_lang", lang) } @@ -571,7 +571,7 @@ func processXgen(process *process.Process) interface{} { "assistant_name": ast.Name, "assistant_avatar": ast.Avatar, "assistant_deleteable": false, - "placeholder": ast.Placeholder, + "placeholder": ast.GetPlaceholder(lang), } } agent["connectors"] = connector.AIConnectors