Enhance internationalization support and logging in Assistant and LLM components
- Integrated i18n translations for trace logging messages in the Assistant's Stream method, improving localization for user-facing messages. - Updated logging in the OpenAI provider and CUI writer to utilize i18n for error messages, enhancing clarity and consistency across different locales. - Added new embedded template handling in the i18n package, allowing for more flexible message formatting with variable substitutions. - Improved test coverage for i18n functionalities, ensuring robust handling of various translation scenarios and edge cases.
This commit is contained in:
parent
fe0d93fa04
commit
30778bbf2f
8 changed files with 863 additions and 150 deletions
|
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
"github.com/yaoapp/gou/connector"
|
"github.com/yaoapp/gou/connector"
|
||||||
"github.com/yaoapp/yao/agent/context"
|
"github.com/yaoapp/yao/agent/context"
|
||||||
|
"github.com/yaoapp/yao/agent/i18n"
|
||||||
"github.com/yaoapp/yao/agent/llm"
|
"github.com/yaoapp/yao/agent/llm"
|
||||||
"github.com/yaoapp/yao/agent/output"
|
"github.com/yaoapp/yao/agent/output"
|
||||||
"github.com/yaoapp/yao/trace/types"
|
"github.com/yaoapp/yao/trace/types"
|
||||||
|
|
@ -35,10 +36,11 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa
|
||||||
var agentNode types.Node = nil
|
var agentNode types.Node = nil
|
||||||
if trace != nil {
|
if trace != nil {
|
||||||
agentNode, _ = trace.Add(inputMessages, types.TraceNodeOption{
|
agentNode, _ = trace.Add(inputMessages, types.TraceNodeOption{
|
||||||
Label: fmt.Sprintf("Assistant %s", ast.Name),
|
Label: i18n.Tr(ast.ID, ctx.Locale, "assistant.agent.stream.label"), // "Assistant {{name}}"
|
||||||
Icon: "assistant",
|
Icon: "assistant",
|
||||||
Description: fmt.Sprintf("Assistant %s is processing the request", ast.Name),
|
Description: i18n.Tr(ast.ID, ctx.Locale, "assistant.agent.stream.description"), // "Assistant {{name}} is processing the request"
|
||||||
})
|
})
|
||||||
|
|
||||||
if agentNode != nil {
|
if agentNode != nil {
|
||||||
// Mark the node as complete when the function returns
|
// Mark the node as complete when the function returns
|
||||||
defer agentNode.Complete()
|
defer agentNode.Complete()
|
||||||
|
|
@ -56,7 +58,7 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa
|
||||||
|
|
||||||
// Log the chat history
|
// Log the chat history
|
||||||
if agentNode != nil {
|
if agentNode != nil {
|
||||||
agentNode.Info("Get Chat History", map[string]any{"messages": fullMessages})
|
agentNode.Info(i18n.Tr(ast.ID, ctx.Locale, "assistant.agent.stream.history"), map[string]any{"messages": fullMessages}) // "Get Chat History"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request Create hook ( Optional )
|
// Request Create hook ( Optional )
|
||||||
|
|
@ -116,7 +118,9 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa
|
||||||
trace.Add(
|
trace.Add(
|
||||||
map[string]any{"messages": completionMessages, "options": completionOptions},
|
map[string]any{"messages": completionMessages, "options": completionOptions},
|
||||||
types.TraceNodeOption{
|
types.TraceNodeOption{
|
||||||
Label: fmt.Sprintf("LLM %s", conn.ID()), Icon: "llm", Description: fmt.Sprintf("LLM %s is processing the request", conn.ID()),
|
Label: fmt.Sprintf(i18n.Tr(ast.ID, ctx.Locale, "llm.openai.stream.label"), conn.ID()), // "LLM %s"
|
||||||
|
Icon: "llm",
|
||||||
|
Description: fmt.Sprintf(i18n.Tr(ast.ID, ctx.Locale, "llm.openai.stream.description"), conn.ID()), // "LLM %s is processing the request"
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -187,9 +191,7 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa
|
||||||
// Close the output writer to send [DONE] marker
|
// Close the output writer to send [DONE] marker
|
||||||
if err := output.Close(ctx); err != nil {
|
if err := output.Close(ctx); err != nil {
|
||||||
if trace, _ := ctx.Trace(); trace != nil {
|
if trace, _ := ctx.Trace(); trace != nil {
|
||||||
trace.Error("Agent: Failed to close output", map[string]any{
|
trace.Error(i18n.Tr(ast.ID, ctx.Locale, "assistant.agent.stream.close_error"), map[string]any{"error": err.Error()}) // "Failed to close output"
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
177
agent/i18n/builtin.go
Normal file
177
agent/i18n/builtin.go
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
package i18n
|
||||||
|
|
||||||
|
// init registers built-in global messages
|
||||||
|
func init() {
|
||||||
|
// Initialize __global__ if not exists
|
||||||
|
if Locales["__global__"] == nil {
|
||||||
|
Locales["__global__"] = make(map[string]I18n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Built-in English messages
|
||||||
|
Locales["__global__"]["en"] = I18n{
|
||||||
|
Locale: "en",
|
||||||
|
Messages: map[string]any{
|
||||||
|
// Assistant: agent.go Stream() function
|
||||||
|
"assistant.agent.stream.label": "Assistant {{name}}",
|
||||||
|
"assistant.agent.stream.description": "Assistant {{name}} is processing the request",
|
||||||
|
"assistant.agent.stream.history": "Get Chat History",
|
||||||
|
"assistant.agent.stream.capabilities": "Get Connector Capabilities",
|
||||||
|
"assistant.agent.stream.create_hook": "Call Create Hook",
|
||||||
|
"assistant.agent.stream.closing": "Closing output (root call)",
|
||||||
|
"assistant.agent.stream.skipping": "Skipping output close (nested call)",
|
||||||
|
"assistant.agent.stream.close_error": "Failed to close output",
|
||||||
|
|
||||||
|
// LLM: providers/openai/openai.go Stream() function
|
||||||
|
"llm.openai.stream.label": "LLM %s",
|
||||||
|
"llm.openai.stream.description": "LLM %s is processing the request",
|
||||||
|
"llm.openai.stream.starting": "Starting stream request",
|
||||||
|
"llm.openai.stream.request": "Stream Request",
|
||||||
|
"llm.openai.stream.retry": "Stream request failed, retrying",
|
||||||
|
"llm.openai.stream.api_error": "OpenAI API returned error response",
|
||||||
|
"llm.openai.stream.error": "OpenAI Stream Error",
|
||||||
|
"llm.openai.stream.no_data": "Request body that caused empty response",
|
||||||
|
"llm.openai.stream.no_data_info": "Request details",
|
||||||
|
"llm.openai.post.api_error": "OpenAI API error response",
|
||||||
|
|
||||||
|
// LLM: handlers/stream.go (general LLM stream handler)
|
||||||
|
"llm.handlers.stream.info": "LLM Stream",
|
||||||
|
"llm.handlers.stream.raw_output": "LLM Raw Output",
|
||||||
|
|
||||||
|
// Output: adapters/openai/writer.go
|
||||||
|
"output.openai.writer.sending_chunk": "Sending chunk to client",
|
||||||
|
"output.openai.writer.sending_done": "Sending [DONE] to client",
|
||||||
|
"output.openai.writer.adapt_error": "Failed to adapt message",
|
||||||
|
"output.openai.writer.chunk_error": "Failed to send chunk",
|
||||||
|
"output.openai.writer.group_error": "Failed to write message in group",
|
||||||
|
"output.openai.writer.send_error": "Failed to send data to client",
|
||||||
|
"output.openai.writer.marshal_error": "Failed to marshal chunk",
|
||||||
|
"output.openai.writer.done_error": "Failed to send [DONE] to client",
|
||||||
|
|
||||||
|
// Output: adapters/cui/writer.go
|
||||||
|
"output.cui.writer.sending_chunk": "Sending chunk to client",
|
||||||
|
"output.cui.writer.adapt_error": "Failed to adapt message",
|
||||||
|
"output.cui.writer.chunk_error": "Failed to send chunk",
|
||||||
|
"output.cui.writer.group_error": "Failed to send message group",
|
||||||
|
"output.cui.writer.send_error": "Failed to send data to client",
|
||||||
|
"output.cui.writer.marshal_error": "Failed to marshal chunk",
|
||||||
|
|
||||||
|
// Common status messages
|
||||||
|
"common.status.processing": "Processing",
|
||||||
|
"common.status.completed": "Completed",
|
||||||
|
"common.status.failed": "Failed",
|
||||||
|
"common.status.retrying": "Retrying",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Built-in Chinese (Simplified) messages
|
||||||
|
Locales["__global__"]["zh-cn"] = I18n{
|
||||||
|
Locale: "zh-cn",
|
||||||
|
Messages: map[string]any{
|
||||||
|
// Assistant: agent.go Stream() function
|
||||||
|
"assistant.agent.stream.label": "助手 {{name}}",
|
||||||
|
"assistant.agent.stream.description": "助手 {{name}} 正在处理请求",
|
||||||
|
"assistant.agent.stream.history": "获取聊天历史",
|
||||||
|
"assistant.agent.stream.capabilities": "获取连接器能力",
|
||||||
|
"assistant.agent.stream.create_hook": "调用 Create Hook",
|
||||||
|
"assistant.agent.stream.closing": "关闭输出(根调用)",
|
||||||
|
"assistant.agent.stream.skipping": "跳过输出关闭(嵌套调用)",
|
||||||
|
"assistant.agent.stream.close_error": "关闭输出失败",
|
||||||
|
|
||||||
|
// LLM: providers/openai/openai.go Stream() function
|
||||||
|
"llm.openai.stream.label": "LLM %s",
|
||||||
|
"llm.openai.stream.description": "LLM %s 正在处理请求",
|
||||||
|
"llm.openai.stream.starting": "开始流式请求",
|
||||||
|
"llm.openai.stream.request": "流式请求",
|
||||||
|
"llm.openai.stream.retry": "流式请求失败,正在重试",
|
||||||
|
"llm.openai.stream.api_error": "OpenAI API 返回错误响应",
|
||||||
|
"llm.openai.stream.error": "OpenAI 流错误",
|
||||||
|
"llm.openai.stream.no_data": "导致空响应的请求体",
|
||||||
|
"llm.openai.stream.no_data_info": "请求详情",
|
||||||
|
"llm.openai.post.api_error": "OpenAI API 错误响应",
|
||||||
|
|
||||||
|
// LLM: handlers/stream.go (general LLM stream handler)
|
||||||
|
"llm.handlers.stream.info": "LLM 流式输出",
|
||||||
|
"llm.handlers.stream.raw_output": "LLM 原始输出",
|
||||||
|
|
||||||
|
// Output: adapters/openai/writer.go
|
||||||
|
"output.openai.writer.sending_chunk": "向客户端发送数据块",
|
||||||
|
"output.openai.writer.sending_done": "向客户端发送 [DONE]",
|
||||||
|
"output.openai.writer.adapt_error": "适配消息失败",
|
||||||
|
"output.openai.writer.chunk_error": "发送数据块失败",
|
||||||
|
"output.openai.writer.group_error": "写入消息组中的消息失败",
|
||||||
|
"output.openai.writer.send_error": "发送数据到客户端失败",
|
||||||
|
"output.openai.writer.marshal_error": "序列化数据块失败",
|
||||||
|
"output.openai.writer.done_error": "发送 [DONE] 到客户端失败",
|
||||||
|
|
||||||
|
// Output: adapters/cui/writer.go
|
||||||
|
"output.cui.writer.sending_chunk": "向客户端发送数据块",
|
||||||
|
"output.cui.writer.adapt_error": "适配消息失败",
|
||||||
|
"output.cui.writer.chunk_error": "发送数据块失败",
|
||||||
|
"output.cui.writer.group_error": "发送消息组失败",
|
||||||
|
"output.cui.writer.send_error": "发送数据到客户端失败",
|
||||||
|
"output.cui.writer.marshal_error": "序列化数据块失败",
|
||||||
|
|
||||||
|
// Common status messages
|
||||||
|
"common.status.processing": "处理中",
|
||||||
|
"common.status.completed": "已完成",
|
||||||
|
"common.status.failed": "失败",
|
||||||
|
"common.status.retrying": "重试中",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Built-in Chinese (short code) - same as zh-cn
|
||||||
|
Locales["__global__"]["zh"] = I18n{
|
||||||
|
Locale: "zh",
|
||||||
|
Messages: map[string]any{
|
||||||
|
// Assistant: agent.go Stream() function
|
||||||
|
"assistant.agent.stream.label": "助手 {{name}}",
|
||||||
|
"assistant.agent.stream.description": "助手 {{name}} 正在处理请求",
|
||||||
|
"assistant.agent.stream.history": "获取聊天历史",
|
||||||
|
"assistant.agent.stream.capabilities": "获取连接器能力",
|
||||||
|
"assistant.agent.stream.create_hook": "调用 Create Hook",
|
||||||
|
"assistant.agent.stream.closing": "关闭输出(根调用)",
|
||||||
|
"assistant.agent.stream.skipping": "跳过输出关闭(嵌套调用)",
|
||||||
|
"assistant.agent.stream.close_error": "关闭输出失败",
|
||||||
|
|
||||||
|
// LLM: providers/openai/openai.go Stream() function
|
||||||
|
"llm.openai.stream.label": "LLM %s",
|
||||||
|
"llm.openai.stream.description": "LLM %s 正在处理请求",
|
||||||
|
"llm.openai.stream.starting": "开始流式请求",
|
||||||
|
"llm.openai.stream.request": "流式请求",
|
||||||
|
"llm.openai.stream.retry": "流式请求失败,正在重试",
|
||||||
|
"llm.openai.stream.api_error": "OpenAI API 返回错误响应",
|
||||||
|
"llm.openai.stream.error": "OpenAI 流错误",
|
||||||
|
"llm.openai.stream.no_data": "导致空响应的请求体",
|
||||||
|
"llm.openai.stream.no_data_info": "请求详情",
|
||||||
|
"llm.openai.post.api_error": "OpenAI API 错误响应",
|
||||||
|
|
||||||
|
// LLM: handlers/stream.go (general LLM stream handler)
|
||||||
|
"llm.handlers.stream.info": "LLM 流式输出",
|
||||||
|
"llm.handlers.stream.raw_output": "LLM 原始输出",
|
||||||
|
|
||||||
|
// Output: adapters/openai/writer.go
|
||||||
|
"output.openai.writer.sending_chunk": "向客户端发送数据块",
|
||||||
|
"output.openai.writer.sending_done": "向客户端发送 [DONE]",
|
||||||
|
"output.openai.writer.adapt_error": "适配消息失败",
|
||||||
|
"output.openai.writer.chunk_error": "发送数据块失败",
|
||||||
|
"output.openai.writer.group_error": "写入消息组中的消息失败",
|
||||||
|
"output.openai.writer.send_error": "发送数据到客户端失败",
|
||||||
|
"output.openai.writer.marshal_error": "序列化数据块失败",
|
||||||
|
"output.openai.writer.done_error": "发送 [DONE] 到客户端失败",
|
||||||
|
|
||||||
|
// Output: adapters/cui/writer.go
|
||||||
|
"output.cui.writer.sending_chunk": "向客户端发送数据块",
|
||||||
|
"output.cui.writer.adapt_error": "适配消息失败",
|
||||||
|
"output.cui.writer.chunk_error": "发送数据块失败",
|
||||||
|
"output.cui.writer.group_error": "发送消息组失败",
|
||||||
|
"output.cui.writer.send_error": "发送数据到客户端失败",
|
||||||
|
"output.cui.writer.marshal_error": "序列化数据块失败",
|
||||||
|
|
||||||
|
// Common status messages
|
||||||
|
"common.status.processing": "处理中",
|
||||||
|
"common.status.completed": "已完成",
|
||||||
|
"common.status.failed": "失败",
|
||||||
|
"common.status.retrying": "重试中",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package i18n
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/yaoapp/gou/application"
|
"github.com/yaoapp/gou/application"
|
||||||
|
|
@ -68,11 +69,9 @@ func (i18n I18n) Parse(input any) any {
|
||||||
func (i18n I18n) parseString(in string) string {
|
func (i18n I18n) parseString(in string) string {
|
||||||
trimed := strings.TrimSpace(in)
|
trimed := strings.TrimSpace(in)
|
||||||
|
|
||||||
// Check if it's a template expression {{...}}
|
// Check if it's a direct message key (no template markers)
|
||||||
hasExp := strings.HasPrefix(trimed, "{{") && strings.HasSuffix(trimed, "}}")
|
if !strings.Contains(trimed, "{{") && !strings.Contains(trimed, "}}") {
|
||||||
if hasExp {
|
if val, ok := i18n.Messages[trimed]; ok {
|
||||||
exp := strings.TrimSpace(strings.TrimPrefix(strings.TrimSuffix(trimed, "}}"), "{{"))
|
|
||||||
if val, ok := i18n.Messages[exp]; ok {
|
|
||||||
if s, ok := val.(string); ok {
|
if s, ok := val.(string); ok {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
@ -80,13 +79,48 @@ func (i18n I18n) parseString(in string) string {
|
||||||
return in
|
return in
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a direct message key
|
// Check if it's a full template expression {{...}} (exact match - entire string is one template)
|
||||||
if val, ok := i18n.Messages[trimed]; ok {
|
hasExp := strings.HasPrefix(trimed, "{{") && strings.HasSuffix(trimed, "}}")
|
||||||
if s, ok := val.(string); ok {
|
if hasExp {
|
||||||
return s
|
// Check if there's only ONE template pattern (no text before/after or multiple templates)
|
||||||
|
re := regexp.MustCompile(`\{\{\s*([^}]+?)\s*\}\}`)
|
||||||
|
matches := re.FindAllString(trimed, -1)
|
||||||
|
|
||||||
|
// Only treat as full template if there's exactly one match and it equals the trimmed string
|
||||||
|
if len(matches) == 1 && matches[0] == trimed {
|
||||||
|
exp := strings.TrimSpace(strings.TrimPrefix(strings.TrimSuffix(trimed, "}}"), "{{"))
|
||||||
|
if val, ok := i18n.Messages[exp]; ok {
|
||||||
|
if s, ok := val.(string); ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle embedded template variables: "text {{var}} more {{var2}}"
|
||||||
|
if strings.Contains(in, "{{") && strings.Contains(in, "}}") {
|
||||||
|
result := in
|
||||||
|
// Use regex to find all {{...}} patterns
|
||||||
|
re := regexp.MustCompile(`\{\{\s*([^}]+?)\s*\}\}`)
|
||||||
|
matches := re.FindAllStringSubmatch(in, -1)
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
if len(match) >= 2 {
|
||||||
|
fullMatch := match[0] // Full match including {{ }}
|
||||||
|
varName := strings.TrimSpace(match[1]) // Variable name without {{ }}
|
||||||
|
|
||||||
|
// Try to replace with value from Messages
|
||||||
|
if val, ok := i18n.Messages[varName]; ok {
|
||||||
|
if s, ok := val.(string); ok {
|
||||||
|
result = strings.Replace(result, fullMatch, s, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
return in
|
return in
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,97 +157,167 @@ func GetLocales(path string) (Map, error) {
|
||||||
return i18ns, nil
|
return i18ns, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flatten flatten the i18n map
|
// Flatten flattens the map of locales by adding short language codes and region codes
|
||||||
func (i18ns Map) Flatten() Map {
|
// e.g., "en-us" will also create "en" and "us" entries
|
||||||
new := Map{}
|
// If __global__ locales exist, they are merged (local/user messages override global built-in messages)
|
||||||
for lang, i18n := range i18ns {
|
func (m Map) Flatten() Map {
|
||||||
new[lang] = I18n{Locale: lang, Messages: maps.MapOf(i18n.Messages).Dot()}
|
flattened := make(Map)
|
||||||
|
|
||||||
// Add short lang
|
// First, process local messages with Dot() flattening
|
||||||
parts := strings.Split(lang, "-")
|
for localeCode, i18n := range m {
|
||||||
|
// Flatten nested messages to dot notation (e.g., {"local": {"key": "value"}} -> {"local.key": "value"})
|
||||||
// en
|
flattened[localeCode] = I18n{
|
||||||
if parts[0] != lang {
|
Locale: localeCode,
|
||||||
new[parts[0]] = new[lang]
|
Messages: maps.MapOf(i18n.Messages).Dot(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// us
|
// Add short language codes
|
||||||
|
parts := strings.Split(localeCode, "-")
|
||||||
if len(parts) > 1 {
|
if len(parts) > 1 {
|
||||||
new[parts[1]] = new[lang]
|
// Add short language code (e.g., "en" from "en-us")
|
||||||
}
|
if _, ok := flattened[parts[0]]; !ok {
|
||||||
}
|
flattened[parts[0]] = flattened[localeCode]
|
||||||
return new
|
}
|
||||||
}
|
// Add region code (e.g., "us" from "en-us")
|
||||||
|
if _, ok := flattened[parts[1]]; !ok {
|
||||||
// FlattenWithGlobal flatten the i18n map with global i18n
|
flattened[parts[1]] = flattened[localeCode]
|
||||||
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
|
// Merge with global locales if they exist
|
||||||
parts := strings.Split(lang, "-")
|
// Strategy: Start with global (built-in), then override with local (user)
|
||||||
|
globalLocales, hasGlobal := Locales["__global__"]
|
||||||
|
if !hasGlobal {
|
||||||
|
return flattened
|
||||||
|
}
|
||||||
|
|
||||||
// en
|
for globalLocaleCode, globalI18n := range globalLocales {
|
||||||
if parts[0] != lang {
|
// Ensure global messages are also flattened (though builtin.go already uses flat keys)
|
||||||
new[parts[0]] = new[lang]
|
globalFlattened := maps.MapOf(globalI18n.Messages).Dot()
|
||||||
}
|
|
||||||
|
|
||||||
// us
|
if localI18n, ok := flattened[globalLocaleCode]; ok {
|
||||||
if len(parts) > 1 {
|
// Both global and local exist: merge with local overriding global
|
||||||
new[parts[1]] = new[lang]
|
mergedMessages := make(map[string]any)
|
||||||
|
// First copy all global messages
|
||||||
|
for k, v := range globalFlattened {
|
||||||
|
mergedMessages[k] = v
|
||||||
|
}
|
||||||
|
// Then override with local messages
|
||||||
|
for k, v := range localI18n.Messages {
|
||||||
|
mergedMessages[k] = v
|
||||||
|
}
|
||||||
|
flattened[globalLocaleCode] = I18n{
|
||||||
|
Locale: globalLocaleCode,
|
||||||
|
Messages: mergedMessages,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Only global exists, add it with flattened messages
|
||||||
|
flattened[globalLocaleCode] = I18n{
|
||||||
|
Locale: globalLocaleCode,
|
||||||
|
Messages: globalFlattened,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new
|
return flattened
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translate translate the input
|
// FlattenWithGlobal is deprecated. Use Flatten() instead, which now automatically merges with global locales.
|
||||||
|
// Kept for backward compatibility.
|
||||||
|
func (m Map) FlattenWithGlobal() Map {
|
||||||
|
return m.Flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate translate the input with recursive variable resolution
|
||||||
|
// Fallback strategy: assistant locale -> assistant short codes -> global locale -> global short codes
|
||||||
func Translate(assistantID string, locale string, input any) any {
|
func Translate(assistantID string, locale string, input any) any {
|
||||||
|
|
||||||
locale = strings.ToLower(strings.TrimSpace(locale))
|
locale = strings.ToLower(strings.TrimSpace(locale))
|
||||||
i18ns, has := Locales[assistantID]
|
|
||||||
if !has {
|
// Helper function to try translation with a specific i18n object
|
||||||
i18ns = map[string]I18n{}
|
tryTranslate := func(i18n I18n, input any) (any, bool) {
|
||||||
|
result := i18n.Parse(input)
|
||||||
|
// For string input, check if translation was found by comparing with input
|
||||||
|
// For other types, Parse always returns a result (transformed or original)
|
||||||
|
if inputStr, ok := input.(string); ok {
|
||||||
|
if resultStr, ok := result.(string); ok {
|
||||||
|
// Translation found if result is different from input
|
||||||
|
if resultStr != inputStr {
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
return input, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For non-string inputs (maps, slices), Parse always processes them
|
||||||
|
return result, true
|
||||||
}
|
}
|
||||||
|
|
||||||
i18n, has := i18ns[locale]
|
// Helper function to process recursive templates
|
||||||
if !has {
|
processTemplates := func(result any, assistantID string, locale string) any {
|
||||||
|
if resultStr, ok := result.(string); ok && strings.Contains(resultStr, "{{") && strings.Contains(resultStr, "}}") {
|
||||||
|
re := regexp.MustCompile(`\{\{\s*([^}]+?)\s*\}\}`)
|
||||||
|
resultStr = re.ReplaceAllStringFunc(resultStr, func(match string) string {
|
||||||
|
varName := strings.TrimSpace(strings.TrimPrefix(strings.TrimSuffix(match, "}}"), "{{"))
|
||||||
|
translated := Translate(assistantID, locale, varName)
|
||||||
|
if translatedStr, ok := translated.(string); ok && translatedStr != varName {
|
||||||
|
return translatedStr
|
||||||
|
}
|
||||||
|
return match
|
||||||
|
})
|
||||||
|
return resultStr
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try assistant locale first
|
||||||
|
if i18ns, has := Locales[assistantID]; has {
|
||||||
|
// Try exact locale
|
||||||
|
if i18n, hasLocale := i18ns[locale]; hasLocale {
|
||||||
|
if result, found := tryTranslate(i18n, input); found {
|
||||||
|
return processTemplates(result, assistantID, locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try short codes
|
||||||
parts := strings.Split(locale, "-")
|
parts := strings.Split(locale, "-")
|
||||||
if len(parts) > 1 {
|
if len(parts) > 1 {
|
||||||
i18n, has = i18ns[parts[1]]
|
if i18n, hasLocale := i18ns[parts[1]]; hasLocale {
|
||||||
}
|
if result, found := tryTranslate(i18n, input); found {
|
||||||
if !has {
|
return processTemplates(result, assistantID, locale)
|
||||||
i18n, has = i18ns[parts[0]]
|
}
|
||||||
|
}
|
||||||
|
if i18n, hasLocale := i18ns[parts[0]]; hasLocale {
|
||||||
|
if result, found := tryTranslate(i18n, input); found {
|
||||||
|
return processTemplates(result, assistantID, locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !has {
|
// Fallback to global locales
|
||||||
var hasGlobal bool = false
|
if globalI18ns, hasGlobal := Locales["__global__"]; hasGlobal {
|
||||||
i18ns, hasGlobal = Locales["__global__"]
|
// Try exact locale
|
||||||
if hasGlobal {
|
if i18n, hasLocale := globalI18ns[locale]; hasLocale {
|
||||||
i18n, has = i18ns[locale]
|
if result, found := tryTranslate(i18n, input); found {
|
||||||
|
return processTemplates(result, assistantID, locale)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if has {
|
// Try short codes
|
||||||
return i18n.Parse(input)
|
parts := strings.Split(locale, "-")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
if i18n, hasLocale := globalI18ns[parts[1]]; hasLocale {
|
||||||
|
if result, found := tryTranslate(i18n, input); found {
|
||||||
|
return processTemplates(result, assistantID, locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i18n, hasLocale := globalI18ns[parts[0]]; hasLocale {
|
||||||
|
if result, found := tryTranslate(i18n, input); found {
|
||||||
|
return processTemplates(result, assistantID, locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return input
|
return input
|
||||||
|
|
@ -243,3 +347,25 @@ func TranslateGlobal(locale string, input any) any {
|
||||||
}
|
}
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// T is a short alias for TranslateGlobal that returns string
|
||||||
|
// Usage: i18n.T(ctx.Locale, "assistant.agent.stream.label")
|
||||||
|
// Variables in templates like {{variable}} will be recursively resolved from the global language pack
|
||||||
|
func T(locale string, key string) string {
|
||||||
|
result := TranslateGlobal(locale, key)
|
||||||
|
if str, ok := result.(string); ok {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tr translates with assistantID and returns string
|
||||||
|
// Supports recursive translation of {{variable}} templates
|
||||||
|
// Usage: i18n.Tr(assistantID, locale, "key")
|
||||||
|
func Tr(assistantID string, locale string, key string) string {
|
||||||
|
result := Translate(assistantID, locale, key)
|
||||||
|
if str, ok := result.(string); ok {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,37 @@ func TestParseString(t *testing.T) {
|
||||||
input: "",
|
input: "",
|
||||||
expected: "",
|
expected: "",
|
||||||
},
|
},
|
||||||
|
// Embedded template tests (new feature)
|
||||||
|
{
|
||||||
|
name: "Embedded single template",
|
||||||
|
input: "Hello {{hello}}",
|
||||||
|
expected: "Hello Hello",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Embedded multiple templates",
|
||||||
|
input: "{{hello}} {{world}}!",
|
||||||
|
expected: "Hello World!",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Embedded template with spaces",
|
||||||
|
input: "Say {{ hello }} to the {{ world }}",
|
||||||
|
expected: "Say Hello to the World",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Embedded template mixed with text",
|
||||||
|
input: "Message: {{greeting}} - {{description}}",
|
||||||
|
expected: "Message: Hello, World! - This is a test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Embedded template not found",
|
||||||
|
input: "Hello {{notfound}} World",
|
||||||
|
expected: "Hello {{notfound}} World",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Embedded template partial match",
|
||||||
|
input: "{{hello}} {{notfound}} {{world}}",
|
||||||
|
expected: "Hello {{notfound}} World",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|
@ -358,6 +389,12 @@ func TestMapFlatten(t *testing.T) {
|
||||||
|
|
||||||
// TestMapFlattenWithGlobal tests the FlattenWithGlobal method
|
// TestMapFlattenWithGlobal tests the FlattenWithGlobal method
|
||||||
func TestMapFlattenWithGlobal(t *testing.T) {
|
func TestMapFlattenWithGlobal(t *testing.T) {
|
||||||
|
// Save and restore __global__
|
||||||
|
originalGlobal := Locales["__global__"]
|
||||||
|
defer func() {
|
||||||
|
Locales["__global__"] = originalGlobal
|
||||||
|
}()
|
||||||
|
|
||||||
// Setup global locales
|
// Setup global locales
|
||||||
Locales["__global__"] = map[string]I18n{
|
Locales["__global__"] = map[string]I18n{
|
||||||
"en": {
|
"en": {
|
||||||
|
|
@ -369,8 +406,6 @@ func TestMapFlattenWithGlobal(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
defer delete(Locales, "__global__")
|
|
||||||
|
|
||||||
i18ns := Map{
|
i18ns := Map{
|
||||||
"en": I18n{
|
"en": I18n{
|
||||||
Locale: "en",
|
Locale: "en",
|
||||||
|
|
@ -405,6 +440,12 @@ func TestMapFlattenWithGlobal(t *testing.T) {
|
||||||
|
|
||||||
// TestMapFlattenWithGlobalNoGlobal tests FlattenWithGlobal when no global exists
|
// TestMapFlattenWithGlobalNoGlobal tests FlattenWithGlobal when no global exists
|
||||||
func TestMapFlattenWithGlobalNoGlobal(t *testing.T) {
|
func TestMapFlattenWithGlobalNoGlobal(t *testing.T) {
|
||||||
|
// Save and restore __global__
|
||||||
|
originalGlobal := Locales["__global__"]
|
||||||
|
defer func() {
|
||||||
|
Locales["__global__"] = originalGlobal
|
||||||
|
}()
|
||||||
|
|
||||||
// Make sure no global exists
|
// Make sure no global exists
|
||||||
delete(Locales, "__global__")
|
delete(Locales, "__global__")
|
||||||
|
|
||||||
|
|
@ -430,6 +471,12 @@ func TestMapFlattenWithGlobalNoGlobal(t *testing.T) {
|
||||||
|
|
||||||
// TestMapFlattenWithGlobalKeyConflict tests FlattenWithGlobal when local keys already exist
|
// TestMapFlattenWithGlobalKeyConflict tests FlattenWithGlobal when local keys already exist
|
||||||
func TestMapFlattenWithGlobalKeyConflict(t *testing.T) {
|
func TestMapFlattenWithGlobalKeyConflict(t *testing.T) {
|
||||||
|
// Save and restore __global__
|
||||||
|
originalGlobal := Locales["__global__"]
|
||||||
|
defer func() {
|
||||||
|
Locales["__global__"] = originalGlobal
|
||||||
|
}()
|
||||||
|
|
||||||
// Setup global with keys in flat format (after Dot())
|
// Setup global with keys in flat format (after Dot())
|
||||||
Locales["__global__"] = map[string]I18n{
|
Locales["__global__"] = map[string]I18n{
|
||||||
"en": {
|
"en": {
|
||||||
|
|
@ -441,7 +488,6 @@ func TestMapFlattenWithGlobalKeyConflict(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
defer delete(Locales, "__global__")
|
|
||||||
|
|
||||||
// Local messages in nested format (will be flattened by Dot())
|
// Local messages in nested format (will be flattened by Dot())
|
||||||
i18ns := Map{
|
i18ns := Map{
|
||||||
|
|
@ -543,6 +589,12 @@ func TestTranslate(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Translate with fallback to global", func(t *testing.T) {
|
t.Run("Translate with fallback to global", func(t *testing.T) {
|
||||||
|
// Save and restore __global__
|
||||||
|
originalGlobal := Locales["__global__"]
|
||||||
|
defer func() {
|
||||||
|
Locales["__global__"] = originalGlobal
|
||||||
|
}()
|
||||||
|
|
||||||
Locales["__global__"] = map[string]I18n{
|
Locales["__global__"] = map[string]I18n{
|
||||||
"es": {
|
"es": {
|
||||||
Locale: "es",
|
Locale: "es",
|
||||||
|
|
@ -551,7 +603,6 @@ func TestTranslate(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
defer delete(Locales, "__global__")
|
|
||||||
|
|
||||||
result := Translate(assistantID, "es", "{{greeting}}")
|
result := Translate(assistantID, "es", "{{greeting}}")
|
||||||
if result != "Hola" {
|
if result != "Hola" {
|
||||||
|
|
@ -578,35 +629,68 @@ func TestTranslate(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestTranslateGlobal tests the TranslateGlobal function
|
// TestTranslateGlobal tests the TranslateGlobal function with custom messages
|
||||||
func TestTranslateGlobal(t *testing.T) {
|
func TestTranslateGlobal(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf)
|
test.Prepare(t, config.Conf)
|
||||||
defer test.Clean()
|
defer test.Clean()
|
||||||
|
|
||||||
Locales["__global__"] = map[string]I18n{
|
// Save existing __global__ and restore after test
|
||||||
"en": {
|
originalGlobal := make(map[string]I18n)
|
||||||
Locale: "en",
|
if existing, ok := Locales["__global__"]; ok {
|
||||||
Messages: map[string]any{
|
for k, v := range existing {
|
||||||
"button.ok": "OK",
|
originalGlobal[k] = v
|
||||||
"button.cancel": "Cancel",
|
}
|
||||||
},
|
}
|
||||||
},
|
defer func() {
|
||||||
"zh-cn": {
|
Locales["__global__"] = originalGlobal
|
||||||
Locale: "zh-cn",
|
}()
|
||||||
Messages: map[string]any{
|
|
||||||
"button.ok": "确定",
|
// Add custom test messages to existing global (not replacing)
|
||||||
"button.cancel": "取消",
|
if Locales["__global__"] == nil {
|
||||||
},
|
Locales["__global__"] = make(map[string]I18n)
|
||||||
},
|
}
|
||||||
"zh": {
|
|
||||||
Locale: "zh",
|
// Extend existing English messages
|
||||||
Messages: map[string]any{
|
enMessages := make(map[string]any)
|
||||||
"button.ok": "确定",
|
if existing, ok := Locales["__global__"]["en"]; ok {
|
||||||
"button.cancel": "取消",
|
for k, v := range existing.Messages {
|
||||||
},
|
enMessages[k] = v
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
enMessages["button.ok"] = "OK"
|
||||||
|
enMessages["button.cancel"] = "Cancel"
|
||||||
|
Locales["__global__"]["en"] = I18n{
|
||||||
|
Locale: "en",
|
||||||
|
Messages: enMessages,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend existing Chinese messages
|
||||||
|
zhcnMessages := make(map[string]any)
|
||||||
|
if existing, ok := Locales["__global__"]["zh-cn"]; ok {
|
||||||
|
for k, v := range existing.Messages {
|
||||||
|
zhcnMessages[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zhcnMessages["button.ok"] = "确定"
|
||||||
|
zhcnMessages["button.cancel"] = "取消"
|
||||||
|
Locales["__global__"]["zh-cn"] = I18n{
|
||||||
|
Locale: "zh-cn",
|
||||||
|
Messages: zhcnMessages,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend existing Chinese short code messages
|
||||||
|
zhMessages := make(map[string]any)
|
||||||
|
if existing, ok := Locales["__global__"]["zh"]; ok {
|
||||||
|
for k, v := range existing.Messages {
|
||||||
|
zhMessages[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zhMessages["button.ok"] = "确定"
|
||||||
|
zhMessages["button.cancel"] = "取消"
|
||||||
|
Locales["__global__"]["zh"] = I18n{
|
||||||
|
Locale: "zh",
|
||||||
|
Messages: zhMessages,
|
||||||
}
|
}
|
||||||
defer delete(Locales, "__global__")
|
|
||||||
|
|
||||||
t.Run("TranslateGlobal with match", func(t *testing.T) {
|
t.Run("TranslateGlobal with match", func(t *testing.T) {
|
||||||
result := TranslateGlobal("en", "{{button.ok}}")
|
result := TranslateGlobal("en", "{{button.ok}}")
|
||||||
|
|
@ -637,13 +721,17 @@ func TestTranslateGlobal(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("TranslateGlobal no global", func(t *testing.T) {
|
t.Run("TranslateGlobal no global", func(t *testing.T) {
|
||||||
|
// Temporarily remove global
|
||||||
|
temp := Locales["__global__"]
|
||||||
delete(Locales, "__global__")
|
delete(Locales, "__global__")
|
||||||
|
|
||||||
result := TranslateGlobal("en", "{{button.ok}}")
|
result := TranslateGlobal("en", "{{button.ok}}")
|
||||||
if result != "{{button.ok}}" {
|
if result != "{{button.ok}}" {
|
||||||
t.Errorf("Expected '{{button.ok}}', got %v", result)
|
t.Errorf("Expected '{{button.ok}}', got %v", result)
|
||||||
}
|
}
|
||||||
// Restore for cleanup
|
|
||||||
Locales["__global__"] = map[string]I18n{}
|
// Restore
|
||||||
|
Locales["__global__"] = temp
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -815,3 +903,341 @@ func TestEdgeCases(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBuiltinMessages tests the built-in global messages
|
||||||
|
func TestBuiltinMessages(t *testing.T) {
|
||||||
|
// Save and restore __global__ to avoid test interference
|
||||||
|
originalGlobal := make(map[string]I18n)
|
||||||
|
if existing, ok := Locales["__global__"]; ok {
|
||||||
|
for k, v := range existing {
|
||||||
|
originalGlobal[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
Locales["__global__"] = originalGlobal
|
||||||
|
}()
|
||||||
|
|
||||||
|
t.Run("English built-in messages", func(t *testing.T) {
|
||||||
|
// Test assistant messages
|
||||||
|
result := TranslateGlobal("en", "{{assistant.agent.stream.label}}")
|
||||||
|
expected := "Assistant {{name}}"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = TranslateGlobal("en", "{{assistant.agent.stream.description}}")
|
||||||
|
expected = "Assistant {{name}} is processing the request"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = TranslateGlobal("en", "{{assistant.agent.stream.history}}")
|
||||||
|
expected = "Get Chat History"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test LLM messages (note: LLM uses %s for fmt.Sprintf, not {{name}} for recursive translation)
|
||||||
|
result = TranslateGlobal("en", "{{llm.openai.stream.label}}")
|
||||||
|
expected = "LLM %s"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = TranslateGlobal("en", "{{llm.handlers.stream.info}}")
|
||||||
|
expected = "LLM Stream"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test common messages
|
||||||
|
result = TranslateGlobal("en", "{{common.status.processing}}")
|
||||||
|
expected = "Processing"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Chinese (zh-cn) built-in messages", func(t *testing.T) {
|
||||||
|
// Test assistant messages
|
||||||
|
result := TranslateGlobal("zh-cn", "{{assistant.agent.stream.label}}")
|
||||||
|
expected := "助手 {{name}}"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = TranslateGlobal("zh-cn", "{{assistant.agent.stream.description}}")
|
||||||
|
expected = "助手 {{name}} 正在处理请求"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = TranslateGlobal("zh-cn", "{{assistant.agent.stream.history}}")
|
||||||
|
expected = "获取聊天历史"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test LLM messages
|
||||||
|
result = TranslateGlobal("zh-cn", "{{llm.handlers.stream.info}}")
|
||||||
|
expected = "LLM 流式输出"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test common messages
|
||||||
|
result = TranslateGlobal("zh-cn", "{{common.status.processing}}")
|
||||||
|
expected = "处理中"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Chinese (zh) short code", func(t *testing.T) {
|
||||||
|
result := TranslateGlobal("zh", "{{assistant.agent.stream.label}}")
|
||||||
|
expected := "助手 {{name}}"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = TranslateGlobal("zh", "{{common.status.processing}}")
|
||||||
|
expected = "处理中"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Embedded template with built-in messages", func(t *testing.T) {
|
||||||
|
// English
|
||||||
|
result := TranslateGlobal("en", "Status: {{common.status.processing}}")
|
||||||
|
expected := "Status: Processing"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chinese
|
||||||
|
result = TranslateGlobal("zh-cn", "状态: {{common.status.processing}}")
|
||||||
|
expected = "状态: 处理中"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Non-existent key in global", func(t *testing.T) {
|
||||||
|
result := TranslateGlobal("en", "{{unknown.key}}")
|
||||||
|
if result != "{{unknown.key}}" {
|
||||||
|
t.Errorf("Expected '{{unknown.key}}', got '%v'", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTAlias tests the T function alias
|
||||||
|
func TestTr(t *testing.T) {
|
||||||
|
// Save original global locales
|
||||||
|
originalGlobal := Locales["__global__"]
|
||||||
|
defer func() {
|
||||||
|
if originalGlobal != nil {
|
||||||
|
Locales["__global__"] = originalGlobal
|
||||||
|
} else {
|
||||||
|
delete(Locales, "__global__")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Setup test locales with nested templates
|
||||||
|
Locales["__global__"] = map[string]I18n{
|
||||||
|
"en": {
|
||||||
|
Locale: "en",
|
||||||
|
Messages: map[string]any{
|
||||||
|
"assistant.label": "Assistant {{assistant.name}}", // Use full key path
|
||||||
|
"assistant.name": "AI Helper",
|
||||||
|
"assistant.description": "{{assistant.label}} is processing",
|
||||||
|
"llm.label": "LLM {{model.deepseek}}", // Use full key path
|
||||||
|
"model.deepseek": "DeepSeek",
|
||||||
|
"deeply.nested": "Level1 {{level2}}",
|
||||||
|
"level2": "Level2 {{level3}}",
|
||||||
|
"level3": "Level3 End",
|
||||||
|
"simple.message": "Hello World",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"zh-cn": {
|
||||||
|
Locale: "zh-cn",
|
||||||
|
Messages: map[string]any{
|
||||||
|
"assistant.label": "助手 {{assistant.name}}", // Use full key path
|
||||||
|
"assistant.name": "智能助手",
|
||||||
|
"assistant.description": "{{assistant.label}} 正在处理",
|
||||||
|
"llm.label": "模型 {{model.deepseek}}", // Use full key path
|
||||||
|
"model.deepseek": "深度求索",
|
||||||
|
"deeply.nested": "第一层 {{level2}}",
|
||||||
|
"level2": "第二层 {{level3}}",
|
||||||
|
"level3": "第三层结束",
|
||||||
|
"simple.message": "你好世界",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup assistant-specific locale (overrides assistant.name, but inherits assistant.label from global)
|
||||||
|
Locales["test-assistant"] = map[string]I18n{
|
||||||
|
"en": {
|
||||||
|
Locale: "en",
|
||||||
|
Messages: map[string]any{
|
||||||
|
"assistant.name": "Custom Assistant", // This will override global when assistant.label is resolved
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
defer delete(Locales, "test-assistant")
|
||||||
|
|
||||||
|
t.Run("Simple translation without variables", func(t *testing.T) {
|
||||||
|
result := Tr("__global__", "en", "simple.message")
|
||||||
|
if result != "Hello World" {
|
||||||
|
t.Errorf("Expected 'Hello World', got '%s'", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = Tr("__global__", "zh-cn", "simple.message")
|
||||||
|
if result != "你好世界" {
|
||||||
|
t.Errorf("Expected '你好世界', got '%s'", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("One level nested variable", func(t *testing.T) {
|
||||||
|
// "Assistant {{name}}" -> "Assistant AI Helper"
|
||||||
|
result := Tr("__global__", "en", "assistant.label")
|
||||||
|
if result != "Assistant AI Helper" {
|
||||||
|
t.Errorf("Expected 'Assistant AI Helper', got '%s'", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = Tr("__global__", "zh-cn", "assistant.label")
|
||||||
|
if result != "助手 智能助手" {
|
||||||
|
t.Errorf("Expected '助手 智能助手', got '%s'", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Two levels nested variables", func(t *testing.T) {
|
||||||
|
// "{{assistant.label}} is processing" -> "Assistant AI Helper is processing"
|
||||||
|
result := Tr("__global__", "en", "assistant.description")
|
||||||
|
if result != "Assistant AI Helper is processing" {
|
||||||
|
t.Errorf("Expected 'Assistant AI Helper is processing', got '%s'", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = Tr("__global__", "zh-cn", "assistant.description")
|
||||||
|
if result != "助手 智能助手 正在处理" {
|
||||||
|
t.Errorf("Expected '助手 智能助手 正在处理', got '%s'", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Three levels deeply nested", func(t *testing.T) {
|
||||||
|
// "Level1 {{level2}}" -> "Level1 Level2 {{level3}}" -> "Level1 Level2 Level3 End"
|
||||||
|
result := Tr("__global__", "en", "deeply.nested")
|
||||||
|
if result != "Level1 Level2 Level3 End" {
|
||||||
|
t.Errorf("Expected 'Level1 Level2 Level3 End', got '%s'", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = Tr("__global__", "zh-cn", "deeply.nested")
|
||||||
|
if result != "第一层 第二层 第三层结束" {
|
||||||
|
t.Errorf("Expected '第一层 第二层 第三层结束', got '%s'", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Assistant-specific override", func(t *testing.T) {
|
||||||
|
// When assistant locale exists but doesn't have a key, it WILL fallback to global
|
||||||
|
// This is key-level fallback: try assistant first, then fallback to global
|
||||||
|
result := Tr("test-assistant", "en", "assistant.label")
|
||||||
|
// "Assistant {{assistant.name}}" from global, then {{assistant.name}} -> "Custom Assistant" from assistant
|
||||||
|
if result != "Assistant Custom Assistant" {
|
||||||
|
t.Errorf("Expected 'Assistant Custom Assistant' (fallback to global with assistant override), got '%s'", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// assistant has 'en' locale but doesn't have this key, fallback to global
|
||||||
|
result = Tr("test-assistant", "en", "simple.message")
|
||||||
|
if result != "Hello World" {
|
||||||
|
t.Errorf("Expected 'Hello World' (fallback to global), got '%s'", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If assistant locale has the key, it will use assistant's value
|
||||||
|
result = Tr("test-assistant", "en", "assistant.name")
|
||||||
|
if result != "Custom Assistant" {
|
||||||
|
t.Errorf("Expected 'Custom Assistant' (from assistant locale), got '%s'", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Non-existent key returns original", func(t *testing.T) {
|
||||||
|
result := Tr("__global__", "en", "non.existent.key")
|
||||||
|
if result != "non.existent.key" {
|
||||||
|
t.Errorf("Expected 'non.existent.key', got '%s'", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("LLM with model variable", func(t *testing.T) {
|
||||||
|
result := Tr("__global__", "en", "llm.label")
|
||||||
|
if result != "LLM DeepSeek" {
|
||||||
|
t.Errorf("Expected 'LLM DeepSeek', got '%s'", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = Tr("__global__", "zh-cn", "llm.label")
|
||||||
|
if result != "模型 深度求索" {
|
||||||
|
t.Errorf("Expected '模型 深度求索', got '%s'", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTAlias(t *testing.T) {
|
||||||
|
// Save and restore __global__ to avoid test interference
|
||||||
|
originalGlobal := make(map[string]I18n)
|
||||||
|
if existing, ok := Locales["__global__"]; ok {
|
||||||
|
for k, v := range existing {
|
||||||
|
originalGlobal[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
Locales["__global__"] = originalGlobal
|
||||||
|
}()
|
||||||
|
|
||||||
|
t.Run("T alias works like TranslateGlobal", func(t *testing.T) {
|
||||||
|
// Test that T and TranslateGlobal return the same results
|
||||||
|
input := "{{assistant.agent.stream.label}}"
|
||||||
|
|
||||||
|
resultT := T("en", input)
|
||||||
|
resultGlobal := TranslateGlobal("en", input)
|
||||||
|
|
||||||
|
if resultT != resultGlobal {
|
||||||
|
t.Errorf("T and TranslateGlobal should return same result. T: %v, TranslateGlobal: %v", resultT, resultGlobal)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "Assistant {{name}}"
|
||||||
|
if resultT != expected {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expected, resultT)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("T alias with Chinese", func(t *testing.T) {
|
||||||
|
result := T("zh-cn", "{{assistant.agent.stream.history}}")
|
||||||
|
expected := "获取聊天历史"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("T alias with embedded template", func(t *testing.T) {
|
||||||
|
result := T("en", "Status: {{common.status.completed}}")
|
||||||
|
expected := "Status: Completed"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("T with nested template (template in template value)", func(t *testing.T) {
|
||||||
|
// assistant.agent.stream.label = "Assistant {{name}}" (contains {{name}} template)
|
||||||
|
// This tests if we can get the template string itself
|
||||||
|
result := T("en", "{{assistant.agent.stream.label}}")
|
||||||
|
expected := "Assistant {{name}}"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify Chinese version too
|
||||||
|
resultZh := T("zh-cn", "{{assistant.agent.stream.label}}")
|
||||||
|
expectedZh := "助手 {{name}}"
|
||||||
|
if resultZh != expectedZh {
|
||||||
|
t.Errorf("Expected '%s', got '%v'", expectedZh, resultZh)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/yaoapp/yao/agent/context"
|
"github.com/yaoapp/yao/agent/context"
|
||||||
|
"github.com/yaoapp/yao/agent/i18n"
|
||||||
"github.com/yaoapp/yao/agent/output"
|
"github.com/yaoapp/yao/agent/output"
|
||||||
"github.com/yaoapp/yao/agent/output/message"
|
"github.com/yaoapp/yao/agent/output/message"
|
||||||
)
|
)
|
||||||
|
|
@ -20,7 +21,7 @@ func DefaultStreamHandler(ctx *context.Context) context.StreamFunc {
|
||||||
return func(chunkType context.StreamChunkType, data []byte) int {
|
return func(chunkType context.StreamChunkType, data []byte) int {
|
||||||
trace, _ := ctx.Trace()
|
trace, _ := ctx.Trace()
|
||||||
if trace != nil {
|
if trace != nil {
|
||||||
trace.Info("LLM Stream", map[string]any{"data": string(data)})
|
trace.Info(i18n.T(ctx.Locale, "llm.handlers.stream.info"), map[string]any{"data": string(data)}) // "LLM Stream"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle different chunk types
|
// Handle different chunk types
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/yaoapp/gou/connector"
|
"github.com/yaoapp/gou/connector"
|
||||||
"github.com/yaoapp/gou/http"
|
"github.com/yaoapp/gou/http"
|
||||||
"github.com/yaoapp/yao/agent/context"
|
"github.com/yaoapp/yao/agent/context"
|
||||||
|
"github.com/yaoapp/yao/agent/i18n"
|
||||||
"github.com/yaoapp/yao/agent/llm/adapters"
|
"github.com/yaoapp/yao/agent/llm/adapters"
|
||||||
"github.com/yaoapp/yao/agent/llm/providers/base"
|
"github.com/yaoapp/yao/agent/llm/providers/base"
|
||||||
"github.com/yaoapp/yao/utils/jsonschema"
|
"github.com/yaoapp/yao/utils/jsonschema"
|
||||||
|
|
@ -686,9 +687,7 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
|
||||||
if errorDetected && errorBuffer.Len() > 0 {
|
if errorDetected && errorBuffer.Len() > 0 {
|
||||||
errorJSON := errorBuffer.String()
|
errorJSON := errorBuffer.String()
|
||||||
if trace != nil {
|
if trace != nil {
|
||||||
trace.Error("OpenAI API returned error response", map[string]any{
|
trace.Error(i18n.T(ctx.Locale, "llm.openai.stream.api_error"), map[string]any{"response": errorJSON}) // "OpenAI API returned error response"
|
||||||
"response": errorJSON,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to parse error
|
// Try to parse error
|
||||||
|
|
@ -711,9 +710,7 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
|
||||||
|
|
||||||
// Log any error from streaming
|
// Log any error from streaming
|
||||||
if err != nil && trace != nil {
|
if err != nil && trace != nil {
|
||||||
trace.Error("OpenAI Stream Error", map[string]any{
|
trace.Error(i18n.T(ctx.Locale, "llm.openai.stream.error"), map[string]any{"error": err.Error()}) // "OpenAI Stream Error"
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if error is due to context cancellation
|
// Check if error is due to context cancellation
|
||||||
|
|
@ -768,11 +765,9 @@ func (p *Provider) streamWithRetry(ctx *context.Context, messages []context.Mess
|
||||||
|
|
||||||
// Log request details for debugging
|
// Log request details for debugging
|
||||||
if requestBodyJSON, err := jsoniter.Marshal(requestBody); err == nil {
|
if requestBodyJSON, err := jsoniter.Marshal(requestBody); err == nil {
|
||||||
trace.Error("Request body that caused empty response", map[string]any{
|
trace.Error(i18n.T(ctx.Locale, "llm.openai.stream.no_data"), map[string]any{"body": string(requestBodyJSON)}) // "Request body that caused empty response"
|
||||||
"body": string(requestBodyJSON),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
trace.Error("Request details", map[string]any{
|
trace.Error(i18n.T(ctx.Locale, "llm.openai.stream.no_data_info"), map[string]any{ // "Request details"
|
||||||
"url": url,
|
"url": url,
|
||||||
"model": accumulator.model,
|
"model": accumulator.model,
|
||||||
"created": accumulator.created,
|
"created": accumulator.created,
|
||||||
|
|
@ -1058,9 +1053,7 @@ func (p *Provider) postWithRetry(ctx *context.Context, messages []context.Messag
|
||||||
// Log full response data for debugging
|
// Log full response data for debugging
|
||||||
if trace != nil {
|
if trace != nil {
|
||||||
if respJSON, err := jsoniter.Marshal(resp.Data); err == nil {
|
if respJSON, err := jsoniter.Marshal(resp.Data); err == nil {
|
||||||
trace.Error("OpenAI API error response", map[string]any{
|
trace.Error(i18n.T(ctx.Locale, "llm.openai.post.api_error"), map[string]any{"response": string(respJSON)}) // "OpenAI API error response"
|
||||||
"response": string(respJSON),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/yaoapp/yao/agent/context"
|
"github.com/yaoapp/yao/agent/context"
|
||||||
|
"github.com/yaoapp/yao/agent/i18n"
|
||||||
"github.com/yaoapp/yao/agent/output/message"
|
"github.com/yaoapp/yao/agent/output/message"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -27,7 +28,7 @@ func (w *Writer) Write(msg *message.Message) error {
|
||||||
chunks, err := w.adapter.Adapt(msg)
|
chunks, err := w.adapter.Adapt(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if trace, _ := w.ctx.Trace(); trace != nil {
|
if trace, _ := w.ctx.Trace(); trace != nil {
|
||||||
trace.Error("CUI Writer: Failed to adapt message", map[string]any{
|
trace.Error(i18n.T(w.ctx.Locale, "output.cui.writer.adapt_error"), map[string]any{ // "CUI Writer: Failed to adapt message"
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
"message_type": msg.Type,
|
"message_type": msg.Type,
|
||||||
})
|
})
|
||||||
|
|
@ -39,9 +40,7 @@ func (w *Writer) Write(msg *message.Message) error {
|
||||||
for _, chunk := range chunks {
|
for _, chunk := range chunks {
|
||||||
if err := w.sendChunk(chunk); err != nil {
|
if err := w.sendChunk(chunk); err != nil {
|
||||||
if trace, _ := w.ctx.Trace(); trace != nil {
|
if trace, _ := w.ctx.Trace(); trace != nil {
|
||||||
trace.Error("CUI Writer: Failed to send chunk", map[string]any{
|
trace.Error(i18n.T(w.ctx.Locale, "output.cui.writer.chunk_error"), map[string]any{"error": err.Error()}) // "CUI Writer: Failed to send chunk"
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -58,7 +57,7 @@ func (w *Writer) WriteGroup(group *message.MessageGroup) error {
|
||||||
// Send the group
|
// Send the group
|
||||||
if err := w.sendChunk(group); err != nil {
|
if err := w.sendChunk(group); err != nil {
|
||||||
if trace, _ := w.ctx.Trace(); trace != nil {
|
if trace, _ := w.ctx.Trace(); trace != nil {
|
||||||
trace.Error("CUI Writer: Failed to send message group", map[string]any{
|
trace.Error(i18n.T(w.ctx.Locale, "output.cui.writer.group_error"), map[string]any{ // "CUI Writer: Failed to send message group"
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
"group_id": group.ID,
|
"group_id": group.ID,
|
||||||
})
|
})
|
||||||
|
|
@ -88,9 +87,7 @@ func (w *Writer) sendChunk(chunk interface{}) error {
|
||||||
data, err := json.Marshal(chunk)
|
data, err := json.Marshal(chunk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if trace, _ := w.ctx.Trace(); trace != nil {
|
if trace, _ := w.ctx.Trace(); trace != nil {
|
||||||
trace.Error("CUI Writer: Failed to marshal chunk", map[string]any{
|
trace.Error(i18n.T(w.ctx.Locale, "output.cui.writer.marshal_error"), map[string]any{"error": err.Error()}) // "CUI Writer: Failed to marshal chunk"
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -106,9 +103,7 @@ func (w *Writer) sendChunk(chunk interface{}) error {
|
||||||
// The context knows how to send data based on the connection type (SSE, WebSocket, etc.)
|
// The context knows how to send data based on the connection type (SSE, WebSocket, etc.)
|
||||||
if err := w.ctx.Send(data); err != nil {
|
if err := w.ctx.Send(data); err != nil {
|
||||||
if trace, _ := w.ctx.Trace(); trace != nil {
|
if trace, _ := w.ctx.Trace(); trace != nil {
|
||||||
trace.Error("CUI Writer: Failed to send data to client", map[string]any{
|
trace.Error(i18n.T(w.ctx.Locale, "output.cui.writer.send_error"), map[string]any{"error": err.Error()}) // "CUI Writer: Failed to send data to client"
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/yaoapp/yao/agent/context"
|
"github.com/yaoapp/yao/agent/context"
|
||||||
|
"github.com/yaoapp/yao/agent/i18n"
|
||||||
"github.com/yaoapp/yao/agent/output/message"
|
"github.com/yaoapp/yao/agent/output/message"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -32,7 +33,7 @@ func (w *Writer) Write(msg *message.Message) error {
|
||||||
chunks, err := w.adapter.Adapt(msg)
|
chunks, err := w.adapter.Adapt(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if trace, _ := w.ctx.Trace(); trace != nil {
|
if trace, _ := w.ctx.Trace(); trace != nil {
|
||||||
trace.Error("OpenAI Writer: Failed to adapt message", map[string]any{
|
trace.Error(i18n.T(w.ctx.Locale, "output.openai.writer.adapt_error"), map[string]any{ // "OpenAI Writer: Failed to adapt message"
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
"message_type": msg.Type,
|
"message_type": msg.Type,
|
||||||
})
|
})
|
||||||
|
|
@ -56,9 +57,7 @@ func (w *Writer) Write(msg *message.Message) error {
|
||||||
|
|
||||||
if err := w.sendChunk(chunk); err != nil {
|
if err := w.sendChunk(chunk); err != nil {
|
||||||
if trace, _ := w.ctx.Trace(); trace != nil {
|
if trace, _ := w.ctx.Trace(); trace != nil {
|
||||||
trace.Error("OpenAI Writer: Failed to send chunk", map[string]any{
|
trace.Error(i18n.T(w.ctx.Locale, "output.openai.writer.chunk_error"), map[string]any{"error": err.Error()}) // "OpenAI Writer: Failed to send chunk"
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +73,7 @@ func (w *Writer) WriteGroup(group *message.MessageGroup) error {
|
||||||
for _, msg := range group.Messages {
|
for _, msg := range group.Messages {
|
||||||
if err := w.Write(msg); err != nil {
|
if err := w.Write(msg); err != nil {
|
||||||
if trace, _ := w.ctx.Trace(); trace != nil {
|
if trace, _ := w.ctx.Trace(); trace != nil {
|
||||||
trace.Error("OpenAI Writer: Failed to write message in group", map[string]any{
|
trace.Error(i18n.T(w.ctx.Locale, "output.openai.writer.group_error"), map[string]any{ // "OpenAI Writer: Failed to write message in group"
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
"group_id": group.ID,
|
"group_id": group.ID,
|
||||||
"message_type": msg.Type,
|
"message_type": msg.Type,
|
||||||
|
|
@ -106,9 +105,7 @@ func (w *Writer) sendChunk(chunk interface{}) error {
|
||||||
data, err := json.Marshal(chunk)
|
data, err := json.Marshal(chunk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if trace, _ := w.ctx.Trace(); trace != nil {
|
if trace, _ := w.ctx.Trace(); trace != nil {
|
||||||
trace.Error("OpenAI Writer: Failed to marshal chunk", map[string]any{
|
trace.Error(i18n.T(w.ctx.Locale, "output.openai.writer.marshal_error"), map[string]any{"error": err.Error()}) // "OpenAI Writer: Failed to marshal chunk"
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -127,9 +124,7 @@ func (w *Writer) sendChunk(chunk interface{}) error {
|
||||||
// Send via context's writer
|
// Send via context's writer
|
||||||
if err := w.ctx.Send(sseData); err != nil {
|
if err := w.ctx.Send(sseData); err != nil {
|
||||||
if trace, _ := w.ctx.Trace(); trace != nil {
|
if trace, _ := w.ctx.Trace(); trace != nil {
|
||||||
trace.Error("OpenAI Writer: Failed to send data to client", map[string]any{
|
trace.Error(i18n.T(w.ctx.Locale, "output.openai.writer.send_error"), map[string]any{"error": err.Error()}) // "OpenAI Writer: Failed to send data to client"
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -148,9 +143,7 @@ func (w *Writer) sendDone() error {
|
||||||
doneData := []byte("data: [DONE]\n\n")
|
doneData := []byte("data: [DONE]\n\n")
|
||||||
if err := w.ctx.Send(doneData); err != nil {
|
if err := w.ctx.Send(doneData); err != nil {
|
||||||
if trace, _ := w.ctx.Trace(); trace != nil {
|
if trace, _ := w.ctx.Trace(); trace != nil {
|
||||||
trace.Error("OpenAI Writer: Failed to send [DONE] to client", map[string]any{
|
trace.Error(i18n.T(w.ctx.Locale, "output.openai.writer.done_error"), map[string]any{"error": err.Error()}) // "OpenAI Writer: Failed to send [DONE] to client"
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue