From 6467a9c37e041eea00e21450cdf7f32aaac623ab Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 24 May 2025 20:36:00 +0800 Subject: [PATCH] Refactor API response handling and enhance locale support in assistant functions - Removed the generateResponse struct and its associated methods to simplify response handling. - Added locale support in handleAssistantList and handleAssistantDetail functions to allow for localized responses based on user input. - Updated the Context struct to include locale information for better customization. - Refactored the Load function in load.go to incorporate i18n support for assistant localization. --- neo/api.go | 104 ++++++++++++----------------------------- neo/assistant/api.go | 3 +- neo/assistant/i18n.go | 51 ++++++++++++++++++++ neo/assistant/load.go | 69 ++++++++++++++++++++++++++- neo/assistant/types.go | 74 +++++++++++++++++------------ neo/context/context.go | 51 ++++++++++---------- neo/neo.go | 8 ---- 7 files changed, 219 insertions(+), 141 deletions(-) create mode 100644 neo/assistant/i18n.go diff --git a/neo/api.go b/neo/api.go index aa6d6dc4..6ed5aefd 100644 --- a/neo/api.go +++ b/neo/api.go @@ -732,80 +732,6 @@ func (neo *DSL) handleChatsDeleteAll(c *gin.Context) { c.Done() } -// generateResponse is a helper struct to handle both SSE and HTTP responses -type generateResponse struct { - c *gin.Context - sid string - content string - result interface{} - err error -} - -// validate checks common validation rules -func (r *generateResponse) validate() bool { - if r.sid == "" { - if strings.Contains(r.c.GetHeader("Accept"), "text/event-stream") { - r.c.Header("Content-Type", "text/event-stream;charset=utf-8") - r.c.Header("Cache-Control", "no-cache") - r.c.Header("Connection", "keep-alive") - msg := message.New(). - Error("sid is required"). - Done() - msg.Write(r.c.Writer) - } else { - r.c.JSON(400, gin.H{"message": "sid is required", "code": 400}) - } - return false - } - - if r.content == "" { - if strings.Contains(r.c.GetHeader("Accept"), "text/event-stream") { - r.c.Header("Content-Type", "text/event-stream;charset=utf-8") - r.c.Header("Cache-Control", "no-cache") - r.c.Header("Connection", "keep-alive") - msg := message.New(). - Error("content is required"). - Done() - msg.Write(r.c.Writer) - } else { - r.c.JSON(400, gin.H{"message": "content is required", "code": 400}) - } - return false - } - - return true -} - -// send handles both SSE and HTTP responses -func (r *generateResponse) send(key string) { - if r.err != nil { - if strings.Contains(r.c.GetHeader("Accept"), "text/event-stream") { - r.c.Header("Content-Type", "text/event-stream;charset=utf-8") - r.c.Header("Cache-Control", "no-cache") - r.c.Header("Connection", "keep-alive") - msg := message.New(). - Error(r.err.Error()). - Done() - msg.Write(r.c.Writer) - } else { - r.c.JSON(500, gin.H{"message": r.err.Error(), "code": 500}) - } - return - } - - if strings.Contains(r.c.GetHeader("Accept"), "text/event-stream") { - r.c.Header("Content-Type", "text/event-stream;charset=utf-8") - r.c.Header("Cache-Control", "no-cache") - r.c.Header("Connection", "keep-alive") - msg := message.New(). - Map(gin.H{key: r.result}). - Done() - msg.Write(r.c.Writer) - } else { - r.c.JSON(200, gin.H{key: r.result}) - } -} - // handleGenerateTitle handles generating a chat title func (neo *DSL) handleGenerateTitle(c *gin.Context) { // Set headers for SSE @@ -963,6 +889,22 @@ func (neo *DSL) handleAssistantList(c *gin.Context) { return } + locale := "en-us" // Default locale + for i, ast := range response.Data { + id := ast["assistant_id"].(string) + if locales, ok := assistant.Locales[id]; ok { + if loc := c.Query("locale"); loc != "" { + loc = strings.ToLower(strings.TrimSpace(loc)) + if _, ok := locales[loc]; ok { + locale = loc + } + } + if i18n, ok := locales[locale]; ok { + response.Data[i] = i18n.Parse(ast).(map[string]interface{}) + } + } + } + c.JSON(200, response) c.Done() } @@ -1052,6 +994,20 @@ func (neo *DSL) handleAssistantDetail(c *gin.Context) { return } + locale := "en-us" // Default locale + id := response.Data[0]["assistant_id"].(string) + if locales, ok := assistant.Locales[id]; ok { + if loc := c.Query("locale"); loc != "" { + loc = strings.ToLower(strings.TrimSpace(loc)) + if _, ok := locales[loc]; ok { + locale = loc + } + } + if i18n, ok := locales[locale]; ok { + response.Data[0] = i18n.Parse(response.Data[0]).(map[string]interface{}) + } + } + c.JSON(200, map[string]interface{}{"data": response.Data[0]}) c.Done() } diff --git a/neo/assistant/api.go b/neo/assistant/api.go index dd112f79..c207742d 100644 --- a/neo/assistant/api.go +++ b/neo/assistant/api.go @@ -88,7 +88,8 @@ func (ast *Assistant) execute(c *gin.Context, ctx chatctx.Context, userInput int options := ast.withOptions(userOptions) // Add RAG、Vision and Search support - ctx.RAG = rag != nil + // ctx.RAG = rag != nil + ctx.Knowledge = false ctx.Vision = ast.vision ctx.Search = ast.search && search != nil diff --git a/neo/assistant/i18n.go b/neo/assistant/i18n.go new file mode 100644 index 00000000..bb1d455e --- /dev/null +++ b/neo/assistant/i18n.go @@ -0,0 +1,51 @@ +package assistant + +import ( + "strings" +) + +// 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 +} diff --git a/neo/assistant/load.go b/neo/assistant/load.go index 7258c3e2..9e9b9f31 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/kun/maps" "github.com/yaoapp/yao/neo/store" neovision "github.com/yaoapp/yao/neo/vision" "github.com/yaoapp/yao/openai" @@ -25,7 +26,7 @@ import ( var loaded = NewCache(200) // 200 is the default capacity var storage store.Store = nil var rag *RAG = nil -var search *Search = nil +var search interface{} = nil var connectorSettings map[string]ConnectorSetting = map[string]ConnectorSetting{} var vision *neovision.Vision = nil var defaultConnector string = "" // default connector @@ -313,7 +314,37 @@ func LoadPath(path string) (*Assistant, error) { updatedAt = max(updatedAt, ts) } - // load flow + // 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 + err = application.Parse(locale, localeData, &messages) + if err != nil { + return nil, err + } + if messages != nil { + name := strings.ToLower(strings.TrimSuffix(filepath.Base(locale), ".yml")) + i18ns[name] = I18n{Locale: name, Messages: messages.Dot()} + namer := strings.Split(name, "-") + if len(namer) > 1 { + i18ns[namer[0]] = I18n{Locale: name, Messages: messages.Dot()} + } + } + } + data["locales"] = i18ns + } return loadMap(data) } @@ -455,6 +486,40 @@ func loadMap(data map[string]interface{}) (*Assistant, error) { assistant.Description = v } + // locales + if locales, ok := data["locales"].(map[string]I18n); ok { + Locales[id] = locales + } + + // Search options + if v, ok := data["search"].(map[string]interface{}); ok { + assistant.Search = &SearchOption{} + raw, err := jsoniter.Marshal(v) + if err != nil { + return nil, err + } + + // Unmarshal the raw data + err = jsoniter.Unmarshal(raw, assistant.Search) + if err != nil { + return nil, err + } + } + + // Knowledge options + if v, ok := data["knowledge"].(map[string]interface{}); ok { + assistant.Knowledge = &KnowledgeOption{} + raw, err := jsoniter.Marshal(v) + if err != nil { + return nil, err + } + // Unmarshal the raw data + err = jsoniter.Unmarshal(raw, assistant.Knowledge) + if err != nil { + return nil, err + } + } + // prompts if prompts, has := data["prompts"]; has { diff --git a/neo/assistant/types.go b/neo/assistant/types.go index fa6dc8aa..a4166694 100644 --- a/neo/assistant/types.go +++ b/neo/assistant/types.go @@ -84,10 +84,19 @@ type RAG struct { Setting RAGSetting } -// Search the search interface -// @todo: add search engine -type Search struct { - Engine interface{} +// SearchOption the search option +type SearchOption struct { + WebSearch *bool `json:"web_search,omitempty" yaml:"web_search,omitempty"` // Whether to search the web + Knowledge *bool `json:"knowledge,omitempty" yaml:"knowledge,omitempty"` // Whether to search the knowledge +} + +// KnowledgeOption the knowledge option +type KnowledgeOption struct { + Collections []string `json:"collections,omitempty" yaml:"collections,omitempty"` // The Global Collections + ChunkingMethod string `json:"chunking_method,omitempty" yaml:"chunking_method,omitempty"` + ChunkSize int `json:"chunk_size,omitempty" yaml:"chunk_size,omitempty"` + ChunkOverlap int `json:"chunk_overlap,omitempty" yaml:"chunk_overlap,omitempty"` + SearchMethod string `json:"search_method,omitempty" yaml:"search_method,omitempty"` } // RAGSetting the RAG setting @@ -112,32 +121,37 @@ type QueryParam struct { // Assistant the assistant type Assistant struct { - ID string `json:"assistant_id"` // Assistant ID - Type string `json:"type,omitempty"` // Assistant Type, default is assistant - Name string `json:"name,omitempty"` // Assistant Name - Avatar string `json:"avatar,omitempty"` // Assistant Avatar - Connector string `json:"connector"` // AI Connector - Path string `json:"path,omitempty"` // Assistant Path - BuiltIn bool `json:"built_in,omitempty"` // Whether this is a built-in assistant - Sort int `json:"sort,omitempty"` // Assistant Sort - Description string `json:"description,omitempty"` // Assistant Description - Tags []string `json:"tags,omitempty"` // Assistant Tags - Readonly bool `json:"readonly,omitempty"` // Whether this assistant is readonly - Mentionable bool `json:"mentionable,omitempty"` // Whether this assistant is mentionable - Automated bool `json:"automated,omitempty"` // Whether this assistant is automated - Options map[string]interface{} `json:"options,omitempty"` // AI Options - Prompts []Prompt `json:"prompts,omitempty"` // AI Prompts - Tools *ToolCalls `json:"tools,omitempty"` // Assistant Tools - Flows []map[string]interface{} `json:"flows,omitempty"` // Assistant Flows - Placeholder *Placeholder `json:"placeholder,omitempty"` // Assistant Placeholder - Script *v8.Script `json:"-" yaml:"-"` // Assistant Script - CreatedAt int64 `json:"created_at"` // Creation timestamp - UpdatedAt int64 `json:"updated_at"` // Last update timestamp - openai *api.OpenAI // OpenAI API - vision bool // Whether this assistant supports vision - search bool // Whether this assistant supports search - toolCalls bool // Whether this assistant supports tool_calls - initHook bool // Whether this assistant has an init hook + ID string `json:"assistant_id"` // Assistant ID + Type string `json:"type,omitempty"` // Assistant Type, default is assistant + Name string `json:"name,omitempty"` // Assistant Name + Avatar string `json:"avatar,omitempty"` // Assistant Avatar + Connector string `json:"connector"` // AI Connector + Path string `json:"path,omitempty"` // Assistant Path + BuiltIn bool `json:"built_in,omitempty"` // Whether this is a built-in assistant + Sort int `json:"sort,omitempty"` // Assistant Sort + Description string `json:"description,omitempty"` // Assistant Description + Tags []string `json:"tags,omitempty"` // Assistant Tags + Readonly bool `json:"readonly,omitempty"` // Whether this assistant is readonly + Mentionable bool `json:"mentionable,omitempty"` // Whether this assistant is mentionable + Automated bool `json:"automated,omitempty"` // Whether this assistant is automated + Options map[string]interface{} `json:"options,omitempty"` // AI Options + Prompts []Prompt `json:"prompts,omitempty"` // AI Prompts + Tools *ToolCalls `json:"tools,omitempty"` // Assistant Tools + Flows []map[string]interface{} `json:"flows,omitempty"` // Assistant Flows + Placeholder *Placeholder `json:"placeholder,omitempty"` // Assistant Placeholder + 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 + UpdatedAt int64 `json:"updated_at"` // Last update timestamp + Script *v8.Script `json:"-" yaml:"-"` // Assistant Script + + // Internal + // =============================== + openai *api.OpenAI // OpenAI API + search bool // Whether this assistant supports search + vision bool // Whether this assistant supports vision + toolCalls bool // Whether this assistant supports tool_calls + initHook bool // Whether this assistant has an init hook } // ToolCalls the tool calls diff --git a/neo/context/context.go b/neo/context/context.go index 5253391c..979c7762 100644 --- a/neo/context/context.go +++ b/neo/context/context.go @@ -12,30 +12,32 @@ import ( // Context the context type Context struct { context.Context - Sid string `json:"sid" yaml:"-"` // Session ID - ChatID string `json:"chat_id,omitempty"` // Chat ID, use to select chat - AssistantID string `json:"assistant_id,omitempty"` // Assistant ID, use to select assistant - Stack string `json:"stack,omitempty"` // will be removed in the future - Path string `json:"pathname,omitempty"` // wiil be rename to path - FormData map[string]interface{} `json:"formdata,omitempty"` - Field *Field `json:"field,omitempty"` - Namespace string `json:"namespace,omitempty"` - Config map[string]interface{} `json:"config,omitempty"` - Signal interface{} `json:"signal,omitempty"` + Sid string `json:"sid" yaml:"-"` // Session ID + ChatID string `json:"chat_id,omitempty"` // Chat ID, use to select chat + AssistantID string `json:"assistant_id,omitempty"` // Assistant ID, use to select assistant + // CUI Context + Namespace string `json:"namespace,omitempty"` + Stack string `json:"stack,omitempty"` // will be removed in the future + Path string `json:"pathname,omitempty"` // wiil be rename to path + FormData map[string]interface{} `json:"formdata,omitempty"` + Field *Field `json:"field,omitempty"` + Config map[string]interface{} `json:"config,omitempty"` + Signal interface{} `json:"signal,omitempty"` + + // Locale information Locale string `json:"locale,omitempty"` // Locale Theme string `json:"theme,omitempty"` // Theme - Silent bool `json:"silent,omitempty"` // Silent mode - ClientType string `json:"client_type,omitempty"` // The request client type. SDK, Desktop, Web, JSSDK, etc. the default is Web - HistoryVisible bool `json:"history_visible,omitempty"` // History visible, default is true, if false, the history will not be displayed in the UI - Retry bool `json:"retry,omitempty"` // Retry mode - RetryTimes uint8 `json:"retry_times,omitempty"` // Retry times - Upload *FileUpload `json:"upload,omitempty"` // Will be removed in the future + Silent bool `json:"silent,omitempty"` // Silent mode + ClientType string `json:"client_type,omitempty"` // The request client type. SDK, Desktop, Web, JSSDK, etc. the default is Web + HistoryVisible bool `json:"history_visible,omitempty"` // History visible, default is true, if false, the history will not be displayed in the UI + Retry bool `json:"retry,omitempty"` // Retry mode + RetryTimes uint8 `json:"retry_times,omitempty"` // Retry times - Vision bool `json:"vision,omitempty"` // Vision support - Search bool `json:"search,omitempty"` // Search support - RAG bool `json:"rag,omitempty"` // RAG support + Vision bool `json:"vision,omitempty"` // Vision support + Search bool `json:"search,omitempty"` // Search support + Knowledge bool `json:"knowledge,omitempty"` // Knowledge support Args []interface{} `json:"args,omitempty"` // Arguments for call SharedSpace plan.Space `json:"-"` // Shared space @@ -188,10 +190,10 @@ func (ctx *Context) Release() { // Map the context to a map func (ctx *Context) Map() map[string]interface{} { data := map[string]interface{}{ - "sid": ctx.Sid, - "rag": ctx.RAG, - "vision": ctx.Vision, - "search": ctx.Search, + "sid": ctx.Sid, + "knowledge": ctx.Knowledge, + "vision": ctx.Vision, + "search": ctx.Search, } if ctx.ChatID != "" { @@ -246,9 +248,6 @@ func (ctx *Context) Map() map[string]interface{} { if ctx.Signal != nil { data["signal"] = ctx.Signal } - if ctx.Upload != nil { - data["upload"] = ctx.Upload - } // Locale data["locale"] = ctx.Locale diff --git a/neo/neo.go b/neo/neo.go index 6fd09f8f..11315d01 100644 --- a/neo/neo.go +++ b/neo/neo.go @@ -57,14 +57,6 @@ func (neo *DSL) Upload(ctx chatctx.Context, c *gin.Context) (*assistant.File, er } } - // Get file info - ctx.Upload = &chatctx.FileUpload{ - Name: tmpfile.Filename, - Type: tmpfile.Header.Get("Content-Type"), - Size: tmpfile.Size, - TempFile: tmpfile.Filename, - } - // Default use the assistant in context ast := neo.Assistant if ctx.ChatID == "" {